I have a custom converter for UUID to transfer it to a string instead a binary:
package de.kaiserpfalzEdv.commons.jee.db;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.UUID;
@Converter(autoApply = true)
public class UUIDJPAConverter implements AttributeConverter<UUID, String> {
@Override
public String convertToDatabaseColumn(UUID attribute) {
return attribute.toString();
}
@Override
public UUID convertToEntityAttribute(String dbData) {
return UUID.fromString(dbData);
}
}
The converters (i have some other espacially for time/date handling) reside in a library .jar file.
Then I have entities in a .jar file. Like this one:
package de.kaiserpfalzEdv.office.core.security;
import de.kaiserpfalzEdv.commons.jee.db.OffsetDateTimeJPAConverter;
import de.kaiserpfalzEdv.commons.jee.db.UUIDJPAConverter;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(
name = "tickets"
)
public class SecurityTicket implements Serializable {
private final static ZoneId TIMEZONE = ZoneId.of("UTC");
private final static long DEFAULT_TTL = 600L;
private final static long DEFAULT_RENEWAL = 600L;
@Id @NotNull
@Column(name = "id_", length=50, nullable = false, updatable = false, unique = true)
@Convert(converter = UUIDJPAConverter.class)
private UUID id;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "account_id_", nullable = false, updatable = false, unique = true)
private Account account;
@Convert(converter = OffsetDateTimeJPAConverter.class)
@Column(name = "created_", nullable = false, updatable = false)
private OffsetDateTime created;
@Convert(converter = OffsetDateTimeJPAConverter.class)
@Column(name = "validity_", nullable = false, updatable = false)
private OffsetDateTime validity;
@Deprecated
public SecurityTicket() {
}
public SecurityTicket(@NotNull final Account account) {
id = UUID.randomUUID();
this.account = account;
created = OffsetDateTime.now(TIMEZONE);
validity = created.plusSeconds(DEFAULT_TTL);
}
public void renew() {
validity = OffsetDateTime.now(TIMEZONE).plusSeconds(DEFAULT_RENEWAL);
}
public boolean isValid() {
OffsetDateTime now = OffsetDateTime.now(TIMEZONE);
System.out.println(validity.toString() + " is hopefully after " + now.toString());
return validity.isAfter(now);
}
public UUID getId() {
return id;
}
public OffsetDateTime getValidity() {
return validity;
}
public String getAccountName() {
return account.getAccountName();
}
public String getDisplayName() {
return account.getDisplayName();
}
public Set<String> getRoles() {
HashSet<String> result = new HashSet<>();
account.getRoles().forEach(t -> result.add(t.getDisplayNumber()));
return Collections.unmodifiableSet(result);
}
public Set<String> getEntitlements() {
return Collections.unmodifiableSet(new HashSet<>());
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj.getClass() != getClass()) {
return false;
}
SecurityTicket rhs = (SecurityTicket) obj;
return new EqualsBuilder()
.append(this.id, rhs.id)
.isEquals();
}
@Override
public int hashCode() {
return new HashCodeBuilder()
.append(id)
.toHashCode();
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("id", id)
.append("account", account)
.append("validity", validity)
.toString();
}
}
When running integration tests via maven and testng the database works quite fine. But when I start the application (the third .jar file), I get a nasty exception which boils down to:
Caused by: org.hibernate.HibernateException: Wrong column type in kpoffice.tickets for column id_. Found: varchar, expected: binary(50)
at org.hibernate.mapping.Table.validateColumns(Table.java:372)
at org.hibernate.cfg.Configuration.validateSchema(Configuration.java:1338)
at org.hibernate.tool.hbm2ddl.SchemaValidator.validate(SchemaValidator.java:175)
at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:525)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1859)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl$4.perform(EntityManagerFactoryBuilderImpl.java:852)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl$4.perform(EntityManagerFactoryBuilderImpl.java:845)
at org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl.withTccl(ClassLoaderServiceImpl.java:398)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:844)
at org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:60)
at org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:343)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:318)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1625)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1562)
... 120 more
The autoApply of convert does not work. I tried to annotate the converter to the class and to the attribute itself. But the converter is not used. But when I added the hibernate UUID type via hibernate specific annotation hibernate complaint that it can't have a converter and a hibernate type definition for the same attribute. So hibernate reads the converter configuration.
When using envers, the JPA 2.1 converter don't work. But I don't use envers in my software.
I hope there is someone out there who knows what I'm doing wrong ...
Andy Wilkinson gave the correct answer. Reading the spec helps in a lot of times.
JPA 2.1 Converters are not applied to
@Id
annotated attributes.Thank you Andy.
Another option is to embed the conversion logic in alternative getters/setters, like so:
The
@Transient
annotation will tell JPA to ignore this getter so it doesn't think there's a separate UUID property. It's inelegant, but it worked for me using JPA on classes with a UUID as PK. You do run the risk of other code setting bad values via the setId( String ) method, but it seems the only workaround. It may be possible for this method to be protected/private?While normal Java code would be able to distinguish to setters with the same name based on different argument types, JPA will complain if you don't name them differently.
It's annoying that JPA doesn't support Converters on Ids or that it doesn't follow the JAXB convention of not requiring converters for classes with standard conversion methods (ie. toString/fromString, intValue/parseInt, etc. ).