Hibernate Bytecode Enhancement: Association Management
Let's see how to enable the bytecode enhancement.
Join the DZone community and get the full member experience.
Join For FreeIn the previous article, Hibernate Bytecode Enhancement. Dirty Tracking, I explained how to optimize Hibernate’s Dirty Tracking mechanism.
The bytecode enhancement, however, can be achieved via one more property: association management. When this feature is enabled, Hibernate will take care of automatically updating the “other side” of a bidirectional relation with a reverse mapping defined when one side changes. Similar to Dirty Tracking, this will as well result in additional changes made to the bytecode of the entities.
Let’s see how can this be done.
How to Enable the Bytecode Enhancement: Association Management
To enhance all @Entity
classes, you need to add the following Maven plugin:
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>${hibernate.version}</version>
<executions>
<execution>
<configuration>
<enableAssociationManagement>true</enableAssociationManagement>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
As soon as the Java classes are compiled, the plugin goes through all @Entity
classes and modifies their bytecode representation according to the provided configuration.
Notice the property specified within the <configuration>
tags. With this setup, Hibernate will manipulate the bytecode of the classes to add instructions that would invoke the setter of the “other side” of a bidirectional relation.
Changes Made to the Bytecode
When the association management is enabled, the bytecode of the classes changes. For instance, before enabling the bytecode enhancement, a class with two reverse mapping fields had 9.10 KB, and after enabling it, the compiled class size is 12.5 KB. The same thing happened to the targeted entity; before, it had 9.66 KB, and after the enhancement, its size grew to 13.5 KB.
If we would inspect the bytecode of the classes, we’ll see that inside each setter method of the fields, which are part of a bidirectional relationship, Hibernate inserted some code — a call to the setter method of the “other side.” Besides the changes made to the setters, after the enhancement, the class implements one more interface: ManagedEntity
.
setUserGroup(UserGroup userGroup) before enhancement:
public void setUserGroup(UserGroup userGroup) {
this.userGroup = userGroup;
}
Here's how the bytecode of this method looks before the enhancement:
public void setUserGroup(com.hibernate.bytecode.enhancement.UserGroup);
Code:
0: aload_0
1: aload_1
2: checkcast #76 // class com/hibernate/bytecode/enhancement/UserGroup
5: putfield #72 // Field userGroup:Lcom/hibernate/bytecode/enhancement/UserGroup;
8: return
setUserGroup(UserGroup userGroup) after enhancement, these methods are generated by Hibernate:
public void setUserGroup(UserGroup userGroup) {
$$_hibernate_write_userGroup(userGroup);
}
public void $$_hibernate_write_userGroup(UserGroup paramUserGroup) {
if (this.userGroup != null && Hibernate.isPropertyInitialized(this.userGroup, "users")) {
Set set = ((UserGroup) this.userGroup).$$_hibernate_read_users();
if (set != null)
set.remove(this);
}
User user = this;
UserGroup userGroup1 = paramUserGroup;
user.userGroup = userGroup1;
if (paramUserGroup != null && Hibernate.isPropertyInitialized(paramUserGroup, "users")) {
Set set = ((UserGroup) paramUserGroup).$$_hibernate_read_users();
if (set != null && !set.contains(this))
set.add(this);
}
}
And now the bytecode looks as follows:
public void setUserGroup(com.hibernate.bytecode.enhancement.UserGroup);
Code:
0: aload_0
1: aload_1
2: checkcast #96 // class com/hibernate/bytecode/enhancement/UserGroup
5: invokevirtual #100 // Method $$_hibernate_write_userGroup:(Lcom/hibernate/bytecode/enhancement/UserGroup;)V
8: return
public void $$_hibernate_write_userGroup(com.hibernate.bytecode.enhancement.UserGroup);
Code:
0: aload_0
1: getfield #298 // Field userGroup:Lcom/hibernate/bytecode/enhancement/UserGroup;
4: aconst_null
5: if_acmpeq 21
8: aload_0
9: getfield #298 // Field userGroup:Lcom/hibernate/bytecode/enhancement/UserGroup;
12: ldc_w #302 // String users
15: invokestatic #308 // Method org/hibernate/Hibernate.isPropertyInitialized:(Ljava/lang/Object;Ljava/lang/String;)Z
18: ifne 24
21: goto 35
24: aload_0
25: getfield #298 // Field userGroup:Lcom/hibernate/bytecode/enhancement/UserGroup;
28: invokevirtual #312 // Method com/hibernate/bytecode/enhancement/UserGroup.$$_hibernate_read_users:()Ljava/util/Set;
31: aconst_null
32: if_acmpne 38
35: goto 52
38: aload_0
39: getfield #298 // Field userGroup:Lcom/hibernate/bytecode/enhancement/UserGroup;
42: invokevirtual #312 // Method com/hibernate/bytecode/enhancement/UserGroup.$$_hibernate_read_users:()Ljava/util/Set;
45: aload_0
46: invokeinterface #316, 2 // InterfaceMethod java/util/Set.remove:(Ljava/lang/Object;)Z
51: pop
52: aload_0
53: aload_1
54: putfield #298 // Field userGroup:Lcom/hibernate/bytecode/enhancement/UserGroup;
57: goto 60
60: aconst_null
61: astore_3
62: aload_1
63: aconst_null
64: if_acmpeq 77
67: aload_1
68: ldc_w #302 // String users
71: invokestatic #308 // Method org/hibernate/Hibernate.isPropertyInitialized:(Ljava/lang/Object;Ljava/lang/String;)Z
74: ifne 80
77: goto 115
80: aload_1
81: invokevirtual #312 // Method com/hibernate/bytecode/enhancement/UserGroup.$$_hibernate_read_users:()Ljava/util/Set;
84: astore 4
86: aload 4
88: aconst_null
89: if_acmpeq 103
92: aload 4
94: aload_0
95: invokeinterface #321, 2 // InterfaceMethod java/util/Collection.contains:(Ljava/lang/Object;)Z
100: ifeq 106
103: goto 115
106: aload 4
108: aload_0
109: invokeinterface #324, 2 // InterfaceMethod java/util/Collection.add:(Ljava/lang/Object;)Z
114: pop
115: return
As can be seen from the bytecode above, after the enhancement and inside the setter method, a call to $$_hibernate_write_userGroup(...)
has been introduced. If we look into the method generated by Hibernate, we’ll notice that once the UserGroup has been set on the User entity, a call to UserGroup.$$_hibernate_read_users()
is expected. It reads the collection from the other side (the collection users from UserGroup.java), followed by some checks, and in the end, a call to Collection.add(...)
is performed, which adds the current User to that collection on the other side of the relation.
So, having the Association management enabled, when we do a call to user.setUserGroup(userGroup)
, on the other side of the relation, via reflection, the method userGroup.getUsers().add(user)
is triggered. This way bytecode-enhanced bi-directional association management is achieved.
Test Case
Given User and UserGroup entities with the following class structure:
User.java
@Named
@Entity
@Table(name = "USER")
public class User extends AbstractLogicalIdEntity {
@ManyToOne(targetEntity = UserGroup.class, optional = false)
private UserGroup userGroup;
// rest of the code has been omitted for brevity
UserGroup.java
@Named
@Entity
@Table(name = "USERGROUP")
public class UserGroup extends AbstractLogicalIdEntity {
@OneToMany(mappedBy = "userGroup", targetEntity = User.class)
private Set<User> users;
// rest of the code has been omitted for brevity
Using the following test code:
public static void main(String[] args) {
setUserOnUserGroup();
setUserGroupOnUser();
}
public static User setUserOnUserGroup() {
User user = new User();
user.setLogicalId("User 1");
UserGroup userGroup = new UserGroup();
userGroup.setLogicalId("UserGroup 1");
userGroup.setDescription("Some description.");
Set<User> users = new HashSet<>();
users.add(user);
userGroup.setUsers(users);
System.out.println(“Setting user on userGroup:”);
System.out.println(user);
System.out.println(userGroup);
return user;
}
public static UserGroup setUserGroupOnUser() {
User user = new User();
user.setLogicalId("User 2");
UserGroup userGroup = new UserGroup();
userGroup.setLogicalId("UserGroup 2");
userGroup.setDescription("Some description.");
Set<User> users = new HashSet<>();
userGroup.setUsers(users);
user.setUserGroup(userGroup);
System.out.println(“Setting userGroup on user:”);
System.out.println(user);
System.out.println(userGroup);
return userGroup;
}
We have the following output:
Before applying bytecode enhancement
Setting user on userGroup:
User{id=fa0f98f7-eec9-4b4f-9041-ac60df4a0fd3, logicalId=User 1, userGroup=null}
UserGroup{id=03b9d0ea-c41e-4c5f-8abb-b629739df82d, logicalId=UserGroup 1, description=Some description., users=[User{id=fa0f98f7-eec9-4b4f-9041-ac60df4a0fd3, logicalId=User 1}]}
Setting userGroup on user:
User{id=fa0f98f7-eec9-4b4f-9041-ac60df4a0fd3, logicalId=User 2, userGroup=UserGroup{id=03b9d0ea-c41e-4c5f-8abb-b629739df82d, logicalId=UserGroup 2, description=Some description.}}
UserGroup{id=03b9d0ea-c41e-4c5f-8abb-b629739df82d, logicalId=UserGroup 2, description=Some description., users=[]}
As demonstrated by the execution of the code from above, when the user was added to the collection of users of the userGroup entity, the userGroup field from the user entity was not automatically updated. To achieve that, we would’ve had to execute one more line of code:
// Here we set the users on the userGroup entity
userGroup.setUsers(users);
// Then, for each user we would’ve had to set the userGroup
user.setUserGroup(userGroup);
After applying bytecode enhancement
Setting user on userGroup:
User{id=fa0f98f7-eec9-4b4f-9041-ac60df4a0fd3, logicalId=User 1, userGroup=UserGroup{id=03b9d0ea-c41e-4c5f-8abb-b629739df82d, logicalId=UserGroup 1, description=Some description.}
UserGroup{id=03b9d0ea-c41e-4c5f-8abb-b629739df82d, logicalId=UserGroup 1, description=Some description., users=[User{id=fa0f98f7-eec9-4b4f-9041-ac60df4a0fd3, logicalId=User 1}]}
Setting userGroup on user:
User{id=fa0f98f7-eec9-4b4f-9041-ac60df4a0fd3, logicalId=User 2, userGroup=UserGroup{id=03b9d0ea-c41e-4c5f-8abb-b629739df82d, logicalId=UserGroup 2, description=Some description.}}
UserGroup{id=03b9d0ea-c41e-4c5f-8abb-b629739df82d, logicalId=UserGroup 2, description=Some description., users=[User{id=fa0f98f7-eec9-4b4f-9041-ac60df4a0fd3, logicalId=User 2}]}
From the output above, you can notice that with the Association management set up, the “other side” of a bidirectional association is automatically managed whenever one side is manipulated.
Project Build
During the usual clean and build of the project, you will notice some log lines written by the Hibernate’s plugin:
Part of project build log
--- hibernate-enhance-maven-plugin:5.2.9.Final:enhance (default) @ core ---
Starting Hibernate enhancement for classes on D:\projects\DEMO\core\target\classes
...
Apr 22, 2019 12:31:14 PM org.hibernate.bytecode.enhance.internal.javassist.EnhancerImpl enhance
INFO: Enhancing [com.hibernate.bytecode.enhancement.UserGroup] as Entity
Successfully enhanced class [D:\projects\DEMO\core\target\classes\com\hibernate\bytecode\enhancement\UserGroup.class]
...
Here’s the build time of a Maven module containing 27 Hibernate reverse mappings:
Bytecode enhancement disabled: 23.188 s
Bytecode enhancement enabled: 30.870 s
The overhead that is added to the compile-time is nearly 25 percent. However, the important thing that we should focus on is that we no longer need to bother assigning objects to EACH side of a bidirectional relationship.
Opinions expressed by DZone contributors are their own.
Comments