Slide 1

Slide 1 text

ORM と向き合う 2024.09.27 PyCon JP 2024

Slide 2

Slide 2 text

今日のお話 → 全編を通して「理論というよりは、経験に基づく主観」が強い感じでお送りします 「それってあなたの感想ですよね?」と言われたら「はい…」としか言えない

Slide 3

Slide 3 text

AGENDA • ORM がなぜ必要か • ORM に期待すること • Python における選択肢

Slide 4

Slide 4 text

ORM がなぜ必要か

Slide 5

Slide 5 text

ORM とは? Object-Relational Mapping = オブジェクトとリレーショナルをマッピングするやつ アプリケーションから データベースを触るときに なんか使うやつ (という認識)

Slide 6

Slide 6 text

Python で利用できる ORM ライブラリ けっこうたくさんある

Slide 7

Slide 7 text

ORM のむずかしさ • ORM をうまく扱いきれなくて、無理やり制御するために Dirty Hack っぽいコードが生まれてしまった • 複雑な問い合わせをする方法が ORM でうまく表現できなくて結局 SQL を手で書いてしまった • ORM が提供するモデルクラスにドメインロジックを適当にどんどん書いていたらコードの見通しが悪くなってしまった → ORM に対してなんとなく苦手意識があった 漠然と使っていたらアプリケーションがよくわからないことになってしまったこと、ない?

Slide 8

Slide 8 text

ていうか ORM って必要?

Slide 9

Slide 9 text

SQL だけで何でもできる ORM も裏では SQL を組み立てて実行しているだけなのだから、素朴に SQL だけ書いて アプリケーション開発をしていればいいのではないか? psycopg2 を使用して Python から PostgreSQL に SQL を投げる例

Slide 10

Slide 10 text

でもやっぱり生 SQL でのアプリケーション開発はしんどい 生 SQL でのアプリケーション開発は、規模が大きくなるにつれて以下のような点での開発効率の悪さが 顕著に現れてきてしんどくなってくる ① SQL を書く際に IDE やツールチェインが 提供する便利機能の恩恵を受けることが難しい (入力補完 / 型チェック / 自動フォーマット / etc…) ② 取得したデータの型が自動で得られない (いちいち手で書きたくもない) ③ 関連テーブルのデータを扱う処理を 書くのがしんどい (→ 後述)

Slide 11

Slide 11 text

関連データを取得する戦略: こういう場合どうする?? 例えばブログサービスを開発しているとして、テーブルに対して以下のような操作をしたくなる 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件の投稿とそれらについたコメントの一覧」を 右のようなデータ構造として取得してアプリケーションで扱いたい

Slide 12

Slide 12 text

関連データを取得する戦略 (その1) 先に Article をとってきて、各 Article に対して Comment 一覧を取得する 最も素朴なアプローチで、直感的でわかりやすい いわゆる「N+1 問題」でパフォーマンスが悪い 先に article だけ取得 for ループで 各 article に紐づく comment を取得

Slide 13

Slide 13 text

関連データを取得する戦略 (その2) JOIN するクエリを投げて、結果をアプリケーション側で変換する パフォーマンスは悪くない かたちを整えるコードを手で書くとあまりに非効率 「関連するデータを取得したい」だけなのに何故こんな苦行を? ※ SQLAlchemy でいう Joined Loading ※ Ruby / ActiveRecord でいう Eager Load 1回ですべてを取得する クエリを実行 実行結果を 欲しいかたちに 整える

Slide 14

Slide 14 text

関連データを取得する戦略 (その3) Articles 一覧と Comment 一覧を別で取得してきてからアプリケーション側で結合する (その2) よりかはだいぶマシ JOIN するたびに手でこれを書くのはまだまだ手間 ※ SQLAlchemy でいう Select IN Loading ※ Ruby / ActiveRecord でいう Preload 先に article だけ取得 WHERE IN で 必要な comment を まとめて取得

Slide 15

Slide 15 text

どのやり方にも難点がある

Slide 16

Slide 16 text

関連データ取得 (JOIN) は 手で SQL を組み立てていたら やってられない

Slide 17

Slide 17 text

そのつらさはデータモデルの違いからくる 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 リレーショナルデータモデル テーブルとリレーションによって データの関係性を表現 オブジェクトデータモデル オブジェクトが別のオブジェクトを内包する ことでデータの関係性を表現 🫠 つらい

