One of the main benefits of using a query builder over raw SQL is that you can pull bits of your query out into functions and reuse them. In this guide, we’ll look at common patterns for extracting your code into re-usable pieces. We’ll also look at best practices for how to structure your code.
All of our code examples are based on code from crates.io, a real world application which uses Diesel extensively. All of our examples will be focused on functions which return queries or pieces of queries. None of these examples will include a function which takes a database connection. We will go into the benefits of this structure at the end of the guide.
crates.io has a canon_crate_name
SQL function which is
always used when comparing crate names. Rather than continuously writing
canon_crate_name(crates::name).eq(canon_crate_name("some name"))
,
we can instead pull this out into a function.
use diesel::dsl::Eq;
use diesel::prelude::define_sql_function;
use diesel::sql_types::Text;
define_sql_function!(fn canon_crate_name(x: Text) -> Text);
type WithName<'a> = diesel::dsl::Eq<canon_crate_name<crates::name>, canon_crate_name<&'a str>>;
fn with_name(name: &str) -> WithName {
crates::name).eq(canon_crate_name(name))
canon_crate_name(}
We need to specify the return type of this function. For this we
define the WithName
type which is composed of a helper type
generated by the function macro (canon_crate_name
) and
helper types defined in the diesel::dsl
.
The later types closely mirror the functions used to construct the
query.
Now when we want to find a crate by name, we can write
crates::table.filter(with_name("foo"))
instead. If we want
to accept types other than a &'str
, we can make the
method generic.
use diesel::dsl::Eq;
use diesel::prelude::define_sql_function;
use diesel::sql_types::Text;
define_sql_function!(fn canon_crate_name(x: Text) -> Text);
type WithName<T> = diesel::dsl::Eq<canon_crate_name<crates::name>, canon_crate_name<T>>;
fn with_name<T>(name: T) -> WithName<T>
where
: AsExpression<Text>,
T{
crates::name).eq(canon_crate_name(name))
canon_crate_name(}
The AsExpression
trait describes any type that can be converted to a expression of the
SQL type Text
. In this particular case it extends to
function to accept String
, Cow<str>
and
any SQL side text expression (e.g. crates::name
) as
argument.
It’s up to you whether you make your functions generic, or only take
a single type. We recommend only making these functions generic if it’s
actually needed, since it requires additional bounds in your
where
clause. The bounds you need might not be clear,
unless you are familiar with Diesel’s lower levels.
In these examples, we are using helper types from diesel::dsl
to write the return type explicitly. Nearly every method in Diesel has a
helper type like this. The first type parameter is the method receiver
(the thing before the .
). The remaining type parameters are
the arguments to the method. If we want to avoid writing this return
type, or dynamically return a different expression, we can box the value
instead.
use diesel::pg::Pg;
use diesel::prelude::sql_function;
use diesel::sql_types::Text;
sql_function!(fn canon_crate_name(x: Text) -> Text);
fn with_name<'a, T>(name: T) -> Box<BoxableExpression<crates::table, Pg, SqlType = Bool> + 'a>
where
: AsExpression<Text>,
TT::Expression: BoxableExpression<crates::table, Pg>,
{
crates::name).eq(canon_crate_name(name))
canon_crate_name(}
In order to use BoxableExpression
,
Diesel needs to know three things:
- The table you intend to use it on
- The backend you plan to execute it against
- The SQL type it represents
This is all the information Diesel uses to type check your query. Normally we can get this information from the type, but since we’ve erased the type by boxing, we have to supply it.
The table is used to make sure that you don’t try to use
users::name
on a query against posts::table
.
We need to know the backend you will execute it on, so we don’t
accidentally use a PostgreSQL function on SQLite. The SQL type is needed
so we know what functions this can be passed to.
Boxing an expression also implies that it has no aggregate functions.
You cannot box an aggregate expression in Diesel. As of Diesel 2.0, a
boxed expression can only be used with exactly the from clause
given. You cannot use a boxed expression for crates::table
with an inner join to another table.
Finally it’s possible to use the
#[diesel::dsl::auto_type]
attribute macro, to automatically
construct the correct return type for you.
impl Crate {
#[diesel::dsl::auto_type(no_type_alias)]
fn with_name<'a>(name: &'a str) -> _ {
crates::name).eq(canon_crate_name.eq(name))
canon_crate_name(}
}
For any function annotated with #[auto_type]
the
procedural macro will replace the _
return type with a
specific type. Additionally a type definition with the same name as the
function name is generated. For this specific example the generation of
the type definition is suppressed via the no_type_alias
option as stable rust does not support associated types for non-trait
impls yet.
For most built-in query DSL constructs the #[auto_type]
macro will infer the correct type and use that information to construct
the return type. For custom functions it might require an explicit type
annotation to correctly infer the return type. For lifetimes, as in this
example, an explict type annotation is required.
In addition to extracting expressions, you can also pull out entire
queries into functions. Going back to crates.io, the Crate
struct doesn’t use every column from the crates
table.
Because we almost always select a subset of these columns, we have an
all
function which selects the columns we need.
use diesel::backend::Backend;
use diesel::dsl::{AsSelect, Select};
use diesel::pg::Pg;
#[derive(Selectable, Queryable)]
#[diesel(table_name = crates, check_for_backend(diesel::pg::Pg))]
struct Crate {
pub id: i32,
pub name: String,
pub updated_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub description: Option<String>,
pub homepage: Option<String>,
pub documentation: Option<String>,
pub repository: Option<String>,
pub max_upload_size: Option<i32>,
pub max_features: Option<i16>,
}
type All = Select<crates::table, AsSelect<Crate, Pg>>;
impl Crate {
pub fn all() -> All {
crates::table.select(Crate::as_select())
}
}
We also frequently found ourselves writing
Crate::all().filter(with_name(crate_name))
. We can pull
that into a function as well.
use diesel::dsl::Filter;
type ByName<'a> = Filter<All, WithName<'a>>;
impl Crate {
fn by_name(name: &str) -> ByName<'_> {
Self::all().filter(with_name(name))
}
}
And just like with expressions, if we don’t want to write the return types, or we want to dynamically construct the query differently, we can box the whole query.
use diesel::expression::{Expression, AsExpression};
use diesel::pg::Pg;
use diesel::sql_types::Text;
use diesel::dsl::{SqlTypeOf, AsSelect};
type SqlType = SqlTypeOf<AsSelect<Crate, Pg>>;
type BoxedQuery<'a> = crates::BoxedQuery<'a, Pg, SqlType>;
impl Crate {
fn all() -> BoxedQuery<'static> {
crates::table.select(Crate::as_select()).into_boxed()
}
fn by_name<'a, T>(name: &'a str) -> BoxedQuery<'a> {
Self::all().filter(with_name(name))
}
}
Once again, we have to give Diesel some information to box the query:
- The SQL type of the
SELECT
clause - The
FROM
clause - The backend you are going to execute it against
The SQL type is needed so we can determine what structs can be
deserialized from this query. The FROM
clause is needed so
we can validate the arguments to future calls to filter
and
other query builder methods. The backend is needed to ensure you don’t
accidentally use a PostgreSQL function on SQLite.
Finally it’s again possible to use
#[diesel::dsl::auto_type]
to let the proc-macro infer the
correct return type for you.
impl Crate {
#[diesel::dsl::auto_type(no_type_alias)]`
fn by_name(name: &str) -> _ {
let all: All = Crate::all();
let filter: WithName<'a> = Self::with_name(name);
.filter(filter)
all}
}
In this case the #[auto_type]
macro needs additional
type expressions to work correctly.
Note that in all of our examples, we are writing functions which return queries or expressions. None of these functions execute the query. In general you should always prefer functions which return queries, and avoid functions which take a connection as an argument. This allows you to re-use and compose your queries.
For example, if we had written our by_name
function like
this:
impl Crate {
fn by_name(name: &str, conn: &mut PgConnection) -> QueryResult<Self> {
Self::all()
.filter(with_name(name))
.first(conn)
}
}
Then we would never be able to use this query in another context, or modify it further. By writing the function as one that returns a query, rather than executing it, we can do things like use it as a subselect.
let version_id = versions
.select(id)
.filter(crate_id.eq_any(Crate::by_name(crate_name).select(crates::id)))
.filter(num.eq(version))
.first(conn)?;
Or use it to do things like get all of its downloads: Example
let recent_downloads = Crate::by_name(crate_name)
.inner_join(crate_downloads::table)
.filter(CrateDownload::is_recent())
.select(sum(crate_downloads::downloads))
.get_result(conn)?;
All code in this guide is based on real code from crates.io. You can find the source on GitHub