Skip to content

Commit c2e32d8

Browse files
authored
Merge pull request diesel-rs#1436 from diesel-rs/sg-composing-diesel-applications
Guide draft: Composing Applications with Diesel
2 parents 060b828 + 7303ae1 commit c2e32d8

1 file changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
Composing Applications with Diesel
2+
----------------------------------
3+
4+
One of the main benefits of using a query builder over raw SQL
5+
is that you can pull bits of your query out into functions and reuse them.
6+
In this guide,
7+
we'll look at common patterns for extracting your code into re-usable pieces.
8+
We'll also look at best practices for how to structure your code.
9+
10+
All of our code examples are based on code from crates.io,
11+
a real world application which uses Diesel extensively.
12+
All of our examples will be focused on functions which *return*
13+
queries or pieces of queries.
14+
None of these examples will include a function which takes a database
15+
connection.
16+
We will go into the benefits of this structure at the end of the guide.
17+
18+
crates.io has a `canon_crate_name` SQL function
19+
which is always used when comparing crate names.
20+
Rather than continuously writing
21+
`canon_crate_name(crates::name).eq("some name")`,
22+
we can instead pull this out into a function.
23+
24+
```
25+
use diesel::dsl::Eq;
26+
use diesel::types::Text;
27+
28+
sql_function!(canon_crate_name, CanonCrateName, (x: Text) -> Text);
29+
30+
type WithName<'a> = Eq<canon_crate_name<crates::name>, canon_crate_name<&'a str>>;
31+
32+
fn with_name(name: &str) -> WithName {
33+
canon_crate_name(crates::name).eq(canon_crate_name(name))
34+
}
35+
```
36+
37+
Now when we want to find a crate by name, we can write
38+
`crates::table.filter(with_name("foo"))` instead.
39+
If we want to accept types other than a string,
40+
we can make the method generic.
41+
42+
```
43+
use diesel::dsl::Eq;
44+
use diesel::types::Text;
45+
46+
sql_function!(canon_crate_name, CanonCrateName, (x: Text) -> Text);
47+
48+
type WithName<T> = Eq<canon_crate_name<crates::name>, canon_crate_name<T>>;
49+
50+
fn with_name<T>(name: T) -> WithName<T>
51+
where
52+
T: AsExpression<Text>,
53+
{
54+
canon_crate_name(crates::name).eq(canon_crate_name(name))
55+
}
56+
```
57+
58+
It's up to you whether you make your functions generic,
59+
or only take a single type.
60+
We recommend only making these functions generic if it's actually needed,
61+
since it requires additional bounds in your `where` clause.
62+
The bounds you need might not be clear,
63+
unless you are familiar with Diesel's lower levels.
64+
65+
In these examples,
66+
we are using helper types from `diesel::dsl`
67+
to write the return type explicitly.
68+
Nearly every method in Diesel has a helper type like this.
69+
The first type parameter is the method receiver
70+
(the thing before the `.`).
71+
The remaining type parameters are the arguments to the method.
72+
If we want to avoid writing this return type,
73+
or dynamically return a different expression,
74+
we can box the value instead.
75+
76+
```
77+
use diesel::pg::Pg;
78+
use diesel::types::Text;
79+
80+
sql_function!(canon_crate_name, CanonCrateName, (x: Text) -> Text);
81+
82+
fn with_name<'a, T>(name: T) -> Box<BoxableExpression<crates::table, Pg, SqlType = Bool> + 'a>
83+
where
84+
T: AsExpression<Text>,
85+
T::Expression: BoxableExpression<crates::table, Pg>,
86+
{
87+
canon_crate_name(crates::name).eq(canon_crate_name(name))
88+
}
89+
```
90+
91+
In order to box an expression, Diesel needs to know three things:
92+
93+
- The table you intend to use it on
94+
- The backend you plan to execute it against
95+
- The SQL type it represents
96+
97+
This is all the information Diesel uses to type check your query.
98+
Normally we can get this information from the type,
99+
but since we've erased the type by boxing,
100+
we have to supply it.
101+
102+
The table is used to make sure that you don't try to use `users::name`
103+
on a query against `posts::table`.
104+
We need to know the backend you will execute it on,
105+
so we don't accidentally use a PostgreSQL function on SQLite.
106+
The SQL type is needed so we know what functions this can be passed to.
107+
108+
Boxing an expression also implies that it has no aggregate functions.
109+
You cannot box an aggregate expression in Diesel.
110+
As of Diesel 1.0, a boxed expression can only be used with *exactly* the from
111+
clause given.
112+
You cannot use a boxed expression for `crates::table` with an inner join to
113+
another table.
114+
115+
In addition to extracting expressions,
116+
you can also pull out entire queries into functions.
117+
Going back to crates.io,
118+
the `Crate` struct doesn't use every column from the `crates` table.
119+
Because we almost always select a subset of these columns,
120+
we have an `all` function which selects the columns we need.
121+
122+
```
123+
use diesel::dsl::Select;
124+
125+
type AllColumns = (
126+
crates::id,
127+
crates::name,
128+
crates::updated_at,
129+
crates::created_at,
130+
);
131+
132+
const ALL_COLUMNS = (
133+
crates::id,
134+
crates::name,
135+
crates::updated_at,
136+
crates::created_at,
137+
);
138+
139+
type All = Select<crates::table, AllColumns>;
140+
141+
impl Crate {
142+
pub fn all() -> All {
143+
crates::table.select(ALL_COLUMNS)
144+
}
145+
}
146+
```
147+
148+
We also frequently found ourselves writing
149+
`Crate::all().filter(with_name(crate_name))`.
150+
We can pull that into a function as well.
151+
152+
```
153+
use diesel::dsl::Filter;
154+
155+
type ByName<T> = Filter<All, WithName<T>>;
156+
157+
impl Crate {
158+
fn by_name<T>(name: T) -> ByName<T> {
159+
Self::all().filter(with_name(name))
160+
}
161+
}
162+
```
163+
164+
And just like with expressions,
165+
if we don't want to write the return types,
166+
or we want to dynamically construct the query differently,
167+
we can box the whole query.
168+
169+
```rust
170+
use diesel::expression::{Expression, AsExpression};
171+
use diesel::pg::Pg;
172+
use diesel::types::Text;
173+
174+
type SqlType = <AllColumns as Expression>::SqlType;
175+
type BoxedQuery<'a> = crates::BoxedQuery<'a, Pg, SqlType>;
176+
177+
impl Crate {
178+
fn all() -> BoxedQuery<'static> {
179+
crates::table().select(ALL_COLUMNS).into_boxed()
180+
}
181+
182+
fn by_name<'a, T>(name: T) -> BoxedQuery<'a>
183+
where
184+
T: AsExpression<Text>,
185+
T::Expression: BoxableExpression<crates::table, Pg>,
186+
{
187+
Self::all().filter(by_name(name))
188+
}
189+
}
190+
```
191+
192+
Once again, we have to give Diesel some information to box the query:
193+
194+
- The SQL type of the `SELECT` clause
195+
- The `FROM` clause
196+
- The backend you are going to execute it against
197+
198+
The SQL type is needed so we can determine what structs can be
199+
deserialized from this query.
200+
The `FROM` clause is needed so we can validate the arguments
201+
to future calls to `filter` and other query builder methods.
202+
The backend is needed to ensure you don't accidentally use a
203+
PostgreSQL function on SQLite.
204+
205+
Note that in all of our examples,
206+
we are writing functions which *return* queries or expressions.
207+
None of these functions execute the query.
208+
In general you should always prefer functions which return queries,
209+
and avoid functions which take a connection as an argument.
210+
This allows you to re-use and compose your queries.
211+
212+
For example, if we had written our `by_name` function like this:
213+
214+
```
215+
impl Crate {
216+
fn by_name(name: &str, conn: &PgConnection) -> QueryResult<Self> {
217+
Self::all()
218+
.filter(with_name(name))
219+
.first(conn)
220+
}
221+
}
222+
```
223+
224+
Then we would never be able to use this query in another context,
225+
or modify it further.
226+
By writing the function as one that returns a query,
227+
rather than executing it,
228+
we can do things like use it as a subselect.
229+
230+
```
231+
let version_id = versions
232+
.select(id)
233+
.filter(crate_id.eq_any(Crate::by_name(crate_name).select(crates::id)))
234+
.filter(num.eq(version))
235+
.first(&*conn)?;
236+
```
237+
238+
Or use it to do things like get all of its downloads:
239+
240+
```
241+
let recent_downloads = Crate::by_name(crate_name)
242+
.inner_join(crate_downloads::table)
243+
.filter(CrateDownload::is_recent())
244+
.select(sum(crate_downloads::downloads))
245+
.get_result(&*conn)?;
246+
```

0 commit comments

Comments
 (0)