メインコンテンツへスキップ

Rust の ORM:Diesel を使ってみる

·3 分
Rust Diesel PostgreSQL

Rust での DB を扱いを楽にする Diesel を利用してみます。他にも ORM はありそうなのですが、これが一番メジャーっぽいです。

今回は、Postgres の DB と連携させることを目標とします。

サンプルプログラム
#

DB の立ち上げを Docker でできるようにしておきました:
GitHub のリポジトリ

準備
#

依存関係
#

cargo new で適当なプロジェクトを作成し、 Cargo.toml には以下のように記述しておきます:

1[dependencies]
2diesel = { version = "1.4.4", features = ["postgres", "chrono"] }
3dotenv = "0.15.0"
4chrono = "0.4"

dieselfeatures には2つ指定しています:

  • postgres : PostgreSQL の DB を扱うため
  • chrono : 日付型のデータを扱うため

Diesel CLI のインストール
#

Diesel には CLI ツール が用意されているため、インストールします。 Rust のプロジェクトルートで以下のコマンドを実行します:

1cargo install diesel_cli --no-default-features --features postgres

ここで、もし以下のようなエラーが出た場合:

1note: ld: library not found for -libpq
2
3clang: error: linker command failed with exit code 1 (use -v to see invocation)

libpq のライブラリがインストールされていませんので、各 OS の環境に合わせてインストールします。

Diesel のセットアップ
#

Diesel のセットアップをする前に DB を立ち上げておきます。今回のプロジェクトの場合、プロジェクトルートで

1docker-compose up -d db

を実行すると PostgreSQL の DB が立ち上がります。 DBeaver などのツールで DB を閲覧できるようになります。

環境変数 DATABASE_URLpostgres://admin:password@localhost:5432/postgres です。これを Rust のプロジェクトルートの .env に書き込んでおきます:

1echo DATABASE_URL=postgres://admin:password@localhost:5432/postgres > .env

次に、以下のコマンドでセットアップができます:

1diesel setup

マイグレーション
#

以下のコマンドを実行します:

1diesel migration generate create_table

すると、以下のようにマイグレーション用の SQL ファイルが作成されます:

1$ diesel migration generate create_table
2Creating migrations/2021-12-24-150710_create_table/up.sql
3Creating migrations/2021-12-24-150710_create_table/down.sql

up.sql を以下の内容にします:

  • idSERIAL を指定し、自動で採番されるようにします。
1CREATE TABLE posts (
2   id SERIAL PRIMARY KEY,
3   title TEXT NOT NULL,
4   body TEXT NOT NULL,
5   is_published BOOLEAN NOT NULL DEFAULT false,
6   create_time TIMESTAMP NOT NULL DEFAULT current_timestamp,
7   update_time TIMESTAMP NOT NULL DEFAULT current_timestamp
8);

また、down.sql を以下のようにします:

1DROP TABLE posts;

この状態で以下のコマンド

1diesel migration run

を実行すると、up.sql の内容が実行され、DB にテーブルが作成されます。

また、src/schema.rs が以下の内容で作成されます。

 1table! {
 2   posts (id) {
 3       id -> Int4,
 4       title -> Text,
 5       body -> Text,
 6       is_published -> Bool,
 7       create_time -> Timestamp,
 8       update_time -> Timestamp,
 9   }
10}

マイグレーションをロールバックしたい場合は以下のコマンドを実行します:

1diesel mifration revert

このとき、 down.sql が実行され、テーブルも無事に消え、schema.rs の内容も消えます。

CRUD 操作
#

Rust で CRUD 操作を記述します。

DB の接続
#

まずは、 DB とのコネクションを確立するためのメソッドを以下の src/utils.rs のように記述します:

 1use diesel::prelude::*;
 2use diesel::pg::PgConnection;
 3use dotenv::dotenv;
 4use std::env;
 5
 6pub fn establish_connection() -> PgConnection {
 7   dotenv().ok();
 8   let database_url = env::var("DATABASE_URL")
 9       .expect("DATABASE_URL must be set");
10   PgConnection::establish(&database_url)
11       .expect(&format!("Error connecting to {}", database_url))
12}

モデルの定義
#

モデルの構造体を以下のように定義します(models.rs )。

  • SELECT 文で取得してきたものを入れる構造体には Queryable を付与します。
  • UPDATE 文でテーブルに要素を追加するものには Insertable を付与します。
    • table_name="posts" も指定します。
 1use chrono::{DateTime, Utc};
 2use crate::schema::posts;
 3
 4#[derive(Debug, Queryable)]
 5pub struct Post {
 6   pub id: i32,
 7   pub title: String,
 8   pub body: String,
 9   pub is_published: bool,
10   pub create_time: DateTime<Utc>,
11   pub update_time: DateTime<Utc>,
12}
13
14#[derive(Insertable)]
15#[table_name="posts"]
16pub struct NewPost<'a> {
17   pub title: &'a str,
18   pub body: &'a str,
19}

単一の要素を登録する
#

登録するときは以下のように書きます。

 1pub fn create_post<'a>(conn: &PgConnection, id: i32, title: &'a str, body: &'a str) -> Post {
 2   use crate::schema::posts;
 3   let new_post = NewPost {
 4       id,
 5       title,
 6       body,
 7   };
 8
 9   diesel::insert_into(posts::table)
10       .values(&new_post)
11       .get_result(conn)
12       .expect("Error saving new post")
13}

要素を取得する
#

こんな感じで書けます。filter を使用して条件の絞り込み、 limit で取得上限数を指定します。

1pub fn show_posts(conn: &PgConnection) -> Vec<Post> {
2   use crate::schema::posts::dsl::*;
3   posts.filter(is_published.eq(true))
4       .limit(5)
5       .load::<Post>(conn)
6       .expect("Error loading posts")
7}

要素を更新する
#

update を用いて更新します。引数に更新対象を指定し、set で更新内容を記述します。

1pub fn publish_post(conn: &PgConnection, id: i32) -> Post {
2   use crate::schema::posts::dsl::{posts, is_published};
3   diesel::update(posts.find(id))
4       .set(is_published.eq(true))
5       .get_result::<Post>(conn)
6       .expect(&format!("Unable to find post {}", id))
7}

要素を削除する
#

ここでは LIKE 句でパターンを指定して削除を行う処理を記述します。

1pub fn delete_post(conn: &PgConnection, target: &str) -> usize {
2   use crate::schema::posts::dsl::*;
3   let pattern = format!("%{}%", target);
4   diesel::delete(posts.filter(title.like(pattern)))
5       .execute(conn)
6       .expect("Error deleting posts")
7}

参考
#