Hibernate не может загрузить конвертер JPA 2.1 при загрузке с spring-boot и 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);
    }
}

Преобразователи (у меня есть некоторые другие, особенно для обработки времени/даты) находятся в файле библиотеки .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$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

Автоприменение конвертации не работает. Я попытался аннотировать конвертер к классу и к самому атрибуту. Но преобразователь не используется. Но когда я добавил тип UUID гибернации через специальную аннотацию гибернации, жалоба спящего режима на то, что у него не может быть преобразователя и определения типа гибернации для одного и того же атрибута. Таким образом, hibernate считывает конфигурацию преобразователя.

При использовании envers конвертер JPA 2.1 не работает. Но я не использую envers в своем программном обеспечении.

Я надеюсь, что есть кто-то, кто знает, что я делаю неправильно...


person klenkes74    schedule 05.02.2015    source источник
comment
Спецификация JPA прямо указывает, что преобразование не будет применяться к атрибуту id сущности. Возможно, вы можете удалить свои аннотации преобразования JPA и вместо этого попробовать использовать подход, специфичный для Hibernate?   -  person Andy Wilkinson    schedule 05.02.2015
comment
stackoverflow.com/questions/39547615/   -  person Ashish Ratan    schedule 17.09.2016


Ответы (2)


Энди Уилкинсон дал правильный ответ. Чтение спецификации помогает во многих случаях.

Преобразователи JPA 2.1 не применяются к @Id аннотированным атрибутам.

Спасибо, Энди.

person klenkes74    schedule 08.02.2015
comment
JSR-000338 JavaTM Persistence 2.1 глава 11.1.10 Преобразование аннотации, < i>Аннотацию Convert не следует использовать для указания преобразования следующих элементов: атрибутов идентификаторов (включая атрибуты встроенных идентификаторов и производных идентификаторов), атрибутов версий, атрибутов отношений и - person Tom Lensburry; 30.01.2017
comment
Согласно nailedtothex.org/roller/kyle/entry/ конвертеры должны работать с EmbeededId. Однако в моем случае это также не удается: stackoverflow.com/questions/48188365/ Есть предложения? - person nimo23; 11.01.2018
comment
Если вы читаете спецификации (процитированные Томом), вы видите, что спецификации не включают атрибуты встроенного идентификатора. Так что, если это сработало, это проприетарное расширение, на которое вам не стоит рассчитывать. - person klenkes74; 12.01.2018

Другой вариант — встроить логику преобразования в альтернативные геттеры/сеттеры, например:

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 игнорировать этот геттер, чтобы он не думал, что существует отдельное свойство UUID. Это неэлегантно, но у меня сработало использование JPA для классов с UUID как PK. Вы рискуете тем, что другой код установит неверные значения с помощью метода setId( String ), но это кажется единственным обходным путем. Возможно ли, чтобы этот метод был защищенным/приватным?

В то время как обычный код Java мог бы различать сеттеры с одинаковыми именами на основе разных типов аргументов, JPA будет жаловаться, если вы не назовете их по-разному.

Раздражает, что JPA не поддерживает конвертеры для идентификаторов или не следует соглашению JAXB о том, что не требуются конвертеры для классов со стандартными методами преобразования (например, toString/fromString, intValue/parseInt и т. д.).

person Tristan Nixon    schedule 27.07.2015