Rust Tiny Orm

一个基于sqlx的极简orm,本项目将sql代码以derive属性方式和结构体进行关联,并自动生成相关CRUD相关API,可根据需要实现任意复杂度的数据获取。

本项目开发主要原因:

  1. 目前rust的orm产品都太大而全了,习惯了Python Django的自动化的实现,对DSL相当不习惯,个人觉得太繁琐了;
  2. 实际orm就是api和sql的转换,因为orm的实现限制,sql的复杂查询的实现都比较别扭,为了与数据库交互还得额外学习大量orm dsl的用法,为什么不直接用sql查询呢?!
  3. orm为了满足各种需要,实现均较为复杂,带来了大量的学习成本。但是,实际工程角度来看,实际我们项目与数据库的交互是有限的,基本就是根据特定字段查询数据,新增数据和更新数据等,各种查询的逻辑也基本是固定的,并不需要高度灵活性的查询接口,且一般ORM实现高度灵活的查询,是以增加运行时消耗为代价的;
  4. 实际orm的核心生成sql交互的代码,整个和rust的宏高度契合,因此使用宏生成我们工程需要的数据库接口,是ORM的最佳实践!

因此,是否可以将数据查询的sql和rust结合,由程序员自行控制sql的表现,这样在rust的高性能助力下,我们可以写成性能超级赞的数据库应用,并实现如下功能:

  1. 自动实现rust结构体到数据库表的映射;
  2. 自动生成数据库交互相关的接口,如insert、update、query、delete和exist类方法;
  3. 在自动生成方法基础上,可根据sql自动生成具有高度灵活性的数据库CRUD方法;
  4. 在以上基础上最大化的减少手写sql的工作量,减少数据库交互逻辑的处理工作量。

项目组成

本项目由两个库组成,分别为:tinyormcore和tinyormmacroderive,其中tinyormcore为项目核心库,tinyormmacroderive为项目派生宏库

特性

根据你的数据库类型选择不同特性,缺省为mysql

  1. mysql
  2. postgres
  3. sqlite
  4. mssql
  5. any

核心库说明

  1. tinyormcore::TinyOrmDbMeta

结构体对应数据表的相关配置信息,一般由派生宏进行自动生成,并提供如下方法

  1. tinyormcore::TinyOrmDbModel

trait,实现了db_query创建数据库查询器,方法定义如下:

fn db_query(sql: &str) -> Query

  1. tinyormcore::TinyOrmDbQuery

trait,实现了各种数据库交互方法,均以db_开头,该trait中的方法不自动进行数据转换.

在派生宏无法满足需求的情况下,可使用该trait中的方法构建灵活的crud方法。

  1. tinyormcore::TinyOrmData

trait,实现了与结构体关联的相关数据库交互放阿飞,均以orm开头,该trait中的查询类方法均实现数据库行与struct的转换,转换时调用如下方法: fn ormrow_map(row: TinyOrmSqlRow) -> Self; 该方法需要自行实现。

在派生宏无法满足需求的情况下,可使用该trait中的方法构建灵活的crud方法。

派生宏说明

  1. #[derive(TinyOrm)]

为结构体自动实现TinyOrmDbModel数据库模型

用法

本宏定义了如下属性,用于设置数据映射接口和参数

应用于结构体的属性

  1. #[ormtablename = "core_user"]

可选属性,可设置多个。

自定义数据库表名称。 未设置则表名称为结构名称的蛇形命名, 例如:TestUser转换为test_user。

  1. #[ormtablename_pref = "core"]

可选属性,可设置多个。

为自动生成的数据库表名称增加前缀。 例如:TestUser转换为coretestuser.

  1. #[orm_query]

可选属性,可设置多个。

生成Self::ormquerywith开头的获取多条记录的查询方法。

有三个子属性:

例如:

