はじめに
最近Rustの勉強をしていてAPI+DB構成のアプリを作ろうとしています。
DBをPostgresにしてDieselから接続します。
DBの用意は以前の記事で紹介した通りです。
Rustの環境は開発ということもあってホスト側に構築しようと思い、dieselをインストールしようとしたらエラーが...
$ cargo install diesel_cli --no-default-features --features postgres
error: linking with `link.exe` failed: exit code: 1181
・
・
・
error: aborting due to previous error
error: failed to compile `diesel_cli v1.4.1`, intermediate artifacts can be found at `C:\Users\Yukimura\AppData\Local\Temp\cargo-installTfifUZ`
Caused by:
could not compile `diesel_cli`
To learn more, run the command again with --verbose.
調べてみると、libpqが足りないとかVC++のツールセットが足りないなど出てきました。
解決するのが面倒だったのでRustの環境もコンテナで作ることにしました。
コンテナ作成~RustのアプリケーションからPostgresに接続してCRUD操作をするところまでを備忘録として残します。
Dockerfileをつくってコンテナ作成
FROM rust:1.49
RUN cargo install diesel_cli --no-default-features --features postgres chrono network-address
CMD ["/bin/bash"]
ビルドして起動します。
Windowsなので%cd%でマウントします。
$ docker build -t rust-dev:latest -f Dockerfile .
$ docker run --name rust-dev --net host -v %cd%:/usr/src/application -it rust-dev
DieselでDBの作成
コンテナに入るとdieselコマンドが既に使えるようになっています。
$ diesel -V
diesel 1.4.1
アプリケーションが格納されているディレクトリまで移動してDBの作成を行います。
$ cd /usr/src/application
$ DATABASE_URL=postgres://root:root@localhost/rust-dev > .env
$ diesel setup
Creating migrations directory at: /usr/src/application/migrations
Creating database: rust-dev
スキーマの作成
diesel setup
した際に
migrationsディレクトリが作成され、その中に
00000000000000_diesel_initial_setup
というディレクトリができます。
このディレクトリの中のup.sql/down.sqlにはヘルパー機能等を設定するための記述がされています。
このup.sql/down.sqlに自前のスキーマを書くこともできるようですが、基本は別々に作るようです。
マシンを管理することを想定してスキーマを作成してみます。
マイグレーションします。
$ diesel migration generate create_machines
Creating migrations/2021-03-09-130738_create_machines/up.sql
Creating migrations/2021-03-09-130738_create_machines/down.sql
up.sqlとdown.sqlにSQLを書く。
CREATE TABLEを書いてテーブルの作成をするのがメインだが、マスタであればINSERTしたいケースもある。
-- マシンステータステーブル
CREATE TABLE machine_statuses (
id SERIAL PRIMARY KEY,
name VARCHAR(20) NOT NULL
);
INSERT INTO machine_statuses (name) VALUES ('stop');
INSERT INTO machine_statuses (name) VALUES ('running');
-- マシンテーブル
CREATE TABLE machines (
id SERIAL PRIMARY KEY,
name VARCHAR(20) NOT NULL,
mac_address MACADDR NOT NULL,
status_id INTEGER NOT NULL,
create_at timestamptz NOT NULL DEFAULT current_timestamp,
update_at timestamptz NOT NULL DEFAULT current_timestamp,
FOREIGN KEY (status_id) REFERENCES machine_statuses(id)
);
DROP TABLE machines;
DROP TABLE machine_statuses;
DBに反映する。
$ diesel migration run
src/schema.rsが自動で生成される。
table! {
machine_statuses (id) {
id -> Int4,
name -> Varchar,
}
}
table! {
machines (id) {
id -> Int4,
name -> Varchar,
mac_address -> Macaddr,
status_id -> Int4,
create_at -> Timestamptz,
update_at -> Timestamptz,
}
}
joinable!(machines -> machine_statuses (status_id));
allow_tables_to_appear_in_same_query!(
machine_statuses,
machines,
);
テーブルができてますね。
INSERTしたデータも入っています。
CRUD操作
RustのプログラムからCRUD操作をしてみます。
まずは必要なライブラリを追加します。
$ cargo add diesel --no-default-features --features "postgres chrono network-address"
$ cargo add chrono
$ cargo add dotenv
$ cargo add hex-literal
[package]
name = "rust-dev"
version = "0.1.0"
authors = ["Yukimura"]
edition = "2018"
[dependencies]
chrono = "0.4.19"
diesel = { version = "1.4.6", features = ["postgres", "chrono", "network-address"], default-features = false }
dotenv = "0.15.0"
hex-literal = "0.3.1"
コネクションを作成する関数を定義します。
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;
pub fn establish_connection() -> PgConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
PgConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}
モデルの構造体を定義します。
INSERTするための構造体も定義しています。
use super::schema::machines;
#[derive(Debug, Queryable)]
pub struct MachineStatus {
pub id: i32,
pub name: String,
}
#[derive(Debug, Queryable)]
pub struct Machine {
pub id: i32,
pub name: String,
pub mac_address: [u8; 6],
pub status_id: i32,
pub create_at: chrono::NaiveDateTime,
pub update_at: chrono::NaiveDateTime,
}
#[derive(Insertable)]
#[table_name="machines"]
pub struct NewMachine {
pub name: String,
pub mac_address: [u8; 6],
pub status_id: i32
}
ライブラリを管理するファイルを作成します。
#[macro_use]
extern crate diesel;
mod models;
mod schema;
mod utils;
CRUD操作を行う処理を作成します。
#[macro_use]
extern crate diesel;
mod models;
mod schema;
mod utils;
use hex_literal;
use diesel::prelude::*;
use models::*;
use schema::machines as machines_schema;
use utils::establish_connection;
fn main() {
// create connection
let connection = establish_connection();
// create
let mac_address_a = hex_literal::hex!("52 42 00 1a a9 44");
let mac_address_b = hex_literal::hex!("52 42 00 1a a9 45");
let new_machine = vec![
NewMachine {
name: String::from("machineA"),
mac_address: mac_address_a,
status_id: 1
},
NewMachine {
name: String::from("machineB"),
mac_address: mac_address_b,
status_id: 1
},
];
diesel::insert_into(machines_schema::dsl::machines)
.values(&new_machine)
.execute(&connection)
.expect("Error creating machines");
// read
let machines = machines_schema::dsl::machines
.load::<Machine>(&connection)
.expect("Error loading machines");
for machine in &machines {
println!("{:?}", machine);
}
// update
let update_machine = diesel::update(machines_schema::dsl::machines.find(machines[0].id))
.set(machines_schema::name.eq("machineC"))
.get_result::<Machine>(&connection)
.expect("Error updating machines");
println!("--------------------");
println!("{:?}", update_machine);
// delete
diesel::delete(machines_schema::dsl::machines.find(machines[1].id))
.execute(&connection)
.expect("Error deleting machines");
// select
let machines = machines_schema::dsl::machines
.load::<Machine>(&connection)
.expect("Error loading machines");
println!("--------------------");
for machine in &machines {
println!("{:?}", machine);
}
}
まず
名前が「machineA」、MACアドレスが「52:42:00:1a:a9:44」というモデルと
名前と「machineB」、MACアドレスが「52:42:00:1a:a9:45」というモデルを追加しています。
そのあと全件読み込んで2件存在するか確認しています。
で、読み込んだ1件目の名前を「machineC」という名前に変えるように更新、読み込んだ2件目を削除しています。
再度全件読み込んで名前が変わっていること、1件になっていることを確認しています。
実行した結果が以下(MACアドレスは10進に変換されています)。
$ cargo run
Machine { id: 1, name: "machineA", mac_address: [82, 66, 0, 26, 169, 68], status_id: 1, create_at: 2021-03-18T14:01:48.632112, update_at: 2021-03-18T14:01:48.632112 }
Machine { id: 2, name: "machineB", mac_address: [82, 66, 0, 26, 169, 69], status_id: 1, create_at: 2021-03-18T14:01:48.632112, update_at: 2021-03-18T14:01:48.632112 }
--------------------
Machine { id: 1, name: "machineC", mac_address: [82, 66, 0, 26, 169, 68], status_id: 1, create_at: 2021-03-18T14:01:48.632112, update_at: 2021-03-18T14:01:48.632112 }
--------------------
Machine { id: 1, name: "machineC", mac_address: [82, 66, 0, 26, 169, 68], status_id: 1, create_at: 2021-03-18T14:01:48.632112, update_at: 2021-03-18T14:01:48.632112 }
pgAdmin上も以下のようになっています。あっていそうですね。
まとめ
Dockerのコンテナ内でRustのDieselからPostgresに接続してCRUD操作する流れを書きました。
ORMマッパーってプログラムでモデルを定義してコマンドを打つとSQLが生成されて実行されるというものが多い印象です。
が、DieselではSQL文は自分で書く必要があります。
(面倒ですが、SQLは書けたほうがよいのでOKです)
CRUD操作はORMマッパーらしい操作でとても書きやすいですね。
次はAPIと絡めてアプリケーションとして作っていきたいと思います。