web-dev-qa-db-fra.com

@OneToMany et clés primaires composites?

J'utilise Hibernate avec des annotations (au printemps) et j'ai un objet qui a une relation ordonnée et plusieurs-à-un objet enfant qui a une clé primaire composite, dont un composant est une clé étrangère renvoyée au id de l'objet parent.

La structure ressemble à ceci:

+=============+                 +================+
| ParentObj   |                 | ObjectChild    |
+-------------+ 1          0..* +----------------+
| id (pk)     |-----------------| parentId       |
| ...         |                 | name           |
+=============+                 | pos            |
                                | ...            |
                                +================+

J'ai essayé diverses combinaisons d'annotations, dont aucune ne semble fonctionner. C'est le plus proche que j'ai pu trouver:

@Entity
public class ParentObject {
    @Column(nullable=false, updatable=false)
    @Id @GeneratedValue(generator="...")
    private String id;

    @OneToMany(mappedBy="parent", fetch=FetchType.EAGER, cascade={CascadeType.ALL})
    @IndexColumn(name = "pos", base=0)
    private List<ObjectChild> attrs;

    ...
}

@Entity
public class ChildObject {
    @Embeddable
    public static class Pk implements Serializable {
        @Column(nullable=false, updatable=false)
        private String parentId;

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

        @Column(nullable=false, updatable=false)
        private int pos;

        @Override
        public String toString() {
            return new Formatter().format("%s.%s[%d]", parentId, name, pos).toString();
        }

        ...
    }

    @EmbeddedId
    private Pk pk;

    @ManyToOne
    @JoinColumn(name="parentId")
    private ParentObject parent;

    ...
}

J'y suis arrivé après une longue période d'expérimentation au cours de laquelle la plupart de mes autres tentatives ont abouti à des entités que l'hibernation ne pouvait même pas charger pour diverses raisons.

UPDATE: Merci à tous pour les commentaires; J'ai fait des progrès. J'ai fait quelques ajustements et je pense que c'est plus proche (j'ai mis à jour le code ci-dessus). Maintenant, cependant, le problème est sur insert. L'objet parent semble bien sauvegarder, mais les objets enfants ne sont pas enregistrés. Ce que j'ai pu déterminer, c'est qu'étendre en veille prolongée ne remplit pas la partie parentId de la clé primaire (composite) des objets enfants. m obtenir une erreur non-unique:

