🙆

RustでSQLビルダーを使ってみた

に公開

目的

RustでSQLビルダーを使ってみたくなりました。
作成したSQLは文字列なので、実行はsqlxを使いました。

コード

sql_query_builder

本家のサンプルは以下のようになります。

use sql_query_builder as sql;

let mut select = sql::Select::new()
  .select("id, login")
  .from("users")
  .where_clause("login = $1");

let is_admin = true;

if is_admin {
  select = select.where_clause("is_admin = true");
}

let query = select.as_string();

println!("{query}");

正直言ってテーブル名やカラム名を文字列で扱うのはRustっぽく無いと思います。
なのでERDの情報からあらかじめフィールドの情報やSQLの一部を作成することにしました。

作ったコード

companies.rs
// ERDから自動生成
use crate::Pool;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use derive_builder::Builder;
use sqlx::prelude::*;
use sql_query_builder as sqb;

pub struct CompaniesFields {
    pub uuid: &'static str,
    pub company_name: &'static str,
    pub created_at: &'static str,
    pub created_pg: &'static str,
    pub created_uuid: &'static str,
    pub deleted_at: &'static str,
    pub deleted_pg: &'static str,
    pub deleted_uuid: &'static str,
    pub updated_at: &'static str,
    pub updated_pg: &'static str,
    pub updated_uuid: &'static str,
    pub bk: &'static str,
}

#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default, FromRow)]
#[builder(setter(into), default, field(public))]
pub struct Companies {
    pub uuid: Uuid,
    pub company_name: String,
    pub created_uuid: Uuid,
    pub updated_uuid: Uuid,
    pub deleted_uuid: Uuid,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub deleted_at: Option<DateTime<Utc>>,
    pub created_pg: String,
    pub updated_pg: String,
    pub deleted_pg: String,
    pub bk: Option<String>,
}

impl Companies {
    pub const TABLE_NAME: &'static str = "public.companies";
    
    pub const FIELDS: CompaniesFields = CompaniesFields {
        uuid: "uuid",
        company_name: "company_name",
        created_at: "created_at",
        created_pg: "created_pg",
        created_uuid: "created_uuid",
        deleted_at: "deleted_at",
        deleted_pg: "deleted_pg",
        deleted_uuid: "deleted_uuid",
        updated_at: "updated_at",
        updated_pg: "updated_pg",
        updated_uuid: "updated_uuid",
        bk: "bk",
    };

    pub const SELECT_SQL: &str = r#"
    SELECT
        uuid
        ,company_name
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    FROM
        public.companies
    "#;

    pub const INSERT_SQL: &str = r#"
    INSERT INTO public.companies (
        uuid
        ,company_name
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    ) VALUES (
        $1
        ,$2
        ,$3
        ,$4
        ,$5
        ,$6
        ,$7
        ,$8
        ,$9
        ,$10
        ,$11
        ,$12
    ) RETURNING
        uuid
        ,company_name
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    "#;

    pub const UPDATE_SQL: &str = r#"
    UPDATE public.companies SET
        company_name = $2
        ,created_uuid = $3
        ,updated_uuid = $4
        ,deletded_uuid = $5
        ,created_at = $6
        ,updated_at = $7
        ,deleted_at = $8
        ,created_pg = $9
        ,updated_pg = $10
        ,deleted_pg = $11
        ,bk = $12
    WHERE
        uuid = $1
    RETURNING
        uuid
        ,company_name
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    "#;

    pub const DELETE_SQL: &str = r#"
    DELETE FROM public.companies
    WHERE
        uuid = $1
    RETURNING
        uuid
        ,company_name
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    "#;

    pub const DELETE_ALL_SQL: &str = r#"
    DELETE FROM public.companies
    "#;

    pub async fn insert(&self, pool: &Pool) -> Result<Self, sqlx::Error> {
        sqlx::query_as(Self::INSERT_SQL)
            .bind(self.uuid)
            .bind(&self.company_name)
            .bind(self.created_uuid)
            .bind(self.updated_uuid)
            .bind(self.deleted_uuid)
            .bind(self.created_at)
            .bind(self.updated_at)
            .bind(self.deleted_at)
            .bind(&self.created_pg)
            .bind(&self.updated_pg)
            .bind(&self.deleted_pg)
            .bind(&self.bk)
            .fetch_one(pool)
            .await
    }

    pub async fn update(&self, pool: &Pool) -> Result<Self, sqlx::Error> {
        sqlx::query_as(Self::UPDATE_SQL)
            .bind(self.uuid)
            .bind(&self.company_name)
            .bind(self.created_uuid)
            .bind(self.updated_uuid)
            .bind(self.deleted_uuid)
            .bind(self.created_at)
            .bind(self.updated_at)
            .bind(self.deleted_at)
            .bind(&self.created_pg)
            .bind(&self.updated_pg)
            .bind(&self.deleted_pg)
            .bind(&self.bk)
            .fetch_one(pool)
            .await
    }

