Подтвердить что ты не робот

Hibernate - @ElementCollection - странное поведение удаления/вставки

@Entity
public class Person {

    @ElementCollection
    @CollectionTable(name = "PERSON_LOCATIONS", joinColumns = @JoinColumn(name = "PERSON_ID"))
    private List<Location> locations;

    [...]

}

@Embeddable
public class Location {

    [...]

}

Учитывая следующую структуру классов, когда я пытаюсь добавить новое местоположение в список локаций Person, это всегда приводит к следующим SQL-запросам:

DELETE FROM PERSON_LOCATIONS WHERE PERSON_ID = :idOfPerson

и

A lotsa' inserts into the PERSON_LOCATIONS table

Hibernate (3.5.x/JPA 2) удаляет все связанные записи для данного Person и повторно вставляет все предыдущие записи, а также новый.

У меня возникла идея, что метод equals/hashcode на Location разрешит проблему, но ничего не изменит.

Любые подсказки оценены!

4b9b3361

Ответ 1

Проблема как-то объясняется на странице о ElementCollection JPA wikibook:

Первичные ключи в CollectionTable

В спецификации JPA 2.0 нет предоставить способ определения Id в Embeddable. Однако для удаления или обновить элемент ElementCollection, некоторые уникальные ключ обычно требуется. В противном случае, при каждом обновлении поставщик JPA необходимо удалить все из CollectionTable для Entity и затем вставьте значения обратно. Итак, Поставщик JPA, скорее всего, предположит что комбинация всех поля в Embeddable уникальны, в сочетании с внешним ключом (JoinColunm (ы)). Это может быть неэффективно или просто невозможно, если Embeddable является большим или сложным.

И это точно (часть жирным шрифтом), что здесь происходит (Hibernate не генерирует первичный ключ для таблицы коллекции и не имеет способа определить, какой элемент коллекции был изменен, и удалит старое содержимое из таблицы для добавления нового содержимого).

Однако , если вы определяете @OrderColumn (чтобы указать столбец, используемый для поддержания постоянного порядка списка, что будет иметь смысл, поскольку вы используете List), Hibernate создаст первичный ключ (сделанный из столбца порядка и столбца объединения) и сможет обновить таблицу коллекций без удаления всего содержимого.

Что-то вроде этого (если вы хотите использовать имя столбца по умолчанию):

@Entity
public class Person {
    ...
    @ElementCollection
    @CollectionTable(name = "PERSON_LOCATIONS", joinColumns = @JoinColumn(name = "PERSON_ID"))
    @OrderColumn
    private List<Location> locations;
    ...
}

Ссылки

  • Спецификация JPA 2.0
    • Раздел 11.1.12 "Аннотация элемента ElementCollection"
    • Раздел 11.1.39 "Аннотации к порядковому номеру"
  • JPA Wikibook

Ответ 2

В дополнение к ответу Паскаля, вы должны также установить хотя бы один столбец как NOT NULL:

@Embeddable
public class Location {

    @Column(name = "path", nullable = false)
    private String path;

    @Column(name = "parent", nullable = false)
    private String parent;

    public Location() {
    }

    public Location(String path, String parent) {
        this.path = path;
        this.parent= parent;
    }

    public String getPath() {
        return path;
    }

    public String getParent() {
        return parent;
    }
}

Это требование задокументировано в AbstractPersistentCollection:

Обходной путь для ситуаций, подобных HHH-7072. Если элемент коллекции является компонентом, который полностью состоит из обнуляемых свойств, в настоящее время нам необходимо принудительно воссоздать всю коллекцию. Посмотрите использование hasNotNullableColumns в конструкторе AbstractCollectionPersister для получения дополнительной информации. Чтобы удалить строку за строкой, для этого потребуется SQL, например "WHERE (COL =? OR (COL равен нулю, а? Равен нулю))", а не текущий "WHERE COL =?" (сбой для нуля для большинства БД). Обратите внимание, что параметр должен быть связан дважды. До тех пор, пока мы в конечном итоге не добавим понятия "точки привязки параметров" в AST в ORM 5+, обработка этого типа условия является либо чрезвычайно сложной, либо невозможной. Принудительный отдых не идеален, но на самом деле это не какой-то другой вариант в ORM 4.

Ответ 3

Мы обнаружили, что сущности, которые мы определили как наши типы ElementCollection, не имеют метода equals или hashcode и имеют обнуляемые поля. Мы предоставили их (через @lombok для того, что это стоит) для типа сущности, и это позволило hibernate (v 5.2.14) идентифицировать, что коллекция была или не была грязной.

Кроме того, эта ошибка проявилась у нас, потому что мы были в методе службы, который был помечен аннотацией @Transaction(readonly = true). Поскольку hibernate будет пытаться очистить коллекцию связанных элементов и вставить ее заново, транзакция завершится неудачно, когда произойдет сброс, и что-то сломалось с этим очень трудным для отслеживания сообщением:

HHH000346: Error during managed flush [Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1]

Вот пример нашей модели сущности, которая имела ошибку

@Entity
public class Entity1 {
@ElementCollection @Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}

public class Entity2 {
  private UUID someUUID;
}

Меняя это на это

@Entity
public class Entity1 {
@ElementCollection @Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}

@EqualsAndHashCode
public class Entity2 {
  @Column(nullable = false)
  private UUID someUUID;
}

Исправлена наша проблема. Удачи.

Ответ 4

У меня была та же проблема, но я хотел отобразить список перечислений: List<EnumType>.

Я получил это работает так:

@ElementCollection
@CollectionTable(
        name = "enum_table",
        joinColumns = @JoinColumn(name = "some_id")
)
@OrderColumn
@Enumerated(EnumType.STRING)
private List<EnumType> enumTypeList = new ArrayList<>();

public void setEnumList(List<EnumType> newEnumList) {
    this.enumTypeList.clear();
    this.enumTypeList.addAll(newEnumList);
}

Проблема со мной заключалась в том, что объект List всегда заменялся с помощью установщика по умолчанию, и поэтому hibernate рассматривал его как полностью "новый" объект, хотя перечисления не менялись.