Slide 18

Slide 18 text

余談:「インピーダンスミスマッチ」という言葉 ここまでで説明した “つらさ” は俗に (?) 「インピーダンスミスマッチ」と呼ばれる いったい誰がそう呼びはじめたのかは知らないが もともとの意味 電気電子分野において「信号を伝送する際に出力と入力のインピーダンスが一致していないこと」を意味する言葉で、 伝送効率が悪化する要因となる (らしい) ソフトウェア開発における意味 おおむね「異なるシステムの境界に何らかのギャップがあり、それに対処するためのプログラミングが非効率であること」を意味する 多くの場合は「RDB」と「アプリケーション」間のギャップ (データモデルの違い) と、その対処の非効率さという文脈で使われる …が、「システム境界で発生するギャップ」は様々あるため、その他の用例もある (その他の例: 「API 仕様とそのユースケースの乖離」など)

Slide 19

Slide 19 text

データモデル間の変換が つらいのはわかった

Slide 20

Slide 20 text

そもそも、 データモデルって 2種類もいる?

Slide 21

Slide 21 text

我々はなぜ RDB を使うか なぜ苦しい思いをしてまで永続化レイヤに RDB を採用するのか? オブジェクトデータモデルのまま永続化できればいいのではないか? (→ NoSQL) アプリケーションの永続化レイヤとして採用されるような NoSQL データベース製品にもいくつか選択肢がある

Slide 22

Slide 22 text

オブジェクトデータモデルを永続化する場合の課題 オブジェクトデータモデルは「特定のユースケースのためのデータの断面」であって、 そのまま永続化するとデータのユースケースが極めて限定されてしまう 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 あるユーザの投稿一覧と 各投稿についたコメント一覧を 画面に表示したい オブジェクトデータモデル間の変換は パフォーマンスが悪くて現実的でない

Slide 23

Slide 23 text

ここまでの まとめ

Slide 24

Slide 24 text

ここまでのまとめ なぜなら • 永続化レイヤでリレーショナルデータモデルを採用することに合理性がある • 一方で、アプリケーションはオブジェクトデータモデル (ユースケースのための断面) を扱いたい • 異なるデータモデル間の相互変換には本質的なつらさ (インピーダンスミスマッチ) が発生し、 その解決に ORM が必要となる 我々は中規模以上のアプリケーション開発をするにあたっては ORM から逃れることはできないのだ

Slide 25

Slide 25 text

ORM に期待すること

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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箇所に書いていて嫌

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Python における選択肢

Slide 35

Slide 35 text

前提 (余談) FastAPI すきポイント • 「HTTP エンドポイントは関数」という思想 • ASGI / asyncio サポート • シンプルでわかりやすい DI • Pydantic に頼った強力な型サポートとバリデーション • デフォルトで OpenAPI Spec を吐き出せる 当方「FastAPI めっちゃすき」派 そういうバイアスがかかっていることは否めない

Slide 36

Slide 36 text

SQLAlchemy + Alembic

Slide 37

Slide 37 text

SQLAlchemy 言わずと知れた著名 ORM ライブラリ 「活発にメンテナンスされている」「多機能」といった点が魅力 Special Relationship Persistence Patterns — SQLAlchemy 2.0 Documentation https://docs.sqlalchemy.org/en/20/orm/relationship_persistence.html

Slide 38

Slide 38 text

SQLAlchemy + Alembic Alembic は SQLAlchemy のモデルクラスからマイグレーションスクリプト自動生成するツールなので、 前述の「二重管理したくない」が実現できる、すごい! マイグレーションスクリプト (自動生成) データ型 (モデルクラス) 定義 class User(Base): id = Column(String, primary_key=True) name = Column(String, nullable=False) password_hash = Column(String, nullable=False) アプリケーション マイグレーションスクリプトを モデルクラスから自動生成

Slide 39

Slide 39 text

いや、さすがに定番すぎる 30分も話してそのオチはどうなの???

Slide 40

Slide 40 text

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 詳しい話はたぶん このセッションで聞ける →

Slide 41

Slide 41 text

もう1パターンご紹介

Slide 42

Slide 42 text

Prisma ORM

Slide 43

Slide 43 text

