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.
Want more content ? Keep up-to-date on my new blog
Or stay in touch on twitter @rmannibucau
Like this:
Like Loading...