2022/01/26

[Rust][Psql]Dieselのよくやる使い方まとめ

rustpostgresql

最近では、WebアプリのバックエンドにRustを使うことが多く、
そのRDBのDriverとして、dieselを採用しています。
(使っているDBはpostgreqlです。)

今回は、私がよく使うdieselの使い方をまとめてみました。

どうやって実装したっけ?とよく昔のプロジェクトのソースコードを参照することが多かったので、自分向けのメモとしても残しておきます。

Scheme編

jsonフィールドを扱う

CREATE TABLE items (
    id serial primary key,
    content json,
);

Cargo.toml

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# serde_jsonのfeatureが必要です。
diesel = { version = "1.4.8", features = ["postgres", "chrono", "r2d2", "serde_json", "numeric"] }

# sumなどを行うときはbigdecimalが便利です。
[dependencies.bigdecimal]
version = ">= 0.0.10, < 0.2.0"

scheme.rs

table! {
    items (id) {
        id -> Int4,
        content -> Nullable<Json>,
    }
}
use crate::scheme::items;
use serde_json;

#[derive(Queryable, Debug)]
pub struct Item {
    pub id: i32,
    pub content: Option<serde_json::Value>,
}

#[derive(Insertable, Debug)]
#[table_name = "items"]
pub struct NewItem {
    pub content: Option<serde_json::Value>,
}

connection編

connectionの作成

use diesel::pg::PgConnection;
use diesel::prelude::*;

pub fn establish_connection<S: Into<String>>(database_url: S) -> PgConnection {
    let database_url = database_url.into();
    PgConnection::establish(&database_url).expect(&format!("Error connecting to {}", database_url))
}

connection poolの作成

use diesel::r2d2::{ConnectionManager, Pool};
use diesel::PgConnection;

pub type PgPool = Pool<ConnectionManager<PgConnection>>;

pub fn new_pool<S: Into<String>>(database_url: S, max_size: u32) -> PgPool {
    let manager = ConnectionManager::<PgConnection>::new(database_url);
    Pool::builder()
        .max_size(max_size)
        .build(manager)
        .expect("Failed to create pool")
}

SQL編

get by id

// your_schemesというテーブルの主キー検索
your_schemes::table.find(id).first::<YourScheme>(conn);

get by unique field value

// your_schemes.unique_field_name = value の limit 1検索
your_schemes::table
    .filter(your_schemes::unique_field_name.eq(value))
    .first::<YourScheme>(conn);

select for update by id

your_schemes::table.find(id).for_update().first::<YourScheme>(conn);

select with multiple and conditions

// where your_schemes.some_field1 = value1 and your_schemes.some_field2 = value2
your_schemes::table
    .filter(your_schemes::some_field1.eq(value1))
    .filter(your_schemes::some_field2.eq(value2))
    .load::<YourScheme>(conn);

select with complex and/or conditions

// select * from your_schemes
// where
//   some_field1 = value1
//   and (
//     some_field2 < value2
//     or
//     (some_field3 = value3 and some_field4 <= value4)
//   )
// order by id desc
your_schemes::table
    .filter(your_schemes::some_field1.eq(value1))
    .filter(
        your_schemes::some_field2.lt(value2)
        .or(your_schemes::some_field3.eq(value3).and(your_schemes::some_field4.le(value4))),
    )
    .order(your_schemes::id.desc())
    .load::<YourScheme>(conn)

subquery

/// select * from your_schemes
///   where
///     id in (select your_scheme_id from your_schemes2 where some_field1 = value1)
///   order by id asc
let target_ids = your_schemes2::table
    .filter(your_schemes2::some_field1.eq(value1))
    .select(your_schemes2::your_scheme_id);
your_schemes::table
    .filter(your_schemes::id.eq_any(target_ids))
    .order(your_schemes::id.asc())
    .load::<YourScheme>(conn);

select sum

use diesel::dsl::sum;
// select sum(your_field) from your_schemes
your_schemes::table
    .select(sum(your_schemes::your_field))
    .first::<Option<bigdecimal::BigDecimal>>(conn)

select max

use diesel::dsl::max;
// select max(your_field) from your_schemes
your_schemes::table
    .select(max(your_schemes::your_field))
    .first(conn)

select count

// select count(*) from your_schemes where your_schemes.some_field = value
your_schemes::table
    .filter(your_schemes::some_field.eq(value))
    .count()
    .get_result(conn)

