10 个版本 (破坏性更新)
0.8.1 | 2024 年 2 月 22 日 |
---|---|
0.7.0 | 2023 年 5 月 31 日 |
0.6.0 | 2023 年 3 月 16 日 |
0.4.0 | 2022 年 11 月 27 日 |
#7 在 #surrealdb
150KB
2.5K SLoC
虚幻简单查询构建器
为 SurrealDB 提供的简单查询构建器,用于 Surreal 查询语言,旨在简单易用且不过分冗长。
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct IUser {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Id>,
pub handle: String,
pub messages: ForeignVec<IMessage>,
}
model!(User {
id,
pub handle,
pub messages
});
impl IUser {
pub fn find_by_handle(handle: &str) -> ApiResult<Vec<Self>> {
use surreal_simple_querybuilder::queries::select;
use schema::model as user;
let (query, params) = select("*", user, (Where(user.handle, handle), Fetch([&*user.messages]))?;
let items = DB.query(query).bind(params).await?.take(0)?;
items
}
}
摘要
为什么需要一个查询构建器
查询构建器允许您通过一些编译时检查动态构建查询,以确保它们产生有效的 SQL 查询。与 ORM 不同,查询构建器旨在轻量级且易于使用,这意味着您决定何时何地使用它。对于简单查询,您可以坚持使用硬编码的字符串,但对于需要参数和变量且可能根据这些变量更改的复杂查询,则可以使用构建器。
虽然该库最初是作为查询构建实用工具设计的,但它还附带了一些宏和泛型类型,这些类型在您在 Rust 代码中管理 SQL 模型时可能会有所帮助。请参阅节点宏和外键类型示例。
SQL 注入
您传递给查询构建器的字符串没有进行任何清理。请使用参数化查询,例如使用 SET username = $username
,以避免注入问题。然而,该软件包附带了一些实用函数,可以轻松创建参数化字段,请参阅 NodeBuilder
特性。
编译器需求和功能
该软件包使用常量表达式来创建其 模型创建宏,以便使用由编译器推导的堆栈数组大小。因此,任何使用该软件包的程序都必须在主文件的根目录中添加以下内容
#![allow(incomplete_features)]
#![feature(generic_const_exprs)]
示例
请注意,所有展示的功能都可以独立使用。如果您想,它们都可以组合在一起,但如果您更倾向于轻量级解决方案,也是可以的。
默认情况下,仅提供查询构建器,其他模块需要您启用它们各自的软件包功能。
- 提供了一系列 示例,以提供对软件包核心功能的引导介绍。
- 一个综合示例可以在替代的 surrealdb-architecture 仓库中找到。
- 有关软件包中每个组件的解释,请参阅下面的章节。
具有动态参数的预定义查询(queries
功能)
该软件包提供了一组预定义查询,您可以在 surreal_simple_querybuilder::queries::*;
或在引言中访问,以便更容易访问。
use surreal_simple_querybuilder::prelude::*;
fn main() {
let (query, _bindings) = select("*", "user", ());
assert_eq!(query, "SELECT * FROM user");
}
这些预定义查询函数接受所有类型的参数,以进一步扩展查询。如果在这些参数之间传递动态值(变量),则函数将自动将它们添加到绑定列表中。
use surreal_simple_querybuilder::prelude::*;
use serde_json::json;
fn main() {
let (query, bindings) = select("*", "user", Where(json!({ "name": "John" })));
assert_eq!(query, "SELECT * FROM user WHERE name = $name");
// 👇 the bindings were updated with the $name variable
assert_eq!(bindings.get("name"), Some("John".to_owned()));
}
为什么需要动态参数
乍一看,这些预定义查询似乎没有查询构建器不具备的功能,但实际上,它们允许您轻松地在后端(例如)创建函数,如果需要,可以扩展它们。
第一个想到的场景是标准函数,用于按作者检索书籍
impl Book {
fn find_by_author_id(id: &str) -> Vec<Self> {
// ...
}
}
在某些情况下,您可能只需要书单,而在另一些情况下,您可能需要分页的结果,有时您可能还希望获取书籍上的作者数据。考虑到您可能还需要具有分页和获取书籍的功能,这可能会导致至少编写 4 个不同的函数和查询。
使用动态参数,您可以更新您的 find
函数以接受可选参数,从而只需要一个简单的函数。
use serde_json::json;
impl Book {
fn find_by_author_id<'a>(id: &str, params: impl QueryBuilderInjecter<'a> + 'a) -> Vec<Self> {
let filter = Where(json!({"author": id}));
let combined_params = (filter, params);
let (query, params) = select("*", "Book", combined_params).unwrap();
DB.query(query)
.bind(params)
.await.unwrap()
.get(..).unwrap()
}
}
因此,您现在可以这样做
let books = Book::find_by_author_id("User:john", ());
let paginated_books = Book::find_by_author_id("User:john", Pagination(0..25));
let paginated_books_with_author_data = Book::find_by_author_id(
"User:john",
(
Pagination(0..25),
Fetch(["author"])
)
);
动态参数和预定义查询是为了 模型 宏而设计的,您不一定需要它,但如果您想要的话,这两个系统都可以用于一些编译时检查和动态参数,以享受动态参数提供的额外自由度,同时确保由于模型的存在,您引用的所有字段和节点都是有效的。如何结合这两个系统的完整示例可以在 这里 获得。
或者,如果您不喜欢使用泛型参数,您可以使用一个实现了 QueryBuilderInjecter
的枚举。surrealdb-architecture 仓库演示了如何设置一个。
预定义查询和参数的限制和建议
以下简例(简例)和完整的测试用例(测试用例)展示了预先定义的查询在99%的情况下可以正常工作,并且可以极大地简化您编写的代码。然而,在深入使用预先定义的查询之前,有一些限制需要您注意。
预先定义的查询和可组合的参数是为了那些简单的用例而设计的,在这些用例中,您只需要在WHERE子句中执行简单的选择、创建等操作,而不需要进行复杂的过滤。例如,通过其中一个字段选择书籍非常适合预先定义的查询,因为您可以添加一个fetch子句,而不需要重写任何内容。它允许您在简单的情况下,在代码库中拥有一些通用的函数。
但是,一旦变得复杂,就应该使用QueryBuilder
类型而不是预先定义的查询。它将提供更好的性能和更可预测的结果(嵌套大量参数可能会生成意外的查询)。请注意,如果需要,您仍然可以使用查询构建器并传递参数(即注入器)。
use surreal_simple_querybuilder::prelude::*;
let params = (
Where(("name", "john")),
Fetch(["articles"])
);
let query = QueryBuilder::new()
.select("*")
.from("user")
.injecter(¶ms) // <-- pass the injecter to the builder
.build();
let _params = bindings(params); // <-- get the variables so you can bind them
assert(query, "SELECT * FROM user WHERE name = $name FETCH articles");
如您所见,即使在更复杂的情况下,参数仍然可以使用,但是预先定义的查询则不应该使用。
model
宏(model
功能)
model
宏允许您快速创建与数据库节点匹配的字段的结构体(即模型)。
示例
use surreal_simple_querybuilder::prelude::*;
struct Account {
id: Option<String>,
handle: String,
password: String,
email: String,
friends: Foreign<Vec<Account>>
}
model!(Account {
id,
handle,
password,
friends<Vec<Account>>
});
fn main() {
// the schema module is created by the macro
use schema::model as account;
let query = format!("select {} from {account}", account.handle);
assert_eq!("select handle from Account", query);
}
这允许您为字段创建编译时检查的常量,这使得您在构建查询时可以引用它们,而无需担心拼写错误或使用很久以前重命名的字段。
模型中的公共和私有字段
QueryBuilder类型提供了一系列方法,可以快速在SET或UPDATE语句中列出您模型的字段,这样您就不需要逐个编写字段和变量名。由于您可能不希望序列化某些字段,例如id
,因此模型宏使用pub
关键字将字段标记为可序列化。任何没有在前面添加pub
关键字的字段将不会被这些方法序列化。
model!(Project {
id, // <- won't be serialized
pub name, // <- will be serialized
})
fn example() {
use schema::model as project;
let query = QueryBuilder::new()
.set_model(project)
.build();
assert_eq!(query, "SET name = $name");
}
模型之间的关系
如果您想在模型中包含关系(即边),则model
宏对这些关系有特殊的语法。
mod account {
use surreal_simple_querybuilder::prelude::*;
use super::project::schema::Project;
model!(Account {
id,
->manage->Project as managed_projects
});
}
mod project {
use surreal_simple_querybuilder::prelude::*;
use super::project::schema::Project;
model!(Project {
id,
name,
<-manage<-Account as authors
});
}
fn main() {
use account::schema::model as account;
let query = format!("select {} from {account}", account.managed_projects);
assert_eq!("select ->manage->Project from Account");
let query = format!("select {} from {account}", account.managed_projects().name.as_alias("project_names"))
assert_eq!("select ->manage->Project.name as project_names from Account", query);
}
部分构建生成
该宏支持可以传递给生成更多代码的条件标志。其中之一是生成“部分”构建器。部分类型是您创建的模型的副本,其中所有字段都设置为Option<serde_json::Value>
,并使用serde标志跳过序列化时为None
的字段。
这样的部分构建器可以像这样使用
// notice the `with(partial)`
model!(Project with(partial) {
id,
pub name
});
let partial_user = PartialProject::new()
.name("John Doe");
这种部分类型由于其ok()
方法而非常方便
let partial_post = PartialPost::new()
.title("My post title")
.author(PartialUser::new().name("John Doe"))
.ok()?;
这将输出以下展开的JSON
{
"title": "My post title",
"author.name": "John Doe"
}
如果您想得到一个正常的嵌套对象,则可以省略ok
调用,并将对象传递给您的选择的序列化函数。
然后您可以在查询中使用构建器
let user = DB.update(user_id)
.merge(PartialUser::new()
.name("Jean")
.posts(vec![post1_id, post2_id])
.ok()?
).await?
// ...
let filter = Where(PartialPost::new()
.title("My post title")
.author(PartialUser::new().name("John Doe"))
.ok()?);
let posts = queries.select("*", "post", filter).await?;
请注意,部分构建器是使用serde_json::json!
宏和模型构建JSON对象的替代语法。上面的示例与以下示例相同,因此请选择您喜欢的任何解决方案
let user = DB.update(user_id)
.merge(json!({
model.name: "Jean",
model.posts: vec![post1_id, post2_id]
})).await?
// ...
// the wjson! macro is a shortcut to `Where(json!())`
let filter = wjson!({
model.title: "My post title",
model.author().name: "John Doe"
});
let posts = select("*", "post", filter).await?;
NodeBuilder
特性(querybuilder
功能)
这些特性为 String
和 str
类型添加了一些实用函数,可以与查询构建器一起使用,提供更大的灵活性。
use surreal_simple_querybuilder::prelude::*;
let my_label = "John".as_named_label("Account");
assert_eq!("Account:John", &my_label);
let my_relation = my_label
.with("FRIEND")
.with("Mark".as_named_label("Account"));
assert_eq!("Account:John->FRIEND->Account:Mark", my_relation);
QueryBuilder
类型(querybuilder
功能)
它允许您通过 段 和易于使用的方法动态构建复杂或简单的查询。
简单示例
use surreal_simple_querybuilder::prelude::*;
let query = QueryBuilder::new()
.select("*")
.from("Account")
.build();
assert_eq!("SELECT * FROM Account", &query);
复杂示例
use surreal_simple_querybuilder::prelude::*;
let should_fetch_authors = false;
let query = QueryBuilder::new()
.select("*")
.from("File")
.if_then(should_fetch_authors, |q| q.fetch("author"))
.build();
assert_eq!("SELECT * FROM Account", &query);
let should_fetch_authors = true;
let query = QueryBuilder::new()
.select("*")
.from("File")
.if_then(should_fetch_authors, |q| q.fetch("author"))
.build();
assert_eq!("SELECT * FROM Account FETCH author", &query);
ForeignKey
和 Foreign
类型(foreign
功能)
SurrealDB 具有从外键中获取数据的能力。例如
create Author:JussiAdlerOlsen set name = "Jussi Adler-Olsen";
create File set name = "Journal 64", author = Author:JussiAdlerOlsen;
select * from File;
select * from File fetch author;
这给我们带来了
// without FETCH author
{
"author": "Author:JussiAdlerOlsen",
"id":"File:rg30uybsmrhsf7o6guvi",
"name":"Journal 64"
}
// with FETCH author
{
"author": {
"id":"Author:JussiAdlerOlsen",
"name":"Jussi Adler-Olsen"
},
"id":"File:rg30uybsmrhsf7o6guvi",
"name":"Journal 64"
}
这个功能的问题在于,我们的结果可能包含指向作者的ID,没有值,或者根据查询是否包含 fetch
以及查询内容,可能包含完全检索到的作者及其数据。
ForeignKey
类型正是为了解决这个问题而生的。它是一个有3个变体的枚举
- 当它被检索时加载的数据
- 当它只是一个ID时的键数据
- 当它是空时未加载的数据(如果您希望支持缺失数据,您必须使用
#serde(default)
属性到字段中)
该类型包含对 Deserialize 和 Serialize serde 特性的实现,以便在需要时回退到找到或需要的数据。但是,任何由 ForeignKey
引用的类型都必须实现 IntoKey
特性,以允许它在序列化过程中安全地将它序列化到一个ID。
示例
/// For the tests, and as an example we are creating what could be an Account in
/// a simple database.
#[derive(Debug, Serialize, Deserialize, Default)]
struct Account {
id: Option<String>,
handle: String,
password: String,
email: String,
}
impl IntoKey<String> for Account {
fn into_key<E>(&self) -> Result<String, E>
where
E: serde::ser::Error,
{
self
.id
.as_ref()
.map(String::clone)
.ok_or(serde::ser::Error::custom("The account has no ID"))
}
}
#[derive(Debug, Serialize, Deserialize)]
struct File {
name: String,
/// And now we can set the field as a Foreign node
author: Foreign<Account>,
}
fn main() {
// ...imagine `query` is a function to send a query and get the first result...
let file: File = query("SELECT * from File FETCH author");
if let Some(user) = file.author.value() {
// the file had an author and it was loaded
dbg!(&user);
}
// now we could also support cases where we do not want to fetch the authors
// for performance reasons...
let file: File = query("SELECT * from File");
if let Some(user_id) = file.author.key() {
// the file had an author ID, but it wasn't fetched
dbg!(&user_id);
}
// we can also handle the cases where the field was missing
if file.author.is_unloaded {
panic!("Author missing in file {file}");
}
}
ForeignKey
和序列化过程中的加载数据
ForeignKey
默认会尝试将其自身序列化到一个ID。这意味着如果外键包含值而不是ID,它将调用值上的 IntoKey
特性以获取用于序列化的ID。
在某些情况下,这可能会引起问题,例如在一个API中,您希望序列化一个包含 ForeignKey
字段的 struct,以便用户可以在单个请求中获得所有需要的数据。
默认情况下,如果您要序列化一个包含检索到的 author
的 File
(来自上面的示例) struct,它将自动转换为作者的ID。
ForeignKey
struct 提供了两种方法来控制这种行为
// ...imagine `query` is a function to send a query and get the first result...
let file: File = query("SELECT * from File FETCH author");
file.author.allow_value_serialize();
// ... serializing `file` will now serialize its author field as-is.
// to go back to the default behaviour
file.author.disallow_value_serialize();
您可能会注意到,不需要可变性,这些方法使用内部可变性,即使在不可变的 ForeignKeys 上也需要。
结合官方 SurrealDB 客户端使用查询构建器
关于这个查询构建器 crate,有一件重要的事情需要记住,它旨在作为一个完全独立的实用程序 crate,不依赖于您使用的客户端。因此,它不提供直接发送查询和获取响应的功能,但是由于您很少会不使用客户端而使用此 crate,我维护了一个 外部存储库,演示如何结合官方客户端和 surreal-simple-querybuilder crate。
虽然自己编写这些函数不太方便,但它允许您使用查询构建器 crate 的固定版本,同时仍然获得您最喜欢的客户端的最新破坏性更新。
依赖关系
~0.8–3MB
~51K SLoC