RustのDieselでPostgresのMACADDR型を使う

はじめに

最近、RustのDieselを使い始めました。
DieselはPostgres/MySQL/SQLiteに接続することができます。
今回、PostgresのMACADDR型を使う際に躓いたので、解決策を備忘録的に残しておきます。

困ったこと

以下のようにMACADDRを使うスキーマとモデルを定義しました。

up.sql
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
);
model.rs
#[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,
}

全件SELECTする処理を書いて実行すると…

let machines = machines_schema::dsl::machines
    .load::<Machine>(&connection)
    .expect("Error loading machines");
for machine in &machines {
    println!("{:?}", machine);
}
$ cargo run
・
・
error[E0277]: the trait bound `[u8; 6]: Queryable<MacAddr, Pg>` is not satisfied
  --> src/main.rs:66:10
   |
66 |         .load::<Machine>(&connection)
   |          ^^^^ the trait `Queryable<MacAddr, Pg>` is not implemented for `[u8; 6]`

モデルにパースする際、MACADDRを[u8; 6]に変換する実装がない、というエラーです。
Referenceを見ると[u8; 6]への変換で正しそうです。

解決策

RustのDieselはfeaturesを指定することで使えるようになる機能があります。
例えば、timestamp等の時間に関わるものであれば「chrono」を追加する必要があります。

Cargo.toml



[dependencies]
diesel = { version = "1.4.6", features = ["postgres", "chrono"], default-features = false }

ネットワーク系も同様にfeatures「network-address」を追加する必要がありました。
(DieselのGitHubのCargo.tomlに一覧があるのでここから探しました…)

Cargo.toml



[dependencies]
diesel = { version = "1.4.6", features = ["postgres", "chrono", "network-address"], default-features = false }

これでもう1度動作させると、無事SELECTできました。

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: 3, name: "machineA", mac_address: [82, 66, 0, 26, 169, 68], status_id: 1, create_at: 2021-03-18T14:53:06.281686, update_at: 2021-03-18T14:53:06.281686 }
Machine { id: 4, name: "machineB", mac_address: [82, 66, 0, 26, 169, 69], status_id: 1, create_at: 2021-03-18T14:53:06.281686, update_at: 2021-03-18T14:53:06.281686 }

まとめ

Dieselで「~ is not implemented for ~」というエラーが出た際はfeaturesを確認してみると良いよというお話でした。
機能を選択させることでCrateを軽くする設計でしょうね(必要なものだけ選択する)。

PS)
この記事を書く際にReferenceを見ていたら、先頭に「The MACADDR SQL type. This type can only be used with feature = “network-address”」と記載がありました…
Referenceはちゃんと読まないとだめですね。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする

RustのDieselでPostgresに接続する with Docker

はじめに

最近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をつくってコンテナ作成

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

pgAgminでDBが出来ているのを確認します。
pgAdmin.png

スキーマの作成

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したいケースもある。

up.sql
-- マシンステータステーブル
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)
);
down.sql
DROP TABLE machines;
DROP TABLE machine_statuses;

DBに反映する。

$ diesel migration run

src/schema.rsが自動で生成される。

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,
);

pgAdminでも確認してみます。
pgAdmin2.png

テーブルができてますね。
INSERTしたデータも入っています。
pgAdmin3.png

CRUD操作

RustのプログラムからCRUD操作をしてみます。
まずは必要なライブラリを追加します。

$ cargo add diesel --no-default-features --features "postgres chrono network-address"
$ cargo add chrono
$ cargo add dotenv
$ cargo add hex-literal
Cargo.toml
[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"

コネクションを作成する関数を定義します。

src/util.rs
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するための構造体も定義しています。

model.rs
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
}

ライブラリを管理するファイルを作成します。

lib.rs
#[macro_use]
extern crate diesel;

mod models;
mod schema;
mod utils;

CRUD操作を行う処理を作成します。

main.rs
#[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上も以下のようになっています。あっていそうですね。
pgAdmin4.png

まとめ

Dockerのコンテナ内でRustのDieselからPostgresに接続してCRUD操作する流れを書きました。

ORMマッパーってプログラムでモデルを定義してコマンドを打つとSQLが生成されて実行されるというものが多い印象です。
が、DieselではSQL文は自分で書く必要があります。
(面倒ですが、SQLは書けたほうがよいのでOKです)
CRUD操作はORMマッパーらしい操作でとても書きやすいですね。

次はAPIと絡めてアプリケーションとして作っていきたいと思います。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする