Query Builder
The query builder makes SQL from chained methods and runs it with bound values on Postgres, MySQL, MariaDB and SQLite.
Set up the database first. See Database.
Start a query
There are two ways to start a query.
// with a model: the result is a list of Actor models
List<Actor> actors = await Actor().where('age', '>=', 23).get();
// with a table name: the result is a list of maps, one map per row
List<Map<String, dynamic>> rows = await QueryBuilder.table('actor')
.where('age', '>=', 23)
.get();
The examples on this page use an Actor model on the table actor.
The SQL in the examples is for Postgres. MySQL quotes names with ` instead of ".
Start each query with a new object
A query object keeps its where, orderBy and other parts.
Use Actor() (or QueryBuilder.table(...)) again for each new query.
Read rows
get
Runs the query and returns all rows that match.
List<Actor> actors = await Actor().where('name', 'John Wick').get();
// SELECT * FROM "actor" WHERE "name" = @name_p0
all
Returns all rows that match the where part.
all() ignores join, orderBy and limit. Use get() for those.
find
Returns one row, or null when there is no match.
// by primary key
Actor? actor = await Actor().find(1);
// SELECT * FROM "actor" WHERE "id" = @id_p0 LIMIT 1
// by another column
Actor? byName = await Actor().find('name', 'John Wick');
// SELECT * FROM "actor" WHERE "name" = @name_p0 LIMIT 1
getFirst and first
Returns the first row of the query, or null.
first() does the same, with the Laravel name.
Actor? actor = await Actor().where('age', '>', 60).orderBy('age').getFirst();
// SELECT * FROM "actor" WHERE "age" > @age_p0 ORDER BY "age" LIMIT 1
Actor? same = await Actor().where('age', '>', 60).orderBy('age').first();
firstOrFail and findOrFail
They work like getFirst() and find().
When no row matches, they throw a ModelNotFoundException.
So the result is never null.
Actor actor = await Actor().where('name', 'John Wick').firstOrFail();
Actor byId = await Actor().findOrFail(1);
Actor byName = await Actor().findOrFail('name', 'John Wick');
The exception has the table, and for findOrFail() also the column and the id it looked for.
When a route does not catch it, the client gets 404 with {"code": "not_found", "message": "Not Found"}.
try {
Actor actor = await Actor().findOrFail(id);
} on ModelNotFoundException catch (e) {
print(e.table); // actor
print(e.id); // the id
}
count
Returns the number of rows as an int.
int total = await Actor().count();
int adults = await Actor().where('age', '>=', 18).count();
// SELECT count(*) as total FROM "actor" WHERE "age" >= @age_p0
count()ignoreslimit,offsetandorderBy.- With
groupBy,count()returns the number of groups. - With
distinct(),count()returns the number of distinct rows. Seedistinct.
hasRows and hasNoRows
hasRows() returns true when at least one row matches.
hasNoRows() returns the opposite. doesntExist() is the same as hasNoRows().
They read one row at most, so they are faster than count() > 0.
bool taken = await User().where('email', email).hasRows();
if (await User().where('email', email).hasNoRows()) {
await User().create(<String, dynamic>{'email': email});
}
A model also has the exists getter.
It tells if that one model is saved in the table.
hasRows() checks a query.
There is no exists() method on a query, because a model already has the getter exists. Use hasRows().
There is no value() either, because value is a common column name. Use valueOf.
pluck and pluckMap
pluck() returns the values of one column, as a list.
pluckMap() returns a map. The second column gives the keys.
When two rows have the same key, the last row wins.
Map<dynamic, dynamic> names = await Actor().pluckMap('name', 'id');
// {1: 'John Wick', 2: 'John Doe'}
Both use where, join, orderBy, limit and distinct.
A date comes back as an ISO string.
valueOf
Returns the value of one column of the first row.
It returns null when no row matches.
paginate
Returns one page of rows and the page info.
It runs a count() query and a get() query.
Pagination<Actor> page = await Actor()
.where('age', '>=', 18)
.paginate(currentPage: 2, perPage: 10);
List<Actor> actors = page.data;
print(page.total); // rows that match
print(page.lastPage); // number of pages
page.toJson() gives total, per_page, last_page, current_page and data.
So you can return a Pagination from a controller.
perPagemust be 1 or more. A smaller value throws anArgumentError.- A
currentPagebelow 1 is read as page 1.
simplePaginate
Like paginate(), but without the total count. So it runs one query, not two.
It reads one row more to know if there is a next page.
SimplePagination<Actor> page = await Actor()
.orderBy('id')
.simplePaginate(currentPage: 2, perPage: 10);
List<Actor> actors = page.data;
print(page.hasMorePages); // true when there is a next page
page.toJson() gives per_page, current_page, from, to, has_more_pages and data.
from and to are the numbers of the first and last row of the page. They are null on an empty page.
cursorPaginate
Returns one page of rows after a cursor.
It uses a WHERE on the order columns, not OFFSET, so it stays fast on a big table.
Use it for an endless list or an API.
CursorPagination<Actor> page = await Actor()
.where('age', '>=', 18)
.orderBy('age', 'desc')
.cursorPaginate(perPage: 10, cursor: req.input('cursor'));
List<Actor> actors = page.data;
print(page.nextCursor); // send it back as `cursor` to read the next page
print(page.prevCursor); // send it back as `cursor` to read the page before
page.toJson() gives per_page, next_cursor, prev_cursor and data.
next_cursor is null on the last page. prev_cursor is null on the first page.
- The rows are ordered by all the
orderBy()columns. You can mixascanddesc. - With no
orderBy(), they are ordered by the primary key. Or usecolumnanddirection:cursorPaginate(perPage: 10, column: 'created_at', direction: 'desc'). - The primary key is added as the last order column, so rows with the same value are never skipped.
orderBy()together withcolumn,orderByRaw()andinRandomOrder()throw aStateError.- It does not work on a
unionor afromSubquery. It throws aStateErrorthere. Usepaginate()orsimplePaginate(). - The order columns must be in the select, and they must have no
nullvalues. - The cursor is safe to take from a user. The columns and the order come from your code. The values of the cursor are only sent as query values, never as SQL.
- A cursor that is not valid gives the first page. This is also true for a changed cursor with a value of the wrong kind, like a text for a number column.
chunk and chunkById
Use them to work on a big table. They read a few rows at a time, so the rows do not fill the memory.
await Actor().where('age', '>=', 18).chunk(100, (List<Actor> actors) {
for (Actor actor in actors) {
print(actor.name);
}
return true; // go on to the next page
});
- The function gets one page of rows.
- Return
trueto go on, orfalseto stop. After a stop,chunk()returnsfalse. Otherwise it returnstrue. - Without
orderBy(),chunk()orders by the primary key. So each page has other rows.
chunk() pages with OFFSET.
When the function changes or deletes rows, some rows can be skipped or read twice.
Use chunkById() then.
await Actor().where('age', '>', 100).chunkById(100, (List<Actor> actors) async {
for (Actor actor in actors) {
await actor.delete();
}
return true;
});
chunkById()reads the rows after the last id:WHERE "id" > @last.- It orders by the id itself. A query with
orderBy()throws aStateError. chunkById()does not work on aunionor afromSubquery. It throws aStateErrorthere. Usechunk().- To use another column, add
column: 'uuid'. The column must be unique.
Aggregates
sum, avg, min and max return one value for all rows that match.
They return null when no row matches.
num? total = await Actor().where('age', '>=', 18).sum('age');
double? average = await Actor().avg('age');
dynamic youngest = await Actor().min('age');
dynamic newest = await Actor().max('created_at');
sum()returns anum. It is anintor adouble, by driver. UsetoInt()ortoDouble().avg()returns adouble.min()andmax()return the value as the driver gives it. A date is an ISO string. A Postgresnumericis aString. A MySQLDECIMALis adouble.- Like
count(), they usewhere,joinand soft deletes. They ignorelimit,offsetandorderBy. - After
distinct(),sum()andavg()use only the distinct values. - With
groupBy()they throw aStateError. UseselectRaw()andget()for groups.
Where
where
// equal
await Actor().where('name', 'John Wick').get();
// WHERE "name" = @name_p0
// other operator
await Actor().where('age', '>=', 23).get();
await Actor().where('name', '!=', 'John Wick').get();
await Actor().where('name', 'like', 'John%').get();
Many where calls are joined with AND.
Info
The operator must be one of =, !=, <>, <, >, <=, >=, like,
not like or ilike (any case). Any other value throws an ArgumentError.
The column must be a plain name. Use whereRaw for expressions.
null values
A null value makes an IS NULL or IS NOT NULL check.
await Actor().where('deleted_at', null).get();
// WHERE "deleted_at" IS NULL
await Actor().where('deleted_at', '=', null).get();
// WHERE "deleted_at" IS NULL
await Actor().where('deleted_at', '!=', null).get();
// WHERE "deleted_at" IS NOT NULL
=withnullisIS NULL.!=or<>withnullisIS NOT NULL.- Any other operator with
nullthrows anArgumentError. where('name')with no value throws anArgumentError.
whereNull and whereNotNull do the same. See whereNull.
orWhere
await Actor().where('name', 'John Doe').orWhere('name', 'John Wick').get();
// WHERE "name" = @name_p0 OR "name" = @name_p1
await Actor().where('age', '<', 18).orWhere('age', '>', 60).get();
orWhere takes the same arguments as where, also null.
whereGroup and orWhereGroup
Use them to put conditions in brackets. The function gets the query. Add the conditions to it.
await Actor()
.where('status', 'active')
.whereGroup((QueryBuilder<Actor> q) {
q.where('age', '<', 18).orWhere('age', '>', 60);
})
.get();
// WHERE "status" = @status_p0 AND ("age" < @age_p1 OR "age" > @age_p2)
await Actor()
.where('id', 1)
.orWhereGroup((QueryBuilder<Actor> q) {
q.where('status', 'active').where('age', '>', 60);
})
.get();
// WHERE "id" = @id_p0 OR ("status" = @status_p1 AND "age" > @age_p2)
- A group can have another group in it.
- An empty group adds nothing.
- Add only
whereconditions in the function. The function gets the same query, so anorderBy()there is for the whole query.
whereNot and orWhereNot
whereNot keeps the rows where the conditions in the brackets are not true.
await Actor()
.whereNot((QueryBuilder<Actor> q) {
q.where('status', 'draft').orWhere('age', '<', 18);
})
.get();
// WHERE NOT ("status" = @status_p0 OR "age" < @age_p1)
await Actor()
.where('id', 1)
.orWhereNot((QueryBuilder<Actor> q) => q.where('status', 'draft'))
.get();
They also take a column and a value, like where:
await Actor().whereNot('status', 'draft').get();
await Actor().whereNot('age', '<', 18).get();
await Actor().where('id', 1).orWhereNot('status', 'draft').get();
Give the function its type, like (QueryBuilder<Actor> q) {...}. Without it, q is dynamic.
Note
A row with NULL in a column of the brackets matches neither the brackets nor NOT of them.
This is how SQL works with NULL.
whereIn and whereNotIn
await Actor().whereIn('id', <int>[1, 2]).get();
// WHERE "id" IN (@id_p0,@id_p1)
await Actor().whereNotIn('id', <int>[1, 2]).get();
await Actor().where('age', 60).orWhereIn('id', <int>[1, 2]).get();
await Actor().where('age', 60).orWhereNotIn('id', <int>[1, 2]).get();
- On Postgres, a list of only
intor onlyStringvalues is sent as one array parameter:"id" = ANY(@id_p0)(<> ALLforwhereNotIn). - An empty list matches no rows for
whereIn, and all rows forwhereNotIn.
The list can also be another Iterable, like a Set.
The value can also be a query that selects one column.
Its values are bound too.
await Actor()
.whereIn('id', QueryBuilder.table('award').select('actor_id').where('year', 2024))
.get();
// WHERE "id" IN (SELECT "actor_id" FROM "award" WHERE "year" = @dox_sub0_year_p0)
- The sub query is built when it is added. A change to it after
whereInis not used. This is the same forjoinSub,fromSubandunion. - MySQL and MariaDB do not allow
LIMITin anIN (sub query).
whereNull and whereNotNull
await Actor().whereNull('deleted_at').get();
await Actor().whereNotNull('deleted_at').get();
await Actor().where('age', 60).orWhereNull('deleted_at').get();
await Actor().where('age', 60).orWhereNotNull('deleted_at').get();
whereBetween and whereNotBetween
The list has two values: from and to. Both are in the range.
await Actor().whereBetween('age', <int>[18, 60]).get();
// WHERE "age" BETWEEN @age_p0 AND @age_p1
await Actor().whereNotBetween('age', <int>[18, 60]).get();
await Actor().where('id', 1).orWhereBetween('age', <int>[18, 60]).get();
await Actor().where('id', 1).orWhereNotBetween('age', <int>[18, 60]).get();
The list must have two values that are not null. Otherwise it throws an ArgumentError.
whereColumn
Compares two columns. Both names are columns, not values.
await Actor().whereColumn('created_at', 'updated_at').get();
// WHERE "created_at" = "updated_at"
await Actor().whereColumn('updated_at', '>', 'created_at').get();
// WHERE "updated_at" > "created_at"
await Actor().where('id', 1).orWhereColumn('updated_at', '>', 'created_at').get();
whereLike and whereNotLike
Matches a column with a pattern.
% is any text. _ is one character.
await Actor().whereLike('name', '%wick%').get();
await Actor().whereNotLike('name', '%doe%').get();
await Actor().where('id', 1).orWhereLike('name', 'John%').get();
await Actor().where('id', 1).orWhereNotLike('name', 'John%').get();
By default, the match does not care about upper or lower case.
Add caseSensitive: true to care about case.
| Database | Default | caseSensitive: true |
|---|---|---|
| Postgres | ILIKE |
LIKE |
| MySQL, MariaDB | LIKE. The column collation decides. The default collations do not care about case. |
LIKE BINARY |
| SQLite | LIKE. It does not care about case for ASCII letters only. |
GLOB. Dox changes the pattern for you. |
whereExists and whereNotExists
whereExists keeps the rows where a subquery finds a row.
The function gets a new query on the other table.
List<User> users = await User()
.whereExists('post', (QueryBuilder<Map<String, dynamic>> q) {
q.whereColumn('post.user_id', 'user.id');
})
.get();
// WHERE EXISTS (SELECT 1 FROM "post" WHERE "post"."user_id" = "user"."id")
await User()
.whereNotExists('post', (QueryBuilder<Map<String, dynamic>> q) {
q.whereColumn('post.user_id', 'user.id');
})
.get();
orWhereExists and orWhereNotExists join the condition with OR.
Info
The subquery does not use global scopes or soft deletes.
Add those conditions in the function, for example q.whereNull('deleted_at').
For a relation of a model, whereHas is shorter.
whereDate, whereTime, whereYear, whereMonth and whereDay
They compare one part of a date or datetime column.
Like where, they take an optional operator.
await Actor().whereDate('created_at', '2024-05-01').get();
await Actor().whereDate('created_at', '>=', DateTime(2024, 5, 1)).get();
await Actor().whereTime('created_at', '>', '18:00:00').get();
await Actor().whereYear('created_at', 2024).get();
await Actor().whereMonth('created_at', 5).get();
await Actor().whereDay('created_at', 15).get();
whereDatetakes aDateTimeor ayyyy-mm-ddstring.whereTimetakes aDateTimeor ahh:mm:ssstring.- On SQLite, a
whereDateorwhereTimestring must be zero padded, like2024-05-01or09:00:00. A value like2024-5-1matches no row. whereYear,whereMonthandwhereDaytake anint, a number string or aDateTime.- Each one has an
orform, likeorWhereDate. - The column is read as it is stored. On SQLite and Postgres, Dox stores a
DateTimein UTC. - A
DateTimevalue is compared the same waywherebinds it: in UTC on SQLite and Postgres, and as the wall clock on MySQL and MariaDB. - On Postgres, a
timestamptzis read in the session time zone (UTC by default). If the connection sets anothertimeZone, aDateTimevalue does not match. - On Postgres, a column with fractions of a second does not equal a time without them.
For example,
10:00:00.5is not10:00:00. Laravel works the same way.
| Database | whereDate |
whereYear |
|---|---|---|
| Postgres | CAST(col AS date) |
EXTRACT(year FROM col) |
| MySQL, MariaDB | DATE(col) |
YEAR(col) |
| SQLite | date(col) |
strftime('%Y', col) |
whereJsonContains and whereJsonDoesntContain
whereJsonContains keeps the rows where a JSON column has the value.
For a JSON array, the value is one item. A list value needs all its items.
await Actor().whereJsonContains('tags', 'action').get();
await Actor().whereJsonContains('tags', <String>['action', 'drama']).get();
// a key inside the JSON
await Actor().whereJsonContains('options->languages', 'en').get();
await Actor().whereJsonDoesntContain('tags', 'horror').get();
column->keyreads a key inside the JSON. Keys can have letters, digits and_. Any other key throws anArgumentError.- Each one has an
orform, likeorWhereJsonContains. - On SQLite, it also matches a value inside a JSON object, one level down. For example,
{"a": "x"}matches'x'.
| Database | SQL |
|---|---|
| Postgres | CAST(col AS jsonb) @> CAST(@value AS jsonb) |
| MySQL, MariaDB | JSON_CONTAINS(col, @value) |
| SQLite | EXISTS (SELECT 1 FROM json_each(col) WHERE value = @value) |
Note
On SQLite, the value must be a string, a number, a bool or a list of them.
A map throws an ArgumentError.
SQLite stores true as 1, so true, 1 and 1.0 match each other there.
Note
whereJsonDoesntContain on a row that does not have the key is not the same on every database.
SQLite and MariaDB keep the row. Postgres and MySQL drop it. Laravel works the same way.
whereRaw and orWhereRaw
Write the condition yourself. Put values in a map, and use @key in the SQL.
The column names are not quoted.
await Actor().whereRaw('name = @name', <String, dynamic>{'name': 'John Wick'}).get();
await Actor()
.where('age', 60)
.orWhereRaw('lower(name) = @name', <String, dynamic>{'name': 'john wick'})
.get();
Warning
Never put user input into the SQL text. Always send it in the map.
Filter by a relation
whereHas, whereDoesntHave and has keep the rows that have (or do not have) related rows.
See Query by relations.
when
Runs the function only when the condition is true.
So you can keep the query in one chain.
List<Actor> actors = await Actor()
.when(status != null, (QueryBuilder<Actor> q) {
q.where('status', status);
})
.get();
A third function runs when the condition is false.
await Actor()
.when(
sort == 'name',
(QueryBuilder<Actor> q) => q.orderBy('name'),
(QueryBuilder<Actor> q) => q.latest(),
)
.get();
unless and whenValue
unless is the opposite of when. It runs the function only when the condition is false.
await Actor()
.unless(showDrafts, (QueryBuilder<Actor> q) {
q.where('status', 'published');
})
.get();
whenValue runs the function when the value is not null. The function gets the value, and it is not null there.
It is like Laravel when($value, fn).
await Actor()
.whenValue(req.query['status'], (QueryBuilder<Actor> q, status) {
q.where('status', status);
})
.get();
Both take a third function too. It runs in the other case.
Select
select sets the columns. Each call replaces the columns of the call before.
// a list
await Actor().select(<String>['id', 'name']).get();
// SELECT "id","name" FROM "actor"
// comma separated
await Actor().select('id, name, age').get();
// with an alias
await Actor().select('name as actor_name').get();
// raw: nothing is quoted
await QueryBuilder.table('actor').selectRaw('count(*) as total, name').groupBy('name').get();
Each part must be a plain name: name, table.name, table.*, * or name as alias.
Anything else, like count(*), throws an ArgumentError. Use selectRaw for it.
Info
A model loaded with select() only has the selected fields.
save() on it only updates the fields it has.
distinct
distinct() returns each row only once (SELECT DISTINCT).
List<Map<String, dynamic>> rows = await QueryBuilder.table('actor')
.select('age')
.distinct()
.get();
// SELECT DISTINCT "age" FROM "actor"
int ages = await QueryBuilder.table('actor').select('age').distinct().count();
Order, limit and group
orderBy
await Actor().orderBy('name').get();
await Actor().orderBy('name', 'desc').get();
await Actor().orderBy('name', 'asc').orderBy('id', 'desc').get();
// ORDER BY "name" asc,"id" desc
- The direction must be
ascordesc. Any other value throws anArgumentError. - The column must be a plain name. Anything else throws an
ArgumentError. - For an expression, use
orderByRaw('count(*) desc'). Nothing is quoted there, so never put user input in it.
latest, oldest and inRandomOrder
Actor? newest = await Actor().latest().getFirst();
// ORDER BY "created_at" desc
await Actor().latest('updated_at').get();
await Actor().oldest().get();
// ORDER BY "created_at" asc
Actor? random = await Actor().inRandomOrder().getFirst();
latest()andoldest()usecreated_atwhen you give no column.inRandomOrder()isRANDOM()on Postgres and SQLite, andRAND()on MySQL. It sorts the whole table, so it is slow on a big table.
limit, take and offset
limit and take do the same thing.
await Actor().limit(10).get();
await Actor().take(10).get();
await Actor().limit(10).offset(20).get();
// LIMIT @dox_limit OFFSET @dox_offset
The numbers are sent as bound parameters.
groupBy
Takes one column, or a list of columns.
List<Map<String, dynamic>> rows = await QueryBuilder.table('actor')
.selectRaw('count(*) as total, age')
.groupBy('age')
.get();
For an expression, use groupByRaw('date(created_at)'). Nothing is quoted there.
Info
Use QueryBuilder.table() for groupBy. A group row is a map, not a model.
On a model, get() gives models (List<Actor>), so a groupBy there throws a StateError.
count() with groupBy works on a model too.
having
having filters the groups. It goes after GROUP BY.
It takes the same arguments as where.
await QueryBuilder.table('actor')
.selectRaw('status, count(*) as total')
.groupBy('status')
.having('status', '!=', 'draft')
.orHaving('status', 'retired')
.get();
// GROUP BY "status" HAVING "status" != @status_p0 OR "status" = @status_p1
Use havingRaw and orHavingRaw for an aggregate, like count(*).
Put values in the map, like in whereRaw.
await QueryBuilder.table('actor')
.selectRaw('status, count(*) as total')
.groupBy('status')
.havingRaw('count(*) > @min', <String, dynamic>{'min': 5})
.get();
Note
Postgres does not allow a select alias (like total) in HAVING.
Write the aggregate again: havingRaw('count(*) > @min', ...).
Join
await Actor().join('actor_info', 'actor_info.actor_id', 'actor.id').get();
// JOIN "actor_info" on "actor_info"."actor_id" = "actor"."id"
await Actor().leftJoin('actor_info', 'actor_info.actor_id', 'actor.id').get();
await Actor().rightJoin('actor_info', 'actor_info.actor_id', 'actor.id').get();
// with an operator
await Actor().join('actor_info', 'actor_info.actor_id', '=', 'actor.id').get();
For other join conditions, use the raw methods.
Nothing is quoted. Values go in the map, like in whereRaw.
await Actor().joinRaw('actor_info on actor_info.actor_id = actor.id').get();
await Actor().leftJoinRaw('actor_info on actor_info.actor_id = actor.id').get();
await Actor().rightJoinRaw(
'actor_info on actor_info.actor_id = actor.id and actor_info.type = @type',
<String, dynamic>{'type': 'main'},
).get();
Note
SQLite before 3.39 has no RIGHT JOIN.
joinSub and leftJoinSub
They join the rows of another query. The second argument is the name of those rows.
QueryBuilder<Map<String, dynamic>> latest = QueryBuilder.table('post')
.selectRaw('actor_id, MAX(created_at) as last_post')
.groupBy('actor_id');
await Actor()
.joinSub(latest, 'latest', 'latest.actor_id', 'actor.id')
.get();
// JOIN (SELECT actor_id, MAX(created_at) as last_post FROM "post"
// GROUP BY "actor_id") AS "latest" on "latest"."actor_id" = "actor"."id"
await Actor()
.leftJoinSub(latest, 'latest', 'latest.actor_id', '=', 'actor.id')
.get();
fromSub
fromSub reads from the rows of another query, not from a table.
QueryBuilder<Map<String, dynamic>> totals = QueryBuilder.table('order')
.selectRaw('actor_id, SUM(amount) as total')
.groupBy('actor_id');
List<Map<String, dynamic>> rows = await QueryBuilder.table('totals')
.fromSub(totals, 'totals')
.where('total', '>', 100)
.get();
// SELECT * FROM (SELECT ... GROUP BY "actor_id") AS "totals"
// WHERE "total" > @total_p0
The name is also the table name of the query, so totals.total works in where and orderBy.
Union
union adds the rows of another query.
It drops rows that are the same. unionAll keeps them, and it is faster.
List<Actor> actors = await Actor()
.where('age', '<', 18)
.union(Actor().where('age', '>', 60))
.orderBy('name')
.get();
// SELECT * FROM "actor" WHERE "age" < @age_p0
// UNION SELECT * FROM (SELECT * FROM "actor" WHERE "age" > @dox_sub0_age_p0) AS "dox_union0"
// ORDER BY "name"
await QueryBuilder.table('actor')
.select('name')
.unionAll(QueryBuilder.table('director').select('name'))
.get();
- Both queries must select the same number of columns.
orderBy,limitandoffsetof the first query sort and cut the whole result. Use a plain column name inorderBy, nottable.column.- The added query can have its own
orderByandlimit. count(),paginate(),sum(),avg(),min()andmax()use the whole result:SELECT SUM("age") as dox_aggregate FROM (...) as dox_union_aggregate.sum(),avg(),min()andmax()keeporderBy,limitandoffset.count()does not.cursorPaginate()andchunkById()do not work on a union. They throw aStateError. Usepaginate(),simplePaginate()orchunk().update,delete,forceDelete,increment,decrement,touchandall()throw aStateErroron a union, because they would use only the first query. Useget()to read it.
Row locks
lockForUpdate() and sharedLock() lock the rows that a select reads.
Use them inside a transaction.
See Row locks.
await QueryBuilder.transaction(() async {
Actor actor = await Actor().lockForUpdate().findOrFail(1);
actor.age = actor.age! + 1;
await actor.save();
});
Insert
create and insert
create and insert do the same thing.
They insert one row, then return it as it is in the table.
Actor actor = await Actor().create(<String, dynamic>{
'name': 'John Wick',
'age': 60,
});
print(actor.id);
Map<String, dynamic> row = await QueryBuilder.table('actor').insert(<String, dynamic>{
'name': 'John Doe',
});
print(row['id']);
- On a model, the result is the model. On
QueryBuilder.table(...), the result is a map. - The result has the column defaults of the table too.
Postgres and SQLite read the row with
RETURNING *. MySQL reads it again by its id. - It works with any primary key type, for example a uuid string.
- On a model with events,
createcallssave(), so the hooks run. - On a model without events,
createdoes not setcreated_atandupdated_at. Put them in the map, or usesave().
MySQL and a table without an auto increment id
MySQL can not read the new row when the table has no auto increment id, and the map has no id (for example a pivot table). Then the result is made from the map you gave.
insertMultiple
Inserts many rows in one query. It does not return models. It returns the new ids:
- Postgres and SQLite: one map per row, like
[{id: 4}, {id: 5}]. - MySQL and MariaDB: one map with the first id, like
[{id: 4}]. It is an empty list when the table has no auto increment id.
await Actor().insertMultiple(<Map<String, dynamic>>[
<String, dynamic>{'name': 'John Wick', 'age': 60},
<String, dynamic>{'name': 'John Doe', 'age': 25},
]);
The columns are the keys from all rows. When a row does not have a key, that column gets its default value.
insertGetId and insertMany
insertGetId inserts one row and returns its new id.
The id is the auto increment id, or the id in the map when the table has no auto increment.
dynamic id = await QueryBuilder.table('actor').insertGetId(<String, dynamic>{
'name': 'John Wick',
});
insertMany inserts many rows, reads nothing back and returns true.
It works on a table with no id column, like a pivot table. An empty list runs no query.
await QueryBuilder.table('actor_movie').insertMany(<Map<String, dynamic>>[
<String, dynamic>{'actor_id': 1, 'movie_id': 2},
<String, dynamic>{'actor_id': 1, 'movie_id': 3},
]);
- Both fire no model events and set no timestamps.
- On a model, the keys that
fillableandguardeddo not allow are dropped.
upsert
upsert inserts rows, or updates the row that is already there.
It runs one query and returns the number of changed rows.
int count = await Flight().upsert(
<Map<String, dynamic>>[
<String, dynamic>{'from': 'Oakland', 'to': 'San Diego', 'price': 99},
<String, dynamic>{'from': 'Chicago', 'to': 'New York', 'price': 150},
],
<String>['from', 'to'], // the unique columns
<String>['price'], // the columns to update
);
- The second list is the columns that find the old row. They must be the primary key or a unique index.
- The third list is the columns to update. Leave it out to update every other column. An empty list keeps the old row as it is.
- Every row must have the same keys. Otherwise it throws an
ArgumentError. - On a model, keys that
fillableandguardeddo not allow are dropped. A model with nofillablethrows, see mass assignment.created_atandupdated_atare set. An update does not changecreated_at. - It fires no model events.
| Database | SQL | Count |
|---|---|---|
| Postgres, SQLite | ON CONFLICT (...) DO UPDATE |
1 for each new or updated row |
| MySQL, MariaDB | ON DUPLICATE KEY UPDATE |
1 for each new row, 2 for each updated row, 0 for a row that did not change |
Note
MySQL and MariaDB do not use the unique columns you give. Any unique index of the table can find the old row. Postgres throws when two rows of one call have the same unique values.
insertOrIgnore
insertOrIgnore inserts rows and skips the rows that clash with a unique key.
It returns the number of inserted rows.
int added = await QueryBuilder.table('tag').insertOrIgnore(<Map<String, dynamic>>[
<String, dynamic>{'name': 'action'},
<String, dynamic>{'name': 'drama'},
]);
- Every row must have the same keys. Otherwise it throws an
ArgumentError. - On a model, keys that
fillableandguardeddo not allow are dropped. A model with nofillablethrows, see mass assignment. - It fires no model events.
| Database | SQL | Also skips |
|---|---|---|
| Postgres | ON CONFLICT DO NOTHING |
nothing else |
| SQLite | INSERT OR IGNORE |
rows that break a NOT NULL or CHECK rule |
| MySQL, MariaDB | INSERT IGNORE |
some bad values become a warning, for example text that is too long is cut |
firstOrCreate, firstOrNew and updateOrCreate
The first map finds the row. The second map has the other values.
// find the user, or create it
User user = await User().firstOrCreate(
<String, dynamic>{'email': 'ann@example.com'},
<String, dynamic>{'name': 'Ann'},
);
// find the user, or make a new model that is not saved yet
User other = await User().firstOrNew(<String, dynamic>{'email': 'bob@example.com'});
if (!other.exists) {
await other.save();
}
// update the user, or create it
User updated = await User().updateOrCreate(
<String, dynamic>{'email': 'ann@example.com'},
<String, dynamic>{'name': 'Ann Lee'},
);
| Method | Row found | No row found |
|---|---|---|
firstOrCreate |
returns it | creates a row from both maps |
firstOrNew |
returns it | returns a new model from both maps, not saved |
updateOrCreate |
updates it with the second map | creates a row from both maps |
- They return the row as it is in the table: a model, or a map for
QueryBuilder.table(). - On a model,
firstOrCreateandupdateOrCreateusecreate()andsave(), so the model events run. - On a model, keys that
fillableandguardeddo not allow are dropped. A model with nofillablethrows, see mass assignment.
Warning
Two requests at the same time can both create a row.
Add a unique index, and use upsert when this matters.
Update
update
Updates all rows that match the where part.
It returns the number of changed rows.
int count = await Actor()
.where('id', 3)
.where('status', 'active')
.update(<String, dynamic>{'name': 'Updated AJ', 'age': 120});
// UPDATE "actor" SET "name" = @name_p2,"age" = @age_p3
// WHERE "id" = @id_p0 AND "status" = @status_p1
- An empty map runs no query and returns 0.
- On a model, keys that
fillableandguardeddo not allow are dropped. A model with nofillablethrows, see mass assignment. When no key is left, it runs no query and returns 0. To set every key of the map, useforceUpdate(), see skip the fillable check. - Postgres and SQLite count every row that the
wherepart matched. - MySQL and MariaDB count only the rows that really changed. A row that already has the new values counts 0.
Warning
On a model, update needs a where (or a global scope).
Actor().update({...}) with no where throws a StateError and runs no query.
To change every row on purpose, see write every row.
A query update does not set updated_at and fires no model events.
To do that, load the model, change it and call save().
increment and decrement
They add to (or take from) a number column in one query. They return the number of changed rows.
await Actor().where('id', 1).increment('views');
// UPDATE "actor" SET "views" = "views" + @views_p1 WHERE "id" = @id_p0
await Actor().where('id', 1).increment('views', 5);
await Actor().where('id', 1).decrement('views', 2);
// set other columns in the same query
await Actor().where('id', 1).increment('views', 1, <String, dynamic>{'status': 'hot'});
- The amount is 1 when you give none.
- A
NULLcolumn staysNULL, like in SQL. - They do not set
updated_atand fire no model events. - On a model, they need a
where, a global scope or a model from the table.Actor().increment('views')with nowherethrows aStateErrorand runs no query.
On a model from the table, they change only the row of that model.
update on a model from the table does the same, like Laravel: await actor!.update({'name': 'AJ'}).
It runs one query and fires no model events. Use save() for the events and updated_at.
The field of the model gets the new value too.
This example has an int? views column field on Actor.
Actor? actor = await Actor().find(1);
await actor!.increment('views');
print(actor.views); // the new value
Delete
delete
Deletes all rows that match the where part.
It returns the number of deleted rows.
int count = await Actor().where('name', 'John Wick').delete();
// DELETE FROM "actor" WHERE "name" = @name_p0
On a model with softDelete: true, delete sets deleted_at and keeps the row.
Rows that are already soft deleted are not counted.
Warning
On a model, delete needs a where, a global scope, or a model from the table with its primary key.
Actor().delete() with no where throws a StateError and runs no query.
A model read without its primary key (like after select('name')) throws too.
So it can not delete every row by mistake. forceDelete does the same.
To delete every row on purpose, see write every row.
forceDelete
On a model with softDelete: true, forceDelete really deletes the rows.
It also deletes rows that are already soft deleted, so you do not need withTrash().
It returns the number of deleted rows.
withTrash
A model with softDelete: true hides soft deleted rows.
withTrash() shows them too.
On a model without soft deletes it does nothing.
onlyTrashed
onlyTrashed() gives only the soft deleted rows.
On a model without soft deletes it throws a StateError.
List<Actor> trashed = await Actor().onlyTrashed().get();
// delete the soft deleted rows for good
await Actor().onlyTrashed().forceDelete();
restore
restore() on a query sets deleted_at back to NULL for every soft deleted row of the query. It sets updated_at too, when the model has timestamps.
It returns true when it changed a row, else false.
It fires no model events.
With no where(), it throws a StateError, so it can not restore every row by mistake.
touch
Sets updated_at of the rows to now. Give a column name to set another column.
It returns the number of changed rows.
On a query, touch() fires no model events.
On a model query with no where() (and no global scope), touch() throws a StateError.
On MySQL and MariaDB, it can return 0 when the new time is the same as the stored one (the same second), because they count only changed rows.
On a model from the table, touch() changes only the row of that model.
See Models.
Write every row
On a model, delete, forceDelete, update, increment, decrement and touch with no where throw a StateError.
To write every row on purpose, use whereRaw('1 = 1'):
await Actor().whereRaw('1 = 1').update(<String, dynamic>{'status': 'old'});
await Actor().whereRaw('1 = 1').delete();
It keeps soft deletes (a soft delete model sets deleted_at) and global scopes.
Warning
QueryBuilder.table('actor').delete() and truncate are not the same.
They skip soft deletes, so they really delete the rows. They also skip global scopes and fillable.
On Postgres, truncate runs CASCADE: it also empties every table with a foreign key to this table.
truncate
Deletes all rows of the table and resets the id counter.
await Actor().truncate();
await Actor().truncate(resetId: false); // keep the id counter on Postgres and SQLite
On Postgres it runs TRUNCATE TABLE ... RESTART IDENTITY CASCADE.
On MySQL it runs TRUNCATE TABLE, and MySQL always resets the id counter.
On SQLite it runs DELETE FROM.
Debug
toSql
Returns the SQL of the query with the values filled in. The values are not quoted or escaped, so use it only to read the query.
String sql = Actor().where('name', 'John Wick').toSql();
print(sql); // SELECT * FROM "actor" WHERE "name" = John Wick
toRawSql
Returns the SQL with each value quoted for the database. Use it for logs and debugging. Run queries with bound values, not with this text.
String sql = Actor().where('name', "it's").toRawSql();
print(sql); // SELECT * FROM "actor" WHERE "name" = 'it''s'
debug
Prints the SQL and the values of the query when it runs.
To print all queries, set debug: true in the ConnectionConfig.
See Database.
Custom drivers
A custom DBDriver must override affectingStatement().
It runs an UPDATE, DELETE or INSERT and returns the number of changed rows.
update(), delete(), increment() and upsert() use it.
The default throws an UnsupportedError.