7.1 Creating your own type converter

Sometimes situations arise when you want to store a fairly complex data type in one column of a table. If Hibernate knows how to convert it to a string (and back), then everything is fine. If not, then you will have to write your own data converter.

Let's say someone decides to store a user's year of birth in the database as YY.MM.DD, for example: 98.12.15. You also need to convert it to a regular date: 15/12/1998. Then you have to write your own converter.

To do this, you need to implement an interface AttributeConverter<EntityType, DbType>.


@Converter(autoApply = true)
public class DateConverter implements AttributeConverter<java.time.LocalDate, String> {
 
    public String convertToDatabaseColumn(java.time.LocalDate date) {
    	return date.format("YY.MM.DD");
    }
 
    public java.time.LocalDate convertToEntityAttribute(String dbData) {
    	String[] data = dbData.split(".");
    	return LocalDate.of(data[2], data[1], "19"+data[0]);
    }
}

And, of course, this converter can be added to any field (provided the types match):


@Entity
@Table(name="user")
class User {
   @Id
   @Column(name="id")
   public Integer id;
 
   @Column(name="join_date")
   @Convert(converter = DateConverter.class)
   public java.time.LocalDate date;
}

Converters very often have to be used if you didn't design the database. The data there can be in "strange formats". Dates can be stored as strings, Booleans as CHARs with Y and N values, and the like.

7.2 Creating our own data type

Remember the table with the list of types that are known to Hibernate? I'm talking about types that are specified along with the annotation @Type. You can write your own data type, which can be used in the same way as other built-in types in Hibernate.

For example, we want to have the LocalTime type, which will be stored in the database not as TIME, but as VARCHAR. And, for example, we have access to such a database, and we are not allowed to change the data types in its columns. Then we can write our own Hibernate type. Let's call it LocalTimeString.

First we need a small class that will describe our new type:

public class LocalTimeStringType extends AbstractSingleColumnStandardBasicType<<LocalTime> {

    public static final LocalTimeStringType  INSTANCE = new LocalTimeStringType ();

    public LocalTimeStringType () {
    	super(VarcharTypeDescriptor.INSTANCE, LocalTimeStringJavaDescriptor.INSTANCE);
    }

    @Override
    public String getName() {
    	return "LocalTimeString";
    }
}

It is something of type Enum which consists of one value. The set of such single enams is all the types known to Hibernate.

We also need a class - an analogue of the converter, which will contain two methods - wrap()and unwrap()for converting values ​​of the LocalTime type to String.

This is how it will look without implementing the methods:

public class LocalTimeStringJavaDescriptor extends AbstractTypeDescriptor<LocalTime> {

    public static final LocalTimeStringJavaDescriptor INSTANCE =  new  LocalTimeStringJavaDescriptor();

    public LocalTimeStringJavaDescriptor() {
    	super(LocalTime.class, ImmutableMutabilityPlan.INSTANCE);
    }

    public <X> X unwrap(LocalTime value, Class<X> type, WrapperOptions options) {

    }

    public <X> LocalTime wrap(X value, WrapperOptions options) {

    }

}

Now let's write the implementation of the methods:

public <X> X unwrap(LocalTime value, Class<X> type, WrapperOptions options) {

    if (value == null)
    	return null;

    if (String.class.isAssignableFrom(type))
    	return (X) LocalTimeType.FORMATTER.format(value);

    throw unknownUnwrap(type);
}

And the second method:

@Override
public <X> LocalTime wrap(X value, WrapperOptions options) {
    if (value == null)
    	return null;

    if(String.class.isInstance(value))
    	return LocalTime.from(LocalTimeType.FORMATTER.parse((CharSequence) value));

    throw unknownWrap(value.getClass());
}

Ready. You can use this class to store time as a string:


@Entity
@Table(name="user")
class User
{
   @Id
   @Column(name="id")
   public Integer id;
 
   @Column(name="join_time")
   @Type(type = "com.codegym.hibernate.customtypes.LocalTimeStringType")  
   public java.time.LocalTime time;
}

7.3 Registering your type

You can also register your data type during Hibernate configuration. This is a bit non-trivial.


ServiceRegistry serviceRegistry = StandardServiceRegistryBuilder()
    .applySettings(getProperties()).build();
                                                                                                                                                              	                                        	 
    MetadataSources metadataSources = new MetadataSources(serviceRegistry);
    Metadata metadata = metadataSources
  	.addAnnotatedClass(User.class)
  	.getMetadataBuilder()
  	.applyBasicType(LocalTimeStringType.INSTANCE)
  	.build();
                                                                                                                                                              	                                        	 
    SessionFactory factory =  metadata.buildSessionFactory();

You will first need to get MetadataSources, get MetadataBuilder from it, and use it to add your class. It is possible through hibernate.cfg.xml, but also a little cumbersome.

But after registration, you can write like this:


@Entity
@Table(name="user")
class User
{
   @Id
   @Column(name="id")
   public Integer id;
 
   @Column(name="join_time")
   @Type(type = "LocalTimeString")  
   public java.time.LocalTime time;
}