Overview
Imagine this scenario, you have a legacy database that store weather data. The previous developer, due to various reason, store the temperature in the following format:
id | location | temperature | capture_date |
1 | Hanoi | 30.50 C | 2023-07-31 22:10:30.000 |
2 | New York | 95.20 F | 2023-07-31 22:10:30.000 |
As you can see, the temperature stored in a sub-optimal way with the value and the unit in the same column. As the business requires, you need to extract the temperature to a value type called Temperature. The POJO is something like this:
enum Scale { CELSIUS, FAHRENHEIT } public class Temperature { private double temperature; private Scale scale; }
without changing current database schema.
In cases like this, using a custom converter makes sense.
Creating a Hibernate Converter For Temperature
Creating a converter in Hibernate is quite straightforward. You need to override the methods that convert the database value to POJO and vice versa.
Here is the implementation of the weather data case:
@Converter(autoApply = true) class TemperatureConverter implements AttributeConverter<Temperature, String> { @Override public String convertToDatabaseColumn(Temperature temperature) { return temperature.getTemperature() + " " + temperature.getScale(); } @Override public Temperature convertToEntityAttribute(String s) { String[] parts = s.split(" "); var valuePart = parts[0]; var scalePart = parts[1]; Temperature temperature = new Temperature(); temperature.setTemperature(Double.parseDouble(valuePart)); temperature.setScale(scalePart.equals("F") ? Scale.FAHRENHEIT : Scale.CELSIUS); return temperature; } }
Now I have the converter ready, let’s see how to configure the field temperature in the entity:
@Entity @Table(name = "legacy_weather_data") public class LegacyWeatherData { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String location; @Convert(converter = TemperatureConverter.class) @Column(name = "temperature") private Temperature temperature; @CreationTimestamp private Date captureDate; public LegacyWeatherData() { } }
As you can see, the field temperature is annotated with @Convert and @Column.
Let’s try inserting and reading some data:
public class LegacyWeatherDemo { private static final StandardServiceRegistry registry = new StandardServiceRegistryBuilder() .configure().build(); private static final SessionFactory factory = new MetadataSources(registry).buildMetadata().buildSessionFactory(); public static void main(String[] args) { //insert legacy data var temperature = new Temperature(30, Scale.CELSIUS); var legacyWeather = new LegacyWeatherData("Hanoi", temperature); try (var session = factory.openSession()) { var tx = session.beginTransaction(); session.persist(legacyWeather); tx.commit(); } var hanoi = findByLocation("Hanoi"); System.out.println("Hanoi temperature: " + hanoi.getTemperature().getTemperature()); } private static LegacyWeatherData findByLocation(String location) { try (var session = factory.openSession()) { return session.createQuery("from LegacyWeatherData where location = :location", LegacyWeatherData.class) .setParameter("location", location) .getSingleResult(); } } }
From the application’s console, the result is expected:
If I look into the database, the data is stored in one field as concatenated string:
Conclusion
In this post, I’ve shown you how to create a custom converter to convert data back and forth between the database and your application.
I build softwares that solve problems. I also love writing/documenting things I learn/want to learn.