How to emulate @CreatedBy and @LastModifiedBy from Spring Data using the @GeneratorType Hibernate annotation

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

Hibernate comes with many additions to the standard JPA specification. One such example is the @GeneratorType annotation which allows you to customize the way a given entity property value is automatically generated.

If you’re using Spring Data, you can simply use the @CreatedBy and @LastModifiedBy annotations and the annotated entity properties are going to be populated with the currently logged user.

If you’re not using Spring Data, then you can easily emulate the same behavior using the Hibernate-specific @GeneratorType annotation and the ValueGenerator callback mechanism.

Domain Model

Assuming we have the following sensor table in our relational database:

Sensor table

We want to map that as a JPA entity. If the name can be mapped as the entity @Id and the value is just a @Basic property, how can we automate the created_by and updated_by columns using the currently logged user?

Currently logged user

For test sake, let’s assume we have the following ThreadLocal utility which stores the currently logged user:

public class LoggedUser {

    private static final ThreadLocal<String> userHolder = 
        new ThreadLocal<>();

    public static void logIn(String user) {
        userHolder.set(user);
    }

    public static void logOut() {
        userHolder.remove();
    }

    public static String get() {
        return userHolder.get();
    }
}

In a web application Servlet Filter, the LoggedUser.logIn method can be called using the currently authenticated user, and the LoggedUser.logOut method is called right after returning from the inner FilterChain.doFilter invocation.

public class LoggedUserFilter implements Filter {

    @Override
    public void init(
        FilterConfig filterConfig) 
            throws ServletException {
    }

    @Override
    public void doFilter(
        ServletRequest request, 
        ServletResponse response,
        FilterChain filterChain)
            throws IOException, ServletException {

        try {
            HttpServletRequest httpServletRequest = 
                (HttpServletRequest) request;

            LoggedUser.logIn(
                httpServletRequest.getRemoteUser()
            );

            filterChain.doFilter(request, response);
        }
        finally {
            LoggedUser.logOut();
        }
    }

    @Override
    public void destroy() {
    }
}

Populating entity properties with the currently logged user

Now, we want to pass the currently logged user to the createdBy and updatedBy properties of our Sensor entity, To do so, we will create the following ValueGenerator Hibernate utility:

public class LoggedUserGenerator
        implements ValueGenerator<String> {

    @Override
    public String generateValue(
            Session session, Object owner) {
        return LoggedUser.get();
    }
}

With the ValueGenerator interface, Hibernate allows us to customize the way a given entity property is going to be generated. Now, we only need to instruct Hibernate to use the LoggedUserGenerator for the createdBy and updatedBy properties of our Sensor entity.

@Entity(name = "Sensor")
@Table(name = "sensor")
public class Sensor {

    @Id
    @Column(name = "sensor_name")
    private String name;

    @Column(name = "sensor_value")
    private String value;

    @Column(name = "created_by")
    @GeneratorType(
        type = LoggedUserGenerator.class,
        when = GenerationTime.INSERT
    )
    private String createdBy;

    @Column(name = "updated_by")
    @GeneratorType(
        type = LoggedUserGenerator.class,
        when = GenerationTime.ALWAYS
    )
    private String updatedBy;

    //Getters and setters omitted for brevity
}

As you can see, the @GeneratorType allows us to map the createdBy and updatedBy properties so that Hibernate uses the LoggedUserGenerator to assign the annotated entity property using the currently logged user.

The when attribute of the @GeneratorType annotation tells if the entity property should be assigned when the entity is persisted (e.g. GenerationTime.INSERT) or modified (e.g. GenerationTime.ALWAYS).

Testing time

To see how the createdBy property is populated when the entity is persisted, consider the following test case:

LoggedUser.logIn("Alice");

doInJPA(entityManager -> {
    Sensor ip = new Sensor();
    ip.setName("ip");
    ip.setValue("192.168.0.101");

    entityManager.persist(ip);

    executeSync(() -> {

        LoggedUser.logIn("Bob");

        doInJPA(_entityManager -> {
            Sensor temperature = new Sensor();
            temperature.setName("temperature");
            temperature.setValue("32");

            _entityManager.persist(temperature);
        });

        LoggedUser.logOut();
    });
});

LoggedUser.logOut();

In the main thread, Alice logs in and inserts the ip sensor while, in a different thread, Bob’s logs in and inserts the temperature sensor.

When running the test case above, Hibernate generates the following SQL INSERT statements:

INSERT INTO sensor (
    created_by, 
    updated_by, 
    sensor_value, 
    sensor_name
) 
VALUES (
    'Bob', 
    'Bob', 
    '32', 
    'temperature'
)

INSERT INTO sensor (
    created_by, 
    updated_by, 
    sensor_value, 
    sensor_name
) 
VALUES (
    'Alice', 
    'Alice', 
    '192.168.0.101', 
    'ip'
)

There are several observations we can make here:

  1. Bob’s INSERT is executed first since he has committed (and flushed) his changes first.
  2. The GenerationTime.ALWAYS strategy of the updatedBy property triggers the ValueGenerator for both INSERT and UPDATE.

When modifying the entities:

LoggedUser.logIn("Alice");

doInJPA(entityManager -> {
    Sensor temperature = entityManager.find(
        Sensor.class, 
        "temperature"
    );

    temperature.setValue("36");

    executeSync(() -> {

        LoggedUser.logIn("Bob");

        doInJPA(_entityManager -> {
            Sensor ip = _entityManager.find(
                Sensor.class, 
                "ip"
            );

            ip.setValue("192.168.0.102");
        });

        LoggedUser.logOut();
    });
});

LoggedUser.logOut();

Hibernate generates the following SQL UPDATE statements:

UPDATE sensor
SET 
    created_by = 'Alice',
    updated_by = 'Bob',
    sensor_value = '192.168.0.102'
WHERE 
    sensor_name = 'ip'

UPDATE sensor
SET 
    created_by = 'Bob',
    updated_by = 'Alice',
    sensor_value = '36'
WHERE 
    sensor_name = 'temperature'

Great! The sensor records were updated properly and the updated_by column captures the user who made the modification.

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

As demonstrated, Hibernate is very flexible, allowing you to customize the way entity properties are automatically generated. Using the @GeneratorType annotation and the ValueGenerator callback mechanism, you can easily populate the created_by and update_by table columns without having to manually populate these entity properties by yourself.

If you’re using Spring Data, you can do the same with the @CreatedBy and @LastModifiedBy annotations, as this functionality can be integrated with the user authentication mechanism defined by Spring Security via the AuditorAware mechanism.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.