```ignore

[orm_query(

name = "nameandtel", sqlwhere = "user.name = ? and mobilephone = ?", args = "name:&str,mobilephone:&str", doc = "根据姓名和手机号查询用户", )] 自动生成如下函数: /// 根据姓名和手机号查询用户 pub async fn ormquerywithnameandtel(pool: &TinyOrmDbPool, name:&str,mobile_phone:&str) ->Result>{...} ```

  1. #[orm_update]

可选属性,可设置多个。

生成Self::ormupdatewith开头的将更新记录方法。

有三个子属性:

例如:

```ignore

[orm_update(

name = "nameandtel", sqlset = "user.name = ? ,user.mobilephone = ?", sqlwhere = "id = ?", args = "id:u32,name:&str,mobilephone:&str", doc = "根据id更新姓名和手机号", )] 自动生成如下更新函数: /// 根据id更新姓名和手机号 pub async ormupdatewithnameandtel(pool: &TinyOrmDbPool, name: &str,mobilephone: &str) -> Result<()>{...} ```

  1. #[orm_delete]

可选属性,可设置多个。

生成Self::ormdeletewith开头的删除记录方法。

有三个子属性:

例如:

```ignore

[orm_delete(

name = "nameandtel", sqlwhere = "user.name = ? and mobilephone = ?" args = "name:&str,mobilephone:&str", doc = "根据姓名和手机号删除用户" )] 自动生成如下函数: /// 根据姓名和手机号查询用户 pub async fn ormdeletewithnameandtel(pool: &TinyOrmDbPool, name:&str,mobile_phone:&str) ->Result<()>{...} ```

  1. #[orm_exist]

可选属性,可设置多个。

生成Self::ormexistwith开头的删除记录方法。

有三个子属性:

例如:

```ignore

[orm_exist(

name = "id", sqlwhere = "id = ?" args = "id:u32", doc = "根据id查询记录是否存在" )] 自动生成如下函数: /// 根据id查询记录是否存在 pub async fn ormexistwithnameandtel(pool: &TinyOrmDbPool, id: u32) ->Result ```

应用于结构体字段的方法

  1. #[orm_pk(name="id", auto="true")]

至少需要设置1个,可设置多个。

设置当前字段为主键字段

有两个子属性:

该属性会自动生成如下方法:

多个主键会自动汇聚为一个方法,并将主键设置为对应的函数方法的参数

示例

```ignore struct TestUser { #[ormpk(name="userid",auto="true")] id:u32, #[orm_pk] tel:String, }

//以上会自动生成如下方法 pub async fn ormgetwithpk(pool: &TinyOrmDbPool,id:u32,tel:&str) -> Result{...} pub async fn ormdeletewithpk(pool: &TinyOrmDbPool,id:u32,tel:&str) -> Result<()>{...} pub async fn ormexistwithpk(pool: &TinyOrmDbPool,id:u32,tel:&str) -> Result{...} pub async fn ormdelete(self,pool: &TinyOrmDbPool) -> Result{...} pub async fn orm_exist(self,pool: &TinyOrmDbPool) -> Result{...}

```

  1. #[orm_join]

可选属性,可设置多个。

设置对应字段为关联其他表的join字段

有六个子属性:

如为设置skip_method="true",该属性会自动生成如下方法:

示例

struct User { #[orm_join( name="user_type_id", select_field="user_type.name as user_type_name, user_type.template", join="JOIN user_type ON user_type.id = user_type_id", link_id="id", link_id_type="u32", )] user_type:UserType } // 会自动生成如下方法 pub async fn orm_query_join_with_user_type(pool: &TinyOrmDbPool,user_type:UserType) -> Result<Vec<Self>>{...} pub async fn orm_query_join_with_user_type_id(pool: &TinyOrmDbPool,query_value:u32) -> Result<Vec<Self>>{...} pub async fn orm_delete_join_with_user_type(pool: &TinyOrmDbPool,user_type:UserType) -> Result<()>{...} pub async fn orm_delete_join_with_user_type_id(pool: &TinyOrmDbPool,query_value:u32) -> Result<()>{...} pub async fn orm_update_join_with_user_type(&self,pool: &TinyOrmDbPool) -> Result<()>{...}

