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