Rails vs Node.js

最終章 「Prisma」

@mizchi

Cloudflare Meetup 2024/10/02

今日の Prisma + Cloudflare の様子

About

前書き

  • フロントエンド/Node.js 視点のポジショントークです
    • Railsに対するチャレンジャーとして Node.js を使ってきた話
    • Rubyの開発者やRubyのユーザーを否定する意図はありませんが、好き嫌いは否定しません。型が好きです
  • 「Rails」は 2010年前後に流行っていた任意なWAFに置き換え可能
    • Symfony(PHP), Django(Python), Spring(Java)
  • OSSは「学習コスト」という有限リソースの奪い合うゼロサムゲームで、何かを使いたければその優位性を主張しなければならないという世界観

Outline

  1. 自分にとっての Rails vs Node.js
  2. Next.jsの視点でRailsを振り返る
  3. 最終章 「Prisma」

自分にとっての Rails vs Node.js

だいたいここ15年

自分にとっての Rails vs Node.js

  • 独学 2009~: 大学生
    • Python, Node.js
  • 就職 2012~: Rails Asset Pipeline の中でコードを書く生活
    • 新卒から Rails,Rails,Rails,フリーランス,Node,フリーランス
    • Node.js の知識で大規模 SPA を開発する人
  • 大規模フロントエンドで疲弊した 2013年 の自分
    • 「Node.jsがRailsになるためには何が必要だろうか」
  • React が普及しそうな2017年の自分
    • 「Node.js周辺の技術優位はコミュニティに受け入れてもらえるだろうか」

2010当時の JavaScript (ES3)

  • 遅くてダサい言語
    • 謎の prototype 指向で、クラスもなく、型もない
    • 仕様的な安定性がない(IE, Firefox, Webkit)
    • ライブラリエコシステムもない
  • 片手間で jQuery を使うための言語

2010 時点のプログラマ界隈の雰囲気(主観)

  • 「イケてる会社は Rails」 を使う雰囲気
    • GitHub, Twitter, Cookpad
    • 新しいものは Ruby で実装されるという期待感
    • 日本国内で Ruby/Rails 人材が豊富
  • Node.js 運用知見の不足
    • 枯れたORMがない(MongoDBの採用は多い)
    • モノリス環境では Rails と Node.js の採用は排他 であり、使われないから知見が溜まらないという時代が長かった

今思うチェックポイント

  • 2012: TypeScript
  • 2013: React
  • 2016: Next.js
  • 2023: RSC (Next@13.4)

