Automated Relationship Generation#
The datum_generator can automatically create the relations getter for RelationalDatumEntity
classes, eliminating the need to manually define relationships.
Overview#
Instead of manually writing the relations getter, you can use relationship annotations on placeholder fields. The generator will automatically create the complete
relations map for you.
Important:
-
If your field name starts with an underscore (e.g.,
_posts), the generator will strip the underscore for the relation name in thedatumRelationsmap (e.g.,'posts'). -
When
generateMixin: trueis used, the generator also adds public getters and setters for these private fields (e.g.,get postsandset posts) to make them easily accessible from outside the entity while keeping the storage private.
Available Relationship Annotations#
1. @BelongsToRelation #
Use when this entity has a foreign key pointing to another entity.
@BelongsToRelation<User>('userId', cascadeDelete: 'none')
final String? _author = null;
Parameters:
foreignKey(required): The foreign key field name in this entitylocalKey(optional, default: 'id'): The local key in the related entity-
cascadeDelete(optional, default: 'none'): Cascade behavior ('none', 'cascade', 'restrict', 'setNull')
2. @HasManyRelation #
Use when another entity has a foreign key pointing to this entity (one-to-many).
@HasManyRelation<Post>('userId', cascadeDelete: 'cascade')
final List<Post>? _posts = null;
Parameters:
foreignKey(required): The foreign key field name in the related entitylocalKey(optional, default: 'id'): The local key in this entitycascadeDelete(optional, default: 'none'): Cascade behavior
3. @HasOneRelation #
Use when another entity has a foreign key pointing to this entity (one-to-one).
@HasOneRelation<Profile>('userId')
final Profile? _profile = null;
Parameters:
foreignKey(required): The foreign key field name in the related entitylocalKey(optional, default: 'id'): The local key in this entitycascadeDelete(optional, default: 'none'): Cascade behavior
4. @ManyToManyRelation<T, P>#
Use for many-to-many relationships through a pivot entity.
@ManyToManyRelation<Tag, PostTag>(
pivotEntity: PostTag,
thisForeignKey: 'postId',
otherForeignKey: 'tagId',
cascadeDelete: 'cascade',
)
final List<Tag>? _tags = null;
Parameters:
pivotEntity(required): The pivot entity typethisForeignKey(required): Foreign key in pivot pointing to this entityotherForeignKey(required): Foreign key in pivot pointing to related entitythisLocalKey(optional, default: 'id'): Local key in this entityotherLocalKey(optional, default: 'id'): Local key in related entitycascadeDelete(optional, default: 'none'): Cascade behavior
Complete Example#
Before (Manual)#
@DatumSerializable(tableName: 'users')
class User extends RelationalDatumEntity {
@override
final String id;
final String name;
final String email;
// ... other fields
// Manual relationship definition
@override
Map<String, Relation> get relations => {
'posts': HasMany<Post>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.cascade),
'profile': HasOne<Profile>(this, 'userId'),
'groups': ManyToMany<Group>(
this,
const UserGroup() as DatumEntityInterface,
'userId',
'groupId',
),
};
}
After (Automated with Mixin)#
part 'user.g.dart';
@DatumSerializable(tableName: 'users', generateMixin: true)
class User extends RelationalDatumEntity with _$UserMixin {
@override
final String id;
final String name;
final String email;
// Define relationships with annotations on private fields
@HasManyRelation<Post>('userId', cascadeDelete: 'cascade')
final List<Post>? _posts = null;
@HasOneRelation<Profile>('userId')
final Profile? _profile = null;
@ManyToManyRelation<Group, UserGroup>(
pivotEntity: UserGroup,
thisForeignKey: 'userId',
otherForeignKey: 'groupId',
)
final List<Group>? _groups = null;
// ... other fields
// Use generated relations
@override
Map<String, Relation> get relations => datumRelations;
factory User.fromMap(Map<String, dynamic> map) {
return _$UserFromMap(map);
}
}
Note: In the example above, the mixin will automatically provide public posts,
profile, and groups getters/setters that proxy to the private _posts,
_profile, and _groups fields while ensuring the datumRelations
map remains synchronized.
// user.g.dart (generated)
extension $UserDatum on User {
// ... other generated methods
Map<String, Relation> get datumRelations => {
'posts': HasMany<Post>(
this,
'userId',
localKey: 'id',
cascadeDeleteBehavior: CascadeDeleteBehavior.cascade,
)..setRaw(_posts),
'profile': HasOne<Profile>(
this,
'userId',
localKey: 'id',
cascadeDeleteBehavior: CascadeDeleteBehavior.none,
)..setRaw(_profile),
'groups': ManyToMany<Group>(
this,
const UserGroup() as DatumEntityInterface,
'userId',
'groupId',
thisLocalKey: 'id',
otherLocalKey: 'id',
cascadeDeleteBehavior: CascadeDeleteBehavior.none,
)..setRaw(_groups),
};
}
mixin _$UserMixin on RelationalDatumEntity {
// ... method overrides (toDatumMap, diff, etc.)
// Generated proxies for private relationship fields
List<Post>? get posts => (this as User)._posts;
set posts(List<Post>? value) {
if (this is User) {
(this as User).datumRelations['posts']?.setRaw(value);
}
}
// ... profile and groups proxies
}
part 'paint_canvas.g.dart';
@DatumSerializable(tableName: 'paint_canvases', generateMixin: true)
class PaintCanvas extends RelationalDatumEntity with _$PaintCanvasMixin {
@override
final String id;
@override
final String userId;
final String title;
final String? description;
final int strokeCount;
// Define relationship using annotation on private field
@HasManyRelation<PaintStroke>('canvasId', cascadeDelete: 'cascade')
final List<PaintStroke>? _strokes = null;
@override
final DateTime createdAt;
@override
final DateTime modifiedAt;
@override
final int version;
@override
final bool isDeleted;
const PaintCanvas({
required this.id,
required this.userId,
required this.title,
this.description,
this.strokeCount = 0,
required this.createdAt,
required this.modifiedAt,
this.version = 1,
this.isDeleted = false,
});
// Use generated relations
@override
Map<String, Relation> get relations => datumRelations;
factory PaintCanvas.fromMap(Map<String, dynamic> map) {
return _$PaintCanvasFromMap(map);
}
}
Benefits#
- Less Boilerplate: No need to manually write the
relationsgetter - Type Safety: Generic type parameters ensure compile-time type checking
- Consistency: All relationships follow the same pattern
- Maintainability: Changes to relationships only require updating annotations
- Discoverability: Annotations make relationships explicit and easy to find
- Documentation: Annotation parameters are self-documenting
- Refactoring: Easier to rename or modify relationships
Migration Guide#
To migrate existing code from manual to automated relationships:
Step 1: Add Placeholder Fields with Annotations#
// Add annotated fields for each relationship
@HasManyRelation<Post>('userId', cascadeDelete: 'cascade')
final List<Post>? _posts = null;
@HasOneRelation<Profile>('userId')
final Profile? _profile = null;
Step 2: Replace Manual Relations Getter#
// Before
@override
Map<String, Relation> get relations => {
'posts': HasMany<Post>(this, 'userId', cascadeDeleteBehavior: CascadeDeleteBehavior.cascade),
'profile': HasOne<Profile>(this, 'userId'),
};
// After
@override
Map<String, Relation> get relations => datumRelations;
Step 3: Run the Generator#
flutter pub run build_runner build --delete-conflicting-outputs
Step 4: Verify Generated Code#
Check the generated .g.dart file to ensure all relationships are correctly generated.
Best Practices#
-
Use Descriptive Field Names: Even though the field is a placeholder, use meaningful names like
_posts,_profile, etc. -
Specify Cascade Behavior: Always explicitly set
cascadeDeleteto make the behavior clear:@HasManyRelation<Comment>('postId', cascadeDelete: 'cascade') -
Document Complex Relationships: Add comments for many-to-many relationships:
// Tags associated with this post through the post_tags pivot table @ManyToManyRelation<Tag, PostTag>( pivotEntity: PostTag, thisForeignKey: 'postId', otherForeignKey: 'tagId', ) final List<Tag>? _tags = null; -
Group Related Annotations: Keep relationship fields together in your class definition for better readability.
Troubleshooting#
Relationship Not Generated#
Problem: The datumRelations getter doesn't include your relationship.
Solutions:
- Ensure the field has a relationship annotation
- Verify the class extends
RelationalDatumEntity - Check that the field is not
static - Run
flutter pub run build_runner cleanand rebuild
Type Mismatch Errors#
Problem: Generated code has type errors for relationships.
Solutions:
- Ensure generic types match your entity types
- For
ManyToManyRelation, verify both generic types are correct - Check that pivot entity is properly defined
Cascade Delete Not Working#
Problem: Cascade delete behavior isn't applied.
Solutions:
- Verify
cascadeDeleteparameter is set correctly - Check spelling: 'cascade', 'restrict', 'setNull', or 'none'
- Ensure you're using the generated
datumRelationsgetter
Next Steps#
- Code Generation Guide: Learn about other code generation features
- Relationships Guide: Understand Datum's relationship system
- Cascading Deletes: Deep dive into cascade behaviors