JPA AttributeConverter – A Beginner’s Guide
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
In this article, we are going to see how the JPA AttributeConverter works and how we can use it to customize the entity attribute to the database column mapping.
For instance, we could use a JPA AttributeConverter to map a Java MonthDay
to a database DATE
column because Hibernate doesn’t offer a built-in MonthDayType
to handle this particular mapping.
Domain Model
Our application uses the following annual_subscription
database table:
An annual subscription is needed to be renewed every year on a given day, so the payment_date
column stores the day and the month when the payment needs to be made.
The annual_subscription
table is mapped to the following AnnualSubscription
JPA entity:
@Entity(name = "AnnualSubscription") @Table(name = "annual_subscription") public class AnnualSubscription { @Id private Long id; @Column( name = "price_in_cents" ) private int priceInCents; @Column( name = "payment_day", columnDefinition = "date" ) @Convert( converter = MonthDayDateAttributeConverter.class ) private MonthDay paymentDay; public Long getId() { return id; } public AnnualSubscription setId( Long id) { this.id = id; return this; } public int getPriceInCents() { return priceInCents; } public AnnualSubscription setPriceInCents( int priceInCents) { this.priceInCents = priceInCents; return this; } public MonthDay getPaymentDay() { return paymentDay; } public AnnualSubscription setPaymentDay( MonthDay paymentDay) { this.paymentDay = paymentDay; return this; } }
The AnnualSubscription
entity uses the Fluent-style API, which as you will soon see, it greatly simplifies the way we can build a JPA entity. For more details about using the FLuent-style API with JPA and Hibernate, check out this article.
The paymentDay
entity attribute type is MonthDay
. However, by default, Hibernate doesn’t support this Java type, so we need to provide a custom mapper.
Without providing a custom mapper, Hibernate will use the SerializableType
for the paymentDay
entity attribute and persist it as a byte[]
array column type, which will not work for us since the payment_day
column type is date
.
So, we have two options. We can either use a Hibernate-specific custom Type or a JPA AttributeConverter to handle the mapping between the MonthDay
Java type and the date
column type.
JPA AttributeConverter
If you don’t need to provide a custom JDBC binding and fetching logic, then the JPA AttributeConverter is a viable solution to define the mapping between a given Java Object type and a database column type.
In our case, we need to create the following MonthDayDateAttributeConverter
class that implements the JPA AttributeConverter
interface:
public class MonthDayDateAttributeConverter implements AttributeConverter<MonthDay, java.sql.Date> { @Override public java.sql.Date convertToDatabaseColumn( MonthDay monthDay) { if (monthDay != null) { return java.sql.Date.valueOf( monthDay.atYear(1) ); } return null; } @Override public MonthDay convertToEntityAttribute( java.sql.Date date) { if (date != null) { LocalDate localDate = date.toLocalDate(); return MonthDay.of( localDate.getMonth(), localDate.getDayOfMonth() ); } return null; } }
The convertToDatabaseColumn
method is called by the JPA provider prior to executing an INSERT or UPDATE statement. The convertToDatabaseColumn
method takes a single parameter, which is the entity attribute and returns the value that needs to be set on the associated table column.
In our case, the convertToDatabaseColumn
method transforms the MonthDay
entity attribute to a java.sql.Date
which will be set in the payment_day
DATE column. Notice that the year is set to a pre-defined value since we are not interested in this temporal field. Note that the supplied monthDay
parameter can be null, hence we need to apply the transformation only for non-null MonthDay
Object references.
The convertToEntityAttribute
method is called by the JPA provider when fetching an entity from the database, via a find
method or when executing a JPQL or Criteria API query. The convertToEntityAttribute
method also takes a single parameter, which is the underlying table column value and returns the value that needs to be set on the associated entity attribute.
Our convertToEntityAttribute
method implementation transforms the java.sql.Date
column value to a MonthDay
Java Object that will be set on the associated entity attribute. Note that the supplied date
parameter can be null, hence we need to apply the transformation only for non-null DATE
database column values.
Mapping the JPA AttributeConverter
To instruct the JPA provider to use a given AttributeConverter
implementation, we can use the @Convert
JPA annotation on the entity attribute that needs to be transformed upon reading from and writing to the database:
@Column( name = "payment_day", columnDefinition = "date" ) @Convert( converter = MonthDayDateAttributeConverter.class ) private MonthDay paymentDay;
Auto-registering the JPA AttributeConverter
If you have multiple entities that use a given Java type that’s handled by the same JPA AttributeConverter, then you could auto-register the converter via the @Converter
annotation on the AttributeConverter
implementation, as illustrated by the following example:
@Converter(autoApply = true) public static class MonthDayDateAttributeConverter implements AttributeConverter<MonthDay, java.sql.Date> { //Code omitted for brevity }
Now, if you’re using Hibernate, you can define a MetadataBuilderContributor
implementation that registers the MonthDayDateAttributeConverter
, like this:
public class AttributeConverterMetadataBuilderContributor implements MetadataBuilderContributor { @Override public void contribute( MetadataBuilder metadataBuilder) { metadataBuilder.applyAttributeConverter( MonthDayDateAttributeConverter.class ); } }
To instruct Hibernate to use the AttributeConverterMetadataBuilderContributor
when bootstrapping the EntityManagerFactory
or SessionFactory
, we need to use the hibernate.metadata_builder_contributor
configuration property.
If you are using Spring Boot, you can define it in the application.properties
file, like this:
hibernate.metadata_builder_contributor=com.vladmihalcea.book.hpjp.hibernate.type.AttributeConverterMetadataBuilderContributor
The hibernate.metadata_builder_contributor
property can take the fully-qualified name of the class that implements a MetadataBuilderContributor
.
Or, if you are using the JPA persistence.xml
file, you can provide the hibernate.metadata_builder_contributor
property in the properties
XML tag:
<property name="hibernate.metadata_builder_contributor" value="com.vladmihalcea.book.hpjp.hibernate.type.AttributeConverterMetadataBuilderContributor" />
Testing time
When persisting an AnnualSubscription
entity:
entityManager.persist( new AnnualSubscription() .setId(1L) .setPriceInCents(700) .setPaymentDay( MonthDay.of(Month.AUGUST, 17) ) );
We can see that Hibernate generates the following SQL INSERT statement:
INSERT INTO annual_subscription ( id, price_in_cents, payment_day ) VALUES ( 1, 700, '0001-08-17' )
And, when fetching the AnnualSubscription
entity, we can see that the paymenentDay
entity attribute is properly transformed from the DATE column value to a MonthDay
Java Object:
AnnualSubscription subscription = entityManager.find( AnnualSubscription.class, 1L ); assertEquals( MonthDay.of(Month.AUGUST, 17), subscription.getPaymentDay() );
That’s it!
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
The JPA AttributeConverter feature is very useful when we need to transform an entity attribute prior to persisting or fetching it from the database.
However, if you want more control over how the underlying JDBC PreparedStatement
parameter is bound or how the ResultSet
column values are fetched, then you need to use a Hibernate-specific custom Type, as explained in this article.
