Relations(Dynamic)
In large-scale business systems, we create numerous models with numerous relations between them. It's impossible to declare all relations using static relations
. This is especially true when there are numerous business modules with models scattered across them. Declaring all relations using static relations
becomes impractical. Without pre-defined static relations
, we need to implement a mechanism for using dynamic relations
in our code to enable querying, type inference, and DTO inference and generation
The following uses test-vona
module as an example to explain how to use dynamic relations
Four kinds of Relations
Vona ORM provides 4 kinds of dynamic relations:
Name | Description |
---|---|
hasOne | 1:1 |
belongsTo | 1:1 /n:1 |
hasMany | 1:n . It can realize the functions of main-details and main-details(multi-level) |
belongsToMany | n:n |
hasOne
Specify dynamic relations directly in CRUD operations using with
class ServicePost {
async relationHasOne() {
// insert
const postCreate = await this.scope.model.post.insert(
{
title: 'Post001',
postContent: {
content: 'This is a post',
},
},
{
with: {
postContent: $relationDynamic.hasOne(() => ModelPostContent, 'postId'),
},
},
);
// get
const post = await this.scope.model.post.get(
{
id: postCreate.id,
},
{
with: {
postContent: $relationDynamic.hasOne(() => ModelPostContent, 'postId', {
columns: ['id', 'content'],
}),
},
},
);
// update
await this.scope.model.post.update(
{
id: postCreate.id,
title: 'Post001-Update',
postContent: {
content: 'This is a post-changed',
},
},
{
with: {
postContent: $relationDynamic.hasOne(() => ModelPostContent, 'postId'),
},
},
);
// delete
await this.scope.model.post.delete(
{
id: postCreate.id,
},
{
with: {
postContent: $relationDynamic.hasOne(() => ModelPostContent, 'postId'),
},
},
);
}
}
Name | Description |
---|---|
with.postContent | Relation Name |
$relationDynamic.hasOne | 1:1 |
ModelPostContent | Taget Model |
'postId' | Foreign key |
columns | List of fields to query |
belongsTo
The belongsTo
relation is only used for query operations. Specify dynamic relations directly in the query operation through with
class ServicePost {
async relationBelongsTo() {
const postContent = await this.scope.model.postContent.select({
with: {
post: $relationDynamic.belongsTo(() => ModelPostContent, () => ModelPost, 'postId', {
columns: ['id', 'title'],
}),
},
});
console.log(postContent[0]?.post?.title);
}
}
Name | Description |
---|---|
with.post | Relation Name |
$relationDynamic.belongsTo | 1:1 /n:1 |
ModelPostContent | Source Model |
ModelPost | Target Model |
'postId' | Foreign key |
columns | List of fields to query |
hasMany
Specify dynamic relations directly in CRUD operations using with
class ServiceOrder {
async relationHasMany() {
// insert
const orderCreate = await this.scope.model.order.insert(
{
orderNo: 'Order001',
products2: [
{ name: 'Apple' },
{ name: 'Pear' },
],
},
{
with: {
products2: $relationDynamic.hasMany(() => ModelProduct, 'orderId'),
},
},
);
// get
await this.scope.model.order.get(
{
id: orderCreate.id,
},
{
with: {
products2: $relationDynamic.hasMany(() => ModelProduct, 'orderId', {
columns: ['id', 'name', 'price', 'quantity', 'amount'],
}),
},
},
);
// update
await this.scope.model.order.update(
{
id: orderCreate.id,
orderNo: 'Order001-Update',
products2: [
// create product: Peach
{ name: 'Peach' },
// update product: Apple
{ id: orderCreate.products?.[0].id, name: 'Apple-Update' },
// delete product: Pear
{ id: orderCreate.products?.[1].id, deleted: true },
],
},
{
with: {
products2: $relationDynamic.hasMany(() => ModelProduct, 'orderId'),
},
},
);
// delete
await this.scope.model.order.delete(
{
id: orderCreate.id,
},
{
with: {
products2: $relationDynamic.hasMany(() => ModelProduct, 'orderId'),
},
},
);
}
}
- When updating main table data, you can also update detail table data simultaneously (including Insert/Update/Delete operations)
Name | Description |
---|---|
with.products2 | Relation Name. Since test-vona module already defines the static relation products which autoload be set true . For demonstration purposes, a different relation name products2 is used |
$relationDynamic.hasMany | 1:n |
ModelProduct | Target Model |
'orderId' | Foreign key |
columns | List of fields to query |
belongsToMany
Directly specifying a dynamic relation using with
in CRUD operations requires providing the intermediate model RoleUser
. It should be emphasized that the CRUD operations here are for the intermediate model, not the target model
class ServiceUser {
async relationBelongsToMany() {
// insert: roles
const roles = await this.scope.model.role.insertBulk([
{ name: 'role-family' },
{ name: 'role-friend' },
]);
const roleIdFamily = roles[0].id;
const roleIdFriend = roles[1].id;
// insert: user
const userCreate = await this.scope.model.user.insert(
{
name: 'Tom',
roles: [{
id: roleIdFamily,
}],
},
{
with: {
roles: $relationDynamic.belongsToMany(() => ModelRoleUser, () => ModelRole, 'userId', 'roleId'),
},
},
);
// get: user
await this.scope.model.user.get(
{
id: userCreate.id,
},
{
with: {
roles: $relationDynamic.belongsToMany(() => ModelRoleUser, () => ModelRole, 'userId', 'roleId', {
columns: ['id', 'name'],
}),
},
},
);
// update: user
await this.scope.model.user.update(
{
id: userCreate.id,
roles: [
// delete
{ id: roleIdFamily, deleted: true },
// insert
{ id: roleIdFriend },
],
},
{
with: {
roles: $relationDynamic.belongsToMany(() => ModelRoleUser, () => ModelRole, 'userId', 'roleId', {
columns: ['id', 'name'],
}),
},
},
);
// delete: user
await this.scope.model.user.delete(
{
id: userCreate.id,
},
{
with: {
roles: $relationDynamic.belongsToMany(() => ModelRoleUser, () => ModelRole, 'userId', 'roleId'),
},
},
);
}
}
Name | Description |
---|---|
with.roles | Relation Name |
$relationDynamic.belongsToMany | n:n |
ModelRoleUser | Middle Model |
ModelRole | Target Model |
'userId' | Foreign key |
'roleId' | Foreign key |
columns | List of fields to query |
Tree structure
Since the tree structure references itself, using a static relation
with autoload: true
is most convenient and concise way to write code
For demonstration purposes, we'll still implement the tree structure using a dynamic relation
class ServiceCategory {
async categoryTreeDynamic() {
// create
const treeCreate = await this.scope.model.category.insert(
{
name: 'Category-1',
children2: [
{
name: 'Category-1-1',
children2: [
{ name: 'Category-1-1-1' },
],
},
{
name: 'Category-1-2',
},
],
},
{
with: {
children2: $relationDynamic.hasMany(() => ModelCategory, 'categoryIdParent', {
with: {
children2: $relationDynamic.hasMany(() => ModelCategory, 'categoryIdParent', {
}),
},
}),
},
},
);
// get
const tree = await this.scope.model.category.get(
{
id: treeCreate.id,
},
{
with: {
children2: $relationDynamic.hasMany(() => ModelCategory, 'categoryIdParent', {
columns: ['id', 'name'],
with: {
children2: $relationDynamic.hasMany(() => ModelCategory, 'categoryIdParent', {
columns: ['id', 'name'],
}),
},
}),
},
},
);
assert.equal(tree?.children2.length, 2);
assert.equal(tree?.children2[0].children2.length, 1);
// update
await this.scope.model.category.update(
{
id: treeCreate.id,
name: 'Category-1-Update',
children2: [
// create
{ name: 'Category-1-3' },
// update
{ id: treeCreate.children2?.[0].id, name: 'Category-1-1-Update' },
// delete
{ id: treeCreate.children2?.[1].id, deleted: true },
],
},
{
with: {
children2: $relationDynamic.hasMany(() => ModelCategory, 'categoryIdParent'),
},
},
);
// delete
await this.scope.model.category.delete(
{
id: treeCreate.id,
},
{
with: {
children2: $relationDynamic.hasMany(() => ModelCategory, 'categoryIdParent', {
with: {
children2: $relationDynamic.hasMany(() => ModelCategory, 'categoryIdParent', {
}),
},
}),
},
},
);
}
}
Name | Description |
---|---|
with.children2 | Relation Name. Since test-vona module already defines the static relation children which autoload be set true . For demonstration purposes, a different relation name children2 is used |
$relationDynamic.hasMany | 1:n |
ModelCategory | Target Model |
'categoryIdParent' | Foreign key |
columns | List of fields to query |
Relation Options
1. $relationDynamic.hasOne/$relationDynamic.belongsTo
Name | Description |
---|---|
columns | List of fields to query |
include | Specifying nested static relations |
with | Specifying nested dynamic relations |
meta.client | Define the datasource used by the relation, which can realize cross-datasource relation query |
meta.table | Define the data table used by the relation |
2. $relationDynamic.hasMany/$relationDynamic.belongsToMany
Name | Description |
---|---|
columns | List of fields to query |
include | Specifying nested static relations |
with | Specifying nested dynamic relations |
meta.client | Define the datasource used by the relation, which can realize cross-datasource relation query |
meta.table | Define the data table used by the relation |
distinct | Whether to enable distinct |
where | Conditional statement |
joins | Related tables |
orders | Sorting |
limit | Can be used for paginated queries |
offset | Can be used for paginated queries |
aggrs | Aggregate query |
groups | Group-by query |
Parameter: Model
When defining a relation, you need to provide the following parameters: Source Model
, Target Model
, and Intermediate Model
. The following types are supported:
Name | Description |
---|---|
ModelPost | Model Class |
() => ModelPost | Use a function to delay loading to avoid circular dependency errors |
'test-vona:post' | When using models across modules, typically use the model name directly |