#query-builder #sql-query #surrealdb #query #builder

nightly surreal-simple-querybuilder

为 SurrealDB 及其 SQL 查询语言提供的简单查询构建和实用工具库

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


用于 surreal-simple-client

MIT 许可证

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(&params) // <-- 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 功能)

这些特性为 Stringstr 类型添加了一些实用函数,可以与查询构建器一起使用,提供更大的灵活性。

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);

ForeignKeyForeign 类型(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,以便用户可以在单个请求中获得所有需要的数据。

默认情况下,如果您要序列化一个包含检索到的 authorFile (来自上面的示例) 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