  1. #[ormfield(name = "customtablefieldname")]

可选属性,可设置多个。

设置对应数据库表的字段名称

有1个子属性:

  1. #[orm_ignore]

可选属性,可设置多个。

默认情况下,结构体的每个字段均对应一个数据库表字段。 设置该属性后,可忽略对应字段,不对应数据库表的字段名称

  1. #[orm_update]

生成更新数据库中的对应字段值的方法,方法定义如下: pub async fn ormupdate字段名称(&self,pool: &TinyOrmDbPool) ->Result<()>

  1. #[orm_query]

生成使用该字段查询数据库记录的方法,方法定义如下: * pub async fn ormquerywith字段名称(pool: &TinyOrmDbPool,queryvalue:字段类型) -> Result> * 结构体字段类型自动转换 * Option 自动转换为 T * String 自动转换为 &str

  1. #[orm_get]

生成使用该字段获取单条数据库记录的方法,方法定义如下: * pub async fn ormgetwith字段名称(pool: &TinyOrmDbPool,queryvalue:字段类型) -> Result * 结构体字段类型自动转换 * Option 自动转换为 T * String 自动转换为 &str

其他自动生成方法

  1. self.orm_insert方法

将当前结构体实例数据插入到数据库表中,用于新增记录的保存

方法定义如下: pub async fn orm_insert(&mut self, pool: &TinyOrmDbPool) -> Result<()>

  1. self.ormupdateall方法

将当前结构体实例数据更新到数据库表中,用于变更记录的保存

方法定义如下: pub async fn ormupdateall(&self,pool: &TinyOrmDbPool) -> Result<()>

  1. #[derive(TinyOrmQuery)]

自动实现TinyOrmQuery trait宏

使用说明

  1. 添加依赖

修改Cargo.toml,增加

``` [dependencies] async-trait = "^0.1" tinyormmacro_derive="^0.3"

[dependencies.tinyormcore] version = "^0.3" features = [ "mysql", ] ```

  1. 引入相关trait

ignore use tiny_orm_core::prelude::*;

  1. 在结构体中使用派生属性

struct User{ #[orm_pk(auto="true")] id: u32, name: String, }