    pub async fn delete(&self, pool: &Pool) -> Result<Self, sqlx::Error> {
        Self::delete_one(pool, &self.uuid).await
    }

    pub async fn delete_one(pool: &Pool, uuid: &Uuid) -> Result<Self, sqlx::Error> {
        sqlx::query_as(Self::DELETE_SQL)
            .bind(uuid)
            .fetch_one(pool)
            .await
    }

    pub async fn delete_all(pool: &Pool) -> Result<(), sqlx::Error> {
        let _ = sqlx::query(Self::DELETE_ALL_SQL)
            .execute(pool)
            .await?;
        Ok(())
    }

    pub async fn select_all(pool: &Pool) -> Result<Vec<Self>, sqlx::Error> {
        let rows: Vec<Self> = sqlx::query_as(Self::SELECT_SQL)
            .fetch_all(pool)
            .await?;
        Ok(rows)
    }

    pub async fn select_one(pool: &Pool, uuid: &Uuid) -> Result<Option<Self>, sqlx::Error> {
        let query = sqb::Select::new()
            .raw(Self::SELECT_SQL)
            .where_clause(&format!("{} = $1", Self::FIELDS.uuid));
        let one: Option<Self> = sqlx::query_as(&query.as_string())
            .bind(uuid)
            .fetch_optional(pool)
            .await?;
        Ok(one)
    }
}
users.rs
// ERDから自動生成
use crate::Pool;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use derive_builder::Builder;
use sqlx::prelude::*;
use sql_query_builder as sqb;

pub struct UsersFields {
    pub uuid: &'static str,
    pub company_uuid: &'static str,
    pub user_name: &'static str,
    pub user_mail: &'static str,
    pub user_kbn: &'static str,
    pub created_at: &'static str,
    pub created_pg: &'static str,
    pub created_uuid: &'static str,
    pub deleted_at: &'static str,
    pub deleted_pg: &'static str,
    pub deleted_uuid: &'static str,
    pub updated_at: &'static str,
    pub updated_pg: &'static str,
    pub updated_uuid: &'static str,
    pub bk: &'static str,
}

#[derive(Serialize, Deserialize, Debug, Clone, Builder, Default, FromRow)]
#[builder(setter(into), default, field(public))]
pub struct Users {
    pub uuid: Uuid,
    pub company_uuid: Uuid,
    pub user_name: String,
    pub user_mail: String,
    pub user_kbn: crate::kbn_constants::UserKbn,
    pub created_uuid: Uuid,
    pub updated_uuid: Uuid,
    pub deleted_uuid: Uuid,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
    pub deleted_at: Option<DateTime<Utc>>,
    pub created_pg: String,
    pub updated_pg: String,
    pub deleted_pg: String,
    pub bk: Option<String>,
}

impl Users {
    pub const TABLE_NAME: &'static str = "public.users";
    
    pub const FIELDS: UsersFields = UsersFields {
        uuid: "uuid",
        company_uuid: "company_uuid",
        user_name: "user_name",
        user_mail: "user_mail",
        user_kbn: "user_kbn",
        created_at: "created_at",
        created_pg: "created_pg",
        created_uuid: "created_uuid",
        deleted_at: "deleted_at",
        deleted_pg: "deleted_pg",
        deleted_uuid: "deleted_uuid",
        updated_at: "updated_at",
        updated_pg: "updated_pg",
        updated_uuid: "updated_uuid",
        bk: "bk",
    };

    pub const SELECT_SQL: &str = r#"
    SELECT
        uuid
        ,company_uuid
        ,user_name
        ,user_mail
        ,user_kbn
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    FROM
        public.users
    "#;

    pub const INSERT_SQL: &str = r#"
    INSERT INTO public.users (
        uuid
        ,company_uuid
        ,user_name
        ,user_mail
        ,user_kbn
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    ) VALUES (
        $1
        ,$2
        ,$3
        ,$4
        ,$5
        ,$6
        ,$7
        ,$8
        ,$9
        ,$10
        ,$11
        ,$12
        ,$13
        ,$14
        ,$15
    ) RETURNING
        uuid
        ,company_uuid
        ,user_name
        ,user_mail
        ,user_kbn
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    "#;

    pub const UPDATE_SQL: &str = r#"
    UPDATE public.users SET
        company_uuid = $2
    ,user_name = $3
    ,user_mail = $4
    ,user_kbn = $5
        ,created_uuid = $6
        ,updated_uuid = $7
        ,deletded_uuid = $8
        ,created_at = $9
        ,updated_at = $10
        ,deleted_at = $11
        ,created_pg = $12
        ,updated_pg = $13
        ,deleted_pg = $14
        ,bk = $15
    WHERE
        uuid = $1
    RETURNING
        uuid
        ,company_uuid
        ,user_name
        ,user_mail
        ,user_kbn
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    "#;

