OpenJPA and Serialization, or how to replace @AttributeConverter in JPA 2.0


JPA 2.1 introduced the @AttributeConverter annotation. The idea is to be able to implement and map programmatically the serialization of a field.
One typical example today is to use it to be able to use Java 8 new types in its entities such as LocalDateTime. This type is not handled yet by JPA specification (JPA 2.1 was too early).

OpenJPA being still JPA 2.0, you surely think it is not possible to get this feature.
However, by entering a bit in OpenJPA you will realize that it has provided this feature since years!

Of course it uses a vendor API, as all vendor API doesn’t map 1-1 the API introduced in the specification. Yet, the use of it is quite simple, and if you think in term of migration path to JPA 2.1, you’ll see that it’s easy to write a one shot tool to migrate all entities to the standard annotation/implementation (I’ll deal with that point at the end of the post).

Externalize me, Factorize me!

OpenJPA provides two specific annotations for the conversion: org.apache.openjpa.persistence.Externalizer and org.apache.openjpa.persistence.Factory.

The first one is used to convert the instance to the value to serialize in the database. The second
one is the symetric: it reads the serialized value and builds an instance used in the entity.

OpenJPA supports instance method reference as well as static method reference. In this case you can pass the “to convert” value
and StoreContext instance giving you some information on the persistence environment.

Note: to ease later migration and keep converter logic simple, sticking to String in the signature is not a bad idea.

Oops, java serialization, seriously?

At that point, you know you can write some code and wire it in your entity thanks to two annotations. Now:

  • you write your converter,
  • start to execute some JPA statements
  • and you realize that OpenJPA serializes the value you return in your externalizer.

Well, it sounds logic if you return a custom object…but it makes it even for a String.

Actually, it is not really an issue because the field is not fully considered as persistent. If you want it to be
so, then use varchar you have to decorate your column with @org.apache.openjpa.persistence.Persistent instead of storing Blob for String.

Once it is done, you get the varchar column and SQL friendly values.

One sample

Let’s take a simple entity having a long ID and a LocalDateTime field.

Converter logic can be:

public interface LocalDateTimes {
    ZoneId ZONE_ID = ZoneId.systemDefault();

    static String toString(final LocalDateTime time) {
        return time.atZone(ZONE_ID).format(DateTimeFormatter.ISO_DATE_TIME);
    }

    static LocalDateTime fromString(final String time) {
        return LocalDateTime.ofInstant(Instant.from(DateTimeFormatter.ISO_DATE_TIME.parse(time)), ZONE_ID);
    }
}

Our naked entity would be:

@Entity
public class DatedEntity { // + getters/setters
    @Id
    private long id;

    private LocalDateTime created;
}

Now let’s wire our converter:

@Entity
public class DatedEntity {
    @Id
    private long id;

    @Persistent
    @Externalizer("com.rmannibucau.java8.LocalDateTimes.toString")
    @Factory("com.rmannibucau.java8.LocalDateTimes.fromString")
    private LocalDateTime created;
}

And here we are. If you dump the SQL statement used by OpenJPA to create the table, you’ll get:

CREATE TABLE DatedEntity (id BIGINT NOT NULL, created VARCHAR(255), PRIMARY KEY (id))

Note: if you remove the @Persistent annotation you’ll get:

CREATE TABLE DatedEntity (id BIGINT NOT NULL, created BLOB, PRIMARY KEY (id))

and the values would be serialized java.

without modifying entities

OpenJPA supports XML configuration through mapping files for externalizer/factory.

Just use the extended orm schema of openjpa:

<entity-mappings xmlns="http://openjpa.apache.org/ns/orm/extendable"
	xmlns:openjpa="http://openjpa.apache.org/ns/orm"
	xmlns:orm="http://java.sun.com/xml/ns/persistence/orm"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	version="2.0">

<entity class="com.github.rmannibucau.domain.DatedEntity">
  <openjpa:attributes>
    <openjpa:persistent name="externalizer"
      externalizer="com.rmannibucau.java8.LocalDateTimes.toString"
      factory="com.rmannibucau.java8.LocalDateTimes.fromString"/>
  </openjpa:attributes>
</attributes>

</entity-mappings>

Test it!

If you reuse OpenJPARule I talked about in a previous post, you can write a test like:

public class DatedEntityTest {
    @Rule
    public final OpenJPARule $ = new OpenJPARule()
            .types(DatedEntity.class)
            .configure("openjpa.Log", "SQL=TRACE")
            .configure(
                "openjpa.ConnectionFactoryProperties",
                "PrintParameters=true, PrettyPrint=true, PrettyPrintLineLength=80");

    @Test
    public void checkDate() {
        final LocalDateTime now = LocalDateTime.now();

        final DatedEntity de = new DatedEntity();
        de.setId(1);
        de.setCreated(now);

        $.transaction((em) -> {
            em.persist(de);
            return null;
        });
        $.run(EntityManager::clear);

        DatedEntity loaded = $.transaction((em) -> em.find(DatedEntity.class, 1L));
        assertNotNull(loaded);
        assertEquals(now, loaded.getCreated());

        $.transaction((em) -> {
            final DatedEntity entity = em.find(DatedEntity.class, 1L);
            em.remove(entity);
            return entity;
        });
    }
}

The log output looks like:

