5
1

More than 3 years have passed since last update.

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

Posted at

はじめに

最近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と絡めてアプリケーションとして作っていきたいと思います。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1