当加载 spring-boot 和 spring-data-jpa 时,Hibernate 无法加载 JPA 2.1 转换器
Hibernate fails to load JPA 2.1 Converter when loaded with spring-boot and spring-data-jpa
我有一个用于 UUID 的自定义转换器,可以将其转换为字符串而不是二进制文件:
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);
}
}
转换器(我还有其他一些专门用于 time/date 处理的转换器)驻留在库 .jar 文件中。
然后我在 .jar 文件中有实体。喜欢这个:
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();
}
}
当 运行 通过 maven 和 testng 进行集成测试时,数据库工作得很好。但是当我启动应用程序(第三个 .jar 文件)时,我得到一个讨厌的异常,归结为:
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.perform(EntityManagerFactoryBuilderImpl.java:852)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.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
convert 的autoApply 不起作用。我试图将转换器注释为 class 和属性本身。但是没有使用转换器。但是当我通过 hibernate 特定注释添加 hibernate UUID 类型时,hibernate 抱怨说它不能为同一属性提供转换器和 hibernate 类型定义。所以休眠读取转换器配置。
使用 envers 时,JPA 2.1 转换器不工作。但是我没有在我的软件中使用envers。
我希望有人知道我做错了什么......
阅读规范在很多时候都有帮助。
JPA 2.1 转换器不适用于 @Id
注释属性。
谢谢安迪。
另一种选择是将转换逻辑嵌入备选方案 getters/setters,如下所示:
public class SecurityTicket implements Serializable
{
...
private UUID id;
@Transient
public UUID getUUID()
{
return id;
}
@Id @NotNull
@Column(name = "id_", length=50, nullable = false, updatable = false, unique = true)
public String getID()
{
return id.toString();
}
public void setUUID( UUID id )
{
this.id = id;
}
public void setID( String id )
{
this.id = UUID.fromString( id );
}
...
@Transient
注释将告诉 JPA 忽略此 getter,因此它认为没有单独的 UUID 属性。它不优雅,但它对我在 类 上使用 JPA 和 UUID 作为 PK 有用。您冒 运行 其他代码通过 setId( String ) 方法设置错误值的风险,但这似乎是唯一的解决方法。这个方法可能是protected/private?
虽然正常的 Java 代码能够根据不同的参数类型区分具有相同名称的 setter,但如果您不以不同的方式命名它们,JPA 会报错。
令人恼火的是,JPA 不支持 ID 上的转换器,或者它不遵循 JAXB 惯例,即不需要使用标准转换方法 类 的转换器(即 toString/fromString,intValue/parseInt, 等等).
我有一个用于 UUID 的自定义转换器,可以将其转换为字符串而不是二进制文件:
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);
}
}
转换器(我还有其他一些专门用于 time/date 处理的转换器)驻留在库 .jar 文件中。
然后我在 .jar 文件中有实体。喜欢这个:
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();
}
}
当 运行 通过 maven 和 testng 进行集成测试时,数据库工作得很好。但是当我启动应用程序(第三个 .jar 文件)时,我得到一个讨厌的异常,归结为:
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.perform(EntityManagerFactoryBuilderImpl.java:852)
at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.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
convert 的autoApply 不起作用。我试图将转换器注释为 class 和属性本身。但是没有使用转换器。但是当我通过 hibernate 特定注释添加 hibernate UUID 类型时,hibernate 抱怨说它不能为同一属性提供转换器和 hibernate 类型定义。所以休眠读取转换器配置。
使用 envers 时,JPA 2.1 转换器不工作。但是我没有在我的软件中使用envers。
我希望有人知道我做错了什么......
JPA 2.1 转换器不适用于 @Id
注释属性。
谢谢安迪。
另一种选择是将转换逻辑嵌入备选方案 getters/setters,如下所示:
public class SecurityTicket implements Serializable
{
...
private UUID id;
@Transient
public UUID getUUID()
{
return id;
}
@Id @NotNull
@Column(name = "id_", length=50, nullable = false, updatable = false, unique = true)
public String getID()
{
return id.toString();
}
public void setUUID( UUID id )
{
this.id = id;
}
public void setID( String id )
{
this.id = UUID.fromString( id );
}
...
@Transient
注释将告诉 JPA 忽略此 getter,因此它认为没有单独的 UUID 属性。它不优雅,但它对我在 类 上使用 JPA 和 UUID 作为 PK 有用。您冒 运行 其他代码通过 setId( String ) 方法设置错误值的风险,但这似乎是唯一的解决方法。这个方法可能是protected/private?
虽然正常的 Java 代码能够根据不同的参数类型区分具有相同名称的 setter,但如果您不以不同的方式命名它们,JPA 会报错。
令人恼火的是,JPA 不支持 ID 上的转换器,或者它不遵循 JAXB 惯例,即不需要使用标准转换方法 类 的转换器(即 toString/fromString,intValue/parseInt, 等等).