2个不稳定版本
0.2.0 | 2023年4月16日 |
---|---|
0.1.0 | 2023年4月15日 |
在 数据库实现 中排名 153
每月下载量 21
165KB
4K SLoC
🧁 Sqlo 🍰
内容
- 安装
- 推导Sqlo
- 关系
- 方法:get save delete remove
- 宏:insert! update! select!
- 子句:where join group by having 分页 子查询
这是什么?
Sqlo 是在Rust中使用关系型数据库创建一个漂亮API的又一次尝试。
Sqlo 基于sqlx构建,并使用sqlx宏,这样你可以在编译时保留sqlx的全部功能,同时减少样板代码。
目前,支持Sqlite、Postgres和MySql。
它具有一些ORM-like功能,但不是真正的ORM。
主要功能
- 几乎没有样板代码。
- 提供
get, save, delete
方法。 - 直观、易于使用的宏API:
select!, insert!, update!
:使用Rust代码和结构体以Rust语法编写Sq queryl。 - 快速访问给定行的外键。
- 支持sqlite、postgres和mysql
安装
#Cargo.toml
sqlo = {version="0.1.0", features=["sqlite"]}
// or
sqlo = {version="0.1.0", features=["postgres"]}
// or
sqlo = {version="0.1.0", features=["mysql"]}
它是如何工作的?
给定这个Sql数据库。
CREATE TABLE my_table (
id INTEGER PRIMARY KEY,
test TEXT NOT NULL,
maybe INTEGER
);
仅推导 Sqlo
宏
#[derive(Sqlo, PartialEq)]
struct MyTable {
id: i64,
text: String,
maybe: Option<i64>,
}
...
use sqlo::{select, update, insert};
//
let pool = get_my_db_pool().await?;
// create row
let a = insert!(. MyTable text="hello")(&pool).await?;
// retrieve row by primary_key
let mut b = MyTable::get(&pool, a.id).await?
//or
let mut b = select!(.MyTable[1])(&pool).await // the `.` means fetch_one
assert_eq!(a,b);
// update a full row with instance
b.text = "bye".to_string();
b.save(&pool).await?;
// select: where order limit
let items : Vec<Maison> = select.await?;
// `*` means fetch_all, use `+` for fetch, `.` for `fetch_one`
// select: sql function, group_by, force non null alias.
let items = select.await?;
// or
let items = select.await?;
// Aliases can be reused along the query
// update with instance (parenthesis)
update.await?; // No `.,+,*` means `execute`.
// or with primary_key (brackets)
let c = update[. MyTable[b.id] text="I'm Back", maybe=Some(12)](&pool).await?; // `.` means fetch_one
// remove by instance
c.remove(&pool).await?
//or delete with pk
MyTable::delete(&pool, pk).await?
推导Sqlo
结构体属性
所有属性(结构体或字段)都应位于 Sqlo
属性之下。
表名
Sqlo 预期表名是结构体名称转换为 snake_case。您可以使用 tablename 属性进行更改。
#[derive(Sqlo)]
#[sqlo(tablename="another_name")]
struct MyTable {}
字段属性
primary_key
默认情况下,使用 id
字段作为主键。可以使用 primary_key
标志进行更改
#[derive(Sqlo)]
struct MyTable {
#[sqlo(primary_key)]
name: String
}
column
默认情况下,字段名称用作列名称。可以使用 column
标志进行更改
#[derive(Sqlo)]
struct MyTable {
#[sqlo(column="what_a_column_name")]
name: String
}
insert_fn
当调用 insert! 时,将使用该函数来填充主键字段。
insert_fn
:提供作为字符串的可调用对象,作为主键值调用。
#[derive(Sqlo)]
struct MyTable {
#[sqlo(insert_fn="uuid::Uuid::new_v4")]
id: Uuid,
name: String
}
//...
let instance = insert!(.MyTable name="some string")(&p.pool).await.unwrap();
assert_eq!(instance.id, Uuid("someuuidv4"))
type_override
在内部,当 Sqlo
使用 sqlx::query_as!
时,它将为列使用类型覆盖(sqlx 类型覆盖),因此它给出 select field as "field:_", ...
而不是 select field, ...
。
关系
可以指定关系并在查询中稍后使用它们。
这是通过向字段添加具有 fk
属性的外键来完成的。然后在查询中使用的相关名称将是 snake_case 相关结构体名称。例如:MyRoom=>my_room。可以使用 related
属性更改相关名称。
#[derive[Sqlo, Debug, PartialEq]]
struct House {
id: i64,
name: String,
width: i64,
height: i64
}
#[derive[Sqlo, Debug, PartialEq]]
struct Room {
id: i64,
#[sqlo(fk = "House")]
house_id: i64
}
// will use myhouse.room in queries
// or
#[derive[Sqlo, Debug, PartialEq]]
struct Room {
id: i64,
#[sqlo(fk = "House", related = "therooms")]
house_id: i64
bed: bool
}
// will use myhouse.therooms in queries.
存在类型检查,因此 fk
字段必须与目标结构体的主键具有相同类型(或 Option
)。
实体和关系保存在在编译时创建的 .sqlo
目录中。根据编译顺序,如果在关系中对 Sqlo 实体
进行了目标但在解析之前,它可能会首次失败。只需重新构建第二次,它就会通过。
.sqlo
可选添加到 VCS。尽管这不是其主要目的,但 .sqlo
的版本控制似乎在代码更改的情况下提供了一些额外的安全性。内容是简单的 JSON 文件,非常易于阅读。
fk
文本可以是标识符("MyRoom"
)或路径("mycrate::mydir::MyRoom"
)。
使用同一结构体中声明的 fk
进行自连接
#[derive(Sqlo)]
struct Employee {
id: i64
name: String
#[sqlo(fk="Employee"), related="manager"]
manager_id: Option<i64> // here the type is not i64 but Option<i64> since en employe may be the bosse and have no manager.
}
方法
简介
- 每个返回派生结构体实例的方法都使用
sqlx::query_as!
。 - 第一个参数始终是数据库连接。
参数类型
i8
、u8
、i16
、u16
、i32
、u32
、i64
、u64
、bool
不是按引用传递的。String
预期&str
。BString
期望&BStr
。Vec<u8>
期望&[u8]
。Option<T>
期望Option<T>
- 其他所有内容都是通过引用传递的。
#[derive(Sqlo)]
struct MyTable {
id: i64,
name: String,
some_type: Option<String>
}
///...
get
通过主键获取一行。
返回值:sqlx::Result<T>
let row = MyTable::get(&pool, 23).await?
assert_eq!(row.id, 23);
save
更新完整行或如果存在则插入。它基于主键的UPSERT。
返回值:sqlx::Result<DB::QueryResult>
#[derive(Sqlo, Debug, PartialEq)]
struct MyTable {
id: i64,
name: String,
alive: bool,
members: Option<i64>
}
//...
let mut mytable = MyTable{id:1, name:"bla".to_string(), alive:true, membres:None};
mytable.save(&pool).await?;
// doesn't exists then equivalent to insert!(Mytable id=1, name="bla", alive=true)(&pool).await?
mytable.members = Some(345);
mytable.save(&pool);
// equivalent to update!(MyTable(mytable) members=Some(345))(&p.pool).await?
let mytable2 = MyTable::get(&pool, 1).await?;
assert_eq!(mytable, mytable2);
delete
通过主键删除一行。
返回值:sqlx::Result<DB::QueryResult>
>>
MyTable::delete(&pool, 43).await?
remove
通过其实例删除一行。remove
将拥有实例的所有权,之后将无法使用。
返回值:sqlx::Result<DB::QueryResult>
myrow.remove(&pool).await?;
myrow.some_field = 1; // compile_error
宏:介绍
Sqlo 支持 select!
、insert
和 update!
宏。我们尽量保持 API 一致,以便更容易记忆和使用。在本章中,我们将解释使用这些宏的核心原则,下一章将解释每个宏。
-
宏仅作为 sqlx 宏
sqlx::query! 和 sqlx:query_as!
的语法糖。 -
宏返回一个闭包,该闭包以
sqlx Executor
作为唯一参数。返回类型取决于您使用的内容,与fetch_one, fetch_all, fetch, execute, ...
相同。 -
这是 Rust 语法而不是 SQL:这就是我们为什么使用
==
而不是=
的原因。 -
Sqlo 宏内容转换为 sqlx 内容
select
// is replaced with
sqlx::query_as!(House, "select * from house h where h.room > ?", 23).fetch_one(&pool)
这意味着,经过 sqlo 的检查后,sqlx 的检查将按常规进行。
-
每个字面量、变量参数等都被作为参数传递给 sqlx 宏。
-
sqlx 方法调用选择使用与常规查询开头相同的标点符号。它遵循众所周知的正则表达式语法
- nothing -> execute(返回无)
- . -> fetch_one(一个)
- * -> fetch_all(零个或多个)
- ? -> fetch_optional(一个或零个)
- + -> fetch(一个或多个。)
有关更多信息,请参阅 sqlx 文档。
update!
宏
它支持以下格式
update![TableStruct[instance_id] field1=value1, field2=value2](&pool).await?
// with square bracket instance id is a u32, string, &str, Uuid, ....
update.await?
// use an instance of TableStruct, primary_key is deduced.
// this format takes ownership of instance sor you can't use instance after.
// To reuse instance you have to specify a return (fetch_one, fetch_all, fetch)
let instance = update.await?
// not the dot `.` meaning fetch_one
// only with sqlite and postgres
#[derive[Sqlo, Debug, PartialEq]]
struct House {
id: i64,
name: String,
width: i64,
height: i64
}
//...
let house = House::get(&pool, 2);
let house = update.await?;
let big_height = 345;
update_House!(House[2] height=big_height)(&pool).await?;
//or
update_House!(House[2] height=::big_height)(&pool).await?;
insert!
宏
它支持以下格式
#[derive[Sqlo, Debug, PartialEq]]
struct House {
#[sqlo(insert_fn="some::func::to_create_ids")]
id: i64,
name: String,
width: i64,
height: Some(i64)
}
// with all fiekds
insert.await?
// with all fields, None explicit
insert.await?
// with all fields, None implicit
insert.await?
// using the `insert_fn` for primary key
insert.await?
// returning instance
let house = insert.await?
// with variable
let a = 1;
insert.await?
//or
insert.await?
如果数据库管理系统支持,主键也可以省略。
返回实例使用 .
,在 SQL 中使用 insert.... returning
。实际上与 MariaDB 不完全兼容
《select!
》宏
使用《select!
》宏执行选择查询。
// query returning a derived sqlo struct
let res: Vec<MyStruct> select.await.unwrap();
// select * from mystruct_table where mystruct_table.myfield >1
// query some specific values/column
let res = select.await.unwrap();
assert_eq!(res.bla, 99)
让我们使用这些结构体来编写本章。
#[derive[Sqlo, Debug, PartialEq]]
struct House {
id: i64,
name: String,
width: i64,
height: i64,
zipcode: i64
}
struct Room {
id: i64,
#[sqlo(fk = "House", related = "therooms")]
house_id: i64
bed: bool
}
简介
基本上,对于平面结构体查询,它底层使用 sqlx::query_as!
,只翻译查询或 sqlx::query!
用于字段/列查询。
select![* House where bed == true].await
//roughly is translated into
query_as![House, "SELECT DISTINCT id, name, width, height FROM house where bed=?", true].fetch_all(&p.pool).await;
select.await;
//roughly is translated into
query!["SELECT DISTINCT max(width) AS width_max FROM house where height > ?", 1].fetch_one(&pool).await
请注意,它假设一个 主 sqlo 结构体(这里为 House
),从中推断出字段/列、关系/相关字段。
一些通用规则
- Sqlo 尝试自动通过添加
DISTINCT
来避免重复,因为重复的需求非常罕见。因此,请注意,每个《select!
》查询都不会有重复的结果。
查询列
默认情况下,《select!
》查询主结构体的所有字段。但如果您想查询某些列,也可以这样做
select.await;
-
它将使用
sqlx::query!
而不是sqlx::query_as!
。 -
但
sqlx::query_as!
也可以用于指向另一个结构体,在结构体名称前添加逗号分隔
struct Total {
all: i32
}
let total = select.await.unwrap();
assert_eq!(total.all, 5);
- 我们支持以下 "列" 格式
- 标识符(
id,
width
,...):一个字段。 - 字段访问(
therooms.bed
):访问相关字段。这将添加一个 INNER JOIN - 字段访问(
therooms=.bed
):访问相关字段。这将添加一个 LEFT JOIN - 带有结构体名称的字段访问:
House.width
- SQL 函数(
sum(id)
,replace(adresse, "1", "345")
) - 二元运算(
id + 3
) - 一元:
-id,
-1
,... - 情况:使用 Rust
match
case when then
- 标识符(
在查询的 "选择" 部分(查询的列)中,函数、操作、一元必须后面跟一个标识符 as
。
SQL 函数的参数可以是标识符字段、字段访问、文字("text"
)或任何 Rust 表达式(数组索引、实例字段访问、简单变量)。在最后一种情况下,如果需要,可以逃逸一个 ::
let myvar = "bla".to_string();
let myarray = ["bli", "ble", "blo"];
select![* House replace(name, ::myvar, ::myarray[1]) as new_name](&pool).await.unwrap();
//sqlx::query!["SELECT REPLACE(name, ?, ?) as new_name FROM house", myvar, myarray[1]].fetch_all(&pool)
- 可以使用与 Sqlx 的覆盖 相同的方式使用
select![* House replace(name, ::myvar, ::myarray[1]) as "new_name!:String"](&pool).await.unwrap();
但与 sqlx
不同,您不需要重复相同的复杂别名以供进一步使用
sqlx::query![r#"SELECT id, count(width) as "total!:i32" group by "total!:i32" "#]
//instead with sqlo, just repeat the alias name without type indication
select![. House id, count(width) as "total!:i32" group_by total]
作为一个方便的快捷方式,可以在别名或字段上使用 !
和 ?
而无需引号。
select![. House id as id!, count(width) as total?]
//or
select![. House id!, count(width) as total?]
- 还可以使用
*
。
select![.House count(*)]
使用 Rust 项目作为参数
Rust 项目可以传递给表达式。如果字段和变量的名称相同,请在名称前添加 ::
以强制使用变量而不是字段。
// Variables
let width = 34;
select![* House where height == ::width] // Right hand part of the expression will refere to the variable width not the field `width` of struct House
select![* House where width == ::width]
select![.House where id == ::width] // variable width is used
// sql : select * from house where id=? (? will be 34 as parameter)
select![.House where id == width] // variable width is ignored, column name wil be used in sql
// sql : select * from house where id=width
目前,索引和其他结构字段的使用必须使用 ::
。
// Indexing
let array = [1 , 2, 3]
select![. House where width == ::array[0]]
// struct field
struct A {b:i32}
let a = A{b:2}
select![. House where width == ::a.b]
Case When Then
我们使用 Rust 的 match
表达式,但没有括号和 _
作为 else 收集器。
select[.House id, match width 33=>"small", 100=>"big", _=>"don't know" as "how_big:String"]
//sqlx::query![r#"SELECT id, CASE width WHEN ? THEN ? WHEN ? THEN ? ELSE ? END as "how_big:String""#,33,"small",100, "big", "dont know"]
select[.House id, match width<33=>"small", width<100=>"big", _=>"very big" as "how_big:String"]
//sqlx::query![r#"SELECT id, CASE WHEN house.width<? THEN ? WHEN house.width<? THEN ? ELSE ? END as "how_big:String""#,33,"small",100, "big", "very big"]
子句
WHERE 子句
它是二进制表达式的聚合,以下是一些使用案例,按 SQL 使用的顺序
- 字段:
select![House where id == 1]
- 二元运算符:
select![House where width >= 1]
- IS NULL:
select![House where width == None]
- IS NOT NULL:
select![House where width != None]
- BETWEEN:
select![House where width > 1 && width <5]
- 使用括号:
select![House where (width==1 || width==2) && height==4]
- 不要在括号中使用
!
:select![House !(width>5)]
- IN:
select![House where id in (1,3,4)
- LIKE:使用
#
运算符:select![House where name # "%bla"]
。 - 来自连接的列:见 WHERE 子句中的 JOIN
- 函数调用:
select![House where trim(name) == "myhouse"]
- AND, OR:使用
&&
、||
连接表达式
关系
简介
您可以通过“虚拟字段”访问相关的行/集合,该字段由 fk
属性 指定。
Sqlo 支持两种处理关系的方式。
- 第一种是不使用
JOIN
,允许您直接查询一些相关条目。 - 第二种使用与常规查询相同的
JOIN
。
不使用 JOIN 检索相关行
您可以通过“虚拟字段”访问相关的行/集合,该字段由 fk
属性指定。
- 通过索引其主键来访问行(
House[1]
、House[myvar]
、House[some.field]
或House[someindex[1]]
)。 - 通过相关名称访问“虚拟”相关字段:
House[1].therooms
。
// select all related rooms of house where there is a bed
let a = 1;
let romms: Vec<Room> = select![* House[a].therooms where bed == true](&pool).await.unwrap();
//sqlx::query_as![Room, r#"SELECT * FROM room where id=? AND bed=?"#, a, true].fetch_all...
使用 JOIN
当使用相关字段时,JOIN 会自动添加到查询中。
使用以下方式选择 JOIN 类型
- 使用
.
进行 INNER JOIN,例如:therooms.bed
- 使用
=.
进行 LEFT JOIN(类似于 Rust 范围中的包含=
)例如:therooms=.bed
select![* House where therooms.bed == true]
// sqlx::query_as![House, "SELECT * FROM house INNER JOIN room ON house.id=room.maison_id WHERE room.bed == ?", true].fetch_all
select![ * House where width>3 && therooms=.bed == true]
// sqlx::query_as![House, "SELECT * FROM house LEFT JOIN room ON house.id=room.maison_id WHERE house.width> ? AND room.bed == ?", 3, true].fetch_all
select![. House id, count(therooms.id) as total]
// sqlx::query_as![House, "SELECT maison.id, count(room.id) as total FROM house JOIN room ON house.id=room.maison_id"].fetch_one
由于 JOIN 类型需要保持一致,请注意。
select![* House id, therooms.id where therooms=.bed == true] // BAD you use to different joins INNER and LEFT (sqlx will fail)
select![* House id, therooms=.id where therooms=.bed == true] // GOOD : the join is expressed in the same way
关于 LEFT JOIN 和 Postgres 的说明:在 Postgres 中,sqlx 无法对 null 可用性做出任何假设,可能会出现 Decode(UnexpectedNullError)
错误。因此,您必须自己推断 null 可用性,并添加 ?
select![* House id, therooms=.id as "rooms_id?"]
Group By 子句
使用 group_by
关键字后跟列或别名名称来对结果进行分组。
使用括号语法 []
。
select![.House width, count(id) as "total!:i32" group_by width order_by total]
select![.House name, count(therooms.house_id) as total group_by name] // follows foreign keys
Having 子句
像在 SQL 中一样使用 having 子句。同样可以使用括号语法 []
select![.House id, sum(width) as total having total > 350]
// with foreign keys
select![.House id, count(therooms.id) as total having total > 4]
Order By 子句
使用 order_by
关键字对结果进行排序。在字段名之前使用 -
指定降序。
使用括号语法 []
。
select![*House order_by -width, height]
select![*House order_by[-width, height]]
select![*House id, width as "bla:i32" order_by bla]
Limit/Offset 和分页
Limit 和 Offset
使用带有可选 offset
的 limit
子句,以逗号分隔。
使用括号语法 []
。
select![*House limit 5] // SELECT * FROM house LIMIT 5
select![*House limit 5,8] // SELECT * FROM house LIMIT 5 OFFSET 8
select![*House limit[5,8]] // SELECT * FROM house LIMIT 5 OFFSET 8
在使用 order by
和 limit
一起时,sqlx 存在一个错误:每个字段都被期望为可空的,这是不正确的。目前,要处理这个用例,您必须强制为每个列设置非空性(除 Option 字段外)。
select![*House, House id as "id!", width as "width!", height as "height!", name as "name!" order_by name limit 4]
// when using fields `select!` uses query_as! behind the back so reinforce using query_as! with House
分页
我们支持自定义 page
来通过 page 查询,必须有一个用 逗号 分隔的 page_size。
使用括号语法 []
。
let limit = select![*House limit 2,4].fetch_all(&p.pool).await.unwrap();
let page = select![*House page 3,2].fetch_all(&p.pool).await.unwrap(); //means page 3 with page size of 2.
// will both select 5th et 6th entries.
assert_eq!(limit, page);
子查询
子查询使用花括号 {}
完成。
select...
// transltates to
// sqlx::query_as!(House, "select * from house where zipcode in (select distinct zip from zip_table where zip > ?)", 260 ).fetch_all...
也可以用于返回值。
select![*House id, {HouseKind count(*) where width == House.width} as kind_total ]
// a few notes here :
// - it needs an alias since it's returned
// - use the struct name to leverage ambigous fields (here width)
// - no `as` is required in the subquery since it's not returned
支持 exists
关键字
select![*House where zipcode where exists {ZipCodeTable zip where zip > 260}].fetch_all...
调试查询
使用环境变量调试所有查询
- SQLO_DEBUG_QUERY:将显示查询是如何被翻译的
- SQLO_DEBUG_QUERY_ALL:将显示查询是如何被翻译的 + 参数
或者
在 macrs 中,使用 dbg!
调试单个查询。
select![dbg! * House where width >30]...
贡献
每个贡献都受到热烈的欢迎。请在花费时间之前先打开一个问题来讨论。
步骤
- 安装 taskfile
- 克隆存储库
- 设置开发数据库
- task run: 设置数据库
- task stop: 取消设置数据库
- task reset: 取消设置 + 设置
- 进行您的更改
- 使用 task test: 运行所有数据库上的所有测试来测试它
- 一些格式化
- task clippy
- cargo fmt
- 备注
- 每个命令都有其数据库专用变体:sq-check、sq-test、pg-test、pg-setup 等。支持的词首 sq、pg 和 my。
- 由于 SQL 语法的一些特定性,每个数据库后端都有自己的迁移文件,但内容最终是相同的。
- 帮助调试查询
- 以以下格式在测试中获得输出:
task sq-test -- some_tests -- --nocapture
- 推送 PR。
依赖关系
~4.5–6.5MB
~119K SLoC