TypeScript (Microsoft) 2012

  • JSに静的型検査とIDE補完を持ち込んだ
    • アンセーフな言語仕様に対し型安全性より表現力を選択
    • 人間が使うドキュメントとしての型(ランタイム意味論を放棄)
  • npm の膨大な既存資産を、漸進的型付けで段階的に攻略
    • *.d.ts でモジュール外から型を定義
    • (@types/*) にコミュニティベースで型を蓄積
  • ES2015~ の標準から離れすぎない安心感でシェアを拡大
    • 他のAltJSは、独自色が強すぎたのが敗因
const x: number = 1; //=> const x = 1;

React (facebook) 2013-

  • テンプレートが動的に動くようになった
    • 多重テンプレート問題はクライアント側で解決する引力が生まれた
  • View = UI(Data) の宣言的UIのパラダイムを発明
export function Counter() {
  const [counter, setCounter] = useState(0);
  return <button onClick={()=>setCounter(n => n+1)}>{counter}</div>
}
  • React を動かすための Webpack として Node.js/npm ツールチェインが普及

Next.js (now -> vercel) 2016-

  • 現実的にSSRを開発できるAPI抽象を発明した
    • SSR/CSR の複雑なクライアント処理を隠蔽
    • ルーティングの画面単位でデータ(props)とViewを切り離して最適化
  • Next.js 型フルスタックアーキテクチャという概念を発明
    • Nuxt(Vue), Remix(React), SvelteKit(Svelte), QwikCity(Qwik)

以降、この資料では Next.js = Next.js 型フルスタックアーキテクチャ

Next.js SSR API の発明 (Page Router)

/* ファイルシステム規約でURLのマッピングを決める
pages/posts.tsx => /posts
*/
// サーバーサイドの Loader
export async function getServerSideProps() {
  return { props: { posts: await db.query('select * from "posts"') }}
}
// ルーティングは必ずルートコンポーネントと対応
// サーバー処理: props を入力したHTMLを返却 (SSR)
// クライアント処理: コンポーネントロジックを注入(Hydrate)
export default function PostListPage(props) {
  return <ul>
    {props.posts.map(p => (
      <li key={p.id}><Link to={`/posts/${p.id}`}>{p.title}</Link></li>
    )}
  </ul>
}

React Server Components (RSC) 2023-

日本における Node.js の普及まとめ

  • 2012: 実質 socket.io | mongoose 専用環境
  • 2015: Reactと共にWebpack(Node/npm)が仕込まれる
  • 2016: マイクロサービス化が進む
  • 2019: Next/Nuxt でフロントサーバーを握りはじめる
  • 2021-:
    • Next/Nuxtがサーバーを深く侵食しつつ知見を蓄積
    • 「もはやFullStack TSでいいよね」の雰囲気(※個人の見解です)
    • FullStack TS と既存のアプリケーション資産の主導権争いが今

今なら Rails ではなく FullStack TS

と言えるか…?
でも、その前に Rails を振り返る

Next.js の視点で Rails を振り返る

Rails を復習してきた

Next.jsから見たRails: Good

  • Rails Guide の完成度がすごい
    • Rails というより、ウェブフレームワークの教科書
    • REST という強い設計思想をインストールさせられる
    • 代謝が速いNode.js界の単一責務に過ぎない Next.js では書けない
  • 全部をRESTという一貫した思想でポリシーで説明できる
    • RESTの強いポリシーでDSLを雰囲気で理解させる力がある
    • Ruby すら REST を補助するDSL提供ツールに過ぎない印象

Rails DSL の力強さ

class ArticlesController < ApplicationController
  #...
  def create
    @article = Article.new(title: "...", body: "...")
    # ...
  end
end
<%= form_with model: @article do |form| %>
  <div>
    <%= form.label :title %><br>
    <%= form.text_field :title %>
  </div>
  <div>
    <%= form.submit %>
  </div>
<% end %>

Rails vs Next.js: 世界観の違い

  • Rails: DB起点のデータモデリング中心の世界観
    • REST(≒DB CRUD)の世界観で、可能な限りREST抽象されたリソースであるという前提
    • View はHTMLの文字列で、クライアントで起きることに興味がない
  • Next.js: ルーティング起点のUI駆動の世界観
    • 外部からデータをロードする以上の興味がない
    • ルーティングに対応する処理をクライアントとサーバーで個別に最適化
    • 動的UIが主な関心だったのが、最適化がサーバーに食い込んでいる

UI開発者視点の REST/GraphQL

  • 現実:
    • REST API 相手にクライアントN+1が頻発
    • 複合的な View の要求に専用エンドポイントを都度作ってられない
  • GraphQL (facebook 2015)
    • クライアント要求でクエリを一つに合成してサーバーで発行(コロケーション)
    • 前提: クライアントからサーバーは遠く、サーバーからDBは近い
query GetPostWithAuthorAndComments {
  post(id: "123") {
    id
    content
    comments {
      id
      content
    }
  }
}

Rails視点だと Next.js は「動的なダッシュボードの塊」なのでは?

  • ダッシュボード = 複合的なリソースを一つのUIで整理したもの
  • UI要求でページ分割単位を作ると複合リソース化する
    • UIは人間の理不尽が直撃するレイヤー (Webに限らず)
    • Next.js は全ページが小さなダッシュボードに相当
    • Rails vs Next.js はUIの複雑度の想定が全く違う
  • UI最適化側の要求: リソース問い合わせに REST ではなく CQRS を採用したい

Next.js(=フロントエンド要求水準) から見た Rails の弱さ

  • REST(ful)な思想がUI要求を汲んでくれていない
    • 複合リソースを効率的にしたい要求(CQRS)と噛み合わない
  • JS(=フロントエンド) を隠蔽したさと現実の要求がコンフリクト
    • Turbo, Stimulus, Hotwire, Webpacker(v6 retired)...
    • フロントエンドを Rails Way に組み込もうとしてきたが失敗し続けている
    • 中途半端な距離感で Ruby-JS という巨大なコンテキストスイッチが残る
    • 弱い前提による弱いアセット最適化(Next.jsがやり過ぎなだけかも)
  • データモデリングの世界観なのに静的検査がない
    • 静的型の不在を規約と動的検査で補うのは、型の影を追っているのでは
    • ソースコードへの型注釈を否定しているので rbs/typeprof/steep/sorbet が歪

Rails視点の Next.js の弱点

  • 学習コストが別に低くはない
    • Node.js+フロントエンドの歴史の全部入り
    • Node.js/npm/TypeScript/React/JSX/Webpack|Vite/Tailwind...
  • フルスタックを避ける文化でデファクトの ORM が生まれない
    • フロントエンド/JS は多様な言語経験の背景の前提があるので、強い前提や強い規約を嫌う傾向が強い
    • 結果、npm は単機能なライブラリを組み合わせる疎結合な文化
    • 公式から Rails Guide みたいな決定版がでない
  • 一貫した開発体験の欠如
    • 疎な単機能ライブラリ同士を都度考えて繋ぐ要求が常に発生する

Rails vs Node.js

最終章 Prisma

そもそも: ORM の必要性

  • 補助輪として必要

    • モデル層を欠いたスタックが流行る気がしない
    • Node.jsがRailsと同じ土俵で比較対象になるために必要
  • IDE補完の生産性と安全性

    • インラインの SQL から出てくるデータの型を書くのが面倒/困難
    • SQLi は主要な攻撃経路であるため、インラインSQLを書きたくない
  • (ORM嫌いな人はSQL習熟度が高いはずなので生SQL書いてもらえばいい)

Node.js ORM / QueryBuilder の歴史

  • 初期: Mongoose / Sequelize / Knex / Bookshelf
  • 中期: TypeORM (Nest.js)
  • 最近: Prisma / Drizzle / Kysely

https://scrapbox.io/uki00a/Node.jsのORMについて
https://scrapbox.io/uki00a/TypeScriptでタイプセーフにSQLを実行する

Prisma 2018-

  • TypeScriptの型推論を生かした ORM (実体はQueryBuilder)
  • 開発リソースが潤沢(2022に$40M調達)で、コミュニティの注目度も高い

Prisma: Schema & Migrate

model User {
  id      Int      @id @default(autoincrement())
  Profile Profile?
}
model Profile {
  id   Int  @id @default(autoincrement())
  user Int  @unique
  User User @relation(fields: [user], references: [id])
}
CREATE TABLE "User" (
    id SERIAL PRIMARY KEY
);
CREATE TABLE "Post" (
    id SERIAL PRIMARY KEY,
    "author" integer NOT NULL,
    FOREIGN KEY ("author") REFERENCES "User"(id)
);

Prisma: Schema -> Client

model User {
  id      Int      @id @default(autoincrement())
  Profile Profile?
}
model Profile {
  id   Int  @id @default(autoincrement())
  user Int  @unique
  User User @relation(fields: [user], references: [id])
}
const result = await prisma.user.create({
  data: {
    name: 'id',
    profile: {
      create: [ { title: 'How to eat an omelette' }],
    },
  },
})

Prisma: TypedSQL - SQL を静的検査する

model Post {
  id      String @id @default(cuid())
  title   String
  content String
}
-- prisma/sql/addPost.sql
INSERT INTO "Post" (id, title, content) VALUES ($1, $2, $3);
import { addPost } from "@prisma/client/sql";
const query = addPost(id, "Title", "Content");
  • prisma generate --sql 時にSQLに静的な型検査をしてクライアントコード生成
    • (どこまでクエリの型検査に対応してるかは要検証)

Prisma の手触り

  • メリット:
    • 複雑なクエリをTSの柔軟な表現力で導出させられてる感がいい
    • pull(introspect) でDBからスキーマを抜いてクライアント生成できる
    • 今は困ったら TypedSQL 書けばいい
  • デメリット:
    • 枯れきってない。生成するクエリ品質を突っ込まれがち
    • Prisma+Postgres版の @kamipo さん(ActiveRecord+MySQL)が必要

(ぶっちゃけ sqlc でもいいが、sqlc が流行る気がしない)

RSC+PrismaのCQRS適正

  • そもそも RSCとGraphQL は同じコロケーション
  • RSC+Prisma CRQSで、巨大なGraphQLスタックと異なる言語コンテキストを剥がせる
    • => FullStack TS

RSC+Prisma 視点だと GraphQL は純粋なオーバーヘッド?

  • 箱に詰めて、箱から取り出してるだけ
  • ※PrismaClientはモバイルアプリと喋る能力はない点は注意
    • prisma-client-go
    • prsima-client-rust

最後に

自分の主張: Rails vs Node.js(=TS+Next.js+Prisma)

  • 現時点ではウェブアプリを作るのに FullStack TSが有利
    • 人間のための型とIDEの生産性 + 動的UIの実現性 + 高い最適化
    • 言語を分割するより Prisma の安定性の方が許容しうるリスク
  • 適材適所でRailsがDBモデリング適正を主張するならRubyに静的型検査がほしい
    • ソースコードへの型注釈は必要だと思うんですが、駄目ですかね…
    • TSがnpmを型で覆うまでにかかった時間を考えると気が遠い
  • UIに言語中立なインターフェースができるまでTS有利は続きそう
    • RSC: JS以外から React Flight Protocol を喋ってもいいはず
    • WASM:Component Model で異なる言語から一つのアプリケーションを構成

技術の螺旋としての Next.js+Prisma (vs Rails) の解釈

  • 言語: Ruby+JS+erb(動的型付) -> TS+JSX (漸進的型付)
  • ORM: ActiveRecord -> Prisma (CQRS, QueryBuilder)
  • テンプレート: HTML文字列 -> 動的UI
  • ルーティング: MPA -> 選択的 MPA, SSR, CSR, PPR
  • ランタイム: サーバー -> サーバー(SSR), ブラウザ(SPA), CDN Edge Worker

(ぶっちゃけ 2010年代のウェブフレームワークを、フロントエンド視点で組み立て直したものに過ぎない)

最後に言いたいこと

  • TS はフロントエンドにとって Better だが Best な言語ではない
    • 本当の型安全ではない。型があると思い込んでるだけのJSに過ぎない
    • 基盤周りはRust化しつつあるがRustはアプリケーション層に向いてない
    • 個人的に MoonBit を推してるが…
  • ここから先は FullStackTS の実践とセキュリティの時代
    • 生産性にフォーカスすると何を最適化していたのかを忘れる
    • Next.js がクラサバを透過に見せすぎた結果、秘匿データのクライアント露出制御が現場的な争点になる

おわり