Hibernate - @ElementCollection - Странное поведение удаления / вставки
@Entity
public class Person {
@ElementCollection
@CollectionTable(name = "PERSON_LOCATIONS", joinColumns = @JoinColumn(name = "PERSON_ID"))
private List<Location> locations;
[...]
}
@Embeddable
public class Location {
[...]
}
Учитывая следующую структуру классов, когда я пытаюсь добавить новое местоположение в список расположений людей, это всегда приводит к следующим запросам 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 решит проблему, но это ничего не изменило.
Любые советы приветствуются!
4 ответа
Проблема как-то объясняется на странице о 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
В дополнение к ответу Паскаля, вы также должны установить как минимум один столбец как 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.
Мы обнаружили, что сущности, которые мы определяли, поскольку наши типы 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;
}
Исправлена наша проблема. Удачи.
У меня была та же проблема, но я хотел отобразить список перечислений: 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 рассматривал его как полностью "новый" объект, хотя перечисления не менялись.