select limit offset

your_schemes::table
    .filter(your_schemes::some_field.eq(value))
    .order(your_schemes::id.desc())
    .limit(limit)
    .offset(offset)
    .load::<YourScheme>(conn)

pager with buiding query

いろんな条件を動的に組み合わせて、ページングするクエリです。

BoxedQueryをうまく使っています。

// まずはqueryを動的に作る関数を作成します。
// こうすることでクエリを作成するロジックを1つに集約することができます。
fn build_query_for_search<'a>(
    some_id: Option<i32>,
    search_text: Option<&'a str>,
 ) -> your_schemes::BoxedQuery<'a, diesel::pg::Pg> {
    let mut query = your_schemes::table.into_boxed();
    if let Some(some_id) = some_id {
        query = query.filter(your_schemes::some_id.eq(some_id))
    }
    let like = format!("%{}%", search_text.unwrap_or(""));
    if search_text.is_some() {
        query = query.filter(
            your_schemes::some_field
                .ilike(like.clone())
                .or(your_schemes::some_field2.like(like)),
        )
    }
    query
}

// 上記の関数を使って、検索の合計件数とページングした結果を返す関数を作ります。
pub fn search(
    conn: &PgConnection,
    some_id: Option<i32>,
    search_text: Option<&'a str>,
    limit: i64,
    offset: i64,
) -> Result<(i64, Vec<YourScheme>), Error> {
    let total = build_query_for_search(some_id, search_text)
        .count()
        .get_result(conn)?;
    let results = build_query_for_search(some_id, search_text)
        .order(your_schemes::id.asc())
        .limit(limit)
        .offset(offset)
        .load::<YourScheme>(conn)?;
    OK((total, results))
}

join

/// select
///     your_schemes1.*, your_schemes2.*
///   from
///     your_schemes1
///     inner join your_schemes2 on
///       your_schemes2.some_foreign_key = your_schemes1.related_key
///   where
///     your_schemes1.some_field1 in (values)
///   order by your_schemes1.id asc, your_schemes2.id asc
your_schemes1::table
    .inner_join(your_schemes2::table)
    .filter(your_schemes1::some_field1.eq_any(values))
    .order((
        your_schemes1::id.asc(),
        your_schemes2::id.asc(),
    ))
    .load::<(YourScheme1, YourScheme2)>(conn);

raw sql

use diesel::sql_types::*;

// まずは、フィールド名からクエリーできる構造体を定義しておきます。
// sql_typeは取得するカラムのデータ型に合わせておきます。(ここが不整合でもコンパイルが通ってしまい、ランタイム時にエラーになるので注意が必要です。)
#[derive(QueryableByName, Debug)]
pub struct YourScheme {
    #[sql_type = "Integer"]
    pub id: i32,
    #[sql_type = "Varchar"]
    pub your_field1: String,
    #[sql_type = "Integer"]
    pub your_field2: i32,
}

// diesel::sql_query を使ってクエリーを作ります。
// Raw SQLを書いて、プレースホルダーをうまく使ってクエリーできるようにします。
let your_field1 = "test";
let your_field2 = 10;
let limit = 100;
let query = r###"SELECT
    id,
    your_field1,
    your_field2
  FROM
    your_schemes
  WHERE
    your_field1 = $1
    AND your_field2 = $2
  LIMIT $3
"###;
diesel::sql_query(query)
  .bind::<Varchar, _>(your_field1)
  .bind::<Integer, _>(your_field2)
  .bind::<BigInt, _>(limit)
  .load::<YourScheme>(conn)

update single field by id

diesel::update(your_schemes::dsl::your_schemes.find(id))
    .set(your_schemes::target_field.eq(value))
    .execute(conn);

update multiple field by id

// タプル形式で指定
diesel::update(your_schemes::dsl::your_schemes.find(id))
    .set((
        your_schemes::target_field1.eq(value1),
        your_schemes::target_field2.eq(value2),
    ))
    .execute(conn);

delete by id

// your_schemesというテーブルの主キーで一致したものを削除
diesel::delete(your_schemes::table.find(id)).execute(conn);

insert

// new_entity is Insertable struct value
diesel::insert_into(your_schemes::table).values(&new_entity).get_result(conn)

以上です。

今後頻繁に使うものがあれば随時追加していこうと思います。