RustでSQLを発行してみる
RustでSQLを発行してみます。
RustでSQLというとDieselばかりが検索に引っかかります。Dieselのように事前のセットアップをしたくない場合とか、すでにデータソースの用意がある場合等、SQLを直接書きたいシーンというのも少なくないと思うのですが、この例があまり出てきません。
SQLを直接記述、発行する場合、r2d2_mysql
というCrateを使います。リポジトリやライブラリの紹介ページは以下のとおりです。
ここでひとつ、注意しなければならないことがあります。それぞれのページからリンクされているドキュメントが古いのです。Crates.ioからリンクされているドキュメントはv0.2.0
、GitHubからリンクされているドキュメントはv3.0.0
となっています。ところが、現在公開されているr2d2_mysql
のバージョンに合致する正しいドキュメントは16.0.0
なのです。
わたしはここで躓いてしまい、検証に随分と時間がかかってしまいました。
環境要件
- Rust
- rustup-1.20.2
- rustc-1.38.0
- r2d2-0.8.6
- r2d2_mysql-16
- mysql-16
- MySQL
$ rustup -V
rustup 1.20.2 (13979c968 2019-10-16)
$ rustc -V
rustc 1.38.0 (625451e37 2019-09-23)
$ cat Cargo.toml | grep dependencies -A 10
[dependencies]
r2d2 = "0.8.6"
r2d2_mysql = "16"
mysql = "16"
デモコード
https://github.com/yo1000/rusql/tree/9319a54
#[macro_use]
extern crate mysql;
extern crate r2d2_mysql;
extern crate r2d2;
use std::env;
use std::sync::Arc;
use mysql::{Opts, OptsBuilder};
use r2d2_mysql::MysqlConnectionManager;
const DATABASE_HOST: &str = "DATABASE_HOST";
const DATABASE_PORT: &str = "DATABASE_PORT";
const DATABASE_USER: &str = "DATABASE_USER";
const DATABASE_PASS: &str = "DATABASE_PASS";
const DATABASE_NAME: &str = "DATABASE_NAME";
const DATABASE_POOL_SIZE: u32 = 4;
fn main() {
// 環境変数の読み取りとチェック
// パラメタの読み取りはコマンドライン引数からでも良いですが、
// 名前から値を引こうとすると別途ライブラリが必要になるため、環境変数を使っています
let db_host = env_var(DATABASE_HOST, Some("127.0.0.1".to_string()));
let db_port = env_var(DATABASE_PORT, Some("3306".to_string()));
let db_user = env_var(DATABASE_USER, None);
let db_pass = env_var(DATABASE_PASS, None);
let db_name = env_var(DATABASE_NAME, None);
assert_ne!(db_host, "");
assert_ne!(db_port, "");
assert_ne!(db_user, "");
assert_ne!(db_pass, "");
assert_ne!(db_name, "");
// ビルダークラスには各パラメタをひとつずつ設定していくインターフェースも用意されていますが、
// ビルダー型が&mutになるためMysqlConnectionManager::newへそのまま渡せなくなり使い勝手が悪いので、
// 接続URLを組み立ててOpts::from_urlに渡す形にしています
let db_url = format!(
"mysql://{user}:{pass}@{host}:{port}/{name}",
user = db_user,
pass = db_pass,
host = db_host,
port = db_port,
name = db_name
);
let opts = Opts::from_url(&db_url).unwrap();
let builder = OptsBuilder::from_opts(opts);
let manager = MysqlConnectionManager::new(builder);
// マルチスレッドでプールからコネクションを取り出すような使い方を想定してArcでラップします
let pool = Arc::new(r2d2::Pool::builder()
.max_size(DATABASE_POOL_SIZE)
.build(manager).unwrap());
let items = query_table_outline(pool, db_name);
for item in items {
println!(
"TableOutline\n\
table_name: {}\n\
table_comment: {}\n\
table_fqn: {}\n",
item.table_name,
item.table_comment.unwrap(),
item.table_fqn
);
}
}
fn env_var(name: &str, def_var: Option<String>) -> String {
let env_var = env::var(name);
return match def_var {
Some(v) => env_var.unwrap_or(v),
_ => env_var.expect(format!("{} must be set", name).as_str()),
}
}
fn query_table_outline(
pool: Arc<r2d2::Pool<MysqlConnectionManager>>,
param: String
) -> Vec<TableOutline> {
// 並列アクセス可能なようにプールへの参照をカウントアップ
let pool = pool.clone();
// プールからコネクションを取り出す
let mut conn = pool.get().unwrap();
// PreparedQueryを発行
// :param_schema_name のようにコロンを頭につけると名前付きのプレースホルダーになります
return conn.prep_exec(r#"
SELECT
tbl.table_name AS table_name,
tbl.table_comment AS table_comment,
CONCAT(tbl.table_schema, '.', tbl.table_name)
AS table_fqn
FROM
information_schema.tables tbl
WHERE
tbl.table_schema = :param_schema_name
AND tbl.table_type = 'BASE TABLE'
ORDER BY
tbl.table_name
"#, params!{
"param_schema_name" => param
})
.map::<Vec<TableOutline>, _>(|result| {
result
.map(|x| x.unwrap())
.map(|row| {
// タプルに結果セットの行をマッピング
let (table_name, table_comment, table_fqn) = mysql::from_row(row);
// 戻り値型に詰め替え
TableOutline {
table_name,
table_comment,
table_fqn,
}
}).collect()
}).unwrap();
}
#[derive(Debug, PartialEq, Eq)]
struct TableOutline {
table_name: String,
table_comment: Option<String>,
table_fqn: String,
}
実行すると、以下のようにテーブルの名前やコメントが表示されます。
$ cargo run --release
Compiling rusql v0.1.0 (/.../rusql)
Finished release [optimized] target(s) in 1.32s
Running `target/release/rusql`
TableOutline
table_name: actor
table_comment:
table_fqn: sakila.actor
TableOutline
table_name: address
table_comment:
table_fqn: sakila.address
TableOutline
table_name: category
table_comment:
table_fqn: sakila.category
...