Skip to main content

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.

Snippet context
  • 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).

The default connection is managed by ConnectionManager and is usually set when you call await dataSource.init().

1. Get a Repository

Future<void> getRepositoryStaticHelpers() async {
// Assumes a default connection is configured (see DataSource docs).
final userRepo = Users.repo();
await userRepo.find(1);
}

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')),
);
}
Important

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 Repository for CRUD operations tied to one model type.
  • Prefer Query Builder for 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 where callbacks are typed where predicate fields are used.

Read This Next