ORM Relationships
neko-orm can handle relationship among models using RelationshipFactory
.
General vocabulary for relation ship
- source: the local or the model you are starting from
- target: the remote or the model(s) you are associating with
- pivot: or the junction table is the intermediary table used to manage M-to-M relationships
Sample data structure
Examples and test code is based on the following database design:
post_viewer
and taggables
are junction(aka intermediary, pivot) tables to help with MtoM relationships, there is no model for them.
1-to-1 aka hasOne
first step is to define relation among models
class User extends BaseModel {
profile() {
return RelationshipFactory.createHasOne<User, Profile>({
source: this,
targetModel: Profile,
});
}
}
class Profile extends BaseModel {
session() {
return RelationshipFactory.createBelongsTo<Profile, User>({
source: this,
targetModel: User,
});
}
}
to make the association, RelationshipFactory predicts possible name of forgein key. if you want to define your own or have multiple primary keys, then define sourceToTargetKeyAssociation
in the options.
once the relationship is established you can get the child relationship
await user.profile().get();
adding to a relationship
await user.profile().associate(profile);
removing from a relationship
await user.profile().dessociate(profile);
If you need to do the save step yourself just pass sync:false
.
await user.profile().associate(profile, { sync: false });
await profile.save();
await user.session().associate(profile, { sync: false });
await profile.save();
1-to-Many aka hasMany
first step is to define relation among models
class User extends BaseModel {
comments() {
return RelationshipFactory.createHasMany<User, Post>({
source: this,
targetModel: Post,
});
}
}
to make the association, RelationshipFactory predicts possible name of forgein key. if you want to define your own or have multiple primary keys, then define sourceToTargetKeyAssociation
in the options.
once the relationship is established you can iterate among children
for (const post of await user.posts().toArray()) {
}
if you run into a situation where you have too many children then you can easily async iterate
for await (const post of user.posts()) {
}
this approaches loads one model at a time from database.
adding to a relationship
await user.posts().associate(post1);
await user.posts().associate([post2, post3]);
removing from a relationship
await user.posts().dessociate(post1);
await user.posts().dessociate([post2, post3]);
If you need to do the save step yourself just pass sync:false
.
await user.posts().associate(post1, { sync: false });
await post1.save();
await user.posts().dessociate(post1, { sync: false });
await post1.save();
Many-to-1 aka belongsTo
As a reverse relationship of hasMany you can define it as such
class Post extends BaseModel {
@Attribute()
declare author_id: number;
author() {
return RelationshipFactory.createBelongsTo<Post, User>({
source: this,
targetModel: User,
sourceToTargetKeyAssociation: {
author_id: "id",
},
});
}
}
get relationship
since there is only 1 object that can be returned, use get() instead
let author = await post.author().get();
manage relationship
await post.author().associate(author);
await post.author().dessociate(author);
//faster way of dessociate
await post.author().unlink();
keep in mind that these methods are modifying source(post model) and leaving target model alone(user)
Many to Many aka belongsToMany
To establish the relationship between two models:
export class Viewer extends BaseModel {
posts() {
return RelationshipFactory.createBelongsToMany<Viewer, Post>({
source: this,
targetModel: Post,
});
}
}
export class Tag extends BaseModel {
viewer() {
return RelationshipFactory.createBelongsToMany<Post, Viewer>({
source: this,
targetModel: Viewer,
});
}
}
if you want to make further configurations you can:
posts() {
return RelationshipFactory.createBelongsToMany<Viewer, Post>({
source: this,
targetModel: Post,
junctionTable: 'post_viewer',
sourceToJunctionKeyAssociation: {id: viewer_id}, //id is viewer.id
junctionToTargetAssociation: { post_id: id} //id is post.id
});
}
to make association between new model objects:
await post.viewers().associate([viewer1, viewer2, viewer3]);
and to remove:
await post.viewers().dessociate([viewer1, viewer2, viewer3]);
NOTE: there is currently no sync=false for MtoM relationships
to get all associated models you can use toArray() or iteration
await post.viewers().toArray();
for await (const viewer of post.viewers()) {
}
queryModifier
In order to pull data from database, RelationshipManager
will generate a query object. you will have an opportunity to modify this query object to further change which records are pulled. queryModifier
can be an async method in case you need to await
in your function.
export class User extends BaseModel {
// ...
posts() {
return RelationshipFactory.createHasMany<User, Post>({
source: this,
targetModel: Post,
sourceToTargetKeyAssociation: {
id: "author_id",
},
});
}
topPosts() {
return RelationshipFactory.createHasMany<User, Post>({
source: this,
targetModel: Post,
sourceToTargetKeyAssociation: {
id: "author_id",
},
queryModifier: async (query: Query) => {
return query.whereOp("rating", ">", 8); //where posts.rating > 0
},
});
}
// ...
}
Polymorphism
Polymorphism referes to the concept where two different type of models can relate to same object. for example, image and posts can have comments.