💡

Rustで使ったsql_query_builderを使いやすくする

に公開

目的

RustでSQLビルダーを使ってみたという記事でモデルクラスを作成することで、Typoを減らすことができました。

しかしWHERE句あたりで文字列リテラルが残るのが気になったのでヘルパーを書いて極力リテラルを書かなくても済むようにしてみました。

コード

sql_builder_helper.rs
pub enum Operator {
    Equal,
    Like,
    ILike,
    GreaterThan,
    LessThan,
    GreaterThanEqual,
    LessThanEqual,
}

impl Operator {
    pub fn as_str(&self) -> &'static str {
        match self {
            Operator::Equal => "=",
            Operator::Like => "LIKE",
            Operator::ILike => "ILIKE",
            Operator::GreaterThan => ">",
            Operator::LessThan => "<",
            Operator::GreaterThanEqual => ">=",
            Operator::LessThanEqual => "<=",
        }
    }

    pub fn where_clause(&self, field: &str, index: usize) -> String {
        format!("{} {} ${}", field, self.as_str(), index)
    }
}

pub const ASTERISK: &str = "*";

pub fn like_pattern(value: &str) -> String {
    format!("%{}%", value.replace("%", "\\%").replace("_", "\\_"))
}

pub struct WhereMaker {
    count: usize,
}

impl WhereMaker {
    pub fn new() -> Self {
        Self::new_with_count(1)
    }

    pub fn new_with_count(count: usize) -> Self {
        WhereMaker { count }
    }

    pub fn next_index(&mut self) -> usize {
        let current = self.count;
        self.count += 1;
        current
    }

    pub fn where_clause(&mut self, operator: &Operator, field: &str) -> String {
        operator.where_clause(field, self.next_index())
    }
}
main.rs
use postgresql::table::{companies::Companies, users::Users};
use sql_query_builder as sql;
use sqlx::postgres::PgPoolOptions;
pub mod sql_builder_helper;
use uuid::Uuid;

use sql_builder_helper::{Operator, ASTERISK, like_pattern, WhereMaker};

async fn get_company_by_name(pool: &sqlx::PgPool, name: &str) -> anyhow::Result<Companies> {
    let mut where_maker = WhereMaker::new();
    let query = sql::Select::new()
        .raw(Companies::SELECT_SQL)
        .where_clause(&where_maker.where_clause(&Operator::Like, Companies::FIELDS.company_name));
    let company: Companies = sqlx::query_as(&query.as_string())
        .bind(like_pattern(name))
        .fetch_one(pool)
        .await?;
    Ok(company)
}

async fn get_user_by_name(pool: &sqlx::PgPool, company_uuid: &Uuid, name: &str) -> anyhow::Result<Users> {
    let mut where_maker = WhereMaker::new();
    let query = sql::Select::new()
        .select(ASTERISK)
        .from(Users::TABLE_NAME)
        .where_clause(&where_maker.where_clause(&Operator::Equal, Users::FIELDS.company_uuid))
        .where_clause(&where_maker.where_clause(&Operator::Like, Users::FIELDS.user_name));

    let user: Users = sqlx::query_as(&query.as_string())
        .bind(company_uuid)
        .bind(like_pattern(name))
        .fetch_one(pool)
        .await?;
    Ok(user)
}

#[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 company = get_company_by_name(&pool, "Sample").await?;
    let user = get_user_by_name(&pool, &company.uuid, "Sample").await?;

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

まとめ

  • Operatorを導入して絞り込み条件のenumから選択できるようにしました
  • WhereMakerで順番を手で書く必要がなくなりました
  • like_patternでLIKEのエスケープを考慮するようになりました

Discussion