#sql #traits #derive #gen-fields #sql-tool-macros #gen-set #gen-values

sql_tool_kit

合并 sql_tool_core 和 sql_tool_macros 并一起导出,后续新增的功能都将从这个库中导出

4 releases

0.1.3 Jan 25, 2024
0.1.2 Jan 24, 2024
0.1.1 Jan 24, 2024
0.1.0 Jan 10, 2024

#520 in Database interfaces

MIT license

29KB
99 lines

SQL 语句辅助生成器

sql_tool_kit 库提供了一系列派生宏(GenFields, GenSelect, GenValues, GenSet, GenWhere), 用于自动派生与 SQL 语句构建相关的 trait 实现。这些宏简化了根据结构体字段生成 SQL 语句的过程。

派生宏介绍

#[derive(GenFields)]#[derive(GenSelect)]

这两个派生宏功能相似,主要区别在于命名。它们都处理字段及其上的 #[field(...)]#[select(...)] 宏。

使用方法:StructName::generate_fields_clause() 和 StructName::generate_select_clause() 分别生成字段名称数组(如 ["field1", "field2", ...])。 应用场景:通过 .join(", ") 方法将返回的数组拼接为字符串,可用于 SQL 语句的 SELECTINSERT 部分。

字段宏参数:

  • ignore - 忽略该字段
  • rename - 字段重命名

导入

/// 导入 GenFields 和对应实现的 trait
use sql_tool_kit::{GenFields, FieldsAttributeMacro};
/// 导入 GenSelect
use sl_tool_kit::{GenSelect, SelectAttributeMacro};

使用

#[derive(GenFields)]
pub struct FieldsStruct {
    field: i32,
    #[field(ignore)]
    field2: i32,
    #[field(rename = "rename_field")]
    field3: i32,
}
FieldsStruct::generate_fields_clause(); // 输出: [“field1", "rename_field"]

#[derive(GenFields)]
pub struct SelectStruct {
    field: i32,
    #[field(ignore)]
    field2: i32,
    #[field(rename = "rename_field")]
    field3: i32,
}
SelectStruct::generate_fields_clause(); // 输出: [“field1", "rename_field"]

#[derive(GenValues)]

GenValues 生成用于 insert 语句中 values 部分,通过 StructName::generate_values_clause() 得到 ["$1", "$2", ...]

需要在结构上使用宏 #[config(...)] 来配置序列化的方式:

宏参数:

  • #[config(...)]: 设置全局配置。
    • database - 指定生成的数据库类型,目前支持 postgresql mysql mariadb sqlite mssql
    • index - 指定开始的序列,仅 postgresql mssql 上有效

#[value(...)] 接受的参数:

  • ignore - 忽略该字段
    • index - 设置当前值的index,当设置了这个参数后,全局的 index 不会加一
    • value - 直接替换当前的 ${index} ,当设置了这个参数后,全局的 index 不会加一
      • 例如:#[value(value = "true")] => ["$1", "true",...]

使用方式

use sql_tool_kit::{GenValues, ValuesAttributeMacro};

#[derive(GenValues)]
#[config(database = "postgres")]
pub struct ValuesStruct {
  #[value(ignore)]
  field1: i32,
  #[value(index = 2)]
  field2: i32,
  #[value(value = "20")]
  field3: i32,
  #[value(value = "{index}::bit(4)")]
  field4: i32,
}

ValuesStruct::generate_values_clause(); // 输出:["$2", "20", "$1::bit(4)"]

#[derive(GenWhere)]

用于生成 SQL WHERE 语句部分。此宏依赖于 WhereAttributeMacro trait。 使用方法 where_data.generate_where_clause() 会返回一个字段和条件组成的字符串数组。 使用方法 where_data.generate_where_clause_with_index(index) 可以设置开始初始的 index 值。

使用方式

use sql_tool_kit::{GenWhere, WhereAttributeMacro};

#[derive(GenWhere)]
#[config(database = "postgres")]
pub struct WhereStruct {
  #[r#where()]
  field1: i32,
  field2: i32,
  #[r#where(rename = "rename_filed")]
  field3: i32,
  #[r#where(condition = ">=")]
  field4: i32,
  #[r#where(condition_all = "{name} not null")]
  field5: i32,
  #[r#where(value = "{index}::bit(4)")]
  field6: i32,
  #[r#where(index = 1)]
  field7: i32,
}

let data = WhereStruct { ... };

data.generate_where_clause(); // 输出:["field1 = $1", "rename_field = $2", "field4 >= $3", "field5 not null", "field5 = $4::bit(4)", "field7 = $1"]