  1. 使用自动生成的相关orm_方法进行数据库交互

示例代码

``` //! 数据库模型的测试 use sqlx::mysql::MySqlPoolOptions; use sqlx::Row; use tinyormmacro_derive::{TinyOrm, TinyOrmQuery};

use super::*;

[derive(Debug, PartialEq, Eq)]

pub struct UserType { /// 类型编号 pub id: Option, /// 类型名称 pub name: String, /// 中断短信模板 pub template: String, } impl UserType { /// 完整创建器 /// /// # 参数说明 /// /// * id 类型编号 /// * name 类型名称 /// * template 中断短信模板 pub fn new(id: u32, name: &str, template: &str) -> Self { UserType { id: Some(id), name: name.into(), template: template.into(), } } }

[derive(Debug, PartialEq, Eq)]

pub struct Organization { /// 行号 pub id: String, /// 机构名称 pub name: String, }

[allow(dead_code)]

[derive(TinyOrm, TinyOrmQuery, Debug, PartialEq, Eq)]

// 自动生成表格名称时附加前缀,生成表名称为:coretestuser //#[ormtablenamepref = "core"] // 指定表名称,未指定则自动生成,规则结构名称转换为蛇形命名,如:testuser

[ormtablename = "user"]

/// 自动生成ormquerywith查询方法,生成后的函数定义如下 /// /// 根据姓名和手机号查询用户 /// pub async fn ormquerywithnameandtel(pool: &TinyOrmDbPool, name:&str,mobilephone:&str) ->Result> { /// let sql = Self::DBMETA.buildselectsql("name = ? and mobilephone = ?"); /// let query = Self::dbquery(&sql) /// .bind(name) /// .bind(mobilephone); /// Self::dbfetchall(pool,query,Self::ormrowmap).await /// }

[orm_query(

name = "name_and_tel",
sql_where = "user.name = ? and mobile_phone = ?",
args = "name:&str,mobile_phone:&str",
doc = "根据姓名和手机号查询用户"

)] /// 自动生成ormdeletewith删除方法,生成后的函数定义如下 /// /// 根据姓名和手机号删除用户 /// pub async fn ormdeletewithnameandtel(pool: &TinyOrmDbPool, name:&str,mobilephone:&str) ->Result> { /// let sql = Self::DBMETA.builddeletesql("name = ? and mobilephone = ?"); /// let query = Self::dbquery(&sql) /// .bind(name) /// .bind(mobilephone); /// Self::db_execute(pool, query).await /// }

[orm_delete(

name = "name_and_tel",
sql_where = "user.name = ? and mobile_phone = ?",
args = "name:&str,mobile_phone:&str",
doc = "根据姓名和手机号删除用户"

)] /// 生成ormexistwith_name方法

[orm_exist(

name = "name",
sql_where = "user.name like ?",
args = "name:&str",
doc = "根据姓名查询用户是否存在"

)] /// 生成ormupdatewith_name方法

[orm_update(

name = "name_and_tel",
sql_set = "name = ? , mobile_phone = ?",
sql_where = "id = ?",
args = "name:&str,mobile_phone:&str,id:u32",
doc = "根据id更新姓名和手机号"

)] pub struct TestUser { /// 类型编号 /// 生成的ormgetbypk函数参数中,id转换为u32 /// 会自动将多个pk字段合并为一个方法的参数,生成如下方法 /// /// ormpk自动实现:通过主键获取记录 /// pub async fn ormgetbypk(pool: &TinyOrmDbPool, id:u32,mobilephone:&str) -> Result{ /// let sql = Self::DBMETA.buildselectsql(#pkwheresql); /// let query = Self::dbquery(&sql) /// .bind(id) /// .bind(mobilephone); /// Self::dbfetchone(pool, query, Self::ormrowmap).await /// .withcontext(|| "根据主键获取记录出错!") /// } /// /// ormpk自动实现:通过主键删除记录 /// pub async fn ormdeletebypk(pool: &TinyOrmDbPool,id:u32,mobilephone:&str) -> Result<()>{ /// let sql = Self::DBMETA.builddeletesql(#pkwheresql); /// let query = Self::dbquery(&sql) /// .bind(id) /// .bind(mobilephone); /// Self::dbexecute(pool, query).await /// .withcontext(|| "根据主键删除记录出错!")?; /// Ok(()) /// } /// /// ormpk自动实现:通过主键删除当前记录 /// pub async fn ormdelete(&self,pool: &TinyOrmDbPool) -> Result<()>{ /// Self::ormdeletebypk(pool,self.id.unwrap(),self.mobilephone.asref()).await /// } /// /// ormpk自动实现:通过主键查询记录是否存在 /// pub async fn ormexistwithpk(pool: &TinyOrmDbPool,,id:u32,mobilephone:&str) -> Result{ /// let sql = Self::DBMETA.buildexistsql(Self::DBMETA.pkwheresql); /// let row: (i64,) = sqlx::queryas(&sql) /// .bind(id) /// .bind(mobilephone) /// .fetchone(pool) /// .await /// .withcontext(|| "通过主键查询记录是否存在时出错!")?; /// Ok(row.0 > 0) /// } /// /// ormpk自动实现:通过主键查询当前记录是否存在 /// pub async fn ormexist(&self,pool: &TinyOrmDbPool) -> Result<()>{ /// Self::ormexistwithpk(pool,self.id.unwrap(),self.mobilephone.asref()).await /// } /// 只能设置一个auto主键 #[ormpk(name = "id", auto = "true")] /// insert时自动生成id pub id: Option, /// 类型名称 // 生成ormquerywithname方法 #[ormquery] pub name: String, /// 手机号码 /// 生成的ormgetbypk函数参数中,mobilephone会自动把String转换为&str #[ormpk] pub mobilephone: String, /// 密码 /// 重命名数表字段名称,否则与字段名称一致 #[ormfield(name = "password")] // 生成self.ormupdatepassword方法 #[ormupdate] password: String, /// 用户类型 /// 定义join字段和join sql /// 自动生成Self::ormqueryjoinwithusertype,Self::ormdeletejoinwithusertype,self.ormupdatejoinwithusertype方法 #[ormjoin( name="usertypeid", // 重命名表字段名称 selectfield="usertype.name as usertypename, usertype.template", join="JOIN usertype ON usertype.id = usertypeid", linkid="id",//update 时保存值使用的字段id,如:usertype.id linkidtype="u32" )] #[allow(unusedvariables)] pub usertype: UserType, /// 所属机构 /// 自动生成Self::ormqueryjoinwithorg,Self::ormdeletejoinwithorg,self.ormupdatejoinwithorg方法 #[ormjoin( name="orgid", // 重命名表字段名称 selectfield="organization.name as orgname", join="JOIN organization ON organization.id = orgid", linkid="id",//update 时保存值使用的字段id,如:org.id linkidtype="&str" )] pub org: Organization, /// 忽略字段,不在数据库中对应 #[ormignore] pub ignore_field: u32, }

impl TestUser { /// 完整创建器 /// /// # 参数说明 /// /// * id 编号 /// * name 姓名 /// * mobilephone 手机 /// * password 密码 /// * usertype 用户类型 /// * org 对应机构 pub fn new( id: u32, name: &str, mobilephone: &str, password: &str, usertype: UserType, org: Organization, ) -> Self { Self { id: Some(id), name: name.into(), mobilephone: mobilephone.into(), password: password.into(), usertype, org, ignorefield: 0, } } /// 完整创建器 /// /// # 参数说明 /// /// * id 编号 /// * name 姓名 /// * mobilephone 手机 /// * password 密码 /// * usertype 用户类型 /// * org 对应机构 pub fn newnoid( name: &str, mobilephone: &str, password: &str, usertype: UserType, org: Organization, ) -> Self { Self { id: None, name: name.into(), mobilephone: mobilephone.into(), password: password.into(), usertype, org, ignorefield: 0, } } } /// 实现数据获取接口 impl TinyOrmData for TestUser { /// 将sql返回数据映射为TestUser fn ormrowmap(row: TinyOrmSqlRow) -> Self { TestUser::new( row.get::("id"), row.get("name"), row.get("mobilephone"), row.get("password"), UserType::new( row.get::("usertypeid"), row.get("usertypename"), row.get("template"), ), Organization { id: row.get("orgid"), name: row.get("org_name"), }, ) } }

async fn getpool() -> Result { let username = "netguard"; let password = "some pass"; let ip = "localhost"; let port = 3306; let dbname = "abcnetguard"; let pool = MySqlPoolOptions::new() .maxconnections(1) .connect(&format!( "mysql://{}:{}@{}:{}/{}", username, password, ip, port, db_name )) .await?; Ok(pool) }

/// .测试SQL生成

[test]

fn testuser() { println!("user sql : \n{}", TestUser::DBMETA.select_sql); }

/// .测试SQL生成

[test]

fn testdbquery() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let data = TestUser::ormget_all(&pool).await.unwrap(); dbg!(data); }); }

/// 测试根据姓名和手机获取用户

[test]

fn testormquerywithnameandmobile() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let user = TestUser::ormquerywithnameandtel(&pool, "张三", "1850703xxxx") .await .unwrap(); dbg!(user); }); } /// 测试根据主键获取获取用户

[test]

fn testormgetwithpk() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let user = TestUser::ormgetwithpk(&pool, 2, "1387038xxxx") .await .unwrap(); dbg!(user); }); }

/// 测试根据主键删除用户

[test]

fn testormdelete() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let user = TestUser::ormgetwithpk(&pool, 15, "1867930xxxx") .await .unwrap(); user.orm_delete(&pool).await.unwrap(); dbg!(user); }); }

/// 测试根据个性删除,根据名称和手机号删除用户

[test]

fn testormdeletewithnameandtel() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); TestUser::ormdeletewithnameandtel(&pool, "盛XX", "1507031xxxx") .await .unwrap(); }); } /// 测试根据姓名和手机获取用户

[test]

fn testormquerywithname() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let user = TestUser::ormquerywithname(&pool, "张三").await.unwrap(); dbg!(user); }); } /// 测试是否存在

[test]

fn testormexistwithpk() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let isexist = TestUser::ormexistwithpk(&pool, 8, "18607031111") .await .unwrap(); dbg!(isexist); }); }

/// 测试是否存在

[test]

fn testormexistwithname() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let isexist = TestUser::ormexistwithname(&pool, "王xx") .await .unwrap(); dbg!(isexist); }); } /// 测试根据姓名和手机获取用户

[test]

fn testormupdatewithnameandtel() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let user = TestUser::ormupdatewithnameandtel(&pool, "张三", "18507032200", 4) .await .unwrap(); dbg!(user); }); } /// 测试filter

[test]

fn testdbfilter() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let sql = TestUser::DBMETA.buildselectsql("user.name like ? "); println!("{sql}"); let key = String::from("%李%"); let data = TestUser::ormfilterwith_sql(&pool, &sql, &key) .await .unwrap(); dbg!(data); }); }

/// 测试join字段查询和更新

[test]

fn testormqueryjoinwithusertype() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let usertype = UserType::new(1, "支行", ""); let mut data = TestUser::ormqueryjoinwithusertype(&pool, usertype) .await .unwrap(); let mut ygl = data.pop().unwrap(); let usertype2 = UserType::new(2, "", ""); ygl.usertype = usertype2; ygl.ormupdatejoinwithusertype(&pool).await.unwrap(); dbg!(ygl); }); }

/// 测试join字段查询和更新

[test]

fn testormqueryjoinwithorgid() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let data = TestUser::ormqueryjoinwithorgid(&pool, "14H700") .await .unwrap(); dbg!(data); }); } /// 测试join字段查询和更新

[test]

fn testormdeletejoinwithusertype() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let usertype = UserType::new(2, "支行", ""); TestUser::ormdeletejoinwithusertype(&pool, usertype) .await .unwrap(); }); } /// 测试更新所有信息

[test]

fn testormupdateall() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let mut user = TestUser::ormgetwithpk(&pool, 2, "13807931111") .await .unwrap(); let usertype = UserType::new(2, "支行", ""); user.name = "王2".into(); user.usertype = usertype; user.ormupdateall(&pool).await.unwrap(); dbg!(user); }); }

/// 测试插入信息

[test]

fn testorminsert() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let org = Organization { id: "14H700".into(), name: "上饶分行".into(), }; let usertype = UserType::new(2, "管理员", ""); let mut user = TestUser::newnoid("李四", "1850703xxxx", "sss", usertype, org); user.orminsert(&pool).await.unwrap(); dbg!(user); }); } /// 测试根据姓名和手机获取用户

[test]

fn testormupdatepassword() { tokio::runtime::Builder::newcurrentthread() .enableall() .build() .unwrap() .blockon(async { let pool = getpool().await.unwrap(); let mut user:TestUser = TestUser::ormquerywithname(&pool, "张三").await.unwrap().pop().unwrap(); user.password="this is a pass".into(); user.ormupdate_password(&pool).await.unwrap(); dbg!(user); }); }

```