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

Active Recordから考える次世代のRuby on Railsの方向性 / Dire...

Active Recordから考える次世代のRuby on Railsの方向性 / Directions for the next generation of Ruby on Rails: From the viewpoint of its Active Record

January 29, 2021 @ 銀座Rails #29

Yuichi Goto

January 29, 2021
Tweet

More Decks by Yuichi Goto

Other Decks in Programming

Transcript

  1. さかのぼること 2 年 2019 年 3 月に Railsdm というイベントで「Ruby on

    Rails の正体と向き 合い方 [1]」というタイトルで発表を行い、当時それなりの反響があった その後もいくつかの発表・エントリで引用していただいた(一部抜粋) 発表: Ruby (off|with) the Rails [2]、Fat Model の倒し方 [3] ブログエントリ: Smart UI パターンが再評価される世界 [4] 5
  2. 前回の発表後に起きたこと: フロントエンドの進化 1. Next.js の現在の主要機能が v9 で概ね実装され、React フレームワーク としての確固たる地位を築く(2019 年

    7 月 [5]~ 2020 年 7 月 [6]) 2. Next.js と Prisma 2 をもとに作られた Rails インスパイアのフルスタック フレームワークである Blitz.js がリリースされる(2020 年 4 月 [7]) 3. JavaScript のフルスタックフレームワークで Rails を置き換えられるの ではないか、という機運が高まる(e.g. Frontend 領域を再定義する [8]) 6
  3. 本発表における問題意識 JavaScript のフルスタックフレームワークで Rails を置き換えられると仮定 したとき、次の問題があると考える。 Prisma 2 などの JavaScript

    の ORM で Rails の Active Record と 同等の開発生産性を実現しようとした場合の課題に関する議論が少ない その前提となる Active Record が果たす役割に関する議論も少ない 7
  4. 本発表の目的とアプローチ 目的: Rails の Active Record が果たす役割を明らかにする 明らかにした内容をもとに 次世代の Rails

    の方向性を考察する ことで、 未来の有益な議論に繋げる アプローチ: ソフトウェアアーキテクチャのパターンを起点とした考察 8
  5. レイヤードアーキテクチャとは 「Pattern-Oriented Software Architecture,Volume 1,A System of Patterns [9]」(※)における定義は次の通り。 アプリケーションを適当数のレイヤーで構造化し、これらを積み重ねたもの

    あるレイヤーにおいて、その構成要素は同一抽象レベルで作業を行う あるレイヤーの提供するサービスは、隣接する下位レイヤーが提供する サービスを利用して実現される ※ 以下、POSA と呼ぶ 11
  6. Web アプリケーションにおけるレイヤリング 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) [10] “ “ ※ 太字強調は引用者によるもの 13
  7. Web アプリケーションを構成する主要なレイヤー Fowler 氏の著書「Patterns of Enterprise Application Architecture [11]」(※)では、次の 3

    つのレイヤーが挙げられている。 プレゼンテーション: ユーザーからのコマンド(例: HTTP リクエスト)を 下位レイヤーの呼び出しへ変換、ユーザーへの情報の表示 ドメイン: 入力データの妥当性確認、入力・格納データにもとづく計算 データソース: データベース、メッセージングシステムなどとの通信 ※ 以下、PoEAA と呼ぶ 14
  8. データソースレイヤーのアーキテクチャパターン PoEAA において、データストアが RDB の場合に利用できるパターンとして 紹介されているものは次の通り。 (テーブル|行)データゲートウェイ: RDB 内の(テーブル|レコード)への 操作をカプセル化するオブジェクト

    アクティブレコード: ドメインロジックを実装した行データゲートウェイ データマッパー: ドメインモデルと RDB 内のレコードを相互変換するもの ※ Rails の Active Record との混同を避けるため、ここでは意図的に訳本のパターン名を用いている 19
  9. 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. トランザクションスクリプト + テーブルデータゲートウェイ ドメインロジックは スクリプト内にべた書き 22
  10. 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 サービスレイヤー 23
  11. Rails の Active Record とは MVC パターンを採用している Rails におけるモデルに相当するもので、 Rails

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

    ようにするもの(例: ある属性が空でないかの確認) コールバック: データベース操作の実行前後に任意のコードを実行できる ようにするもの 設定したコードはデータベース操作と 同一のトランザクション内で実行される 26
  13. 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 つの機能を利用すると「ニュースレター登録機能」がモデルで完結する 27
  14. Rails の Active Record が果たす役割 次の方法で ドメインレイヤー以下をモデルクラスだけで構築できるように して、コードの記述量を減らす ことで、高い開発生産性を実現している。 アクティブレコードを採用して、ドメインレイヤーとデータソースレイヤーを

    1 つのクラスで実装できるようにした バリデーションとコールバックを導入して、サービスレイヤーのロジックを モデルクラスの DSL やインスタンスメソッドで実装できるようにした 29
  15. コードの記述量を減らすその他のアプローチ コードの記述量を減らして開発生産性を向上させるために、Rails は次のアプ ローチを Active Record と併用している。 Convention Over Configuration:

    Rails の規約に従うと、アプリケー ションの設定に関するコードの多くを記述しなくてすむようにした リソースベースのルーティング: REST に則った URI 設計を行うと、多くの コードを自動生成できるようにした 30
  16. ここ 10 年でのあるべき姿の変化 いくつかの理由により(※)、Web アプリケーションの利用を通じて得られる ユーザー体験の重要性が年々高まっている これに伴い、ユーザーと Web アプリケーションの界面であるフロントエンド 領域が確立され、前ページの「あらゆる領域」に加わった

    2021 年 5 月から Google の検索順位の決定にユーザー体験指標(Core Web Vitals)が導入予定のため [14]、直近ではより関心が高まっている Rails が登場したのは 2004 年であることに注意 ※ 例えば、ソフトウェアが企業の業務支援のための手段からビジネスそのものに変化したから、という理由が挙げられる 37
  17. Rails の現状 バックエンドでの高い開発生産性を実現するための仕組みと比較すると、 ユーザー体験を向上させるための仕組みはあまり充実していない 例: Largest Contentful Paint(※)において改善余地のある事項 Turbolinks の実装の都合上、デフォルトでは全ての

    CSS ファイルが application.css に連結されて <head> 内で読み込まれること Sprockets が画像に対してデフォルトでは何の最適化も行わないこと ※ Core Web Vitals の構成指標で、ユーザーがページ内の最も有意義なコンテンツをどのくらい早く見れるかを表す 38
  18. 差分を埋めるアプローチ = 次世代の Rails の方向性 1. フロントエンドからのアプローチ Next.js のような質の高いユーザー体験を実現するための仕組みの中で、 バックエンドを

    Rails と同等の生産性で開発できるようにする 2. バックエンドからのアプローチ Rails のバックエンドでの高い開発生産性を実現するための仕組みの中で、 Next.js などと同等の水準のユーザー体験を実現できるようにする 39
  19. 重要な要素: Prisma v1 は任意のデータストアに GraphQL を被せるサーバーの実装だったが、 2020 年 6 月リリースの

    v2 [15] では単なる TypeScript の ORM となった 主な特長として、独自スキーマで定義したデータモデルをもとにした自動 型生成とマイグレーションの実行機能がある Blitz.js,RedwoodJS といった新興のフルスタックフレームワークの多くで 採用されているため、次世代の Rails を語る上で重要な要素である ※ 以降、バージョン番号なしで Prisma と表記した場合、 Prisma 2 のことを指すものとする 41
  20. 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内の テーブルに対応するオブジェクトの メソッド呼び出しに対応する 42
  21. Prisma は PoEAA のどのパターンを実装しているか 公式ドキュメントでは、"Prisma is a new kind of

    Data Mapper ORM" と 述べられている [16] 前ページで示したように、Prisma は POJO(※)と RDB 内のレコードを 相互変換しているので、広義のデータマッパーと言えなくもない しかし、この POJO にドメインロジックは実装できない(レコードの表現で しかない)ため、テーブルデータゲートウェイの実装と言った方が正しい ※ Plain Old JavaScript Objectの略。素のJavaScriptオブジェクトのこと。 43
  22. ドメインレイヤーのアーキテクチャ選定に働く重力 ドメインレイヤー以下をドメインモデル + Prisma で構築する場合、 Prisma の返す POJO のデータをドメインモデルに詰め直す必要がある 特にアプリケーションの実装初期段階では、ドメインモデルと

    RDB 内の テーブルが 1:1 対応することが多いため、この作業は手間なだけである 結果として、ドメインモデルを利用せずに ドメインレイヤー以下をトラン ザクションスクリプト + Prisma で構築する方向に重力が働く p. 21の理由のひとつがこちら 44
  23. 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, roles: [user.role] }) return user } 実際にトランザクションスクリプト + Prisma に帰着した例(Blitz.js) 出典: blitz/examples/custom-server/app/auth/mutations/signup.ts [17] 45
  24. Prisma 以外の ORM はどうか JavaScript には次のような著名なアクティブレコードの実装が存在するが、 TypeScript 周りで Prisma に劣るため、現時点では甲乙つけがたい。

    Sequelize: 約 10 年の歴史を持つこともあり機能面では充実しているが、 TypeScript 対応が弱く、それなりの量の型宣言を書く必要がある [18] TypeORM: Decorator により Sequelize の問題が緩和されており、機能 面でも十分だが、多くの場面で Prisma よりも型安全性が低い [19] どちらもバリデーションと コールバック機能を持つ 47
  25. Prisma の将来はどうか we re planning to over time (1) add

    more features on a Prisma level that help you express data-related business logic (e.g. extended validation rules) and (2) provide callbacks/hooks for you to make it easier to implement business logic . . . I strongly encourage you and everyone else to share your use cases and feature requests . . . as this helps us to prioritizing and designing upcoming features! ― Johannes Schickling (Founder of Prisma,2019) [20] “ “ ※ 太字強調は引用者によるもの 48
  26. フロントエンドからのアプローチの成否 次のいずれかの展開により、 ドメインレイヤーでの開発生産性の問題が 解消されるかどうかが鍵になる のではないか。 Prisma またはフルスタックフレームワーク側が Prisma の返す POJO

    に ドメインロジックを実装する仕組みを提供する(≠ アクティブレコード化) アクティブレコードを実装する他の ORM の TypeScript 周りが改善される 現状のままでも問題ないとわかる、またはそう考える人が多数派になる 49
  27. 重要な要素: Rails 7 次の DHH 氏の言動から、Rails 7 ではフロントエンドに大きな変更が加わる ことが予想されるため、ここではその方向性について考察する。 2020

    年 12 月に SPA を構築するための新しいアプローチ「Hotwire」を 発表、同時に Rails 向けの実装である hotwire-rails を公開した [21] 2021 年 1 月に出演したある Podcast で、「Rails 7 では Hotwire を導入 してデフォルトで有効にしたい」という旨の発言をした [22] 51
  28. フロントエンドでのもう 1 つの試み: 脱 Webpack Hotwire と並行した次の取り組みにより、DHH 氏は Rails での

    Webpack の 利用を必須ではなくそうとしている [23] [24]。 ブラウザの ES Modules サポートを前提に Import Maps(※)の shim を 利用して、JavaScript のビルドなしで hotwire-rails が動くようにする Tailwind CSS でスタイリングできるようにして、Tailwind CSS 自体以外の CSS のビルドをなくす(この実装が tailwindcss-rails ) 未使用クラスの除去は Sprockets で行う ※ import foo from "foo" をブラウザで実現する議論中の仕様。詳しくは WICG の GitHub [25] を参照のこと 52
  29. 背景にある DHH 氏の問題意識 次の発言から、 現代のフロントエンドは問題に対して解決策が必要以上に 複雑になっていることが多い と DHH 氏は感じているのではないか。 “You

    start super stupid simple and then you just pay as you go for the complexity that you use and no more.” [26] “I also agree that apps that don t want or need that [Webpack] shouldn t have to wrestle with something this complex.” [27] 53
  30. 第一部: Rails の Active Record の正体 Web アプリケーションは、プレゼンテーション、ドメイン、データソースの 3 レイヤーから構成されることが多い

    Rails の Active Record が果たす役割は、アクティブレコードの採用、 バリデーションとコールバックの導入によって、ドメインレイヤー以下を モデルクラスだけで構築できるようにすることである Active Record や CoC、リソースベースのルーティングによってコードの 記述量を減らすことで、Rails はその高い開発生産性を実現している 57
  31. 第二部: 次世代の Rails の方向性 フルスタックフレームワークのあるべき姿を実現するアプローチを次世代の Rails の方向性としたとき、そのアプローチと課題は次の通り。 1. フロントエンドからのアプローチ: JavaScript

    の ORM のアーキテクチャ などに起因するドメインレイヤーでの開発生産性の問題の解消が課題 2. バックエンドからのアプローチ: Rails 7 で起こるだろうフロントエンドでの 変更が現時点ではユーザー体験の向上が主な目的でないことが課題 58
  32. 参考文献 1 1. Yuichi Goto "Ruby on Rails の正体と向き合い方",URL: https://speakerdeck.com/yasaichi/what-is-ruby-

    on-rails-and-how-to-deal-with-it↩ 2. Shinpei Maruyama "Ruby (off|with) the Rails",URL: https://speakerdeck.com/shinpeim/ruby-off-with- the-rails↩ 3. toshimaru "Fat Model の倒し方",URL: https://speakerdeck.com/toshimaru/how-to-deal-with-fat- model↩ 4. Takafumi ONAKA "Smart UI パターンが再評価される世界",URL: https://onk.hatenablog.jp/entry/2020/11/11/024531↩ 5. Vercel,Inc. "Next.js 9",URL: https://nextjs.org/blog/next-9↩ 6. Vercel,Inc. "Next.js 9.5",URL: https://nextjs.org/blog/next-9-5↩ 7. Brandon Bayer " Announcing Blitz.js: Rails-like framework for full-stack React apps — built on Next.js". URL: https://dev.to/flybayer/announcing-blitz-js-rails-like-framework-for-full-stack-react-apps- built-on-next-js-g1o↩ 61
  33. 参考文献 2 8. mizchi "Frontend Study #1: 基調講演 - Frontend

    領域を再定義する",URL: https://zenn.dev/mizchi/articles/c638f1b3b0cd239d3eea↩ 9. Frank Buschmann,Regine Meunier,Hans Rohnert,Peter Sommerlad,and Michael Stal (1996) Pattern- Oriented Software Architecture,Volume 1,A System of Patterns: Wiley.↩ 10. Martin Fowler "PresentationDomainDataLayering",URL: https://martinfowler.com/bliki/PresentationDomainDataLayering.html↩ 11. Martin Fowler (2002) Patterns of Enterprise Application Architecture: Addison-Wesley Professional.↩ 12. "Active Record – Object-relational mapping in Rails",URL: https://github.com/rails/rails/tree/6-1- stable/activerecord#label-Philosophy↩ 13. kawasima "イミュータブルデータモデル",URL: https://scrapbox.io/kawasima/イミュータブルデータモデル↩ 14. Jeffrey Jose "Timing for bringing page experience to Google Search",URL: https://developers.google.com/search/blog/2020/11/timing-for-page-experience↩ 62
  34. 参考文献 3 15. Nikolas Burk "Prisma 2.0: Confidence and productivity

    for your database",URL: https://www.prisma.io/blog/announcing-prisma-2-n0v98rzc8br1↩ 16. "Is Prisma an ORM?",URL: https://www.prisma.io/docs/concepts/overview/prisma-in-your-stack/is- prisma-an-orm↩ 17. "blitz/signup.ts at v0.29.3 · blitz-js/blitz",URL: https://github.com/blitz- js/blitz/blob/v0.29.3/examples/custom-server/app/auth/mutations/signup.ts↩ 18. "TypeScript - Manual | Sequelize",URL: https://sequelize.org/master/manual/typescript.html↩ 19. "Prisma vs TypeORM",URL: https://www.prisma.io/docs/concepts/more/comparisons/prisma-and- typeorm#type-safety↩ 20. "How will prisma2 handle business logic? · Issue #353 · prisma/specs",URL: https://github.com/prisma/specs/issues/353↩ 21. DHH on Twitter,December 23 2020,1:27 AM,URL: https://twitter.com/dhh/status/1341420143239450624↩ 63
  35. 参考文献 4 22. "Remote Ruby | Hotwire, Rails NEXT, and

    the DHH Stack™ with David Heinemeier Hansson",URL: https://share.transistor.fm/s/336e93f9↩ 23. DHH on Twitter,January 14 2021,11:18 PM,URL: https://twitter.com/dhh/status/1349722504483500041↩ 24. DHH on Twitter,Dec 26 2020,7:26 PM,URL: https://twitter.com/dhh/status/1342778751575347200↩ 25. "WICG/import-maps: How to control the behavior of JavaScript imports",URL: https://github.com/WICG/import-maps↩ 26. "Full Stack Radio | 151: DHH – Building HEY with Hotwire",URL: https://share.transistor.fm/s/152b6067↩ 27. "Webpacker presents a more difficult OOB experience for JS Sprinkles than Sprockets did - A May Of WTFs - Ruby on Rails Discussions",URL: https://discuss.rubyonrails.org/t/webpacker-presents-a-more- difficult-oob-experience-for-js-sprinkles-than-sprockets-did/75345/31↩ 64