宏参数:

  • #[config(...)]: 设置全局配置。

    • database: 指定数据库类型,影响占位符格式(支持 postgres, mysql, sqlite, mariadb, mssql)。
    • index: 设置占位符的起始索引。
    • ignore_none: 是否忽略 Option::None 值,默认为 true
    • ignore_no_macro_where: 是否忽略没有 #[r#where(...)] 宏的字段,默认值为 true, 为 true 时可配合 GenSet 宏使用。
  • #[r#where(...)]: 字段级别宏,用于自定义字段在 WHERE 语句中的表现。

    • ignore: 忽略该字段。
    • rename: 字段重命名,接受字符串类型。
    • condition: 指定字段的比较条件,默认值为 ”=“,如果该值设置为空及 "", 会报错。
    • condition_all: 应用于所有字段的通用条件,缺省值为 "{name} {condition} {index}"
      • {name}: 字段名称或 rename 指定的名称。
      • {condition}: condition 参数指定的比较条件。如果 condition_all
      • {index}: index 参数指定的占位符索引。如果字段未设置 index,则使用全局 index
    • ignore_none: 当字段为 Option::None 时是否忽略,接受布尔类型。
    • value: 自定义字段的值,接受字符串类型。
    • index: 自定义占位符序号(如果数据库支持),接受整型。

字段宏属性优先级: ignore > ignore_none > condition_all > rename = condition = value > index

#[derive(GenSet)]

用于生成 SQL UPDATE 语句中的 SET 部分。它依赖于 SetAttributeMacro trait。 例如,update table_name set field1 = $1, field2 = $2 ... where ... 使用方法 update_data.generate_set_clause() 返回值类似于 ["field1 = $1", "field2 = $2", ...]。 为了方便接入后续的 where 语句,在 #[set(...)] 添加了 where 参数,它可以为 wherewhere = "..." 通过方法 generate_set_and_where_clause() 返回值一个元组 (["field1 = $1", ...], ["field5 = $5", "field6 > $6", ...]), 第一个为 set 的值,第二个为 where 的值

使用方式

use sql_tool_kit::{GenSet, SetAttributeMacro};

#[derive(GenSet)]
#[config(database = "postgres")]
pub struct SetStruct {

  #[set(r#where)]
  pub id: i32,
  pub title: Option<String>,
  pub subtitle: Option<String>,
  pub image_url: Option<String>,
  pub link_url: Option<String>,
  pub description: Option<String>,
  #[set(value = "now()")]
  pub updated_at: Option<()>,
}

let data = UpdateForm {
  id: 1,
  title: Some("这是标题".to_string()),
  subtitle: None,
  image_url: None,
  link_url: None,
  description: Some("这是描述".to_string()),
  updated_at: Some(()),
};
let (set_values, where_value) = data.generate_set_and_where_clause(); // ["title = $1", "description = $2", "updated_at = now()"]

宏参数:

  • #[config(...)]: 设置一些配置。

    • database: 指定数据库类型,影响占位符的格式(支持 mysql, postgres, sqlite, mariadb, mssql)。
    • index: 设置占位符的起始索引。
    • ignore_none: 是否忽略 Option::None 值,默认为 true
    • ignore_no_macro_set: 默认忽略没有 #[set(...)] 宏的字段,为 true 时配合 GenWhere 宏使用。
    • ignore_set_and_where: 当 #[set(...)]存在 where 参数是,会忽略 set 值,默认为 false
  • #[set(...)]: 字段级别的宏,用于自定义字段在生成的 SET 语句中的表现。

    • ignore: 忽略该字段。
    • r#where: 将该字段设置为 where,有多种使用方式。1. #[set(r#where)] #[set(r#where = "{field = {index}")]
    • ignore_none: 当字段为 Option::None 时是否忽略,接受布尔类型。
    • ignore_set: 在 set 上忽略该字段。
    • rename: 字段重命名,接受字符串类型。
    • condition: 当设置 r#where 时生效
    • value: 自定义字段的值,接受字符串类型。
    • index: 自定义占位符序号(如果数据库支持),接受整型。

宏的优先级:ignore > ignore_none > r#where = ignore_set > rename = value = condition > index

使用示例

insert 语句

#[derive(GenFields, GenValues)]
#[config(database = "postgres")]
pub struct InsertForm {
  /// 标题
  pub title: String,
  /// 副标题
  #[value(index = 1)]
  pub subtitle: Option<String>,
  /// 图片地址
  pub image_url: String,
  /// 跳转链接
  pub link_url: Option<String>,
  /// 开始时间
  #[value(value = "now()")]
  pub start_time: String,
  /// 结束时间,如果没有结束时间,该广告会一直显示下去
  pub end_time: String,
  /// 描述
  pub description: Option<String>,
  /// 类型
  #[field(rename = "type")]
  pub ty: i32,
  /// 排序
  pub sort: i32,
}

fn main() {
    // 结果:insert into table_name (title, subtitle, image_url, link_url, start_time, end_time, description, type, sort) values ($1, $1, $2, $3, now(), $4, $5, $6, $7)
    let query = format!("insert into table_name ({}) values ({})", InsertForm::generate_fields_clause().join(", "), InsertForm::generate_values_clause().join(", "));
}

