Hibernate enum UserType Test

At my current project, I’ve encountered something that seemed quite strange, but in the end was so logical.
To make sure that I remember what I’ve learned and to share my knowledge with others, I’ve written this blog post.
For this post, I’ve borrowed and adapted some code from my client’s project to make sure I’m working in the same environment. I give attribution to my client for these parts of the source.

In the beginning – Hibernate UserType issues

In this project, I had a failing Hibernate mapping. Running a specific function in the application triggered an IllegalArgumentException: argument type mismatch.
Looking into the stacktrace, I found out that a Hibernate UserType I had implemented was changed. Someone changed the returnedClass() return value. This value didn’t comply with the necessary type for a setter that was being called.
We fixed the problem, and I started writing an integration test to make sure that the mapping would always be validated.

Writing an Integration Test

I’ll now divert to a sample project instead of the real stuff, to be able to explain the technicalities in a better way.

I want to persist a person to the database. A Person has a gender enum. Two enums exist, the Gender enum and the FakeGender enum. I want to write an integration test to make sure that the mapping would fail if the custom Hibernate UserType is using the incorrect FakeGender enum.
The UserType is written generically to use the returnedClass() class when performing a nullSafeGet() and a nullSafeSet(). You can download the source code at the end of the post to see it all in action.

The Gender enum setup and StringEnumUserType class are a copy of the way the architects and other developers have designed the enum usage at my client’s project. It’s not one of my own inventions.

public enum Gender implements EnumWithValue<Gender> {
    MALE("M"), FEMALE("F");

    private static Map<String, Gender> allEnumValuesAndEnums;

    private String enumValue;

    private Gender(String enumValue) {
        this.enumValue = enumValue;
        EnumUtils.initializeEnumeration(getAllValuesAndEnums(), enumValue, this);
    }

    public String getEnumValue() {
        return enumValue;
    }

    public static Map<String, Gender> getAllValuesAndEnums() {
        if (allEnumValuesAndEnums == null) {
            allEnumValuesAndEnums = new HashMap<String, Gender>();
        }
        return allEnumValuesAndEnums;
    }

    public Map<String, Gender> getAllEnumValuesAndEnumerations() {
        return getAllValuesAndEnums();
    }
}
public class GenderUserType extends StringEnumUserType {
    public Class<FakeGender> returnedClass() {
        return FakeGender.class;
    }
}
public class PersonTest {
    ...

    @Test
    public void testPersonMapping() {
        Person person = new Person();
        ...
        person.setGender(Gender.MALE);

        try {
            entityManager.persist(person);
        } catch (Throwable t) {
            LOGGER.error(t.getMessage(), t);
            Assert.fail(t.getMessage());
        }
        Assert.assertNotNull(person.getId());

        Person loadedPerson = null;
        try {
            loadedPerson = entityManager.find(Person.class, person.getId());
        } catch (Throwable t) {
            LOGGER.error(t.getMessage(), t);
            Assert.fail(t.getMessage());
        }
        Assert.assertNotNull(loadedPerson);
        ...
    }

    ...
}

As you can see in the code above, I misconfigured the GenderUserType to use the FakeGender enum instead of the Gender enum. This mimics the behaviour of the real project where a developer changed the returnedClass() method in the UserType I was using for my mapping.
So while I’m setting the correct Gender MALE in the Person instance, I expect the test to fail because the GenderUserType is specifying FakeGender instead of Gender to be used to set the property.
Like it happened when exporting the dossier in the real project, setting FakeGender in setGender() would lead to an argument type mismatch. Or so I expected.

Against all expectations

Against my expectations, the integration test succeeds. It runs green.
I was baffled. It should fail because I’m specifying the mapping to use an incorrect enum type.

After further investigation, I found out the the nullSafeSet() isn’t using the returnedClass(), but the generic EnumWithValue interface to fetch the enumValue. So it’s normal that the error doesn’t occur when persisting the Person instance.
Nevertheless, I didn’t understand why loading the Person instance again, didn’t trigger nullSafeGet() that did use the returnedClass() method.

Then, after some research, the answer made sense. I found a lead on the following blog post: User Types in Hibernate
The nullSafeGet() method is only called when loading an instance directly from the database. So when the instance still exists in the PersistenceContext, it will not be loaded from the database and thus nullSafeGet() will not be called. Because of that, the returnedClass() method will also not be called, and no Exception will be thrown when loading the person instance. The entire object is just loaded from the PersistenceContext.

The solution

What can we do to make sure that a record is effectively loaded from the database in this integration test? Right…
Flush the persistence context to synchronize it to the underlying database, and clear the persistence context causing all managed entities to become detached.
This way, when loading the Person instance again, it will be fetched directly from the database instead of from the persistence context.

    ...
        try {
            entityManager.persist(person);
        } catch (Throwable t) {
            LOGGER.error(t.getMessage(), t);
            Assert.fail(t.getMessage());
        }
        Assert.assertNotNull(person.getId());

        entityManager.flush();
        entityManager.clear();

        Person loadedPerson = null;
        try {
            loadedPerson = entityManager.find(Person.class, person.getId());
        } catch (Throwable t) {
            LOGGER.error(t.getMessage(), t);
            Assert.fail(t.getMessage());
        }
   ...

And voilà … the test fails. I’m happy now. Weird, being happy when a test fails.

If we now change the GenderUserType returnedClass() method to the correct enum type, then the test will succeed again, like it should.

public class GenderUserType extends StringEnumUserType {
    public Class<Gender> returnedClass() {
        return Gender.class;
    }
}

So, what I learned today: If you really want to test your Hibernate or JPA mapping, flush and clear the session after persisting the instance and load it again to retrieve it directly from the database.
Ofcourse, using DbUnit to insert data into the database tables using a dataset, and then retrieving it to test your mappings might even be better, but the object model in this case was so complex that it wasn’t just possible to do this with DbUnit in a short amount of time.

Expanding the tests

In the end, what I did was write a test method that tests the mapping with a correctly configured UserType, and a separate test method with an incorrectly configured UserType. I set the returnedClass in the UserType by using a private static field which is set using reflection.
The first test method sets the Gender enum as returnedClass and expects the test to succeed.
The second test method sets the FakeGender enum as returnedClass and expects a PersistenceException for the test to succeed.

Feel free to download the sample project and post your comments. You’ll need Java 1.5+ and Maven to build and run the project.
Download Source Code
You’ll notice that the second test method fails, unless you uncomment the flush() and clear() method invocations, indicating the whole purpose of this blog post.

Advertisements

About Steve Schols

I am Steve Schols, a senior Java consultant working for several clients in Belgium. I mainly blog about the Java language and relevant frameworks and technologies. All statements made here are solely my own and do not represent the opinion of my employer, colleagues or my clients.
This entry was posted in Hibernate, Java and tagged , , , , , . Bookmark the permalink.

3 Responses to Hibernate enum UserType Test

  1. Pingback: JavaPins

  2. Frisian says:

    Wouldn’t it suffice to simply evict() the Person object?

    • Steve Schols says:

      Hi Frasian,

      You’re right, in this case it would suffice to call session.evict(person) or entityManager.detach(person) to have the same result.

      But imagine you have a Person object with an associated Address object. In the address object you use a custom UserType for a certain field.
      If you only evict / detach the Person instance without also evicting person.getAddress() or having CascadeStyle.EVICT annotated on the address field, it will not become detached as well and other errors may occur.

      But indeed, most of the time you don’t want to clear your whole persistence context of course. It was just meant to be an example.

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