27  INFO   [main] openjpa.Runtime - Starting OpenJPA 2.4.0
144  INFO   [main] openjpa.jdbc.JDBC - Using dictionary class "org.apache.openjpa.jdbc.sql.HSQLDictionary".
919  INFO   [main] openjpa.jdbc.JDBC - Connected to HSQL Database Engine version 2.2 using JDBC driver HSQL Database Engine Driver version 2.3.2.
1271  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 1050065615> executing prepstmnt 265321659
SELECT SEQUENCE_SCHEMA, SEQUENCE_NAME
    FROM INFORMATION_SCHEMA.SYSTEM_SEQUENCES

1272  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 1050065615> [0 ms] spent
1283  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 270056930> executing stmnt 794075965
CREATE TABLE DatedEntity (id BIGINT NOT NULL, created VARCHAR(255), PRIMARY KEY
        (id))
1284  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 270056930> [1 ms] spent
1554  INFO   [main] openjpa.Enhance - Creating subclass and redefining methods for "[class com.github.rmannibucau.openjpa.java8.DatedEntity]". This means that your application will be less efficient than it would if you ran the OpenJPA enhancer.
1698  INFO   [main] openjpa.Runtime - OpenJPA dynamically loaded the class enhancer. Any classes that were not enhanced at build time will be enhanced when they are loaded by the JVM.
1831  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 633240419> executing prepstmnt 1916575798
INSERT INTO DatedEntity (id, created)
    VALUES (?, ?)
[params=(long) 1, (String) 2015-05-24T09:36:20.229+02:00[Europe/Paris]]
1832  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 633240419> [0 ms] spent
1873  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 1222768327> executing prepstmnt 1193471756
SELECT t0.created
    FROM DatedEntity t0
    WHERE t0.id = ?
[params=(long) 1]
1874  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 1222768327> [0 ms] spent
1938  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 76659128> executing prepstmnt 2032169857
DELETE FROM DatedEntity
    WHERE id = ?
[params=(long) 1]
1941  TRACE  [main] openjpa.jdbc.SQL - <t 896644936, conn 76659128> [2 ms] spent

You can identify the serialized date is correctly bound to the prepared statement:

[params=(long) 1, (String) 2015-05-24T09:36:20.229+02:00[Europe/Paris]]

Preparing to JPA 2.1

This post is already too long to detail the implementation but I’ll try to give the overall idea to easily migrate entities to JPA 2.1 converters if needed.

If you don’t have much entities, don’t waste your time writing any tool. Instead, directly do the migration manually.
I know it may sound obvious, but any tool has an investment in terms of time, and here in particular as you need to write it. Personally, if the migration takes less than 20 minutes by hand, I would go for the manual solution.

If you think it would be longer, or if you are in a big company and you want to provide an out-of-the-box tool for all projects, here are few points to succeed in writing such a tool:

  • wrap the conversion in an easy-to-run tool. Since that’s a one-shot migration, I would write a simple main(String[]) which would integrate with maven and gradle easily. In fact, many developers will rely on it, through maven-exec-plugin for maven for instance.
    Though, the packaging can be a all-in-one bundle making it easy to run without integrating to any tooling as well. Once again that’s a one shot
    tool, so no need to update its project pom in absolute.
  • finding entities. Two choices here: either you rely on bytecode parsing or on source code parsing. Bytecode parsing implies sources to be compiled to an annotation processor. This can be a good choice but bytecode parsing is not a big hypothesis. Thus, using any class finder like xbean-finder is an easy option as well.
  • finding converters: once you’ve found entities (see previous step) you need to find converters. It is not that hard: simply check for
    @Externalizer and @Factory in your entities. If you used xbean-finder in the previous step you can skip entities finding and directly find these annotations on fields and methods. Side note: if you configured them through xml, be sure to find persistence.xml files and read mapping, you can reuse OpenJPA implementation parser to get them.
  • parse @Externalizer and @Factory to get the “converter” implementation. Here is the tip: once you get the converter, the idea is to simply generate a JPA 2.1 converter delegating to the old one. OpenJPA supports several signatures for the externalizers/factories. In practice, the most common use is to have a static method thus a plain delegation is enough.
  • once you’ve generated all converters (if they are reused accross the code, no need to generate them twice), add to all entities
    the @AttributeConverter annotation to take them into account and remove openjpa specific imports and decoration on he field/method.

This can look a bit complicated but it actually takes less than 1h of hacking.

Advertisements

2 thoughts on “OpenJPA and Serialization, or how to replace @AttributeConverter in JPA 2.0

  1. struberg

    Hi Romain!
    Colleague just stumbled across this post.
    Please note that I’ve fixed https://issues.apache.org/jira/browse/OPENJPA-2341 quite some time ago. Got released with openjpa-2.4.1.
    So you now can simply write a ValueHandler like shown here
    https://github.com/rsandtner/openjpa-converter/blob/master/src/main/java/com/github/rsandtner/openjpaconverter/JodaTimeValueHandler.java
    + FieldStrategy in persistence.xml:
    https://github.com/rsandtner/openjpa-converter/blob/master/src/test/resources/META-INF/persistence.xml#L61

    With this trick you don’t need to annotate anything in your entities.

    LieGrue,
    strub

    PS: this post probably predates my fix, so just for completeness.

    Reply
    1. rmannibucau Post author

      Hey Mark,

      well this is true but my solution still works and doesn’t need any advanced implementation or any annotation in the code using a custom orm so I still think it is easier for end users than using internals (AbstractValueHandler is quite raw for business code IMHO).

      Reply

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