Aggregate Root Save
Aggregate Root Save
eq implemented aggregate root save mode after 3.1.24, which can track changes to aggregate root objects and differential changes to corresponding value objects in track mode, enabling users to save complete object trees effortlessly.
What benefits does aggregate root save bring?
Use clean and tidy code in exchange for complex relational data saving
Click the link for the demo in this chapter https://github.com/xuejmnet/eq-doc
Aggregate Root Save Flow

Concepts
Before starting with aggregate root save, we need to understand several concepts
Aggregate Root
If tables a and b use a's primary key to associate with b, then a is the aggregate root, and b is the value object.
In one sentence: using your own primary key as the association relationship means you are the aggregate root, and the target navigation is a value object or other.
Value Object
If tables a and b use a's primary key to associate with b, then a is the aggregate root, and b is the value object.
If the current object's association navigation targetProperty is the target table's primary key, then the current table is a value object of the target table.
There are two types of value objects:
- Cascade is null, this kind of value object will not continue to recursively traverse lower-level navigation properties, this kind of value object only has the share of updating association keys during aggregate root save, and will never have
insertanddelete - Cascade delete, this kind of value object is a true value object, driven by the current aggregate root, and will traverse down navigation properties, with three states:
insert,update,delete
Other Relationships
Associations between two tables using non-primary keys like other columns, or many-to-many without a mapping table, or path left-match, these we consider as other relationships
Cascade Dissociate Options
The cascade property of @Navigate has the following enumerations
| Type | Function |
|---|---|
| AUTO | When it comes to object dissociation operations, the system handles automatically, by default using set null, if it's a many-to-many operation on the mapping table, it cannot be determined and needs to be handled by the user themselves |
| NO_ACTION | Dissociation does no processing, user handles it themselves |
| SET_NULL | Dissociation processing sets targetProperty to null, does not delete the target record |
| DELETE | Dissociation processing deletes the target object, for example user and user_role and role, then user_role can be set to delete |
API Introduction
| API | Function |
|---|---|
| configure | Configure options for the current save expression, such as not processing the root node, letting the user handle it themselves for concurrent judgment |
| savePath | Explicitly specify which object paths to save during saving |
| ignoreRoot | Ignore root node save, insert and update |
| removeRoot | Remove root node including removing remaining other include nodes |
What can savable bring
Before savable, when creating a many-to-many relationship, we needed to create user, user_role, role, and needed to assign values to user's id, then to role's id, then to user_role's userId and roleId. This is what needs to be considered for insertion.
If it's an update, we need to find which ones need to be added, which ones need to be deleted, and which ones need to be updated. Even if we're brutal, we can only delete all user_role and then re-insert them, but this would cause the user_role's id to change. If user_role is a mapping table with business fields, then this deletion-then-insertion method will no longer be applicable.
What can we do with save?
private final EasyEntityQuery easyEntityQuery;
@PostMapping("/create")
@Transactional(rollbackFor = Exception.class)
@EasyQueryTrack
public Object create() {
ArrayList<SysRole> sysRoles = new ArrayList<>();
{
SysRole sysRole = new SysRole();
sysRole.setName("Administrator");
sysRole.setCreateTime(LocalDateTime.now());
sysRoles.add(sysRole);
}
{
SysRole sysRole = new SysRole();
sysRole.setName("Guest");
sysRole.setCreateTime(LocalDateTime.now());
sysRoles.add(sysRole);
}
SysUser sysUser = new SysUser();
sysUser.setName("XiaoMing");
sysUser.setAge(18);
sysUser.setCreateTime(LocalDateTime.now());
sysUser.setSysRoleList(sysRoles);
easyEntityQuery.savable(sysRoles).executeCommand();
easyEntityQuery.savable(sysUser).executeCommand();
return "ok";
}We only need to create user and role, then save them respectively. Let's look at the generated SQL:
==> Preparing: INSERT INTO `t_role` (`id`,`name`,`create_time`) VALUES (?,?,?)
==> Parameters: 1a4c554e4b8b4cff81c87d9298b853ed(String),Administrator(String),2025-09-11T22:35:31.143878(LocalDateTime)
==> Preparing: INSERT INTO `t_role` (`id`,`name`,`create_time`) VALUES (?,?,?)
==> Parameters: bb9278911ecb4fe19c8a41a4e26a7c06(String),Guest(String),2025-09-11T22:35:31.143909(LocalDateTime)
==> Preparing: INSERT INTO `t_user` (`id`,`name`,`age`,`create_time`) VALUES (?,?,?,?)
==> Parameters: 2164d097983b4874859b01ef02f53340(String),XiaoMing(String),18(Integer),2025-09-11T22:35:31.143932(LocalDateTime)
==> Preparing: INSERT INTO `t_user_role` (`id`,`user_id`,`role_id`) VALUES (?,?,?)
==> Parameters: b588a3ce9c72484fba366ce7288d8f5c(String),2164d097983b4874859b01ef02f53340(String),1a4c554e4b8b4cff81c87d9298b853ed(String)
==> Preparing: INSERT INTO `t_user_role` (`id`,`user_id`,`role_id`) VALUES (?,?,?)
==> Parameters: 91b39127ba5d499f9cde6a326a49a71d(String),2164d097983b4874859b01ef02f53340(String),bb9278911ecb4fe19c8a41a4e26a7c06(String)