Rust | SQLx の macro `query!` と `query_as!` を使いこなす
SQLx クレート
Rust のデーベース接続のライブラリである SQLx を使っていきます🧰
SQLx の特徴
SQLx の特徴の 1 つに ORM ではないこと が挙げられます。
この ORM ではないこと が私のお気に入りの理由です。
複雑な設計やサービスになってくればなるほど、生の SQL を書く機会が増えるので、
それなら最初から書きたいと思ってしまうのです...
あと、単純にマッパーで実装するのが好きじゃない...笑
(ここは賛否両論あるでしょう🙏)
Macro か Function か
SQLx にはクエリを記述する方法がいくつかあり、
よく使用するものとして、query と query_as があります。
query_as では返り値の型を指定できますが、query ではできません。
そして、それぞれマクロ版と関数版があります。
マクロの場合、コンパイル時にフィールドの有無や型のチェックがおこなわれるので、
個人的にはマクロを使用するのがオススメしたいです。
Macro
Function
query! か query_as! か
以下のような基準で使い分けるのが、良いのかなと思っています。
- 取得(SELECT)系 →
query_as!
- 更新(INSERT、UPDATE、DELETE)系 →
query!
- ただし、RETURNING 句を使う場合は
query_as!
がオススメ
- ただし、RETURNING 句を使う場合は
query! と query_as! の実装サンプル
データベースの準備
テーブルとサンプルデータは以下のクエリで作成しました。
create table if not exists mountains (
id serial primary key,
"name" varchar not null,
created_at timestamp not null default now(),
updated_at timestamp not null default now()
);
insert into
mountains ("name")
values
('富士山'),
('高尾山'),
('大雪山');
クレートと .env の準備
非同期処理ランタイムには tokio を使います。
cargo add sqlx --features "postgres runtime-tokio-native-tls chrono"
cargo add tokio --features=full
cargo add dotenv
データベースの接続 URL を定義しておきます。
DATABASE_URL=postgres://postgres:postgres@localhost:5432/sample_db
query_as!
query_as!
で、単純な SELECT を実装してみました。
SQL の知識があれば、難なく読み解けるのではないでしょうか。
use sqlx::postgres::PgPoolOptions;
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Pool, Postgres};
use std::env;
#[tokio::main]
async fn main() {
// 環境変数を読み取り、URLを生成
dotenv::dotenv().expect("Failed to read .env file");
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// DBコネクションを生成
let db = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await
.unwrap_or_else(|_| panic!("Cannot connect to the database"));
match get_one(&db, 1).await {
Ok(m) => println!("{:?}", m),
Err(e) => println!("{:?}", e),
}
}
#[derive(Debug)]
struct Mountain {
id: i32,
name: String,
created_at: NaiveDateTime,
updated_at: NaiveDateTime,
}
async fn get_one(db: &Pool<Postgres>, id: i32) -> Result<Mountain, sqlx::Error> {
sqlx::query_as!(
Mountain,
r#"
select
id,
name,
created_at,
updated_at
from
mountains
where
id = $1
"#,
id
)
.fetch_one(db)
.await
}
試しに name
の型を i32
に変更して、コンパイルしてみます。
以下のようなエラーが出ました。
コンパイル時にチェックできる👏✨
error[E0277]: the trait bound `i32: From<std::string::String>` is not satisfied
--> src/main.rs:32:5
|
32 | / sqlx::query_as!(
33 | | Mountain,
34 | | r#"
35 | | select
... |
45 | | id
46 | | )
| |_____^ the trait `From<std::string::String>` is not implemented for `i32`
query_as()
関数版でも実装してみました。
こちらも、単純な SELECT 文です。
SQL の知識があれば、難なく読み解けるのではないでしょう。
引数が多くなると、bind
が増えていきます。
ORM に慣れている方にとっては、こちらの方が馴染みがあるかもしれません。
また、データをマップする構造体に FromRow
を derive する必要があります。
use sqlx::postgres::PgPoolOptions;
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{FromRow, Pool, Postgres};
use std::env;
#[tokio::main]
async fn main() {
// 環境変数を読み取り、URLを生成
dotenv::dotenv().expect("Failed to read .env file");
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// DBコネクションを生成
let db = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await
.unwrap_or_else(|_| panic!("Cannot connect to the database"));
match get_one(&db, 1).await {
Ok(m) => println!("{:?}", m),
Err(e) => println!("{:?}", e),
}
}
// FromRow を追加する必要がある
#[derive(FromRow, Debug)]
struct Mountain {
id: i32,
name: String,
created_at: NaiveDateTime,
updated_at: NaiveDateTime,
}
async fn get_one(db: &Pool<Postgres>, id: i32) -> Result<Mountain, sqlx::Error> {
sqlx::query_as::<_, Mountain>(
r#"
select
id,
name,
created_at,
updated_at
from
mountains
where
id = $1
"#,
)
.bind(id)
.fetch_one(db)
.await
}
試しに name
の型を i32
に変更して、コンパイルしてみます。
今回は実行時に以下のようなエラーが出ました。
ColumnDecode { index: "\"name\"", source: "mismatched types; Rust type `i32` (as SQL type `INT4`) is not compatible with SQL type `VARCHAR`" }
コンパイル時にエラーになる方が親切だなぁと感じます😉😉
query!
query!
で INSERT を実装してみました。
これも、SQLの知識があれば、難なく読み解けるのではないでしょう。
use sqlx::postgres::{PgPoolOptions, PgQueryResult};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Pool, Postgres};
use std::env;
#[tokio::main]
async fn main() {
// 環境変数を読み取り、URLを生成
dotenv::dotenv().expect("Failed to read .env file");
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// DBコネクションを生成
let db = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await
.unwrap_or_else(|_| panic!("Cannot connect to the database"));
match create(&db, "比叡山").await {
Ok(res) => println!("{:?}", res),
Err(e) => println!("{:?}", e),
}
}
#[derive(Debug)]
struct Mountain {
id: i32,
name: String,
created_at: NaiveDateTime,
updated_at: NaiveDateTime,
}
async fn create(db: &Pool<Postgres>, name: &str) -> Result<PgQueryResult, sqlx::Error> {
sqlx::query!(
r#"
insert into
mountains ("name")
values
($1)
"#,
name.to_string()
)
.execute(db) // insert なので fetch_one ではなく execute
.await
}
実行結果は以下は出力されます。
query!
の execute()
では、更新された行の数を結果として取得できます。
PgQueryResult { rows_affected: 1 }
RETURNING 句 と query_as!
INSERT、UPDATE、DELETE の各コマンドで、更新した行データをそのまま取得したい場合に、
RETURNING 句を使うことができます。
その際、query_as!
で実装することで型の指定ができます!
use sqlx::postgres::{PgPoolOptions};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Pool, Postgres};
use std::env;
#[tokio::main]
async fn main() {
// 環境変数を読み取り、URLを生成
dotenv::dotenv().expect("Failed to read .env file");
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
// DBコネクションを生成
let db = PgPoolOptions::new()
.max_connections(10)
.connect(&database_url)
.await
.unwrap_or_else(|_| panic!("Cannot connect to the database"));
match create(&db, "比叡山").await {
Ok(m) => println!("{:?}", m),
Err(e) => println!("{:?}", e),
}
}
#[derive(Debug)]
struct Mountain {
id: i32,
name: String,
created_at: NaiveDateTime,
updated_at: NaiveDateTime,
}
async fn create(db: &Pool<Postgres>, name: &str) -> Result<Mountain, sqlx::Error> {
sqlx::query_as!(
Mountain,
r#"
insert into
mountains ("name")
values
($1)
returning
id,
name,
created_at,
updated_at
"#,
name.to_string()
)
.fetch_one(db)
.await
}
SQLx の実装例
公式のリポジトリに多くの examples があります。
非常に参考になるので、ぜひチェックしてみてください
まとめ
SQLx は扱いやすく、お気に入りのクレートです。
今日はこれだけ覚えて帰ってください🥰
- 取得(SELECT)系 →
query_as!
- 更新(INSERT、UPDATE、DELETE)系 →
query!
- ただし、RETURNING 句を使う場合は
query_as!
がオススメ
- ただし、RETURNING 句を使う場合は
SQL ファイルを分離して管理できる query_file! や query_file_as! もあるので、
また触ってみたいと思います!!
追記
触ってみました!!!
Discussion