Chapter 3 — ORM
13 live recipes against the Author / Post fixture from Chapter 2.
Run with DATABASE_URL=... cargo test --test cookbook_chapter03_orm -- --test-threads=1.
Every query below runs against a live database in the test suite:
$ DATABASE_URL=postgres://…/blog \
cargo test --test cookbook_chapter03_orm -- --test-threads=1
test filter_eq_fetch_returns_matching_rows ... ok
test order_by_view_count_desc ... ok
test limit_offset_paginates ... ok
test aggregate_count_and_sum ... ok
test manual_transaction_rolls_back_on_error ... ok
test json_operator_on_jsonb_column ... ok
test raw_sql_escape_via_sqlx ... ok
… (17 total)
test result: ok. 17 passed; 0 failed; 0 ignored
- §3.31
Post::objects().filter("published", Op::Eq, true).fetch_on(&pool)→filter_eq_fetch_returns_matching_rows - §3.34
Op::Gt/Op::Lt/Op::ILike/Op::In/Op::Between/Op::IsNull— six tests covering the full Op surface. - §3.35
.order_by(&[("col", desc)])(true = DESC,false = ASC) →order_by_view_count_desc - §3.36
.limit(N).offset(M)for pagination →limit_offset_paginates - §3.37
.aggregate().annotate("alias", AggregateExpr::Count|Sum|Avg|...)fetch_aggregate(&q, &pool)→Vec<HashMap<String, SqlValue>>→aggregate_count_and_sum
- §3.42
model.save(&pool)does INSERT (PK Unset) or UPDATE (PK Set) →save_inserts_then_updates_in_place - §3.46 raw
sqlx::query_scalar / query_asfor SQL the QuerySet doesn't cover →raw_sql_escape_via_sqlx - §3.47 manual
pool.begin() ... tx.rollback()— atomic rollback on UNIQUE violation →manual_transaction_rolls_back_on_error - §3.48 PG JSONB
@>containment operator on themetadatacolumn →json_operator_on_jsonb_column
Note: aggregates over big-integer columns (
SUM/AVGof aBIGINT) are cast to a decodable type on every dialect, sofetch_aggregatereturns the computed number rather than a surpriseNULL.
3.50 QuerySet inspection + introspection
Eloquent-shape builder helpers covering the common "inspect or branch a queryset" patterns:
// .when(cond, |qs| ...) / .unless(cond, |qs| ...) / .tap(|&qs| ...)
let qs = Post::objects()
.when(only_active, |q| q.filter("active", true))
.unless(is_admin, |q| q.filter("public", true))
.tap(|q| tracing::debug!(?q, "before fetch"));
// .reorder(&[(col, asc)]) — replace ORDER BY instead of appending
let qs = Post::objects()
.with_default_order()
.reorder(&[("views", true)]);
// QuerySet: Clone — divergent branches from a shared base
let base = Post::objects().filter("status__ne", "archived");
let drafts = base.clone().filter("status", "draft");
let pub_now = base.filter("status", "published");
// .pluck::<U>(col, &pool) — single-column projection on a filtered qs
let titles: Vec<String> = Post::objects()
.filter("published", true)
.pluck::<String>("title", &pool).await?;
// .is_empty(&pool) — inverse of exists_pool
if Post::objects().filter("category_id", 7).is_empty(&pool).await? {
return Ok(Response::empty());
}
// .to_sql(&pool) / .to_compiled(&pool) — render SQL without executing
let sql = Post::objects().filter("published", true).to_sql(&pool)?;
// -> "SELECT … FROM \"post\" WHERE \"published\" = $1"
Verified by: tests/queryset_when_unless_tap.rs,
tests/queryset_reorder_sqlite_live.rs,
tests/queryset_clone_sqlite_live.rs,
tests/queryset_pluck_sqlite_live.rs,
tests/queryset_is_empty_sqlite_live.rs,
tests/queryset_to_sql_sqlite_live.rs.
3.51 Eloquent shortcuts — find_or_new / find_many_or_fail / insert_or_ignore / aggregates / locking
// Find-or-default in one call. Returns (row, exists: bool) so
// edit-or-create form handlers know which path was taken.
let (post, exists) = Post::find_or_new(form.id, &pool, || Post {
id: Auto::default(),
title: form.title.clone(),
}).await?;
// Find by multiple PKs, error if any missing. Dedups duplicate
// PKs before counting.
let posts: Vec<Post> = Post::find_many_or_fail([1, 2, 3], &pool).await?;
// INSERT or silently skip on unique-constraint violation.
// PG/SQLite: `ON CONFLICT DO NOTHING`. MySQL: `INSERT IGNORE`.
let inserted: bool = post.insert_or_ignore(&pool).await?;
// Aggregate scalars on a filtered queryset (table-wide versions
// are `Model::sum` / `Model::avg` / etc.):
let total: Option<i64> = Post::objects()
.filter("published", true)
.sum::<i64>("views", &pool).await?;
let avg: Option<f64> = Post::objects().avg::<f64>("views", &pool).await?;
// Row locking — Eloquent muscle-memory alias:
let row = Post::objects()
.filter("id", 42)
.lock_for_update() // == select_for_update()
.first(&pool).await?;
Verified by: tests/model_find_or_new_sqlite_live.rs,
tests/model_find_many_or_fail_sqlite_live.rs,
tests/model_insert_or_ignore_sqlite_live.rs,
tests/queryset_aggregates_sqlite_live.rs,
tests/queryset_lock_for_update_emission.rs.
3.52 Pagination + find-or-insert + single-value reach
// Model::paginate(page, per_page, &pool) -> (rows, total) —
// Eloquent paginate over the whole table.
let (posts, total) = Post::paginate(2, 10, &pool).await?;
// QuerySet::paginate — filtered counterpart. `total` reflects
// matching rows.
let (drafts, total_drafts) = Post::objects()
.filter("status", "draft")
.paginate(1, 10, &pool).await?;
// Model::find_or_insert(pk, &pool, fallback) — Eloquent
// findOrCreate. Persists the fallback if not found. Returns
// (row, exists: bool).
let (post, found) = Post::find_or_insert(
pk,
&pool,
|| Post { id: Auto::default(), title: "new".into() },
).await?;
// QuerySet::value<U>(col, &pool) — Eloquent Builder::value().
// Single column from the first row of a filtered queryset.
let email: Option<String> = User::objects()
.filter("id", 1_i64)
.value::<String>("email", &pool).await?;
Verified by: tests/model_paginate_sqlite_live.rs,
tests/queryset_paginate_sqlite_live.rs,
tests/model_find_or_insert_sqlite_live.rs,
tests/queryset_value_sqlite_live.rs.