Manager
With generated code, drift allows writing SQL queries in type-safe Dart. While this is provides lots of flexibility, it requires familiarity with SQL. As a simpler alternative, drift 2.18 introduced a new set of APIs designed to make common queries much easier to write.
The examples on this page use the database from the setup instructions.
When manager generation is enabled (default), drift will generate a manager for each table in the database.
A collection of these managers are accessed by a getter managers
on the database class.
Each table will have a manager generated for it unless it uses a custom row class.
Select¶
The manager simplifies the process of retrieving rows from a table. Use it to read rows from the table or watch for changes.
Future<void> selectTodoItems() async {
// Get all items
managers.todoItems.get();
// A stream of all the todo items, updated in real-time
managers.todoItems.watch();
// To get a single item, apply a filter and call `getSingle`
await managers.todoItems.filter((f) => f.id(1)).getSingle();
}
The manager provides a really easy to use API for selecting rows from a table. These can be combined with |
and &
and parenthesis to construct more complex queries. Use .not
to negate a condition.
Future<void> filterTodoItems() async {
// All items with a title of "Title"
managers.todoItems.filter((f) => f.title("Title"));
// All items with a title of "Title" and content of "Content"
managers.todoItems.filter((f) => f.title("Title") & f.content("Content"));
// All items with a title of "Title" or content that is not null
managers.todoItems.filter((f) => f.title("Title") | f.content.not.isNull());
}
Every column has filters for equality, inequality and nullability.
Type specific filters for int
, double
, Int64
, DateTime
and String
are included out of the box.
Future<void> filterWithType() async {
// Filter all items created since 7 days ago
managers.todoItems.filter(
(f) => f.createdAt.isAfter(DateTime.now().subtract(Duration(days: 7))));
// Filter all items with a title that starts with "Title"
managers.todoItems.filter((f) => f.title.startsWith('Title'));
}
Referencing other tables¶
The manager also makes it easy to query an entities referenced fields by using the withReferences
method.
This will return a record with the entity and a refs
object which contains the referenced fields.
Future<void> references() async {
/// Get each todo, along with a its categories
final todosWithRefs = await managers.todoItems.withReferences().get();
for (final (todo, refs) in todosWithRefs) {
final category = await refs.category?.getSingle();
}
/// This also works in the reverse
final categoriesWithRefs =
await managers.todoCategory.withReferences().get();
for (final (category, refs) in categoriesWithRefs) {
final todos = await refs.todoItemsRefs.get();
}
}
The problem with the above approach is that it will issue a separate query for each row in the result set. This can be very inefficient if you have a large number of rows.
If there were 1000 todos, this would issue 1000 queries to fetch the category for each todo.
Filter on foreign keys
When filtering on a reference column, drift will apply the filter to the column itself instead of joining the referenced table.
For example, todos.filter((f) => f.category.id(1))
will filter on the category
column on the todos
table, instead of joining the two tables and filtering on the id
column of the categories
table.
How does this affect me?
If you have foreign keys contraints enabled (PRAGMA foreign_keys = ON
) this won't affect you. The database will enfore that the id
column on the categories
table is the same as the category
column on the todos
table.
If you don't have foreign key constraints enabled, you should be aware that the above query will not check that the category with id
1 exists. It will only check that the category
column on the todos
table is 1.
Prefetching references¶
Drift provides a way to prefetch references in a single query to avoid inefficient queries. This is done by using the callback in the withReferences
method. The referenced item will then be available in the referenced managers prefetchedData
field.
Future<void> referencesPrefetch() async {
/// Get each todo, along with a its categories
final categoriesWithReferences = await managers.todoItems
.withReferences(
(prefetch) => prefetch(category: true),
)
.get();
for (final (todo, refs) in categoriesWithReferences) {
final category = refs.category?.prefetchedData?.firstOrNull;
// No longer needed
// final category = await refs.category?.getSingle();
}
/// This also works in the reverse
final todosWithRefs = await managers.todoCategory
.withReferences((prefetch) => prefetch(todoItemsRefs: true))
.get();
for (final (category, refs) in todosWithRefs) {
final todos = refs.todoItemsRefs.prefetchedData;
// No longer needed
//final todos = await refs.todoItemsRefs.get();
}
}
Filtering across tables¶
You can filter across references to other tables by using the generated reference filters. You can nest these as deep as you'd like and the manager will take care of adding the aliased joins behind the scenes.
Future<void> relationalFilter() async {
// Get all items with a category description of "School"
managers.todoItems.filter((f) => f.category.description("School"));
// These can be combined with other filters
// For example, get all items with a title of "Title" or a category description of "School"
await managers.todoItems
.filter(
(f) => f.title("Title") | f.category.description("School"),
)
.exists();
}
You can also filter across back references. This is useful when you have a one-to-many relationship and want to filter the parent table based on the child table.
Future<void> reverseRelationalFilter() async {
// Get the category that has a todo item with an id of 1
managers.todoCategory.filter((f) => f.todoItemsRefs((f) => f.id(1)));
// These can be combined with other filters
// For example, get all categories with a description of "School" or a todo item with an id of 1
managers.todoCategory.filter(
(f) => f.description("School") | f.todoItemsRefs((f) => f.id(1)),
);
}
The code generator will name this filterset using the name of the table that is being referenced. In the above example, the filterset is named todoItemsRefs
, because the TodoItems
table is being referenced.
However, you can also specify a custom name for the filterset using the @ReferenceName(...)
annotation on the foreign key. This may be necessary if you have multiple references to the same table, take the following example:
class Users extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
}
class Groups extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
@ReferenceName("administeredGroups")
IntColumn get admin => integer().nullable().references(Users, #id)();
@ReferenceName("ownedGroups")
IntColumn get owner => integer().references(Users, #id)();
}
We can now use them in a query like this:
Future<void> reverseNamedRelationalFilter() async {
// Get all users who are administrators of a group with a name containing "Business"
// or who own a group with an id of 1, 2, 4, or 5
managers.users.filter(
(f) =>
f.administeredGroups((f) => f.name.contains("Business")) |
f.ownedGroups((f) => f.id.isIn([1, 2, 4, 5])),
);
}
In this example, had we not specified a custom name for the reference, the code generator would have named both filtersets userRefs
for both references to the User
table. This would have caused a conflict. By specifying a custom name, we can avoid this issue.
Name Clashes¶
Drift auto-generates filters and orderings based on the names of your tables and fields. However, many times, there will be duplicates.
When this happens, you will see a warning message from the generator.
To fix this issue, use the @ReferenceName()
annotation to specify what we should name the filter/orderings.
Ordering¶
You can also order the results of a query using the orderBy
method. The syntax is similar to the filter
method.
Use the &
to combine multiple orderings. Orderings are applied in the order they are added.
You can also use ordering across multiple tables just like with filters.
Future<void> orderWithType() async {
// Order all items by their creation date in ascending order
managers.todoItems.orderBy((o) => o.createdAt.asc());
// Order all items by their title in ascending order and then by their content in ascending order
managers.todoItems.orderBy((o) => o.title.asc() & o.content.asc());
}
Count and exists¶
The manager makes it easy to check if a row exists or to count the number of rows that match a certain condition.
Future<void> count() async {
// Count all items
await managers.todoItems.count();
// Count all items with a title of "Title"
await managers.todoItems.filter((f) => f.title("Title")).count();
}
Future<void> exists() async {
// Check if any items exist
await managers.todoItems.exists();
// Check if any items with a title of "Title" exist
await managers.todoItems.filter((f) => f.title("Title")).exists();
}
Updates¶
We can use the manager to update rows in bulk or individual rows that meet a certain condition.
Future<void> updateTodoItems() async {
// Update all items
await managers.todoItems.update((o) => o(content: Value('New Content')));
// Update multiple items
await managers.todoItems
.filter((f) => f.id.isIn([1, 2, 3]))
.update((o) => o(content: Value('New Content')));
}
We can also replace an entire row with a new one. Or even replace multiple rows at once.
Future<void> replaceTodoItems() async {
// Replace a single item
var obj = await managers.todoItems.filter((o) => o.id(1)).getSingle();
obj = obj.copyWith(content: 'New Content');
await managers.todoItems.replace(obj);
// Replace multiple items
var objs =
await managers.todoItems.filter((o) => o.id.isIn([1, 2, 3])).get();
objs = objs.map((o) => o.copyWith(content: 'New Content')).toList();
await managers.todoItems.bulkReplace(objs);
}
Creating rows¶
The manager includes a method for quickly inserting rows into a table. We can insert a single row or multiple rows at once.
Future<void> createTodoItem() async {
// Create a new item
await managers.todoItems
.create((o) => o(title: 'Title', content: 'Content'));
// We can also use `mode` and `onConflict` parameters, just
// like in the `[InsertStatement.insert]` method on the table
await managers.todoItems.create(
(o) => o(title: 'Title', content: 'New Content'),
mode: InsertMode.replace);
// We can also create multiple items at once
await managers.todoItems.bulkCreate(
(o) => [
o(title: 'Title 1', content: 'Content 1'),
o(title: 'Title 2', content: 'Content 2'),
],
);
}
Deleting rows¶
We may also delete rows from a table using the manager. Any rows that meet the specified condition will be deleted.
Future<void> deleteTodoItems() async {
// Delete all items
await managers.todoItems.delete();
// Delete a single item
await managers.todoItems.filter((f) => f.id(5)).delete();
}
Computed Fields¶
Manager queries are great when you need to select entire rows from a database table along with their related data. However, there are situations where you might want to perform more complex operations directly within the database for better efficiency.
Drift offers strong support for writing SQL expressions. These expressions can be used to filter data, sort results, and perform various calculations directly within your SQL queries. This means you can leverage the full power of SQL to handle complex logic right in the database, making your queries more efficient and your code cleaner.
If you want to learn more about how to write these SQL expressions, please refer to the expression documentation.
// First create an computed field with an expression you want to use
final titleLengthField =
db.managers.todoItems.computedField((o) => o.title.length);
/// Create a copy of the manager with the computed fields you want to use
final manager = db.managers.todoItems.withFields([titleLengthField]);
// Then use the computed field in a filter
// This will filter all items whose title has exactly 10 characters
manager.filter((f) => titleLengthField.filter(10));
// You can also use the computed field in an ordering
// This will order all items by the length of their title in ascending order
manager.orderBy((o) => titleLengthField.order.asc());
/// You can read the result of the computed field too
for (final (item, refs) in await manager.get()) {
final titleLength = titleLengthField.read(refs);
print('Item ${item.id} has a title length of $titleLength');
}
You can write expressions which reference other columns in the same table or even other tables. The joins will be created automatically by the manager.
// This computed field will get the name of the user of this todo
final todoUserName =
db.managers.todoItems.computedField((o) => o.category.user.name);
/// Create a copy of the manager with the computed fields you want to use
final manager = db.managers.todoItems.withFields([todoUserName]);
/// You can read the result of the computed field too
for (final (item, refs) in await manager.get()) {
final userName = todoUserName.read(refs);
print('Item ${item.id} has a user with the name $userName');
}
You can also use aggregate functions too.
// You can aggregate over multiple rows in a related table
// to perform calculations on them
final todoCountcomputedField = db.managers.todoCategory
.computedField((o) => o.todoItemsRefs((o) => o.id).count());
/// Create a copy of the manager with the computed fields you want to use
final manager = db.managers.todoCategory.withFields([todoCountcomputedField]);
/// Read the result of the computed field
for (final (category, refs) in await manager.get()) {
final todoCount = todoCountcomputedField.read(refs);
print('Category ${category.id} has $todoCount todos');
}