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:
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:
- Bob’s INSERT is executed first since he has committed (and flushed) his changes first.
- The
GenerationTime.ALWAYS
strategy of theupdatedBy
property triggers theValueGenerator
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.
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
And there is more!
You can earn a significant passive income stream from promoting all these amazing products that I have been creating.
If you're interested in supplementing your income, then join my affiliate program.
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.