org.hibernate.NonUniqueObjectException:
   a different object with the same identifier value was already associated 
   with the session: [org.kpruden.ObjectChild#null.attr1[0]]

Je renseigne les attributs name et pos dans mon propre code, mais bien sûr, je ne connais pas l'ID parent, car il n'a pas encore été enregistré. Des idées sur la façon de convaincre Hibernate de remplir ceci?

Merci!

27
Kris Pruden

Après beaucoup d'expérimentation et de frustration, j'ai finalement déterminé que je ne pouvais pas faire exactement ce que je voulais.

Finalement, je suis allé de l'avant et j'ai donné à l'objet enfant sa propre clé synthétique et laissé Hibernate le gérer. Ce n'est pas idéal, car la clé est presque aussi grosse que le reste des données, mais ça marche.

2
Kris Pruden

Le livre Manning Java Persistence with Hibernate présente un exemple de procédure à suivre dans la section 7.2. Heureusement, même si vous ne possédez pas le livre, vous pouvez voir un exemple de code source en téléchargeant la version JPA de l'exemple de projet Caveat Emptor (lien direct ici ) et en examinant les classes Category et CategorizedItem dans le package auction.model.

Je résumerai également les principales annotations ci-dessous. Faites-moi savoir si c'est toujours un no-go.

ParentObject:

@Entity
public class ParentObject {
   @Id @GeneratedValue
   @Column(name = "parentId", nullable=false, updatable=false)
   private Long id;

   @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
   @IndexColumn(name = "pos", base=0)
   private List<ChildObject> attrs;

   public Long getId () { return id; }
   public List<ChildObject> getAttrs () { return attrs; }
}

ChildObject:

@Entity
public class ChildObject {
   @Embeddable
   public static class Pk implements Serializable {
       @Column(name = "parentId", nullable=false, updatable=false)
       private Long objectId;

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

       @Column(nullable=false, updatable=false)
       private int pos;
       ...
   }

   @EmbeddedId
   private Pk id;

   @ManyToOne
   @JoinColumn(name="parentId", insertable = false, updatable = false)
   @org.hibernate.annotations.ForeignKey(name = "FK_CHILD_OBJECT_PARENTID")
   private ParentObject parent;

   public Pk getId () { return id; }
   public ParentObject getParent () { return parent; }
}
15
RTBarnard

Vous devez incorporer la référence ParentObject dans ChildObject.Pk plutôt que mapper séparément parent et parentId:

(getters, setters, attributs Hibernate non liés au problème et mots clés d'accès aux membres omis)

class ChildObject { 
    @Embeddable
    static class Pk {
        @ManyToOne...
        @JoinColumn(name="parentId")
        ParentObject parent;

        @Column...
        String name...
        ...
    }

    @EmbeddedId
    Pk id;
}

Dans ParentObject, il vous suffit alors de mettre @OneToMany(mappedBy="id.parent") et cela fonctionne.

3
Tomáš Záluský

Tout d’abord, dans la variable ParentObject, "corrigez" l’attribut mappedBy qui doit être défini sur "parent". Aussi (mais c'est peut-être une faute de frappe) ajouter une annotation @Id:

@Entity
public class ParentObject {
    @Id
    @GeneratedValue
    private String id;

    @OneToMany(mappedBy="parent", fetch=FetchType.EAGER)
    @IndexColumn(name = "pos", base=0)
    private List<ObjectChild> attrs;

    // getters/setters
}

Ensuite, dans ObjectChild, ajoutez un attribut name à la objectId dans la clé composite:

@Entity
public class ObjectChild {
    @Embeddable
    public static class Pk implements Serializable {
        @Column(name = "parentId", nullable = false, updatable = false)
        private String objectId;

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

        @Column(nullable = false, updatable = false)
        private int pos;
    }

    @EmbeddedId
    private Pk pk;

    @ManyToOne
    @JoinColumn(name = "parentId", insertable = false, updatable = false)
    private ParentObject parent;

    // getters/setters

}

ETajoutent également insertable = false, updatable = false au @JoinColumn car nous répétons la colonne parentId dans le mappage de cette entité.

Avec ces changements, persister et lire les entités fonctionne bien pour moi (testé avec Derby). 

2
Pascal Thivent

J'ai trouvé cette question en cherchant la réponse à son problème, mais ses réponses n'ont pas résolu mon problème, car je recherchais @OneToMany, qui ne correspond pas aussi bien à la structure de la table que je cherchais. @ElementCollection est la bonne solution dans mon cas. Je crois cependant que l’un des pièges réside dans le fait qu’il considère la rangée entière de relations comme étant unique, pas seulement l’identifiant de la rangée.

@Entity
public class ParentObject {
@Column(nullable=false, updatable=false)
@Id @GeneratedValue(generator="...")
private String id;

@ElementCollection
@CollectionTable( name = "chidren", joinColumns = @JoinColumn( name = "parent_id" ) )
private List<ObjectChild> attrs;

...
}

@Embeddable
public static class ObjectChild implements Serializable {
    @Column(nullable=false, updatable=false)
    private String parentId;

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

    @Column(nullable=false, updatable=false)
    private int pos;

    @Override
    public String toString() {
        return new Formatter().format("%s.%s[%d]", parentId, name, pos).toString();
    }

    ... getters and setters REQUIRED (at least they were for me)
}
1
xenoterracide

Après avoir passé trois jours là-dessus, je pense avoir trouvé une solution, mais pour être honnête, je ne l’aime pas et elle peut certainement être améliorée. Cependant, cela fonctionne et résout notre problème.

Voici le constructeur de votre entité, mais vous pouvez aussi le faire avec la méthode setter . J'ai aussi utilisé un objet Collection, mais il devrait être identique ou similaire à List:

...
public ParentObject(Collection<ObjectChild> children) {
    Collection<ObjectChild> occ = new ArrayList<ObjectChild>();
    for(ObjectChild obj:children){
        obj.setParent(this);
        occ.add(obj);
    }
    this.attrs = occ;
}
...

Fondamentalement, comme l’a suggéré une autre personne, nous devons d’abord définir manuellement l’id du parent de l’enfant avant de sauvegarder le parent (avec tous les enfants).

0
Lorenzo

Il semble que vous soyez très proches et j'essaie de faire la même chose dans mon système actuel. J'ai commencé avec la clé de substitution, mais je souhaitais la supprimer au profit d'une clé primaire composite composée de la clé principale du parent et de l'index de la liste.

J'ai pu obtenir une relation un à un qui partage le PK de la table principale en utilisant un générateur "étranger":

@Entity
@GenericGenerator(
    name = "Parent",
    strategy = "foreign",
    parameters = { @Parameter(name = "property", value = "parent") }
)
public class ChildObject implements Serializable {
    @Id
    @GeneratedValue(generator = "Parent")
    @Column(name = "parent_id")
    private int parentId;

    @OneToOne(mappedBy = "childObject")
    private ParentObject parentObject;
    ...
}

Je me demande si vous pourriez ajouter @GenericGenerator et @GeneratedValue pour résoudre le problème de Hibernate qui n'attribue pas la nouvelle clé acquise au parent lors de l'insertion.

0
David Harkness

Après avoir enregistré l'objet Parent, vous devez définir explicitement le parentId dans les objets Child pour que les insertions sur les objets Child fonctionnent.

0
gabor