web-dev-qa-db-fra.com

Hibernate - @ElementCollection - Étrange comportement de suppression/insertion

@Entity
public class Person {

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

    [...]

}

@Embeddable
public class Location {

    [...]

}

Étant donné la structure de classe suivante, lorsque j'essaie d'ajouter un nouvel emplacement à la liste des emplacements de personne, les requêtes SQL suivantes sont toujours générées:

DELETE FROM PERSON_LOCATIONS WHERE PERSON_ID = :idOfPerson

Et

A lotsa' inserts into the PERSON_LOCATIONS table

Hibernate (3.5.x/JPA 2) supprime tous les enregistrements associés pour la personne donnée et réinsère tous les enregistrements précédents, ainsi que le nouvel enregistrement.

Je pensais que la méthode equals/hashcode sur Location résoudrait le problème, mais cela ne changeait rien.

Tous les indices sont appréciés!

38
nihilist84

Le problème est en quelque sorte expliqué dans la page à propos de ElementCollection du wikibook de JPA:

Clés primaires dans CollectionTable

La spécification JPA 2.0 ne le fait pas fournir un moyen de définir la Id dans le Embeddable. Cependant, supprimer ou mettre à jour un élément du fichier Mappage ElementCollection, certains uniques la clé est normalement requise. Autrement, à chaque mise à jour, le fournisseur JPA aurait .__ besoin de tout supprimer du CollectionTable pour le Entity, et puis insérez les valeurs en arrière. Alors le Le fournisseur JPA supposera probablement que la combinaison de tous les les champs de la Embeddable sont uniques, en combinaison avec la clé étrangère (JoinColunm (s)). Cela pourrait cependant être inefficace, ou tout simplement impossible si la Embeddable est grande ou complexe.

Et c’est exactement (la partie en gras) ce qui se passe ici (Hibernate ne génère pas de clé primaire pour la table de collection et n’a aucun moyen de détecter quel élément de la collection a été modifiée et supprime l’ancien contenu. de la table pour insérer le nouveau contenu).

Cependant, if vous définissez un @OrderColumn (pour spécifier une colonne utilisée pour conserver l'ordre persistant d'une liste - ce qui serait logique puisque vous utilisez une List), Hibernate créera une clé primaire ( constitué de la colonne order et de la colonne de jointure) et pourra mettre à jour la table de collection sans supprimer le contenu entier.

Quelque chose comme ça (si vous voulez utiliser le nom de colonne par défaut):

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

Références

56
Pascal Thivent

En plus de la réponse de Pascal, vous devez également définir au moins une colonne comme 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;
    }
}

Cette exigence est documentée dans AbstractPersistentCollection :

Solution de contournement pour des situations telles que HHH-7072. Si l'élément de collection est un composant entièrement constitué des propriétés nullables, nous devons actuellement recréer avec force la collection entière. Voir l'utilisation de hasNotNullableColumns dans le constructeur AbstractCollectionPersister pour plus d'informations. Pour supprimer ligne par ligne, cela nécessiterait SQL comme "WHERE (COL =? OR (COL est nul et? est nul))" plutôt que. le courant "WHERE COL =?" (échec pour null pour la plupart des bases de données). Notez que le param devrait être lié deux fois. Jusqu'à ce que nous finissions par ajouter les concepts de "points de rattachement aux paramètres" au AST dans ORM 5+, la gestion de ce type de problème est extrêmement difficile, voire impossible. Forçage les loisirs ne sont pas idéaux, mais pas vraiment aucune autre option dans ORM 4.

7
Vlad Mihalcea

Nous avons découvert que les entités que nous définissions comme nos types ElementCollection n'avaient pas de méthode equals ou hashcode définie ni de champs nullable. Nous avons fourni ceux-ci (via @lombok pour ce que cela vaut) sur le type d'entité et cela permettait à Hibernate (v 5.2.14) d'identifier que la collection était ou n'était pas sale.

En outre, cette erreur s'est manifestée pour nous car nous nous trouvions dans une méthode de service marquée avec l'annotation @Transaction(readonly = true). Puisque hibernate tenterait d'effacer la collection d'éléments liés et de l'insérer à nouveau, la transaction échouerait si elle était vidée de son contenu et le message était très difficile à retracer:

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

Voici un exemple de notre modèle d'entité qui avait l'erreur

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

public class Entity2 {
  private UUID someUUID;
}

Le changer pour ceci

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

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

Correction de notre problème. Bonne chance. 

1
Mustafakidd

J'ai eu le même problème, mais je voulais mapper une liste d'énums: List<EnumType>.

Je l'ai eu comme ça:

@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);
}

Le problème avec moi était que l'objet List était toujours remplacé à l'aide du setter par défaut et qu'Hibernate le traitait donc comme un tout "nouvel" objet, même si les enum ne changeaient pas.

0
tomasulo