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

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:

  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.

If you enjoyed this article, I bet you are going to love my book 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.

Enter your email address to follow this blog and receive notifications of new posts by email.

Advertisements

2 thoughts on “How to emulate @CreatedBy and @LastModifiedBy from Spring Data using the @GeneratorType Hibernate annotation

  1. Great tip!

    I’ve never need this before, but it’s a pretty good feature from Hibernate and may help in other scenarios!

    By the way, I think using a public INSTANCE (singleton) on LoggedUser is a little bit confuse! Maybe having just static methods instead would be enough. What do you think?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s