概要
ポスグレにクエリを投げるツールを、DieselというORM(Object-relational mapping)を使ってRustで実装してみる。
より素朴なライブラリとしては、下記のpostgresクレートがあり、素直にSQLを書いて実装できるので、最初はDieselよりはこっちが取っつき易いのかもしれない。
- [参考] postgresクレート(今回これは使わない)
が、Dieselを使うとクエリをRustの思想にしたがって「メソッドチェーン(=関数をつないで処理する書き方)」で表現できたり、型安全が保証されたり、それ以外でも多くの恩恵がある模様。
今回はRustのDieselクレートを使って、予め作成したポスグレのDBに接続してクエリを投げて結果を取得してみる。
ちなみにDieselでDBそのものを作成することも下記サイトの方法等でできるが、今回の記事では対象外とする。
- dieselのサンプル(sqlite)を動かしてみた
準備
- サーバ(PostgreSQL)
- PostgreSQLのサーバを準備。(セットアップ手順は割愛)
- バージョンは11.5
- クライアント(Rust)
- RustのDockerイメージを利用
- バージョン
- rustc : 1.51.0
- cargo : 1.51.0
サーバ準備詳細(データベース作成)
今回接続するDBとテーブルを作成する
まずはDBの作成。sampleという名前にする。
$ psql
postgres=# CREATE DATABASE sample;
CREATE DATABASE
一旦psqlからログアウトして、DB名sampleを指定して入り直し、customerというテーブルを作成。
nameが主キーで、ageはNULLを許容するようにする。
$ psql -d sample
sample=# CREATE TABLE customer (
name TEXT PRIMARY KEY NOT NULL,
age INTEGER,
reg_date DATE NOT NULL
);
CREATE TABLE
テーブルに3つの行を追加。
sample=# INSERT INTO customer
(name, age, reg_date)
VALUES
('田中 一郎', 34, DATE '2021-03-13'),
('佐藤 二郎', NULL, DATE '2021-03-15'),
('鈴木 三郎', 43, DATE '2021-03-14');
INSERT 0 3
行が追加されていることを念のため確認。
sample=# SELECT * FROM customer;
name | age | reg_date
-----------+-----+------------
田中 一郎 | 34 | 2021-03-13
佐藤 二郎 | | 2021-03-15
鈴木 三郎 | 43 | 2021-03-14
クライアント準備詳細(RustのDockerイメージ作成&コンテナ起動)
RustのDockerイメージを以下のDockerfileを使って作成。Rustを使うユーザを追加し、コンテナ実行時に追加したユーザでログインできる状態にしておく。
FROM rust
# ユーザ追加
ARG USERNAME=username
RUN adduser ${USERNAME} && \
apt -y update
USER ${USERNAME}
上記Dockerfileをもとに、Dockerイメージを作成。
$ docker build -t myrust .
コンテナを起動してログイン。
$ docker run -dit --name myrust myrust
$ docker exec -it myrust /bin/bash
username@2b473184441c:/$
以後は全て、「myrustコンテナ」にログインした状態で作業をする。
Dieselのインストール、設定
Diesel CLIのインストール
以下コマンドでインストール。今回はポスグレの機能しか使わないので、オプションでその旨を指定。
※環境によってはlpqがなくてエラーになることがあるので、Ubuntu系OSの場合はsudo apt -y install libpq-dev
で事前にインストールが必要かも。
$ cargo install diesel_cli --no-default-features --features postgres
Cargoプロジェクトの作成と設定
適当なディレクトリでプロジェクトを作成。名前はpsql_clientとする。
以後、作成されたpsql_clientディレクトリに移動して各種設定を行う。
$ cargo new psql_client
Created binary (application) `psql_client` package
$ cd psql_client
まず、Cargo.toml
の[dependencies]
セクションに必要なパッケージを記載。今回はポスグレの機能と、Date型のデータを扱うため、features
に["postgres", "chrono"]
の2つを指定している。
[package]
name = "psql_client"
version = "0.1.0"
authors = ["auther"]
edition = "2018"
[dependencies]
diesel = { version = "1.4.6", features = ["postgres", "chrono"] }
dotenv = "0.15.0"
chrono = "0.4.19"
続いて、ポスグレサーバのURIを記載した.env
ファイルを作成する。参考サイトにも記載があるが、例えば次のように設定すればOK。URIの最後はDB名を指定するため、今回はsample
と書く。
$ echo DATABASE_URL=postgres://[ユーザ名]:[パスワード]@[ホスト名]:[ポート番号]/sample > .env
DBのURIを設定後、以下コマンドを実行。
$ diesel setup
Creating migrations directory at: /path/psql_client/migrations
setupに成功すると、migrations
ディレクトリができる。テーブルを新たに作る場合は、ここにSQLのCREATE文を書けばいいようだが、今回は既にテーブルを作成済なのでスキップする。
もう一つ、diesel.toml
というファイルが生成されており、以下のようにデータベースのスキーマの定義をsrc/schema.rs
に記載せよ、という記述がある。ここは次の「実装」で書いていく。
[print_schema]
file = "src/schema.rs"
実装
今回のプロジェクトのディレクトリ構成は下記。
まず、src/schema.rs
にテーブル情報のマクロを記載し、続いて本体のsrc/main.rs
を実装していく。
.
├── Cargo.toml
├── diesel.toml
└── src
├── main.rs
└── schema.rs
src/schema.rs
の作成
dieselのprint-schemaというコマンドでスキーマ内にあるテーブル情報のマクロを作ってくれる。これをそのままsrc/schema.rs
へリダイレクトすればOK。
$ diesel print-schema > src/schema.rs
src/schema.rs
の中身は以下
table! {
customer (name) {
name -> Text,
age -> Nullable<Int4>,
reg_date -> Date,
}
}
テーブル名customer
の次の括弧部分に記載される変数(本例ではname)は、テーブルの主キー。またageは値がNULLになる可能性があるので、Nullable<T>
型の構造体で定義されていることに注意。
今回は半分手動でsrc/schema.rs
を作成したが、Rust + MySQL + Diesel(1)にも記載があるように、diesel migration run
でテーブルを作成した場合はsrc/schema.rs
は自動で作成される模様。
src/main.rs
の作成
とりあえず以下のように書くと、customerテーブルの各行の情報を取得できる。
#[macro_use]
extern crate diesel;
extern crate dotenv;
use std::env;
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
// customerテーブルの日付型カラム(reg_date)を扱うのに必要
use chrono::NaiveDate;
// "src/schema.rs"で定義したマクロを使えるようにする
mod schema;
use schema::customer::dsl::*;
// テーブルcustomerの各行の情報を格納する構造体
// (Web上のサンプルプロブラムでは別ソースmodels.rsに書かれていることが多い)
// ageはNULLを取ることがあり、Option型で受ける必要あり
#[derive(Queryable, Debug)]
struct Customer {
name: String,
age: Option<i32>,
reg_date: NaiveDate,
}
/// DBの接続を確立
fn establish_connection() -> PgConnection {
dotenv().ok();
// .envファイルに定義された環境変数DATABASE_URLを取得してDBに接続する
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
PgConnection::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}
fn main() {
// DBの接続を確立
let conn = establish_connection();
// customerテーブルの各情報を取得
// SQLで「SELECT * FROM customer;」をやっているのと同じ
let results = customer
.load::<Customer>(&conn)
.expect("Error loading customer");
// 結果を表示
for r in results {
println!("{:?}", r);
}
}
ビルドして実行
customerテーブルの各行の値を出力
$ cargo run
Compiling psql_client v0.1.0 (/path/psql_client)
Finished dev [unoptimized + debuginfo] target(s) in 0.97s
Running `target/debug/psql_client`
Customer { name: "田中 一郎", age: Some(34), reg_date: 2021-03-13 }
Customer { name: "佐藤 二郎", age: None, reg_date: 2021-03-15 }
Customer { name: "鈴木 三郎", age: Some(43), reg_date: 2021-03-14 }
ageはNULLを取り得るので、Option型になっている。佐藤さんのageはNULLで、Customer構造体にはNoneが格納されていることがわかる。
クエリのバリエーション
main関数内でresultsを取得する部分はメソッドチェーンにより様々なクエリを指定可能。
昇順ソート(ORDER BY句相当)
let results = customer
.order(reg_date.asc()) // 追加
.load::<Customer>(&conn)
.expect("Error loading customer");
出力結果
Customer { name: "田中 一郎", age: Some(34), reg_date: 2021-03-13 }
Customer { name: "鈴木 三郎", age: Some(43), reg_date: 2021-03-14 }
Customer { name: "佐藤 二郎", age: None, reg_date: 2021-03-15 }
絞り込み(WHERE句相当)
let results = customer
.filter(name.eq("田中 一郎")) // 追加
.load::<Customer>(&conn)
.expect("Error loading customer");
出力結果
Customer { name: "田中 一郎", age: Some(34), reg_date: 2021-03-13 }
一部カラム(nameとreg_dateのみ)取得
SELECT name, reg_date FROM customer;
と同じ動作
let result = customer
.select((name, reg_date))
.get_results::<(String, NaiveDate)>(&conn)
.unwrap();
出力結果
("田中 一郎", 2021-03-13)
("佐藤 二郎", 2021-03-15)
("鈴木 三郎", 2021-03-14)
※Dieselのドキュメントによると、get_results
メソッドの処理はload
と同じらしい。
集約関数 COUNT
SELECT COUNT(*) FROM sample;
と同じ動作
let count_star = customer
.count()
.get_result::<i64>(&conn)
.unwrap();
assert_eq!(count_star, 3); // 結果が3になることの確認
SELECT COUNT(age) FROM sample;
と同じ動作
use diesel::dsl::*; // count関数を使うのに必要
let count_age = customer
.select(count(age))
.get_result::<i64>(&conn)
.unwrap();
assert_eq!(count_age, 2);
集約関数 AVG
SELECT AVG(age) FROM sample;
と同じ動作
use diesel::dsl::*; // avg関数を使うのにインポート必要
use bigdecimal::BigDecimal; // クエリ結果がBigDecimal型になるので必要
let average = customer
.select(avg(age))
.get_result::<Option<BigDecimal>>(&conn)
.unwrap() // ageがNullable型なので、追加でunwrapが必要
.unwrap();
use std::str::FromStr;
assert_eq!(average, BigDecimal::from_str(&"38.5").unwrap());
平均値だとBigDecimal型を返すようなので、Cargo.toml
の[dependencies]
セクションで、bigdecimalクレートの追加が必要。また、dieselクレートのfeaturesで"numeric"も追記する必要あり。(これに気づくのに2時間近くかかった…)
(略)
[dependencies]
diesel = { version = "1.4.6", features = ["postgres", "chrono", "numeric"] } # "numeric"追加
dotenv = "0.15.0"
chrono = "0.4.19"
bigdecimal = "0.1.2" # 追加
また動作確認はしていないが、最大値(max)はBigInt型で返るようなので、平均(avg)と同様にクレート追加が必要になるかも。
SQL文を直書きする方法
sql_query
関数を使うことで、SQL文を直書きして結果を取得することもできるが、Rust側で値を正しく受け取るための構造体定義など、事前準備が必要となる点に注意。
let result =
diesel::sql_query("SELECT COUNT(name), AVG(age) FROM customer")
.get_result::<CountAvg>(&conn) // CountAvg構造体は別途定義が必要
.unwrap();
println!("{:?}", result);
出力結果
CountAvg { count: 3, avg: Some(BigDecimal("38.5000000000000000")) }
この例の場合、src/schema.rs
とsrc/main.rs
に以下のように、クエリの結果を受け取るためのマクロと構造体を定義する。
// 以下を追記
table! {
count_avg (count) {
count -> Int8,
avg -> Nullable<Numeric>,
}
}
// 以下を追記
use self::schema::count_avg;
// QueryableByNameトレイトの実装が必要
#[derive(QueryableByName, Debug)]
#[table_name = "count_avg"]
struct CountAvg {
count: i64,
avg: Option<bigdecimal::BigDecimal>,
}
直書きSQL文の中で変数指定
DBサーバがポスグレの場合は、以下のように$n
(n=1,2,3...)を設定後、bindメソッドで$n
の値を指定すればOK。
let result =
diesel::sql_query("
SELECT name, age, reg_date FROM customer
WHERE age >= $1
")
.bind::<diesel::sql_types::Integer, _>(40)
.load::<CustomerByName>(&conn)
.unwrap();
println!("{:?}", result);
出力結果
[CustomerByName { name: "鈴木 三郎", age: Some(43), reg_date: 2021-03-14 }]
上で(何の説明もなしに)出てきたCustomerByName
構造体は、事前に以下のように定義しておく。
sql_queryで直書きのクエリを使うため、QueryableByName
トレイトの実装が必要。
// 以下を追記
#[derive(QueryableByName, Debug)]
#[table_name = "customer"]
struct CustomerByName {
name: String,
age: Option<i32>,
reg_date: NaiveDate,
}
動作確認はしていないが、DBサーバがMySQLの場合は$n
ではなく、?
と書けばいいはず。(というか、Webで見つかるほとんどのサンプルは?
で書かれていた。なので筆者は最初、ポスグレも?
で書くのかと思いこんで実装するもエラーで動かず、$n
で書くこと気づくのにえらい時間を要してしまった…)