    pub const DELETE_SQL: &str = r#"
    DELETE FROM public.users
    WHERE
        uuid = $1
    RETURNING
        uuid
        ,company_uuid
        ,user_name
        ,user_mail
        ,user_kbn
        ,created_uuid
        ,updated_uuid
        ,deleted_uuid
        ,created_at
        ,updated_at
        ,deleted_at
        ,created_pg
        ,updated_pg
        ,deleted_pg
        ,bk
    "#;

    pub const DELETE_ALL_SQL: &str = r#"
    DELETE FROM public.users
    "#;

    pub async fn insert(&self, pool: &Pool) -> Result<Self, sqlx::Error> {
        sqlx::query_as(Self::INSERT_SQL)
            .bind(self.uuid)
            .bind(self.company_uuid)
            .bind(&self.user_name)
            .bind(&self.user_mail)
            .bind(&self.user_kbn)
            .bind(self.created_uuid)
            .bind(self.updated_uuid)
            .bind(self.deleted_uuid)
            .bind(self.created_at)
            .bind(self.updated_at)
            .bind(self.deleted_at)
            .bind(&self.created_pg)
            .bind(&self.updated_pg)
            .bind(&self.deleted_pg)
            .bind(&self.bk)
            .fetch_one(pool)
            .await
    }

    pub async fn update(&self, pool: &Pool) -> Result<Self, sqlx::Error> {
        sqlx::query_as(Self::UPDATE_SQL)
            .bind(self.uuid)
            .bind(self.company_uuid)
            .bind(&self.user_name)
            .bind(&self.user_mail)
            .bind(&self.user_kbn)
            .bind(self.created_uuid)
            .bind(self.updated_uuid)
            .bind(self.deleted_uuid)
            .bind(self.created_at)
            .bind(self.updated_at)
            .bind(self.deleted_at)
            .bind(&self.created_pg)
            .bind(&self.updated_pg)
            .bind(&self.deleted_pg)
            .bind(&self.bk)
            .fetch_one(pool)
            .await
    }

    pub async fn delete(&self, pool: &Pool) -> Result<Self, sqlx::Error> {
        Self::delete_one(pool, &self.uuid).await
    }

    pub async fn delete_one(pool: &Pool, uuid: &Uuid) -> Result<Self, sqlx::Error> {
        sqlx::query_as(Self::DELETE_SQL)
            .bind(uuid)
            .fetch_one(pool)
            .await
    }

    pub async fn delete_all(pool: &Pool) -> Result<(), sqlx::Error> {
        let _ = sqlx::query(Self::DELETE_ALL_SQL)
            .execute(pool)
            .await?;
        Ok(())
    }

    pub async fn select_all(pool: &Pool) -> Result<Vec<Self>, sqlx::Error> {
        let rows: Vec<Self> = sqlx::query_as(Self::SELECT_SQL)
            .fetch_all(pool)
            .await?;
        Ok(rows)
    }

    pub async fn select_one(pool: &Pool, uuid: &Uuid) -> Result<Option<Self>, sqlx::Error> {
        let query = sqb::Select::new()
            .raw(Self::SELECT_SQL)
            .where_clause(&format!("{} = $1", Self::FIELDS.uuid));
        let one: Option<Self> = sqlx::query_as(&query.as_string())
            .bind(uuid)
            .fetch_optional(pool)
            .await?;
        Ok(one)
    }
}
main.rs
// 手で書いたコード
use postgresql::table::{companies::Companies, users::Users};
use sql_query_builder as sql;
use sqlx::postgres::PgPoolOptions;

fn make_where_clause(field: &str, operator: &str, index: usize) -> String {
    format!("{} {} ${}", field, operator, index)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    env_logger::init();

    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect("postgres://user:pass@postgresql/web")
        .await?;

    let query = sql::Select::new()
        .raw(Companies::SELECT_SQL)
        .where_clause(&make_where_clause(
            Companies::FIELDS.company_name,
            "LIKE",
            1,
        ));
    let company: Companies = sqlx::query_as(&query.as_string())
        .bind("%Sample%")
        .fetch_one(&pool)
        .await?;

    let query = sql::Select::new()
        .select("*")
        .from(Users::TABLE_NAME)
        .where_clause(&make_where_clause(Users::FIELDS.company_uuid, "=", 1))
        .where_clause(&make_where_clause(Users::FIELDS.user_name, "LIKE", 2));

    let user: Users = sqlx::query_as(&query.as_string())
        .bind(company.uuid)
        .bind("%Sample%")
        .fetch_one(&pool)
        .await?;

    println!("{:?}", user);
    Ok(())
}

まとめ

文字列操作をなるべく避けるような感じで、型チェックの恩恵が受けられるように書いてみました。

Discussion