RustなORMとして有名なのはDieselですが、もう少し手軽に使えるものがないかなと思っていました。
生のクエリは書きたくないけど、手軽には使い始めたい。
そんなあなたにtqlです。
READMEに記載されている例になりますが、以下のような形でデータベースにアクセスできます。
fn main() {
let _ = sql!(Model.create());
let text = String::new();
let id = sql!(Model.insert(text = &text, date_added = Utc::now())).unwrap();
let result = sql!(Model.get(id).update(text = "new-text"));
let result = sql!(Model.get(id).delete());
let items = sql!(Model.sort(-date_added)[..10]);
}
なかなか良さげです。
ただし現時点ではPostgreSQLしかサポートしていないので、SQLiteやMySQLでは使えません。
SQLiteサポートについてはGitHub Issueにあがっているので、近いうちに使えるようになるのではないかと淡い期待を寄せています。今の所は、SQLiteであればrusqlite、MySQLであればrust-mysql-simpleあたりをつかうのが良い気がします。
TQLを使ってみる
さっそく使ってみます。
特に記載がなければnightlyチャネル前提です。
まずはcreatedb
コマンドでデータベース作っておきます。
tql_exampleという名前にします。(特に制約はありません)
$ createdb tql_example
$ psql -l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------------+-----------+----------+-------------+-------------+-----------------------------
tql_example | hogera | UTF8 | ja_JP.UTF-8 | ja_JP.UTF-8 |
ここからRustでアクセスしていきます。
TQLはcrates.ioに登録されているバージョンが古いので、GitHubリポジトリの最新版を使います。
[dependencies]
chrono = "^0.4.0"
tql = { git = "https://github.com/antoyo/tql" }
tql_macros = { git = "https://github.com/antoyo/tql" }
[dependencies.postgres]
features = ["with-chrono"]
version = "^0.15.1"
chronoは時刻データ型をいい感じに扱えるので、一緒にCargo.toml
に追加しておくと良いです。
あとは必要なクレイトを読み込んでおきます。
#![feature(proc_macro)]
extern crate chrono;
extern crate postgres;
extern crate tql;
#[macro_use]
extern crate tql_macros;
use postgres::{Connection, TlsMode};
use tql::PrimaryKey;
use tql_macros::{sql, to_sql};
use chrono::DateTime;
use chrono::offset::Utc;
スキーマ定義とテーブル作成
以下のようなPersonテーブルを定義することにします。
カラム名 | 型 | その他 |
---|---|---|
id | integer | プライマリキー |
name | varchar | not null |
age | integer | nullable |
created | timestamp | not null |
Personテーブルにアクセスするために、Person
構造体をつくります。
#[derive(SqlTable)]
struct Person {
id: PrimaryKey,
name: String,
age: Option<i32>,
created: DateTime<Utc>,
}
データベース接続する関数。
fn get_connection() -> Connection {
Connection::connect("postgres://user:password@hostname/tql_example", TlsMode::None).unwrap()
}
テーブルを作ってみます。
ここはイマイチなところなのですが、get_connection()
で取得するデータベースコネクションオブジェクトは、今の所必ずconnection
変数にする必要があります。
クエリ実行するsql!()
マクロに接続オブジェクトを渡すことができず固定で処理しているようです。
TODO
になってるのでそのうち変わりそうな気がします。
fn main() {
let connection = get_connection();
let ret = sql!(Person.create());
println!("ret={:?}", ret);
}
実行
$ cargo +nightly run --release
ret=Ok(0)
$ psql tql_example -c "\d person"
Table "public.person"
Column | Type | Modifiers
---------+--------------------------+-----------------------------------------------------
age | integer |
created | timestamp with time zone | not null
id | integer | not null default nextval('person_id_seq'::regclass)
name | character varying | not null
Indexes:
"person_pkey" PRIMARY KEY, btree (id)
person
テーブルが作成できました。
INSERT
fn main() {
let connection = get_connection();
for (name, age) in vec!(("たろう", Some(20)), ("はなこ", Some(30)), ("さとし", None),
("せつこ", Some(50)), ("むが", Some(60))) {
let name = String::from(name);
let id = match age {
Some(v) => sql!(Person.insert(name=&name, age=v, created=Utc::now())).expect("fail insert(Person)"),
None => sql!(Person.insert(name=&name, created=Utc::now())).expect("fail insert(Person)"),
};
println!("id={:?}", id);
}
}
実行してみます。
$ cargo +nightly run --release
id=1
id=2
id=3
id=4
id=5
データ挿入した時のidを返します。psql
コマンドでデータ挿入されたかどうか確認します。
$ psql tql_example -c "select * from person"
age | created | id | name
-----+-------------------------------+----+--------
20 | 2017-12-21 19:35:32.29468+09 | 1 | たろう
30 | 2017-12-21 19:35:32.297931+09 | 2 | はなこ
| 2017-12-21 19:35:32.299607+09 | 3 | さとし
50 | 2017-12-21 19:35:32.301274+09 | 4 | せつこ
60 | 2017-12-21 19:35:32.302847+09 | 5 | むが
おっけーそうです。
SELECT
おもむろにデータ取得してみます。
fn main() {
let connection = get_connection();
for row in sql!(Person[2..5]) {
println!("row={:?}", row);
}
$ cargo +nightly run --release
row=Person { id: 3, name: "さとし", age: None, created: 2017-12-21T10:35:32.299607Z }
row=Person { id: 4, name: "せつこ", age: Some(50), created: 2017-12-21T10:35:32.301274Z }
row=Person { id: 5, name: "むが", age: Some(60), created: 2017-12-21T10:35:32.302847Z }
どんなクエリ発行してるか見たい場合は、to_sql!()
マクロで構築されたSQLクエリを返してくれるので、それを出力することで確認することができます。
fn main() {
let connection = get_connection();
let query = to_sql!(Person[2..5]);
println!("query='{}'", query);
}
$ cargo +nightly run --release
query='SELECT Person.age, Person.created, Person.id, Person.name FROM Person OFFSET 2 LIMIT 3'
その他sql!()
マクロで使える構文
このあたりに一通りまとまっています。
stableチャネルで使う
どうしてもstableチャネルで使いたい場合は、以下の変更で使うことができます。
extern crate tql
に#[macro_use]
を追加。
#[macro_use]
extern crate tql;
以下の行を削除。
#![feature(proc_macro)]
use tql_macros::sql;
Cargo.toml
の依存関係のtqlの箇所に
default-features=false`を追加
tql = { git = "https://github.com/antoyo/tql", default-features = false }
stableとnightlyでエラーメッセージが以下のように異なります。
error: mismatched types:
expected `Option<i32>`,
found `floating-point variable`
note: in this expansion of sql! (defined in tql)
--> src/main.rs:32:14
|
32 | let id = sql!(Person.insert(name=&name, age=30.1, created=Utc::now())).expect("fail insert(Person)");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro outside of the current crate
error: mismatched types:
expected `Option<i32>`,
found `floating-point variable`
--> src/main.rs:35:49
|
35 | let id = sql!(Person.insert(name=&name, age=30.1, created=Utc::now())).expect("fail insert(Person)");
| ^^^^
|
= note: in this expansion of sql! (defined in tql)
nightlyチャネルの場合、ピンポイントで誤りのある箇所を指摘してくれます。よいですね。
おわりに
わりとREADMEなぞっただけの内容になってしまいましたが、できることは一通り紹介してみました。
まだまだTODO等残っておりAPI変わりそうな感じもしますが、一度試してみてもよいのではないでしょうか。