Relations and preloading
Relations are values that describe how two tables join. The generator emits
them as static fields on your models (Post.author, Author.posts), and
Query.preload uses them to load related rows in batches instead of one query
per parent.
import 'package:gisila_orm/gisila.dart';import 'models/blog.gisila.g.dart';
final list = await authors() .preload([Author.posts.then(Post.comments)]) .all(db.context());
for (final a in list) { for (final p in a.postsList) { print('${a.name}: ${p.title} (${p.commentsList.length} comments)'); }}Query.preload
Section titled “Query.preload”Query<T> preload(List<Relation<dynamic, dynamic>> relations)Schedules relations to load after all, one or first hydrates the parent
rows. stream does not preload.
When to use: whenever you’ll read a relation for more than one parent, for example when rendering a list. Loading children inside a loop issues one query per parent (the N+1 problem).
How it runs:
| Relation | Queries per relation node |
|---|---|
| belongs-to | 1: SELECT ... FROM child WHERE child.id IN (parent fks) |
| has-many / has-one | 1: SELECT ... FROM child WHERE child.fk IN (parent ids) |
| many-to-many | 2: junction lookup, then SELECT ... FROM child WHERE id IN (...) |
- The cost doesn’t depend on how many parents there are. Each sibling relation in the list and each
.thenlevel adds its own queries. - Keys are de-duplicated and sent in batches of 10,000 (
Preloader.maxKeysPerQuery) to stay under Postgres’s parameter limit. - Parents with a null key get an empty list or
null. - Children are loaded with all their columns; there’s no filtering, ordering or limit on preloaded children. For “the latest 3 posts per author”, write a query.
- Preloading runs on the same
DbContextas the parent query, so inside a transaction it sees the transaction’s writes.
Don’t list a relation both on its own and in a chain
([Author.posts, Author.posts.then(Post.comments)]); that loads posts twice.
The chain alone loads both levels.
Relation<Parent, Child>
Section titled “Relation<Parent, Child>”abstract class Relation<Parent, Child> { String get parentTable; String get childTable; String get name; RelationKind get kind; TableMeta<Child> get childMeta; List<Relation<dynamic, dynamic>> get nested; Relation<Parent, Child> then(Relation<Child, dynamic> next); List<Child> typedListOf(Iterable<Object?> items); Child? typedSingleOf(Object? value);}Base class of the four relation kinds.
When to use: as the element type of a preload list, and then to chain.
| Member | Description |
|---|---|
name |
Key the loaded value is stored under on the parent, and the stem of the generated accessor. |
kind |
RelationKind.hasMany, hasOne, belongsTo or manyToMany. |
childMeta |
The child’s TableMeta, used to select and hydrate children. |
nested |
Relations to load on the children afterwards. |
then(next) |
A copy with next appended to nested. next must start from this relation’s child type. |
typedListOf, typedSingleOf |
Cast loaded values to List<Child> / Child?. Used by the preloader. |
then can be chained and nested: Author.posts.then(Post.comments.then(Comment.author)).
RelationKind
Section titled “RelationKind”enum RelationKind { hasMany, hasOne, belongsTo, manyToMany }BelongsToRelation
Section titled “BelongsToRelation”const BelongsToRelation({ required String parentTable, required String childTable, required String name, required String parentForeignKey, required TableMeta<Child> childMeta, String childPrimaryKey = 'id', List<Relation<dynamic, dynamic>> nested = const [],});The parent holds the foreign key: parent.<parentForeignKey> = child.<childPrimaryKey>.
Generated for every foreign-key column (Post.author). Loaded value: Child?,
read with post.authorLoaded.
HasManyRelation
Section titled “HasManyRelation”const HasManyRelation({ required String parentTable, required String childTable, required String name, required String childForeignKey, required TableMeta<Child> childMeta, String parentPrimaryKey = 'id', List<Relation<dynamic, dynamic>> nested = const [],});The child holds the foreign key: parent.<parentPrimaryKey> = child.<childForeignKey>.
Generated on the target model when a foreign key sets reverse_name
(Author.posts). Loaded value: List<Child>, read with author.postsList.
HasOneRelation
Section titled “HasOneRelation”Same constructor as HasManyRelation. Generated instead of a has-many when the
foreign key is is_unique: true. Loaded value: Child? (the first match),
read with <name>Loaded.
ManyToManyRelation
Section titled “ManyToManyRelation”const ManyToManyRelation({ required String parentTable, required String childTable, required String name, required String junctionTable, required String junctionParentKey, required String junctionChildKey, required TableMeta<Child> childMeta, String parentPrimaryKey = 'id', String childPrimaryKey = 'id', List<Relation<dynamic, dynamic>> nested = const [],});Joins through a junction table. Generated for many_to_many: true columns and
their reverse_name inverse. Loaded value: List<Child>, read with <name>List.
The ORM has no API for writing junction rows; insert and delete them with raw SQL.
Preloadable
Section titled “Preloadable”mixin Preloadable { final Map<String, Object?> $preloaded; T? preloaded<T>(String relationName); bool isPreloaded(String relationName);}Every generated model mixes this in. The preloader writes loaded values into
$preloaded, keyed by relation name, and the generated ...List / ...Loaded
getters read them back.
When to use: call isPreloaded('posts') to tell “not loaded” apart from
“loaded and empty” (the generated postsList returns an empty list in both
cases). Don’t write to $preloaded yourself.
| Member | Description |
|---|---|
$preloaded |
Storage map written by the preloader. |
preloaded<T>(name) |
The stored value cast to T, or null when absent. |
isPreloaded(name) |
true when the relation was loaded for this instance, even if empty. |
Preloader
Section titled “Preloader”class Preloader { const Preloader(); static const maxKeysPerQuery = 10000; Future<void> applyTo(List<Object> parents, List<Relation<dynamic, dynamic>> relations, DbContext db);}The batch loader behind preload. It reads key values through each model’s
toRow(), so parents and children must be generated models.
When to use: to preload onto models you already have, for example rows hydrated from a raw query or loaded in an earlier step.
final result = await db.execute('SELECT * FROM authors WHERE name ~* \$1', parameters: ['^a']);final list = Hydrator<Author>(Author.fromRow).hydrateAll(result);await const Preloader().applyTo(list, [Author.posts], db.context());A parent that doesn’t mix in Preloadable makes it throw
PreloadException.
See also: Eager loading guide, Relations and N+1, Work with relations.