Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

ORM と向き合う

Yuki Ishikawa
September 27, 2024

ORM と向き合う

PyCon JP 2024

※ 資料中で絵文字を使用したら SpeakerDeck アップロード時になんか変なことになってしまいましたが気にしないでください

Yuki Ishikawa

September 27, 2024
Tweet

More Decks by Yuki Ishikawa

Other Decks in Technology

Transcript

  1. ORM のむずかしさ • ORM をうまく扱いきれなくて、無理やり制御するために Dirty Hack っぽいコードが生まれてしまった • 複雑な問い合わせをする方法が

    ORM でうまく表現できなくて結局 SQL を手で書いてしまった • ORM が提供するモデルクラスにドメインロジックを適当にどんどん書いていたらコードの見通しが悪くなってしまった → ORM に対してなんとなく苦手意識があった 漠然と使っていたらアプリケーションがよくわからないことになってしまったこと、ない?
  2. でもやっぱり生 SQL でのアプリケーション開発はしんどい 生 SQL でのアプリケーション開発は、規模が大きくなるにつれて以下のような点での開発効率の悪さが 顕著に現れてきてしんどくなってくる ① SQL を書く際に

    IDE やツールチェインが 提供する便利機能の恩恵を受けることが難しい (入力補完 / 型チェック / 自動フォーマット / etc…) ② 取得したデータの型が自動で得られない (いちいち手で書きたくもない) ③ 関連テーブルのデータを扱う処理を 書くのがしんどい (→ 後述)
  3. 関連データを取得する戦略: こういう場合どうする?? 例えばブログサービスを開発しているとして、テーブルに対して以下のような操作をしたくなる id name password_hash user id author_id title

    body created_at article id article_id author_id body created_at comment 欲しいデータのかたち article comment comment comment article comment comment comment 「あるユーザの直近100件の投稿とそれらについたコメントの一覧」を 右のようなデータ構造として取得してアプリケーションで扱いたい
  4. 関連データを取得する戦略 (その1) 先に Article をとってきて、各 Article に対して Comment 一覧を取得する 最も素朴なアプローチで、直感的でわかりやすい

    いわゆる「N+1 問題」でパフォーマンスが悪い 先に article だけ取得 for ループで 各 article に紐づく comment を取得
  5. そのつらさはデータモデルの違いからくる RDB で用いられる「リレーショナルデータモデル」と プログラム/アプリケーションが扱う「オブジェクトデータモデル」とで、 データ間の関係性を表現するための構造が大きく異なるため、 その相互変換が容易ではない id name password_hash user

    id author_id title body created_at article id article_id author_id body created_at comment user article comment comment comment article comment comment comment リレーショナルデータモデル テーブルとリレーションによって データの関係性を表現 オブジェクトデータモデル オブジェクトが別のオブジェクトを内包する ことでデータの関係性を表現 🫠 つらい
  6. 余談:「インピーダンスミスマッチ」という言葉 ここまでで説明した “つらさ” は俗に (?) 「インピーダンスミスマッチ」と呼ばれる いったい誰がそう呼びはじめたのかは知らないが もともとの意味 電気電子分野において「信号を伝送する際に出力と入力のインピーダンスが一致していないこと」を意味する言葉で、 伝送効率が悪化する要因となる

    (らしい) ソフトウェア開発における意味 おおむね「異なるシステムの境界に何らかのギャップがあり、それに対処するためのプログラミングが非効率であること」を意味する 多くの場合は「RDB」と「アプリケーション」間のギャップ (データモデルの違い) と、その対処の非効率さという文脈で使われる …が、「システム境界で発生するギャップ」は様々あるため、その他の用例もある (その他の例: 「API 仕様とそのユースケースの乖離」など)
  7. オブジェクトデータモデルを永続化する場合の課題 オブジェクトデータモデルは「特定のユースケースのためのデータの断面」であって、 そのまま永続化するとデータのユースケースが極めて限定されてしまう id name password_hash user id author_id title

    body created_at article id article_id author_id body created_at comment アプリケーションのユースケース RDB から関連データを取得する順序 得られるオブジェクトデータ (断面) ある記事についたコメント一覧と コメントしたユーザの名前を 画面に表示したい user article comment comment article comment comment article comment user comment user comment user あるユーザの投稿一覧と 各投稿についたコメント一覧を 画面に表示したい オブジェクトデータモデル間の変換は パフォーマンスが悪くて現実的でない
  8. ORM がカバーしうる周辺領域 O / R の Mapping が本質的な役割なのだから、究極的にはそこだけやってくれればよい …が、現実の ORM

    ライブラリはいろいろな機能が付随している 何が必要で、何が必要ではないか? データ型変換 例: PostgreSQL の timestamp with timezone は Python の datetime (aware) に変換する 型サポート • 取得したデータに型がついている • 問い合わせコードを書くときに補完や型チェックが効く コネクションプーリングできる トランザクションが扱える ドメインロジックとの融合 モデルクラスに独自メソッドを生やしてドメインロジックを 記述できたりする かっこいい (直感的な?) インタフェース • メソッドチェーンによるクエリ組み立て • オブジェクトのフィールドに代入するような操作でデータの 更新が行える • プロパティにアクセスすると裏でデータ取得が走ったりキャッ シュしたり (Lazy Loading) DB マイグレーションができる
  9. ORM に何を期待するか O / R の Mapping が本質的な役割なのだから、究極的にはそこだけやってくれればよい …が、現実の ORM

    ライブラリはいろいろな機能が付随している 何が必要で、何が必要ではないか? データ型変換 例: PostgreSQL の timestamp with timezone は Python の datetime (aware) に変換する 型サポート • 取得したデータに型がついている • 問い合わせコードを書くときに補完や型チェックが効く コネクションプーリングできる トランザクションが扱える ドメインロジックとの融合 モデルクラスに独自メソッドを生やしてドメインロジックを 記述できたりする かっこいい (直感的な?) インタフェース • メソッドチェーンによるクエリ組み立て • オブジェクトのフィールドに代入するような操作でデータの 更新が行える • プロパティにアクセスすると裏でデータ取得が走ったりキャッ シュしたり (Lazy Loading) DB マイグレーションができる • データベースの「あるべき姿」と実態との差分がわかる • 差分を埋めるような変更を適用できる 素朴な DB ライブラリでもできるのだから ORM でも普通にできて欲しい
  10. ORM に何を期待するか O / R の Mapping が本質的な役割なのだから、究極的にはそこだけやってくれればよい …が、現実の ORM

    ライブラリはいろいろな機能が付随している 何が必要で、何が必要ではないか? データ型変換 例: PostgreSQL の timestamp with timezone は Python の datetime (aware) に変換する 型サポート • 取得したデータに型がついている • 問い合わせコードを書くときに補完や型チェックが効く コネクションプーリングできる トランザクションが扱える ドメインロジックとの融合 モデルクラスに独自メソッドを生やしてドメインロジックを 記述できたりする かっこいい (直感的な?) インタフェース • メソッドチェーンによるクエリ組み立て • オブジェクトのフィールドに代入するような操作でデータの 更新が行える • プロパティにアクセスすると裏でデータ取得が走ったりキャッ シュしたり (Lazy Loading) DB マイグレーションができる • データベースの「あるべき姿」と実態との差分がわかる • 差分を埋めるような変更を適用できる とても欲しい、かなり重要 言うまでもない
  11. ORM に何を期待するか O / R の Mapping が本質的な役割なのだから、究極的にはそこだけやってくれればよい …が、現実の ORM

    ライブラリはいろいろな機能が付随している 何が必要で、何が必要ではないか? データ型変換 例: PostgreSQL の timestamp with timezone は Python の datetime (aware) に変換する 型サポート • 取得したデータに型がついている • 問い合わせコードを書くときに補完や型チェックが効く コネクションプーリングできる トランザクションが扱える ドメインロジックとの融合 モデルクラスに独自メソッドを生やしてドメインロジックを 記述できたりする かっこいい (直感的な?) インタフェース • メソッドチェーンによるクエリ組み立て • オブジェクトのフィールドに代入するような操作でデータの 更新が行える • プロパティにアクセスすると裏でデータ取得が走ったりキャッ シュしたり (Lazy Loading) DB マイグレーションができる • データベースの「あるべき姿」と実態との差分がわかる • 差分を埋めるような変更を適用できる 別にあってもなくてもいいような… このへん下手に多機能だと複雑さ・難解さが増しそう
  12. ORM に何を期待するか O / R の Mapping が本質的な役割なのだから、究極的にはそこだけやってくれればよい …が、現実の ORM

    ライブラリはいろいろな機能が付随している 何が必要で、何が必要ではないか? データ型変換 例: PostgreSQL の timestamp with timezone は Python の datetime (aware) に変換する 型サポート • 取得したデータに型がついている • 問い合わせコードを書くときに補完や型チェックが効く コネクションプーリングできる トランザクションが扱える ドメインロジックとの融合 モデルクラスに独自メソッドを生やしてドメインロジックを 記述できたりする かっこいい (直感的な?) インタフェース • メソッドチェーンによるクエリ組み立て • オブジェクトのフィールドに代入するような操作でデータの 更新が行える • プロパティにアクセスすると裏でデータ取得が走ったりキャッ シュしたり (Lazy Loading) DB マイグレーションができる • データベースの「あるべき姿」と実態との差分がわかる • 差分を埋めるような変更を適用できる ?
  13. ORM 周辺領域にある別の課題 DB リソースの定義や管理を ORM の担当領域でないとすると、テーブル構造に関する記述が重複する。 ORM のデータ型定義はテーブル構造に従わざるを得ないので、二重管理になるのはなにかと嫌。 スキーマ定義 CREATE

    TABLE "user" ( "id" TEXT PRIMARY KEY, "name" TEXT NOT NULL, "password_hash" TEXT NOT NULL ); ※ または、マイグレーションの定義 ORM のデータ型 (モデルクラス) 定義 class User(Base): id = Column(String, primary_key=True) name = Column(String, nullable=False) password_hash = Column(String, nullable=False) アプリケーション 同じことを2箇所に書いていて嫌
  14. ORM 周辺領域にある別の課題 同じものを表現しているのだから、どちらか片方だけを記述したらもう片方は自動生成されて欲しい → 向きはどちらでもいいが、ORM ライブラリが DB マイグレーションまでカバーしてくれたほうが嬉しい スキーマ定義 CREATE

    TABLE "user" ( "id" TEXT PRIMARY KEY, "name" TEXT NOT NULL, "password_hash" TEXT NOT NULL ); ※ または、マイグレーションの定義 ORM のデータ型 (モデルクラス) 定義 class User(Base): id = Column(String, primary_key=True) name = Column(String, nullable=False) password_hash = Column(String, nullable=False) アプリケーション スキーマ定義から Python クラスを自動生成 Python クラスからスキーマ定義を自動生成 OR
  15. ここまでのまとめ O / R の Mapping 以外の周辺領域でいろいろな機能があるが、 ORM ライブラリにやってほしいこととそうでもないことがある データ型変換

    例: PostgreSQL の timestamp with timezone は Python の datetime (aware) に変換する 型サポート • 取得したデータに型がついている • 問い合わせコードを書くときに補完や型チェックが効く コネクションプーリングできる トランザクションが扱える ドメインロジックとの融合 モデルクラスに独自メソッドを生やしてドメインロジックを 記述できたりする かっこいい (直感的な?) インタフェース • メソッドチェーンによるクエリ組み立て • オブジェクトのフィールドに代入するような操作でデータの 更新が行える • プロパティにアクセスすると裏でデータ取得が走ったりキャッ シュしたり (Lazy Loading) DB マイグレーションができる どちらでもいい、必須ではなさそう とても欲しい、かなり重要 普通にやってくれ、たのむ 別領域ではあるが、やってもらえると嬉しい
  16. 前提 (余談) FastAPI すきポイント • 「HTTP エンドポイントは関数」という思想 • ASGI /

    asyncio サポート • シンプルでわかりやすい DI • Pydantic に頼った強力な型サポートとバリデーション • デフォルトで OpenAPI Spec を吐き出せる 当方「FastAPI めっちゃすき」派 そういうバイアスがかかっていることは否めない
  17. SQLAlchemy + Alembic Alembic は SQLAlchemy のモデルクラスからマイグレーションスクリプト自動生成するツールなので、 前述の「二重管理したくない」が実現できる、すごい! マイグレーションスクリプト (自動生成)

    データ型 (モデルクラス) 定義 class User(Base): id = Column(String, primary_key=True) name = Column(String, nullable=False) password_hash = Column(String, nullable=False) アプリケーション マイグレーションスクリプトを モデルクラスから自動生成
  18. SQLAlchemy の難点 なんだか FastAPI との組み合わせは相性が悪いような気が…? 非同期処理まわり Pydantic モデルとの混在 前提 FastAPI

    (※1) では、IO バウンドな処理は async/await で行うことが好ましい (※2) ※1 エンドポイント (Path Operation) を async def で書いている場合 ※2 詳しい話はたぶん このセッションで聞ける → 前提 FastAPI は Pydantic に頼ることで手厚い型サポートを実現している 難点 asyncio 対応した SQLAlchemy は DB アクセスが非同期処理になるため await 必要 → 当然、Lazy Loading の際にも await が必要 だが、 EagerLoad / Preload のときは await 不要で紛らわしい → プロパティにアクセスするように関連データを取得できる良さが損なわれているのでは? 難点 Pydantic モデルと SQLAlchemy モデルが混在していると非常にややこしい !!! この問題を解決せんとする SQLModel というライブラリもあるが、うまく使いこなせなかった ※2 詳しい話はたぶん このセッションで聞ける →
  19. Prisma ORM のしくみ – セットアップ時 Prisma Engine という存在が中央にいてデータモデルのすべてを把握しており、これが データベースに対してマイグレーションをしたりアプリケーションが利用するコードを自動生成したりする Prisma

    Engine Prisma Schema RDB アプリケーション マイグレーションファイルを 生成して適用 クライアント + モデルクラス (自動生成) 使用する言語用の クライアントを自動生成 モデルクラスの方が自動生成される (SQLAlchemy + Alembic とは逆) 開発者は Prisma Schema ファイルに 独自文法でデータ構造を記述する マイグレーションファイルも自動生成なので 開発者は Prisma Schema ファイルだけを 面倒見ていればいい (とても楽) データモデルの すべてを知っている存在
  20. Prisma ORM のしくみ – アプリケーション稼働時 アプリケーションからクエリを実行する際も Prisma Engine を経由する Prisma

    Engine Prisma Schema RDB アプリケーション Prisma Engine が 組み立てた SQL を実行 クライアント + モデルクラス (自動生成) クエリ実行リクエスト (HTTP API) データモデルの すべてを知っている存在 「コネクションプールの管理」や 「SQL を組み立てて実行」も Prisma Engine が担当する Prisma Engine はアプリケーションとは 別のプロセスとして動いている (※ binary モード)
  21. Prisma Client Python Prisma Client Python を使うと Prisma で定義したモデルを Pydantic

    型に変換できるし、 Python アプリケーションから Prisma Engine を通して RDB にアクセスすることができる Prisma Engine Prisma Schema RDB Python 製アプリ クライアント + モデルクラス (自動生成) Prisma Client Python で Python 用クライアントを 自動生成 Pydantic のモデルクラスが生成される!!! (とてもうれしい)
  22. モデルクラスが Pydantic だと何がうれしい? ORM のモデルクラスが Pydantic だと、FastAPI のリクエスト/レスポンスの型指定に流用できる ORM (Prisma)

    のモデルクラスを FastAPI の型にも指定できる! ORM で取得した結果を変換せずに そのまま返すことができる! あれ、でも password_hash が含まれるから 丸ごと返してしまうと良くないのでは…?
  23. 余談: PartialTypes が便利 PartialTypes という機能を使うと、「ORM のモデルクラスをちょっとだけ変えたクラスがほしい」が 簡単に実現できる 「password_hash フィールドだけ除外した User

    クラス」 を簡単に作ることができる 「特定のフィールドを除外しつつ、DB から取得したデータを そのまま API のレスポンスとして返す」が実現できた!
  24. Prisma Client Python の難点 • Prisma 本体は人気だが、Prisma Client Python の方は開発がそこまで活発ではなさそう

    • 「困ったら自分が Contribute するんじゃい!」の気概でいこう • JSON 型を扱おうとすると苦しむ • 気になる人は Qiita にまとめたので読んで ↓ Prisma Client Python で JSON 型を扱うときの Dirty Hack https://qiita.com/hoto17296/items/a4a9488cd820c84c0e6a 少なくとも FastAPI との組み合わせにおいてはそれほど困ることはないが、強いて難点を挙げるなら以下
  25. まとめ • 中規模以上のアプリケーション開発をするにあたっては ORM から逃れることはできない • 無慈悲なインピーダンスミスマッチが我々を襲う • NoSQL を採用しても別の苦しみがある、多くの場合で

    RDB は必要とされるであろう • O と R のデータモデルを相互変換するために ORM に頼ったほうが良い • ORM の本質的な役割のほかにも、ORM ライブラリがカバーしうる周辺領域がさまざまある • 型サポートは手厚くして欲しい • DB マイグレーションは ORM ライブラリがまとめて面倒をみることによるメリットが大きい • 「かっこいいインタフェース」「ドメインロジックとの融合」は必須ではないと思う、好みではないか • 上記の期待を満たす Python ライブラリの選択肢としては2パターンが検討できそう • SQLAlchemy + Alembic は 定番, 多機能 • Prisma Client Python は Pydantic が扱えるので FastAPI との相性が良い Python でアプリケーション開発をするにあたって ORM というやつについて思いを馳せていたら このような考えに行きついた
  26. Yuki Ishikawa / @hoto17296 これまでやってきた活動など • PyCon Kyushu in Okinawa

    2019 コアスタッフ & 登壇 • ISUCON 10 & 11 Python 実装担当 • PyCon JP 2021 登壇 「Python をフル活用した工場への AI 導入 & データ活用基盤構築事例」 • PyData Okinawa 運営 (最近活動できていない) • Snowflake CENTRAL User Group 運営 (来月イベントやるよ @名古屋) 気軽に話しかけてね!! • このトークに対する質問とか反論とかお待ちしています • 今日明日はずっと会場内にいるし今日の After Party にもいます • Twitter / X でリプを飛ばしてくれてもいいです ちゅらデータ株式会社 (今年はシルバースポンサー) Web アプリケーションエンジニア