Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Active Recordから考える次の10年を見据えた技術選定 / Architecture decision for the next 10 years at PIXTA

Yuichi Goto
September 16, 2021

Active Recordから考える次の10年を見据えた技術選定 / Architecture decision for the next 10 years at PIXTA

September 15, 2021 @ iCARE Dev Meetup #25

Yuichi Goto

September 16, 2021
Tweet

More Decks by Yuichi Goto

Other Decks in Programming

Transcript

  1. 自己紹介 Yuichi Goto(@_yasaichi) ピクスタ株式会社 執行役員 CTO 兼 開発部長 2020 年

    7 月発売の「パーフェクト Ruby on Rails 【増補改訂版】」の 共著者(Part 5 担当) 2
  2. 会社紹介 ピクスタは本社と複数のグループ会社(ベトナム開発拠点含む)から構成 されており、「クリエイティブ・プラットフォーム事業」を主要事業としている 2015 年に東証マザーズ市場に上場し、2020 年 からはいわゆる第二創業 期を迎えつつある状態 本社社員 84

    名のうち約 2 割がエンジニア職種であり(※)、全ての事業で Ruby on Rails(以下、Rails)を利用している RubyKaigi には 過去 6 回協賛 ※ これらの数値は全て 2021 年 6 月末時点のもの 4
  3. 成長戦略の策定 2020 年に今後 10 年の成長戦略を 策定し、既存事業の周辺市場へ 新規事業を展開することを決定 (右図: 2021 年

    12 月期第 2 四半 期決算説明資料 [1] p. 47) CTO としてこの成長戦略の実現に 向けた体制を作ることになった 6
  4. 3. 要素技術の選定方針 本発表を通じて説明するいくつかの理由から、前述のシステムの要素技術を 次の方針で選定することにした。 フロントエンドとバックエンドの 開発言語を TypeScript で統一する フロントエンドでは Jamstack

    アーキテクチャを採用する バックエンドでは 最近話題の Prisma を見送り、アクティブレコードか データマッパーを実装した ORM を採用する ※ 本資料における「Prisma」は、特に断りがない限り ORM としてのそれを指すものとする 11
  5. 第一部: Active Record から見る Rails 1. Web アプリケーションアーキテクチャ概論 2. Active

    Record から見る Rails のアーキテクチャ 3. なぜ Rails の利用を取りやめていく選択をしたか 14
  6. Web アプリケーションアーキテクチャの主な論点 Web アプリケーションでは、内部を適当数のレイヤーに分割してこれらを 積み重ねる構造(※)によってその複雑さに対処することが多い [4] したがって、Web アプリケーションのアーキテクチャを考えるにあたって 主な論点は次の 2

    つとなる 論点 1: どんな役割のレイヤーをどのように積み重ねるか 論点 2: 各レイヤーをどのように設計するか ※ この構造を「レイヤードアーキテクチャ」と呼ぶ。厳密な定義は「POSA [5]」などを参照のこと 15
  7. 典型的なレイヤリング: 3 レイヤー One of the most common ways to

    modularize an information-rich program is to separate it into three broad layers: presentation (UI), domain logic (aka business logic), and data access. So you often see web applications divided into . . . On the whole Ive found this to be an effective form of modularization for many applications and one that I regularly use and encourage. ― Martin Fowler (2015) [6] “ “ ※ 太字強調は引用者によるもの 16
  8. 各レイヤーの名称と役割 Fowler 氏の著書「Patterns of Enterprise Application Architecture [7]」 (※1)では、3 つレイヤーの名称と役割は次のように定義されている。

    プレゼンテーション(※2): ユーザーからの HTTP リクエストなどを下位 レイヤーの呼び出しへ変換する、ユーザーへ情報を表示する ドメイン: 入力データの妥当性確認、入力・格納データによる計算を行う データソース: データベースやメッセージングシステムなどと通信する ※1 以下、「PoEAA」と呼ぶ ※2 本レイヤーの設計方法は以降の議論とは関係しないため取り上げない 17
  9. データソースレイヤーをどのように設計するか PoEAA において、データストアが RDB の場合に利用できるパターンとして 紹介されているものは次の通り。 (テーブル|行)データゲートウェイ: RDB 内の(テーブル|レコード)への 操作をカプセル化するオブジェクト

    アクティブレコード: ドメインロジックを実装した行データゲートウェイ データマッパー: ドメインモデルと RDB 内のレコードを相互変換するもの ※ Rails の Active Record との混同を避けるため、本発表では訳本のパターン名を意図的に用いている 21
  10. 各組み合わせの実装例 次の正常系のフローからなる「ニュースレター登録機能」を、前ページの組み 合わせ A と B で実装した例を示す。 1. ユーザーは、フォームにメールアドレスを入力して「登録」ボタンを押す 2.

    アプリケーションは、メールアドレスの形式が正しいことを確認する 3. アプリケーションは、メールアドレスとその存在確認用のトークンを DB に 記録して、ユーザーに確認メールを送信する 23
  11. DB = Sequel.connect("connection_url") class SubscribeToNewsletter Result = Struct.new(:errors, :subscription_id, keyword_init:

    true) def self.call(email:) unless URI::MailTo::EMAIL_REGEXP.match?(email) return Result.new(errors: ["Email is invalid"]) end values = { email: email, confirmation_token: SecureRandom.uuid } DB.transaction do id = DB[:newsletter_subscriptions].insert(values) EmailConfirmationMailer.deliver(values) Result.new(subscription_id: id) end rescue StandardError Result.new(errors: ["Something went wrong"]) end end A. トランザクションスクリプト + テーブルデータゲートウェイ ドメインロジックは スクリプト内にべた書き 24
  12. class SubscribeToNewsletterService Result = Struct.new(:errors, :subscription_id, keyword_init: true) def self.call(email:)

    unless URI::MailTo::EMAIL_REGEXP.match?(email) return Result.new(errors: ["Email is invalid"]) end subscription = NewsletterSubscription.new(email: email) subscription.set_confrimation_token NewsletterSubscription.transaction do subscription.save! EmailConfirmationMailer .deliver(subscription.slice(:email, :confirmation_token)) Result.new(subscription_id: subscription.id) end rescue StandardError Result.new(errors: ["Something went wrong"]) end end B. ドメインモデル + アクティブレコード with サービスレイヤー 25
  13. 第一部: Active Record から見る Rails 1. Web アプリケーションアーキテクチャ概論 2. Active

    Record から見る Rails のアーキテクチャ 3. なぜ Rails の利用を取りやめていく選択をしたか 27
  14. Rails の Active Record とは MVC パターンを採用している Rails におけるモデルに相当するもので、 Rails

    を構成する gem の 1 つである activerecord にその実体がある [9] 各アプリケーションでは、この gem が提供する ActiveRecord::Base という 基底クラスを継承して固有のモデルを定義する その名称からも明らかなように、この基底クラスは PoEAA のアクティブ レコードの実装になっている [10] 28
  15. PoEAA のアクティブレコードにはない機能 基底クラス ActiveRecord::Base は PoEAA のアクティブレコードにはない次の 代表的な機能を持っている。 バリデーション: データベース操作の実行前にモデルの状態を検証できる

    ようにするもの(e.g. ある属性が空でないかの確認) コールバック: データベース操作の実行前後に任意のコードを実行できる ようにするもの 設定したコードはデータベース操作と 同一のトランザクション内で実行される 29
  16. class NewsletterSubscription < ActiveRecord::Base validates :email, format: { with: URI::MailTo::EMAIL_REGEXP

    } before_create :set_confrimation_token after_create :send_email_confirmation_instructions private def set_confrimation_token self[:confirmation_token] = SecureRandom.uuid end def send_email_confirmation_instructions EmailConfirmationMailer.deliver(slice(:email, :confirmation_token)) end end NewsletterSubscription.create(email: "[email protected]") 2 つの機能を利用すると「ニュースレター登録機能」がモデルで完結する 30
  17. Rails の Active Record が果たす役割 次の方法で ドメインレイヤー以下をモデルクラスだけで構築できるように してレイヤーの数(≒ コードの記述量)を減らす ことで、高い開発生産性を

    実現している。 アクティブレコードを採用して、ドメインレイヤーとデータソースレイヤーを 1 つのクラスで実装できるようにした バリデーションとコールバックを導入して、サービスレイヤーのロジックを モデルクラスの DSL やインスタンスメソッドで実装できるようにした 32
  18. 第一部: Active Record から見る Rails 1. Web アプリケーションアーキテクチャ概論 2. Active

    Record から見る Rails のアーキテクチャ 3. なぜ Rails の利用を取りやめていく選択をしたか 35
  19. 要因: 今後予想される新規事業の特性 ピクスタは、「クリエイター基盤づくり」、「洗練された UI・UX」、「オンライン マーケティング(SEO・Web 広告)」の 3 つを強みとして掲げている [1] 前述の成長戦略の実現にあたって、これらの

    強みを活かした新規事業を 展開することで、その成功確率を高めようとするのは自然な流れ である 特に 後の 2 つの強みはフロントエンドと関係する ため、今後のシステム アーキテクチャの要素技術の選定にあたって考慮したい 36
  20. 考慮すべき事項の一例: Core Web Vitals Google が提唱するユーザー体験の質を測定するための指標で、2020 年 時点では LCP、FID、CLS の

    3 つ(※)から構成される [13] 2021 年 6 月から Google の検索結果順位を決める要因に加えられており [14]、検索流入に頼ることが多いピクスタでは無視できないものの 1 つ 本指標で自然と高スコアを取れるシステムアーキテクチャにしておけば、 SEO 関連の開発よりもプロダクトの仮説検証に多くの時間を割ける ※ それぞれ「Largest Contentful Paint」、「First Input Delay」、「Cumulative Layout Shift 」の略 37
  21. Rails の フロントエンドの今後 Rails 7 に向けて、フロントエンドの開発生産性の向上に関する次の変更が 行われたが(※)、ユーザー体験指標の向上に関する動きは見られない。 デフォルトで Hotwire を導入して、SPA

    に近い体験の実装を容易にする Import maps を導入して、デフォルトで Webpacker を無効にする デフォルトで Sass のトランスパイルを無効にし、Tailwind CSS のような CSS フレームワークの利用を推奨する ※ 2021 年 9 月中旬時点のコミット内容に基づく記述で、正式リリースまでに変更される可能性がある 38
  22. 2019 年時点での見解 Active Record による Rails のバックエンドでの高い開発生産性は、その 構造的な限界に対処さえできれば依然として魅力的である 一方で Rails

    の出自を考えると、ピクスタで必要なユーザー体験指標の 向上に注力する流れに自然となる可能性は高くない バックエンドは Rails のまま、ユーザー体験指標の向上に注力している フロントエンドのフレームワークを用いれば全て解決するのではないか 40
  23. 新たな問題と解決策 背景: ピクスタでは、 PMF 到達前のチームを事業責任者、デザイナー、 エンジニアの 3 人で構成することが多く、本新規事業も同様であった 起きたこと: フロントエンドとバックエンドでの開発言語の違いから生じる

    認知負荷が想定以上に大きく、 担当エンジニアに負担をかけてしまった 解決策: フロントエンドとバックエンドの 開発言語を統一することで、一人 目のエンジニアにかかる認知負荷をできるだけ下げる 42
  24. どのように開発言語を統一するか ピクスタでは、 MVP(※)を Web サービスとして製品化することが多いため、 次の 2 つの方向性を検討したうえでより現実的な B 案を選択した。

    A. バックエンドでは引き続き Rails を利用し、ハイパフォーマンスな Web フロントエンドを Ruby で開発する方法を模索する B. フロントエンドでは Next.js のような Jamstack アーキテクチャの実装を 利用し、Rails に近い生産性でバックエンドを JavaScript で開発する 方法を模索する 事業領域がクリエイティブ分野で あることに関連している(と思う) ※ 「Minimum Viable Product」の略。詳しくは「MVP の作り方 [15]」などを参照のこと 43
  25. 前提: JavaScript と TypeScript のどちらを選ぶか 次のような良し悪しがあるものの、総合的に見て恩恵の方が大きいと考えて TypeScript を採用した。 以降の議論もこの決定を前提としている。 良い点:

    システムが複雑化する PMF 到達後では静的型の恩恵が大きい。 特にフロントエンドではテストが書きづらいため、より早くから必要になる 悪い点: PMF 到達前の仮説検証において、静的型が素早い開発の妨げに なることがある(が、最終手段として any がある) 46
  26. Prisma とは v1 は任意のデータストアに GraphQL を被せるサーバーの実装だったが、 v2 (2020 年 6

    月リリース [16])以降では TypeScript の ORM となった 独自スキーマによるデータモデルの定義、定義に基づいた型安全なデータ ベース操作とマイグレーション機能が主な特徴として挙げられる 調査したところ、 JavaScript の新興フルスタックフレームワーク(※)の 多くで採用されていたため、まず初めに利用を検討した ※ 本発表では、2020 年前後に開発が始まったものを指している。具体的は、Blitz、RedwoodJS など 47
  27. const prisma = new PrismaClient(); // Create const createdUser =

    await prisma.user.create({ data: { email: '[email protected]' }, }); // Read const user = await prisma.user.findUnique({ where: { id: createdUser.id } }); // Update const updatedUser = await prisma.user.update({ where: { id: user!.id }, data: { email: '[email protected]' }, }); // Delete await prisma.user.delete({ where: { id: updatedUser.id } }); Prisma によるデータベース操作の例 各データベース操作はRDB内の テーブルに対応するオブジェクトの メソッド呼び出しによって行う 48
  28. Prisma は PoEAA のどのパターンを実装しているか 公式ドキュメントでは、"Prisma is a new kind of

    Data Mapper ORM" と 述べられている [17] 前ページで示したように、Prisma は POJO(※)と RDB 内のレコードを 相互変換しているので、広義のデータマッパーと言えなくもない しかし、最新の v3.0.2 時点でもこの POJO にドメインロジックを実装する 方法を提供していないため、テーブルデータゲートウェイの実装と言った 方が正しい ※ 「Plain Old JavaScript Object」の略。素の JavaScript オブジェクトのこと 49
  29. ドメインレイヤーのアーキテクチャ選定に与える影響 ドメインレイヤー以下をドメインモデルと Prisma で構築しようとすると、 Prisma の返す POJO のデータをドメインモデルに詰め直す必要がある 特に Web

    アプリケーションの実装初期では、ドメインモデルと RDB 内の テーブルが 1:1 対応することが多いため、この作業は手間なだけである 結果、データソースレイヤーに Prisma を用いると、 ドメインレイヤーでは 自ずとトランザクションスクリプトを用いることになる p. 22 の理由の 1 つがこちら 50
  30. import {Ctx} from "blitz" import db from "db" import {hashPassword}

    from "app/auth/auth-utils" import {SignupInput, SignupInputType} from "app/auth/validations" export default async function signup(input: SignupInputType, {session}: Ctx) { // This throws an error if input is invalid const {email, password} = SignupInput.parse(input) const hashedPassword = await hashPassword(password) const user = await db.user.create({ data: {email: email.toLowerCase(), hashedPassword, role: "user"}, select: {id: true, name: true, email: true, role: true}, }) await session.$create({userId: user.id}) return user } 実際にトランザクションスクリプトに帰着した例 (Blitz) 出典: blitz/examples/custom-server/app/auth/mutations/signup.ts [18] 51
  31. 本件に対する Prisma 側の認識と姿勢 Prisma 側 にも本件は認識されており、「Explore how to extend the

    Prisma Client [19]」という Issue では次のような趣旨が述べられている。 Prisma で Computed field、Multi-step actions(※)、Custom Validation を実装する場合の指針を示せていない状況を改善したい 上記のロジックをコントローラーに直接実装することは解決策の 1 つだが、 アプリケーションが複雑になるとモデルによる抽象化が必要になる ※ 説明を読む限り、(アプリケーション|ドメイン)サービスに実装するようなロジックのことを指していると思われる 54
  32. 核心をついた Issue だが、直近 2 か月ほど動きがない状態 2021 年 7 月 14

    日に担当者が 変更された後、特に動きなし 画像出典: https://github.com/prisma/prisma/issues/7161 [19] 55
  33. ここまでのまとめ 次の ピクスタのビジネス上の問題を解決するため、言語を TypeScript に 統一し、Jamstack とアクティブレコードの実装を用いる方針を設定した。 A. 成長戦略の実現のため、強みの 1

    つであるオンラインマーケティングが 活きる新規事業を展開し、その成功確率を高めたい B. 高速な仮説検証と SEO 関連の開発を両立させるため、開発生産性が 高く、かつ Core Web Vitals で自然と高スコアが取れる構造を作りたい C. B の実現にあたってエンジニアにかかる認知負荷をできるだけ下げたい 近年の流行を取り入れることを 第一とした技術選定ではない 60
  34. 主な要素技術の選定理由 Next.js: Jamstack アーキテクチャの実装の中でも最も進化が速いため GraphQL: Apollo Federation を利用することで、ほぼコードを書かずに API ゲートウェイ

    を実装できるため Nexus: NestJS と迷ったが、初期の学習コストの高さと持続可能性にやや 懸念があり、まずは最小限の構成から始めることにしたため TypeORM: 機能、型安全性、持続可能性のバランスが取れていたため 62
  35. 今後予定している進め方 直近では新規プロダクトの開発は未定なため、まずは既存の PIXTA (※) サービスを前述のアーキテクチャに近づける取り組みを行い、学びを得る この PIXTA のリアーキテクティングは、数名の専任チームが開発チームと 協力しながら 2

    ~ 3 年かけて行う リアーキテクティングの成否はもちろんのこと、今後生じる新規プロダクト 開発に得られた学びを還元することも同じくらい重視する ※ ピクスタが運営する写真・イラスト・動画・音楽のデジタル素材オンラインマーケットプレイスのこと 63
  36. 参考文献 1 1. ピクスタ株式会社 "2021 年 12 月期第2四半期決算説明資料",URL: https://ssl4.eir- parts.net/doc/3416/ir_material_for_fiscal_ym/104956/00.pdf↩

    2. 馬田隆明 "PMF に到るまでのステージ別指針集 ",URL: https://speakerdeck.com/tumada/pmf-nidao- rumadefalsesutezibie-zhi-zhen-ji↩ 3. "6. 1on1 in Public by texta.fm",URL: https://anchor.fm/textafm/episodes/6--1on1-in-Public-e1078pn↩ 4. "Common web application architectures",URL: https://docs.microsoft.com/en- us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures↩ 5. Frank Buschmann,Regine Meunier,Hans Rohnert,Peter Sommerlad,and Michael Stal (1996) Pattern- Oriented Software Architecture,Volume 1,A System of Patterns: Wiley.↩ 6. Martin Fowler "PresentationDomainDataLayering",URL: https://martinfowler.com/bliki/PresentationDomainDataLayering.html↩ 7. Martin Fowler (2002) Patterns of Enterprise Application Architecture: Addison-Wesley Professional.↩ 70
  37. 参考文献 2 8. Robert C.Martin (2018) Clean Architecture 達人に学ぶソフトウェアの構造と設計: KADOKAWA.↩

    9. "Welcome to Rails",URL: https://github.com/rails/rails/blob/6-1-stable/README.md↩ 10. "Active Record – Object-relational mapping in Rails",URL: https://github.com/rails/rails/tree/6-1- stable/activerecord#label-Philosophy↩ 11. kawasima "イミュータブルデータモデル",URL: https://scrapbox.io/kawasima/イミュータブルデータモデル↩ 12. Yuichi Goto "Ruby on Rails の正体と向き合い方",URL: https://speakerdeck.com/yasaichi/what-is-ruby- on-rails-and-how-to-deal-with-it↩ 13. "Introducing Web Vitals: essential metrics for a healthy site",URL: https://blog.chromium.org/2020/05/introducing-web-vitals-essential-metrics.html↩ 14. "More time, tools, and details on the page experience update",URL: https://developers.google.com/search/blog/2021/04/more-details-page-experience↩ 71
  38. 参考文献 3 15. 馬田隆明 "MVP の作り方 とにかく雑に作る「手作業型 MVP」のススメ",URL: https://speakerdeck.com/tumada/mvp-falsezuo-rifang-tonikakuza-nizuo-ru-shou-zuo-ye-xing-mvp- falsesusume↩

    16. Nikolas Burk "Prisma 2.0: Confidence and productivity for your database",URL: https://www.prisma.io/blog/announcing-prisma-2-n0v98rzc8br1↩ 17. "Is Prisma an ORM?",URL: https://www.prisma.io/docs/concepts/overview/prisma-in-your-stack/is- prisma-an-orm↩ 18. "blitz/signup.ts at v0.40.0-canary.7 · blitz-js/blitz",URL: https://github.com/blitz-js/blitz/blob/v0.40.0- canary.7/examples/custom-server/app/auth/mutations/signup.ts↩ 19. "Explore how to extend the Prisma Client · Issue #7161 · prisma/prisma",URL: https://github.com/prisma/prisma/issues/7161↩ 20. "Prisma vs TypeORM",URL: https://www.prisma.io/docs/concepts/more/comparisons/prisma-and- typeorm#type-safety↩ 72
  39. 参考文献 4 21. "github/graphql-client: A Ruby library for declaring, composing

    and executing GraphQL queries",URL: https://github.com/github/graphql-client↩ This presentation is created by Marp. Great thanks @yhatt ! 73