いや、それは TypeScript 用の ORM なのでは???

Slide 44

Slide 44 text

それはいったん置いておいて 先に Prisma の仕組みを説明

Slide 45

Slide 45 text

Prisma ORM のしくみ – セットアップ時 Prisma Engine という存在が中央にいてデータモデルのすべてを把握しており、これが データベースに対してマイグレーションをしたりアプリケーションが利用するコードを自動生成したりする Prisma Engine Prisma Schema RDB アプリケーション マイグレーションファイルを 生成して適用 クライアント + モデルクラス (自動生成) 使用する言語用の クライアントを自動生成 モデルクラスの方が自動生成される (SQLAlchemy + Alembic とは逆) 開発者は Prisma Schema ファイルに 独自文法でデータ構造を記述する マイグレーションファイルも自動生成なので 開発者は Prisma Schema ファイルだけを 面倒見ていればいい (とても楽) データモデルの すべてを知っている存在

Slide 46

Slide 46 text

補足: Prisma Schema こんな感じ 独自文法だけど初見でもなんとなく読める というかむしろ Python のモデルクラスよりわかりやすいかも シンタックスハイライタ & フォーマッタ もあるので書きやすい

Slide 47

Slide 47 text

Prisma ORM のしくみ – アプリケーション稼働時 アプリケーションからクエリを実行する際も Prisma Engine を経由する Prisma Engine Prisma Schema RDB アプリケーション Prisma Engine が 組み立てた SQL を実行 クライアント + モデルクラス (自動生成) クエリ実行リクエスト (HTTP API) データモデルの すべてを知っている存在 「コネクションプールの管理」や 「SQL を組み立てて実行」も Prisma Engine が担当する Prisma Engine はアプリケーションとは 別のプロセスとして動いている (※ binary モード)

Slide 48

Slide 48 text

気づいた?

Slide 49

Slide 49 text

クライアントコードさえ生成できれば Prisma を利用する側の プログラミング言語はなんでもいい

Slide 50

Slide 50 text

さまざまな言語に対応している Prisma 公式でサポートされているのは JavaScript / TypeScript だが、コミュニティによって開発されている クライアントジェネレータを使うことで他のプログラミング言語でも対応できる

Slide 51

Slide 51 text

Prisma Client Python Prisma Client Python を使うと Prisma で定義したモデルを Pydantic 型に変換できるし、 Python アプリケーションから Prisma Engine を通して RDB にアクセスすることができる Prisma Engine Prisma Schema RDB Python 製アプリ クライアント + モデルクラス (自動生成) Prisma Client Python で Python 用クライアントを 自動生成 Pydantic のモデルクラスが生成される!!! (とてもうれしい)

Slide 52

Slide 52 text

モデルクラスが Pydantic だと何がうれしい? ORM のモデルクラスが Pydantic だと、FastAPI のリクエスト/レスポンスの型指定に流用できる ORM (Prisma) のモデルクラスを FastAPI の型にも指定できる! ORM で取得した結果を変換せずに そのまま返すことができる! あれ、でも password_hash が含まれるから 丸ごと返してしまうと良くないのでは…?

Slide 53

Slide 53 text

余談: PartialTypes が便利 PartialTypes という機能を使うと、「ORM のモデルクラスをちょっとだけ変えたクラスがほしい」が 簡単に実現できる 「password_hash フィールドだけ除外した User クラス」 を簡単に作ることができる 「特定のフィールドを除外しつつ、DB から取得したデータを そのまま API のレスポンスとして返す」が実現できた!

Slide 54

Slide 54 text

Prisma Client Python の難点 • Prisma 本体は人気だが、Prisma Client Python の方は開発がそこまで活発ではなさそう • 「困ったら自分が Contribute するんじゃい!」の気概でいこう • JSON 型を扱おうとすると苦しむ • 気になる人は Qiita にまとめたので読んで ↓ Prisma Client Python で JSON 型を扱うときの Dirty Hack https://qiita.com/hoto17296/items/a4a9488cd820c84c0e6a 少なくとも FastAPI との組み合わせにおいてはそれほど困ることはないが、強いて難点を挙げるなら以下

Slide 55

Slide 55 text

まとめ

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

自己紹介

Slide 58

Slide 58 text

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 アプリケーションエンジニア