Repository
Repositories give you model-oriented CRUD with predictable defaults. They accept tracked models, DTOs, and raw maps, so you can start simple and add type safety as your app grows.
Prerequisites
What You’ll Learn
- How to perform insert/read/update/delete flows with repository APIs
- Which input shapes each operation accepts
- When repository APIs are a better fit than lower-level query composition
Step Outcome
By the end of this page, you should be able to implement common write workflows with repo<T>() and know when to switch to query builder methods.
- Snippets focus on repository calls and omit full setup.
- You can obtain a repository from either:
- a
DataSource/QueryContext(explicit, recommended for multi-database apps), or - generated model helpers (uses the default connection).
- a
The default connection is managed by ConnectionManager and is usually set when you call await dataSource.init().
1. Get a Repository
- Default connection
- From DataSource
Future<void> getRepositoryStaticHelpers() async {
// Assumes a default connection is configured (see DataSource docs).
final userRepo = Users.repo();
await userRepo.find(1);
}
Future<void> getRepositoryExample(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
}
Use default helpers for single-connection apps. Use dataSource.repo<T>() when routing queries by connection name/tenant.
2. Insert Records
Insert one row
Future<void> insertExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
// With tracked model
final user = await userRepo.insert(
$User(id: 0, email: 'john@example.com', name: 'John'),
);
// With insert DTO
final user2 = await userRepo.insert(
UserInsertDto(email: 'john@example.com', name: 'John'),
);
// With raw map
final user3 = await userRepo.insert({
'email': 'john@example.com',
'name': 'John',
});
}
Insert many rows
Future<void> insertManyExample(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
final users = await userRepo.insertMany([
$User(id: 0, email: 'user1@example.com'),
$User(id: 0, email: 'user2@example.com'),
]);
}
Upsert (insert-or-update)
Future<void> upsertExample(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
// Insert if not exists, update if exists
final user = await userRepo.upsert(
$User(id: 1, email: 'john@example.com', name: 'Updated Name'),
);
}
3. Read Records
Find by Primary Key
Future<void> findExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
final user = await userRepo.find(1); // Returns null if not found
final user2 = await userRepo.findOrFail(1); // Throws if not found
final users = await userRepo.findMany([1, 2, 3]);
}
First, count, and exists
Future<void> firstCountExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
final user = await userRepo.first();
final user2 = await userRepo.first(where: {'active': true});
final count = await userRepo.count();
final activeCount = await userRepo.count(where: {'active': true});
final hasActive = await userRepo.exists({'active': true});
}
4. Update Records
Update one model or one matched row
Future<void> updateExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
final user = await userRepo.find(1);
// With tracked model (uses primary key)
if (user != null) {
user.setAttribute('name', 'Updated Name');
final updated = await userRepo.update(user);
}
// With DTO and where clause
final updated2 = await userRepo.update(
UserUpdateDto(name: 'Updated Name'),
where: {'id': 1},
);
// With Query callback
final updated3 = await userRepo.update(
UserUpdateDto(name: 'Updated Name'),
where: (Query<$User> q) => q.whereEquals('email', 'john@example.com'),
);
}
Update many rows/models
Future<void> updateManyExample(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
final updated = await userRepo.updateMany([
$User(id: 1, email: 'user1@example.com', name: 'Name 1'),
$User(id: 2, email: 'user2@example.com', name: 'Name 2'),
]);
}
where accepts multiple input types
The where parameter accepts various input types:
Future<void> whereTypesExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
// Map
await userRepo.update(UserUpdateDto(name: 'Test'), where: {'id': 1});
// Partial entity
await userRepo.update(
UserUpdateDto(name: 'Test'),
where: $UserPartial(id: 1),
);
// DTO
await userRepo.update(
UserUpdateDto(name: 'Test'),
where: UserUpdateDto(email: 'john@example.com'),
);
// Query callback (must type the parameter!)
await userRepo.update(
UserUpdateDto(name: 'Test'),
where: (Query<$User> q) => q.whereEquals('email', 'test@example.com'),
);
}
Typed callback predicates
Future<void> whereTypedCallbackExample(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
await userRepo.update(
UserUpdateDto(name: 'Test'),
where: (Query<$User> q) =>
q.whereTyped((p) => p.email.eq('test@example.com')),
);
}
When using a callback for where, the parameter is dynamic unless you explicitly type it.
Untyped callbacks still run, but you lose static checking.
// ✅ Preferred - typed parameter enables typed predicate fields
// where: (Query<$User> q) => q.whereTyped((p) => p.email.eq('test@example.com'))
//
// ✅ Works, but `q` is dynamic (less static checking)
// where: (q) => q.whereTyped((p) => p.email.eq('test@example.com'))
5. Delete and Restore
Delete one record
Future<void> deleteExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
final user = await userRepo.find(1);
// By primary key
await userRepo.delete(1);
// By tracked model
if (user != null) {
await userRepo.delete(user);
}
// By where clause
await userRepo.delete({'email': 'john@example.com'});
// By Query callback
await userRepo.delete((Query<$User> q) => q.whereEquals('role', 'guest'));
}
Delete many records
Future<void> deleteManyExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
await userRepo.deleteByIds([1, 2, 3]);
await userRepo.deleteMany([
{'id': 1},
(Query<$User> q) => q.whereEquals('role', 'guest'),
]);
}
Soft delete helpers
For models that include SoftDeletes:
Future<void> softDeleteExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
final user = await userRepo.find(1);
if (user != null) {
// Soft delete (sets deleted_at)
await userRepo.delete(user);
// Restore
await userRepo.restore(user);
await userRepo.restore({'id': 1});
// Force delete (permanently removes)
await userRepo.forceDelete(user);
}
}
6. Relation and Error Workflows
Relation loading from repository-fetched models
Future<void> relationExamples(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
final user = await userRepo.find(1);
if (user != null) {
// Load relations
await user.load(['posts', 'profile']);
}
}
Error handling patterns
Future<void> errorHandlingExample(DataSource dataSource) async {
final userRepo = dataSource.repo<$User>();
try {
final user = await userRepo.findOrFail(999);
} on ModelNotFoundException catch (e) {
print('User not found: ${e.key}');
}
try {
await userRepo.update(UserUpdateDto(name: 'Test'), where: {'id': 999});
} on NoRowsAffectedException {
print('No rows were updated');
}
}
7. When to Use Repository vs Query Builder
- Prefer
Repositoryfor CRUD operations tied to one model type. - Prefer
Query Builderfor complex filtering, joins, projections, aggregates, and SQL-shape control. - Combine them freely: read with query builder, mutate with repository.
Verify Your Setup
- You can fetch a repository from both default helpers and
DataSource. - You can insert/update/delete without dropping to raw SQL.
- Your
wherecallbacks are typed where predicate fields are used.