- 「Rustで安全にデータを永続化し、複雑なクエリを簡単に書けて、しかもSQLを1行も書かずに済むとしたら?」
- Rust-query はそれを実現するために開発されたライブラリ
Rustとデータベース
- Rustの既存のデータベースライブラリは、コンパイル時の保証が不十分だったり、使い勝手が悪く、SQLのように直感的でもない
- データベースは、競合を防ぐソフトウェアの構築や、アトミックなトランザクションのサポートにおいて重要な役割を果たす
- SQLはデータベースとやり取りするための標準プロトコルだが、コンピュータが生成するのに適しており、人間が直接書くには非効率
Rust-query 紹介
- rust-query は、Rustの型システムと深く統合されたデータベースクエリライブラリ
- Rustでネイティブな感覚のままデータベース操作を行えるよう設計されている
主な機能と設計上の決定
- 明示的なテーブル別名: テーブル結合後、そのテーブルを表すダミーオブジェクトを提供 (
let user = User::join(rows);)
- Null安全性: クエリ内の任意値は Rust の
Option 型で扱う
- 直感的な集約関数:
GROUP BY なしで行単位の直感的な集約をサポート
- 型安全な外部キー探索: 外部キーに基づく暗黙的な結合を簡単に実行 (
track.album().artist().name())
- 型安全な一意検索: 特定の一意制約を持つ行を取得(
Option<Rating> を返す)
- マルチバージョンスキーマ: 宣言的な方法ですべてのスキーマバージョン差分を確認可能
- 型安全なマイグレーション: 任意の Rust コードを使って行を処理可能
- 型安全な一意衝突処理: 一意制約の衝突時に特定のエラー型を返す
- トランザクションのライフタイムに結び付いた行参照: 行参照はその行が存在する間だけ有効
- カプセル化された型付き行 ID: 行番号は API の外部に公開されない
クエリとデータ挿入
スキーマ定義
#[schema]
enum Schema {
User {
name: String,
},
Story {
author: User,
title: String,
content: String,
},
#[unique(user, story)]
Rating {
user: User,
story: Story,
stars: i64,
},
}
use v0::*;
- Rust の
enum 構文を使ってスキーマを定義
- 外部キー制約は、他テーブル名をカラム型として指定することで作成
#[unique] 属性を使って一意制約を追加
#[schema] マクロは定義を解析して v0 モジュールを生成
データ挿入
fn insert_data(txn: &mut TransactionMut<Schema>) {
let alice = txn.insert(User { name: "alice" });
let bob = txn.insert(User { name: "bob" });
let dream = txn.insert(Story {
author: alice,
title: "My crazy dream",
content: "A dinosaur and a bird...",
});
let rating = txn.try_insert(Rating {
user: bob,
story: dream,
stars: 5,
}).expect("no rating for this user and story exists yet");
}
- 挿入操作は、新たに挿入された行への参照を返す
- 一意制約を持つテーブルへの挿入では
try_insert の使用が必要
try_insert は衝突時に特定のエラー型を返す
データクエリ
fn query_data(txn: &Transaction<Schema>) {
let results = txn.query(|rows| {
let story = Story::join(rows);
let avg_rating = aggregate(|rows| {
let rating = Rating::join(rows);
rows.filter_on(rating.story(), &story);
rows.avg(rating.stars().as_float())
});
rows.into_vec((story.title(), avg_rating))
});
for (title, avg_rating) in results {
println!("story '{title}' has avg rating {avg_rating:?}");
}
}
rows はクエリ内の現在の行集合を表す
aggregate を使って集約演算を実行
- 結果はタプルや構造体のベクタとして収集できる
スキーマ進化とマイグレーション
- 新しいスキーマバージョンを作成する際は
#[version] 属性を使う
新しいスキーマバージョンの追加
#[schema]
#[version(0..=1)]
enum Schema {
User {
name: String,
#[version(1..)]
email: String,
},
// ... 残りのスキーマ ...
}
use v1::*;
データマイグレーション
- マイグレーションは旧スキーマと新スキーマの両方に対して型検査される
- 行データは任意の Rust コードで処理可能(
map_dummy を使用)
let m = m.migrate(v1::update::Schema {
user: Box::new(|old_user| {
Alter::new(v1::update::UserMigration {
email: old_user
.name()
.map_dummy(|name| format!("{name}@example.com")),
})
}),
});
まとめ
- rust-query は、Rust でリレーショナルデータベースとやり取りする新しいアプローチを提示している:
- コンパイル時検査
- Rust と合成可能なクエリ
- 型検査によるスキーマ進化のサポート
- 現在は SQLite を唯一のバックエンドとしており、実験的なアプリケーション開発に適している
- GitHub Issues を通じたフィードバックを歓迎
2件のコメント
| コンピュータが生成するのに適しており、人が直接書くには非効率です。
開発者が100人以上投入される韓国にしかない「次世代」をやってみる立場としては。
とても興味深いですね。
実際、投入される開発者の大半はSQLの専門家?たちなんですよね。
Hacker Newsの意見
アプリケーション定義のスキーマに対する懸念は、それが誤ったシステムによって検証される点にある。データベースこそがスキーマの権威であり、他のすべてのアプリケーション層はそれを前提にしている。RustのSQLxはデータベースの型に基づいて構造体を生成し、コンパイル時に検証するが、本番データベースと同一の型を保証するわけではない。ローカルのPostgres v15でクエリを設計し、本番でPostgres v12を動かしている場合、実行時エラーが発生する可能性がある。アプリケーション定義のスキーマは誤った安心感を与え、エンジニアに余計な作業を課す。
SQLは完璧ではないが、いくつかの利点がある。ほとんどの人は基本的なSQLを知っており、PostgreSQLのようなデータベースのドキュメントもSQLで書かれている。外部ツールもSQLを使い、クエリ変更のたびに高コストなコンパイル工程を必要としない。SQLxはパラメータを型チェックし、データベース自身にクエリを検証させることで、コンパイル時間を増加させる型システムの問題を回避する。新しいデータベースではより良いクエリ言語が勝つかもしれないが、既存のSQLデータベースではSQLxの方がより良い選択だ。
SQLはコンピュータが書くべきだという意見に反対する声がある。SQLは高水準言語であり、PythonやRustよりもさらに高い水準にある。SQLは読みやすく使いやすいよう設計されており、コンパイル時に複数の手続きへと変換される。SQLはWeb開発のボトルネック地点にあり、状態変化が発生する場所でもある。SQLは高水準言語であるがゆえに最適化が難しい。SQLは技術的負債だが、より適切なAPIを開発するよりもSQLを使う方が10倍効率的だ。
Rustにおけるtypesafe-db-accessの探求がうれしいという意見がある。既存のライブラリはコンパイル時保証を提供せず、SQLのように冗長だったり不格好だったりする。dieselはコンパイル時保証を提供する。ORM対非ORMの議論では、typesafeなクエリビルダーを好んでおり、dieselはこのカテゴリに入る。Rust-queryは完全なORM寄りになりそうだ。
スキーマとデータ型を結び付けるアプローチは興味深いという意見がある。例ではSchema列挙型がない点が直感的ではない。マクロ内で定義されていれば、もっと明確だっただろう。
ライブラリAPIで実際の行番号が公開されていない点が混乱を招く。Webサーバーではデータに行IDを渡して、フロントエンドが別のリクエストでそのデータを参照・変更できる必要がある。
SQLはコンピュータが書くべきだという意見には部分的に同意するが、SQLはコードジェネレータが書くのに最も都合の良い言語ではない。単純なプラン最適化でもクエリのレイアウトが完全に変わることがある。GoogleのSQL pipe提案はいくらか改善されているが、新しいクエリ言語が抱える問題を依然として持っている。
SeaQueryを使ってきたが、高度なクエリを生成するにはドキュメントが十分ではないという意見がある。強い型付けのクエリは開発プロセスを遅くする可能性があり、従来のprepared statementと値バインディングに戻ることを検討している。
個々の行レベルの操作によるマイグレーションは、実行速度が非常に遅くなる可能性がある。たとえば、10億行あるテーブルで一般的なUPDATE文が1時間かかることもある。行ごとの更新はさらに時間がかかるだろう。