update 更新语句

#[derive(GenSet)]
// ignore_no_macro_set = false, 设置不忽略没有设置 #[set()] 字段的结构
// index = 1, 设置 index 从 1 开始,默认值:如果不设置,index 则默认为 1
#[config(database = "postgres", index = 1, ignore_no_macro_set = false)] 
pub struct UpdateForm {
    #[set(r#where)]
    pub id: i32,
    pub title: Option<String>,
    pub subtitle: Option<String>,
    pub image_url: Option<String>,
    pub link_url: Option<String>,
    pub description: Option<String>,
    #[set(value = "now()")]
    pub updated_at: Option<()>,
}

async fn main() -> Result<()> {
    let data = UpdateForm {
        id: 1,
        title: Some("这是标题".to_string()),
        subtitle: None,
        image_url: None,
        link_url: None,
        description: Some("这是描述".to_string()),
        updated_at: Some(()),
    };
    let (set_values, where_value) = data.generate_set_and_where_clause();
    // 同等于 update table_name set title = $1, description = $2, updated_at = now() where id = $3
    let query = format!("update table_name set {} where {}", set_values.join(", "), where_value.join(" AND "));
    
    let mut sql = sqlx::query::<_, QueryRow>(&query);
    if data.title.is_some() {
        sql = sql.bind(data.title);
    }
    if data.subtitle.is_some() {
        sql = sql.bind(data.subtitle);
    }
    if data.image_url.is_some() {
        sql = sql.bind(data.image_url);
    }
    if data.link_url.is_some() {
        sql = sql.bind(data.link_url);
    }
    if data.description.is_some() {
        sql = sql.bind(data.description);
    }
    
    let rows_affected = sql.bind(id).execute(pool).await?.rows_affected;
}

select 查询

use sql_tool_kit::*;

#[derive(GenWhere)]
#[config(database = "postgres")]
pub struct QueryForm {
    /// 关键字, 可以在 标题,副标题,描述上查询
    #[r#where(condition_all = "title like {index} AND subtitle like {index} AND description like {index}")]
    pub keyword: Option<String>,
    /// 开始时间
    #[r#where(condition = ">=")]
    pub start_time: Option<String>,
    /// 结束时间
    #[r#where(condition = "<=")]
    pub end_time: Option<String>,
    /// 广告类型
    #[r#where()]
    pub ty: i32,
    /// 页码信息
    #[r#where(ignore)]
    pub page_info: usize,
}

#[derive(GenSelect)]
pub struct QueryRow {
    /// 标题
    pub title: String,
    /// 副标题
    pub subtitle: Option<String>,
    /// 图片地址
    pub image_url: String,
    /// 跳转链接
    pub link_url: Option<String>,
    /// 开始时间
    pub start_time: String,
    /// 结束时间,如果没有结束时间,该广告会一直显示下去
    pub end_time: String,
    /// 描述
    pub description: Option<String>,
    /// 类型
    #[select(rename = "type")]
    pub ty: i32,
    /// 排序
    pub sort: i32,
}

async fn main() -> Result<()> {
    let data = QueryForm {
        keyword: Some("这是标题".to_string()),
        start_time: Some("2024/12/12".to_string()),
        end_time: None,
        ty:1,
        page_info: 0,
    };
    
    // 同等于:select title, subtitle, image_url, link_url, start_time, end_time, description, type, sort from table_name where title like $1 AND subtitle like $1 AND description like $1 AND start_time >= $2 AND ty = $3
    let query = format!("select {} from table_name where {}", QueryRow::generate_select_clause().join(", "), data.generate_where_clause().join(" AND "));

    let mut sql = sqlx::query::<_, QueryRow>(&query);
    if data.keyword.is_some() {
        sql = sql.bind(data.keyword);
    }
    if data.start_time.is_some() {
        sql = sql.bind(data.start_time);
    }
    if data.end_time.is_some() {
        sql = sql.bind(data.end_time);
    }
    let result = sql.bind(ty).fetch_all(pool).await?;
}

delete 语句


#[derive(GenWhere)]
#[config(database = "postgres")]
pub struct DeleteForm {
    #[r#where()]
    pub id: i32,
    #[r#where()]
    pub title: Option<String>,
    #[r#where(condition = ">=")]
    pub start_time: Option<String>,
    #[r#where(condition = "<=")]
    pub end_time: Option<String>,
}

fn main() {
    let data = DeleteForm {
        id: 1,
        title: None,
        start_time: Some("2025/12/12".to_string()),
        end_time: Some("2024/12/12".to_string()),
    };

    // 输出:delete table_name where id = $1 AND start_time >= $2 AND end_time <= $3
    let query = format!("delete table_name where {}", data.generate_where_clause().join(" AND "));
}

后续可能的扩展

  1. 完整的 sql 语句生成
  2. 优化 sqlx 的绑定值步骤

Dependencies

~250–690KB
~17K SLoC