Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to programmatically add attribute converter for specific entity fields?

Tags:

java

hibernate

Let's say I have javax.persistence.AttributeConverter implementation called FooConverter and I want to apply this converter for specific entity fields.

If I would use the annotation approach it would look like:

@Column
@Convert(converter = FooConverter.class)
private String barField;

On orm XML mapping it would look like:

<entity class="com.example.FooBarEntity">
  <convert converter="com.example.FooConverter" attribute-name="barField"/>
</entity>

However, I would like to add this converter programmatically during javax.persistence.EntityManagerFactory configuration.

import org.hibernate.cfg.Configuration;
import org.springframework.orm.hibernate5.LocalSessionFactoryBuilder;

private EntityManagerFactory buildEntityManagerFactory() {
  Configuration config = new LocalSessionFactoryBuilder(dataSource);
  config.addAnnotatedClass(com.example.FooBarEntity.class);
  // I looking for something like config.addConverter(FooConverter, FooBarEntity, barField);
  return config.buildSessionFactory();
}

There is config.addAttributeConverter method but it seems it only creates converter instance and not bind it to the specific entity attribute. However, I want to apply the converter only for specific String fields so autoApply is not an option.

like image 706
user9440008 Avatar asked Feb 14 '19 07:02

user9440008


1 Answers

JPA spec provides only 2 options to apply converters: global @Converter(autoApply = true) and field-level @Convert(converter = FooConverter.class) (or their XML equivalents).

A JPA converter knows only about entity attribute type and database column type. There is no access to the entity instance or even entity type to build conditional (by entity field) conversion logic.

You can add an AttributeConverter converter programmatically like this:

LocalSessionFactoryBuilder builder = new LocalSessionFactoryBuilder(dataSource);
builder.addAnnotatedClasses(FooBarEntity.class);
builder.addAttributeConverter(FooConverter.class, /*autoApply:*/true);
EntityManagerFactory entityManagerFactory = builder.buildSessionFactory();

I'm not sure there is a cleaner way to tell Hibernate to convert only specific entity fields than @Convert(converter = FooConverter.class).

Workarounds to apply the converter to specific entity fields only:

A wrapper class

Create a wrapper for strings requiring conversion:

public class Bar {

  private final String value;

  public Bar(String value) {
    this.value = value;
  }

  public String getValue() {
    return value;
  }
}

@Entity
public class FooBarEntity {

  private Bar barField;

  public void setBarField(String value) {
    this.barField = new Bar(value);
  }

  //...
}

Declare a converter that is auto-applied:

@Converter(autoApply = true)
public class FooConverter implements AttributeConverter<Bar, String> {

  @Override
  public String convertToDatabaseColumn(Bar bar) {
    return Optional.ofNullable(bar).map(Bar::getValue/*anything*/).orElse(null);
  }

  @Override
  public Status convertToEntityAttribute(String value) {
    return Optional.ofNullable(value).map(Bar::new).orElse(null);
  }
}

JPA entity listener

If you convert from String to String (source and target types are the same), you can use entity listeners. Entity listener has access to the entity itself, so can have complex conditions logic.

@Entity
public class FooBarEntity {

  private String barField;
}

Create an entity listener:

public class FooEntityListener {

  @PrePersist
  @PreUpdate
  public void convertToDatabaseColumn(Object entity) {
    if (entity instanceof FooBar) {
      entity.setBarField(/*conversion to DB column*/entity.getBarField());
    }
  }

  @PostLoad
  public void convertToEntityAttribute(Object entity) {
    if (entity instanceof FooBar) {
      entity.setBarField(/*conversion to entity attribute*/entity.getBarField());
    }
  }
}

Register a default entity listener in the META-INF/orm.xml:

<entity-mappings>
    <persistence-unit-metadata>
        <persistence-unit-defaults>
            <entity-listeners>
                <entity-listener class="com.example.FooEntityListener"/>
            </entity-listeners>
        </persistence-unit-defaults>
    </persistence-unit-metadata>
</entity-mappings>
like image 132
Evgeniy Khyst Avatar answered Nov 04 '22 19:11

Evgeniy Khyst