モデルの二重定義にサヨナラ!Pydanticネイティブ×Rust製の爆速非同期ORM「Oxyde」が登場
HNの皆さん、こんにちは!モデルを何度も定義する作業にうんざりして、Oxyde(オクサイド)を作りました。
FastAPIを使っている方なら「あるある」ですよね。API用にPydanticモデルを定義し、データベース用に別のORMモデルを定義して、さらにその間の変換処理を書く……。SQLModelはこの解決を試みていますが、中身はSQLAlchemyのままです。TortoiseはDjango風の素敵なAPIを提供してくれますが、独自のモデルシステムを持っています。Django ORMは素晴らしいですが、フレームワークと密結合すぎます。
私が欲しかったのはシンプルさです。「Pydanticモデルそのものがデータベースモデルになる」ということ。1つのクラスで、入出力の完全なバリデーション、ネイティブな型ヒント、そして重複ゼロを実現しました。クエリAPIは、最高に洗練されたデザインの一つであるDjangoスタイル(.objects.filter()、.exclude()、Q/F式など)を採用しています。
**「暗示的より明示的であること」**
あらゆる「魔法」を排除するよう努めました。クエリは、.all()や.get()、.first()といった終端メソッドを呼び出すまでデータベースにアクセスしません。明示的に.join()や.prefetch()を呼ばない限り、関連データはロードされません。遅延ロード(Lazy Loading)も、裏で勝手に発生する「N+1問題」もありません。コードを読めば、何がデータベースにヒットするか正確に分かります。
**型安全性へのこだわり**
Pythonの弱点は実行時のサプライズ(予期せぬ挙動)です。Oxydeは3つのレベルでこれに対処します。(1) makemigrations実行時に、完全に型定義されたクエリ用の .pyi スタブファイルを自動生成します。これにより、IDEは filter(age__gte=...) にintが必要であることや、.all() が list[Any] ではなく list[User] を返すことを認識します。(2) PydanticがDBへの投入データを検証します。(3) model_validate() を通じてDBからの戻り値も検証します。これらすべてが、たった一つのモデル定義から得られるのです。
**なぜRustなのか?**
目的は単なる「速度」ではありません。言語の優劣を競うつもりもありません。ビジネスロジックの表現において、Pythonの右に出るものはないでしょう。しかし、SQL生成、接続プーリング、行のシリアライズといったインフラ部分は、システム言語が本領を発揮する場所です。そこで役割を分担しました。Pythonはモデルとビジネスロジックを担当し、Rustはデータベースの配管部分(plumbing)を担当します。クエリはPython側で中間表現(IR)として構築され、MessagePackでシリアライズしてRustに送られ、そこで方言に応じたSQLの生成・実行が行われ、結果がストリーミングで返されます。速度はその副産物ですが、利便性のためにパフォーマンスを犠牲にする必要はありません。興味があればベンチマークも見てみてください:https://oxyde.fatalyst.dev/latest/advanced/benchmarks/
**現在の機能:**
Djangoスタイルのマイグレーション、セーブポイント付きトランザクション、JOINとPrefetch、PostgreSQL/SQLite/MySQL対応、FastAPI統合、そしてFastAPI/Litestar/Sanic/Quart/Falconで動作する自動生成アドミンパネル(https://github.com/mr-fatalyst/oxyde-admin)など。
現在はv0.5のベータ版で、活発に開発中です。APIはまだ変更される可能性があります。これは、私が個人的にずっと使いたかったORMを作ろうという試みです。フィードバックや批判、アイデアをぜひお待ちしています!