7.1 Crear su propio convertidor de tipos

A veces surgen situaciones en las que desea almacenar un tipo de datos bastante complejo en una columna de una tabla. Si Hibernate sabe cómo convertirlo en una cadena (y viceversa), entonces todo está bien. De lo contrario, tendrá que escribir su propio convertidor de datos.

Supongamos que alguien decide almacenar el año de nacimiento de un usuario en la base de datos como YY.MM.DD, por ejemplo: 98.12.15. También necesita convertirlo a una fecha regular: 15/12/1998. Entonces tienes que escribir tu propio convertidor.

Para hacer esto, necesita implementar una interfaz 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]);
    }
}

Y, por supuesto, este convertidor se puede agregar a cualquier campo (siempre que los tipos coincidan):


@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;
}

Muy a menudo, los convertidores deben usarse si no diseñó la base de datos. Los datos allí pueden estar en "formatos extraños". Las fechas se pueden almacenar como cadenas, los valores booleanos como CHAR con valores Y y N, y similares.

7.2 Creando nuestro propio tipo de datos

¿Recuerdas la tabla con la lista de tipos que Hibernate conoce? Estoy hablando de tipos que se especifican junto con la anotación @Type. Puede escribir su propio tipo de datos, que se puede usar de la misma manera que otros tipos integrados en Hibernate.

Por ejemplo, queremos tener el tipo LocalTime, que se almacenará en la base de datos no como TIME, sino como VARCHAR. Y, por ejemplo, tenemos acceso a dicha base de datos y no podemos cambiar los tipos de datos en sus columnas. Entonces podemos escribir nuestro propio tipo de Hibernate. Llamémoslo LocalTimeString.

Primero necesitamos una clase pequeña que describa nuestro nuevo tipo:

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";
    }
}

Es algo de tipo Enum que consta de un valor. El conjunto de tales enams individuales es de todos los tipos conocidos por Hibernate.

También necesitamos una clase, un análogo del convertidor, que contendrá dos métodos, wrap()y unwrap()para convertir valores del tipo LocalTime a String.

Así es como se verá sin implementar los métodos:

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) {

    }

}

Ahora escribamos la implementación de los métodos:

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);
}

Y el segundo método:

@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());
}

Listo. Puedes usar esta clase para almacenar el tiempo como una cadena:


@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 Registrando su tipo

También puede registrar su tipo de datos durante la configuración de Hibernate. Esto es un poco no 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();

Primero deberá obtener MetadataSources, obtener MetadataBuilder de él y usarlo para agregar su clase. Es posible a través de hibernate.cfg.xml, pero también un poco engorroso.

Pero después del registro, puedes escribir así:


@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;
}