Hibernate Bytecode Enhancement. Association management

Onze verontschuldigingen, dit bericht is alleen beschikbaar in Amerikaans Engels. Voor het gemak van de kijker, is de inhoud hieronder weergegeven in de alternatieve taal. Je kunt klikken op de link om naar de actieve taal over te schakelen.

  In 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.

public void setUserGroup(UserGroup userGroup) {
    this.userGroup = userGroup;
}
 
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
 
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);
    }
}
 
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, 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:

@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
 
@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:

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);
 
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&build of the project, you will notice some log lines written by the Hibernate’s plugin:

--- 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%. However, the important thing that we should focus on, is that we do not longer need to bother assigning objects to EACH side of a bidirectional relationship.

Share this article:

Nicolae S.
Java Developer