构建应用使用Diesel

使用查询构建器而不是原始SQL的一个主要好处是,您可以将查询的一些内容提取到函数中并重用它们。 在本指南中,我们将介绍将代码提取为可重用片段的常用模式。 我们还将介绍如何构建代码的最佳实践。

我们所有的代码示例都基于crates.io的代码,crates.io是一个广泛使用Diesel的真实应用程序。 我们所有的例子都将集中在返回查询或查询片段的函数上。 这些示例都不包含采用数据库连接的函数。 我们将在指南的最后介绍这种结构的好处。

crates.io有一个canon_crate_name SQL函数,在比较crate名称时总是使用它。 我们不是连续编写canon_crate_name(crates :: name).eq(“some name”),而是将其拉入函数中。

src/krate/mod.rs

  1. use diesel::dsl::Eq;
  2. use diesel::types::Text;
  3. sql_function!(canon_crate_name, CanonCrateName, (x: Text) -> Text);
  4. type WithName<'a> = Eq<canon_crate_name<crates::name>, canon_crate_name<&'a str>>;
  5. fn with_name(name: &str) -> WithName {
  6. canon_crate_name(crates::name).eq(canon_crate_name(name))
  7. }

现在,当我们想要按名称查找包时,我们可以编写crates :: table.filter(with_name(“foo”))。 如果我们想接受字符串以外的类型,我们可以使该方法通用。

src/krate/mod.rs

  1. use diesel::dsl::Eq;
  2. use diesel::types::Text;
  3. sql_function!(canon_crate_name, CanonCrateName, (x: Text) -> Text);
  4. type WithName<T> = Eq<canon_crate_name<crates::name>, canon_crate_name<T>>;
  5. fn with_name<T>(name: T) -> WithName<T>
  6. where
  7. T: AsExpression<Text>,
  8. {
  9. canon_crate_name(crates::name).eq(canon_crate_name(name))
  10. }

无论您是将功能设为通用还是仅采用单一类型,都取决于您。 我们建议只在实际需要时使这些函数成为通用函数,因为它需要在where子句中使用其他边界。 除非您熟悉diesel机的较低级别,否则您需要的界限可能并不明确。

在这些示例中,我们使用diesel :: dsl中的helper类型来显式地写入返回类型。 Diesel中的几乎所有方法都有这样的辅助类型。 第一个类型参数是方法接收器(前面的东西)。 其余的类型参数是方法的参数。 如果我们想避免编写这个返回类型,或者动态返回不同的表达式,我们可以改为使用该值。

src/krate/mod.rs

  1. use diesel::pg::Pg;
  2. use diesel::types::Text;
  3. sql_function!(canon_crate_name, CanonCrateName, (x: Text) -> Text);
  4. fn with_name<'a, T>(name: T) -> Box<BoxableExpression<crates::table, Pg, SqlType = Bool> + 'a>
  5. where
  6. T: AsExpression<Text>,
  7. T::Expression: BoxableExpression<crates::table, Pg>,
  8. {
  9. canon_crate_name(crates::name).eq(canon_crate_name(name))
  10. }

为了box an expression,Diesel需要知道三件事:

  • 您打算使用它的表
  • 您打算执行它的后端
  • 它表示的SQL类型

这是Diesel用于键入检查查询的所有信息。通常我们可以从类型中获取此信息,但由于我们已经通过装箱擦除了类型,我们必须提供它。

该表用于确保您不会尝试在对posts :: table的查询中使用users :: name。我们需要知道你将执行它的后端,所以我们不小心在SQLite上使用PostgreSQL函数。需要SQL类型,因此我们知道可以传递给哪些函数。

装箱表达式也意味着它没有聚合函数。您无法在Diesel中装入聚合表达式。从Diesel 1.0开始,盒装表达式只能与给定的from子句一起使用。您不能将带有内部联接的crates :: table的盒装表达式用于另一个表。

除了提取表达式之外,您还可以将整个查询提取到函数中。回到crates.io,Crate结构不会使用crates表中的每一列。因为我们几乎总是选择这些列的子集,所以我们有一个all函数来选择我们需要的列。

src/krate/mod.rs

  1. use diesel::dsl::Select;
  2. type AllColumns = (
  3. crates::id,
  4. crates::name,
  5. crates::updated_at,
  6. crates::created_at,
  7. );
  8. const ALL_COLUMNS = (
  9. crates::id,
  10. crates::name,
  11. crates::updated_at,
  12. crates::created_at,
  13. );
  14. type All = Select<crates::table, AllColumns>;
  15. impl Crate {
  16. pub fn all() -> All {
  17. crates::table.select(ALL_COLUMNS)
  18. }
  19. }

我们还经常发现自己编写Crate :: all()。filter(with_name(crate_name))。 我们也可以把它拉成一个函数。

src/krate/mod.rs

  1. use diesel::dsl::Filter;
  2. type ByName<T> = Filter<All, WithName<T>>;
  3. impl Crate {
  4. fn by_name<T>(name: T) -> ByName<T> {
  5. Self::all().filter(with_name(name))
  6. }
  7. }

就像表达式一样,如果我们不想编写返回类型,或者我们想要以不同的方式动态构造查询,我们可以打包整个查询。

src/krate/mod.rs

  1. use diesel::expression::{Expression, AsExpression};
  2. use diesel::pg::Pg;
  3. use diesel::types::Text;
  4. type SqlType = <AllColumns as Expression>::SqlType;
  5. type BoxedQuery<'a> = crates::BoxedQuery<'a, Pg, SqlType>;
  6. impl Crate {
  7. fn all() -> BoxedQuery<'static> {
  8. crates::table().select(ALL_COLUMNS).into_boxed()
  9. }
  10. fn by_name<'a, T>(name: T) -> BoxedQuery<'a>
  11. where
  12. T: AsExpression<Text>,
  13. T::Expression: BoxableExpression<crates::table, Pg>,
  14. {
  15. Self::all().filter(by_name(name))
  16. }
  17. }

再一次,我们必须向Diesel提供一些信息来装箱查询:

  • SELECT子句的SQL类型
  • FROM子句
  • 你将要执行它的后端

需要SQL类型,以便我们可以确定可以从此查询反序列化哪些结构。 需要FROM子句,以便我们可以验证未来对filter和其他查询构建器方法的调用的参数。 需要后端以确保您不会在SQLite上意外使用PostgreSQL函数。

请注意,在我们的所有示例中,我们都在编写返回查询或表达式的函数。 这些函数都不执行查询。 通常,您应该始终首选返回查询的函数,并避免将连接作为参数的函数。 这允许您重复使用和撰写查询。

例如,如果我们编写了这样的by_name函数:

src/krate/mod.rs

  1. impl Crate {
  2. fn by_name(name: &str, conn: &PgConnection) -> QueryResult<Self> {
  3. Self::all()
  4. .filter(with_name(name))
  5. .first(conn)
  6. }
  7. }

然后我们将永远无法在另一个上下文中使用此查询,或进一步修改它。 通过将函数编写为返回查询而不是执行查询的函数,我们可以将其用作子选择。

  1. let version_id = versions
  2. .select(id)
  3. .filter(crate_id.eq_any(Crate::by_name(crate_name).select(crates::id)))
  4. .filter(num.eq(version))
  5. .first(&*conn)?;

或者使用它来做一些事情,比如获取所有下载内容:

  1. let recent_downloads = Crate::by_name(crate_name)
  2. .inner_join(crate_downloads::table)
  3. .filter(CrateDownload::is_recent())
  4. .select(sum(crate_downloads::downloads))
  5. .get_result(&*conn)?;