Qu'est-ce que «le côté inverse de l'association» dans une association bidirectionnelle JPA OneToMany / ManyToOne?

167

Dans la section exemple de la @OneToManyréférence d'annotation JPA :

Exemple 1-59 @OneToMany - Classe client avec génériques

@Entity
public class Customer implements Serializable {
    ...
    @OneToMany(cascade=ALL, mappedBy="customer")
    public Set<Order> getOrders() { 
        return orders; 
    }
    ...
}

Exemple 1-60 @ManyToOne - Classe de commande avec génériques

@Entity
public class Order implements Serializable {
    ...
    @ManyToOne
    @JoinColumn(name="CUST_ID", nullable=false)
    public Customer getCustomer() { 
        return customer; 
    }
    ...
}

Il me semble que l' Customerentité est propriétaire de l'association. Cependant, dans l'explication de l' mappedByattribut dans le même document, il est écrit que:

si la relation est bidirectionnelle, définissez l'élément mappedBy du côté inverse (non propriétaire) de l'association sur le nom du champ ou de la propriété qui possède la relation, comme le montre l'exemple 1-60.

Cependant, si je ne me trompe pas, il semble que dans l'exemple, le mappedByest en fait spécifié du côté propriétaire de l'association, plutôt que du côté non propriétaire.

Ma question est donc essentiellement:

  1. Dans une association bidirectionnelle (un à plusieurs / plusieurs à un), laquelle des entités est le propriétaire? Comment pouvons-nous désigner le côté Un comme propriétaire? Comment désigner le côté Many comme propriétaire?

  2. Qu'entend-on par «le côté inverse de l'association»? Comment pouvons-nous désigner le côté Un comme l'inverse? Comment pouvons-nous désigner le côté Multiple comme l'inverse?

Behrang Saeedzadeh
la source
1
le lien que vous avez fourni est obsolète. Veuillez mettre à jour.
MartinL

Réponses:

306

Pour comprendre cela, vous devez prendre du recul. En OO, le client est propriétaire des commandes (les commandes sont une liste dans l'objet client). Il ne peut pas y avoir de commande sans client. Le client semble donc être le propriétaire des commandes.

Mais dans le monde SQL, un élément contiendra en fait un pointeur vers l'autre. Puisqu'il y a 1 client pour N commandes, chaque commande contient une clé étrangère du client auquel elle appartient. C'est la "connexion" et cela signifie que l'ordre "possède" (ou contient littéralement) la connexion (l'information). C'est exactement le contraire du monde OO / modèle.

Cela peut aider à comprendre:

public class Customer {
     // This field doesn't exist in the database
     // It is simulated with a SQL query
     // "OO speak": Customer owns the orders
     private List<Order> orders;
}

public class Order {
     // This field actually exists in the DB
     // In a purely OO model, we could omit it
     // "DB speak": Order contains a foreign key to customer
     private Customer customer;
}

Le côté inverse est le "propriétaire" OO de l'objet, dans ce cas le client. Le client n'a pas de colonnes dans la table pour stocker les commandes, vous devez donc lui indiquer où dans la table des commandes il peut enregistrer ces données (ce qui se passe via mappedBy).

Un autre exemple courant est celui des arbres avec des nœuds qui peuvent être à la fois parents et enfants. Dans ce cas, les deux champs sont utilisés dans une classe:

public class Node {
    // Again, this is managed by Hibernate.
    // There is no matching column in the database.
    @OneToMany(cascade = CascadeType.ALL) // mappedBy is only necessary when there are two fields with the type "Node"
    private List<Node> children;

    // This field exists in the database.
    // For the OO model, it's not really necessary and in fact
    // some XML implementations omit it to save memory.
    // Of course, that limits your options to navigate the tree.
    @ManyToOne
    private Node parent;
}

Ceci explique les travaux de conception plusieurs-à-un de "clé étrangère". Il existe une deuxième approche qui utilise une autre table pour maintenir les relations. Cela signifie que pour notre premier exemple, vous avez trois tables: celle avec les clients, celle avec les commandes et une table à deux colonnes avec des paires de clés primaires (customerPK, orderPK).

Cette approche est plus flexible que celle ci-dessus (elle peut facilement gérer un-à-un, plusieurs-à-un, un-à-plusieurs et même plusieurs-à-plusieurs). Le prix est que

  • c'est un peu plus lent (devoir maintenir une autre table et des jointures utilise trois tables au lieu de seulement deux),
  • la syntaxe de jointure est plus complexe (ce qui peut être fastidieux si vous devez écrire manuellement de nombreuses requêtes, par exemple lorsque vous essayez de déboguer quelque chose)
  • c'est plus sujet aux erreurs car vous pouvez soudainement obtenir trop ou trop peu de résultats lorsque quelque chose ne va pas dans le code qui gère la table de connexion.

C'est pourquoi je recommande rarement cette approche.

Aaron Digulla
la source
36
Juste pour clarifier: le côté multiple est le propriétaire; un côté est l'inverse. Vous n'avez pas le choix (pratiquement).
John
11
Non, Hibernate a inventé cela. Je n'aime pas ça car il expose une partie de l'implémentation au modèle OO. Je préférerais une annotation @Parentou @Childau lieu de "XtoY" pour indiquer ce que signifie la connexion (plutôt que comment elle est implémentée )
Aaron Digulla
4
@AaronDigulla à chaque fois que je dois passer par un mappage OneToMany, je suis venu lire cette réponse, probablement la meilleure sur le sujet sur SO.
Eugene
7
Sensationnel. Si seulement la documentation des frameworks ORM avait une si bonne explication - cela rendrait le tout plus facile à avaler! Excellente réponse!
NickJ du
2
@klausch: La documentation Hibernate est déroutante. Ignorez-le. Regardez le code, le SQL dans la base de données et le fonctionnement des clés étrangères. Si vous le souhaitez, vous pouvez emporter un morceau de sagesse chez vous: la documentation est un mensonge. Utilisez la source, Luke.
Aaron Digulla
41

Incroyablement, en 3 ans, personne n'a répondu à votre excellente question avec des exemples des deux façons de cartographier la relation.

Comme mentionné par d'autres, le côté «propriétaire» contient le pointeur (clé étrangère) dans la base de données. Vous pouvez désigner l'un ou l'autre des côtés comme propriétaire, cependant, si vous désignez le côté Un comme propriétaire, la relation ne sera pas bidirectionnelle (le côté inverse aka «plusieurs» n'aura aucune connaissance de son «propriétaire»). Cela peut être souhaitable pour l'encapsulation / couplage lâche:

// "One" Customer owns the associated orders by storing them in a customer_orders join table
public class Customer {
    @OneToMany(cascade = CascadeType.ALL)
    private List<Order> orders;
}

// if the Customer owns the orders using the customer_orders table,
// Order has no knowledge of its Customer
public class Order {
    // @ManyToOne annotation has no "mappedBy" attribute to link bidirectionally
}

La seule solution de mappage bidirectionnel est que le côté "plusieurs" possède son pointeur vers le "un" et utilise l'attribut @OneToMany "mappedBy". Sans l'attribut "mappedBy", Hibernate s'attend à un double mappage (la base de données aurait à la fois la colonne de jointure et la table de jointure, ce qui est redondant (généralement indésirable)).

// "One" Customer as the inverse side of the relationship
public class Customer {
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "customer")
    private List<Order> orders;
}

// "many" orders each own their pointer to a Customer
public class Order {
    @ManyToOne
    private Customer customer;
}
Steve Jones
la source
2
Dans votre exemple unidirectionnel, JPA s'attend à ce qu'une table customer_orders supplémentaire existe. Avec JPA2, vous pouvez utiliser l'annotation @JoinColumn (que je semble utiliser souvent) sur le champ orders de Customer pour désigner la colonne de clé étrangère de la base de données dans la table Order qui doit être utilisée. De cette façon, vous avez une relation unidirectionnelle en Java tout en ayant toujours une colonne de clé étrangère dans la table Order. Ainsi, dans le monde des objets, la commande ne connaît pas le client tandis que dans le monde de la base de données, le client ne connaît pas la commande.
Henno Vermeulen
1
Pour être ultra-complet, vous pouvez montrer le cas bidirectionnel où le client est le côté propriétaire de la relation.
HDave
35

L'entité qui a la table avec la clé étrangère dans la base de données est l'entité propriétaire et l'autre table, pointée, est l'entité inverse.

Venu
la source
30
encore plus simple: le propriétaire est la table avec la colonne FK
jacktrades
2
Explication simple et bonne. N'importe quel côté peut devenir propriétaire. Si nous utilisons mappedBy dans Order.java, sur le champ Customer <Remove mappedby from Customer.java>, une nouvelle table sera créée quelque chose comme Order_Customer qui aura 2 colonnes. ORDER_ID et CUSTOMER_ID.
HakunaMatata
14

Règles simples des relations bidirectionnelles:

Pour les relations bidirectionnelles plusieurs à un, le côté plusieurs est toujours le côté propriétaire de la relation. Exemple: 1 pièce a plusieurs personnes (une personne appartient à une seule pièce) -> le côté propriétaire est Personne

Pour les relations bidirectionnelles un à un, le côté propriétaire correspond au côté qui contient la clé étrangère correspondante.

Pour les relations bidirectionnelles plusieurs à plusieurs, chaque côté peut être le côté propriétaire.

L'espoir peut vous aider.

Ken Block
la source
Pourquoi avons-nous besoin d'un propriétaire et d'un inverse? Nous avons déjà les concepts significatifs d'un côté et de plusieurs côtés et peu importe qui est le propriétaire dans les situations plusieurs-à-plusieurs. Quelles sont les conséquences de la décision? Il est difficile de croire que quelqu'un d'aussi gauche qu'un ingénieur de bases de données ait décidé d'inventer ces concepts redondants.
Dan Cancro
3

Pour deux classes d'entités Customer et Order, hibernate créera deux tables.

Cas possibles:

  1. mappedBy n'est pas utilisé dans la classe Customer.java et Order.java alors->

    Côté client, une nouvelle table sera créée [nom = CUSTOMER_ORDER] qui conservera le mappage de CUSTOMER_ID et ORDER_ID. Ce sont les clés primaires des tables client et commande. Du côté de la commande, une colonne supplémentaire est requise pour enregistrer le mappage d'enregistrement Customer_ID correspondant.

  2. mappedBy est utilisé dans Customer.java [Comme indiqué dans l'énoncé du problème] La table supplémentaire [CUSTOMER_ORDER] n'est plus créée. Une seule colonne dans le tableau de commande

  3. mappedby est utilisé dans Order.java Une table supplémentaire sera désormais créée par hibernate. [name = CUSTOMER_ORDER] Order Table n'aura pas de colonne supplémentaire [Customer_ID] pour le mappage.

N'importe quel côté peut devenir propriétaire de la relation. Mais il vaut mieux choisir le côté xxxToOne.

Effet de codage -> Seul le côté propriétaire de l'entité peut changer le statut de la relation. Dans l'exemple ci-dessous, la classe BoyFriend est propriétaire de la relation. même si Girlfriend veut rompre, elle ne peut pas.

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "BoyFriend21")
public class BoyFriend21 {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "Boy_ID")
    @SequenceGenerator(name = "Boy_ID", sequenceName = "Boy_ID_SEQUENCER", initialValue = 10,allocationSize = 1)
    private Integer id;

    @Column(name = "BOY_NAME")
    private String name;

    @OneToOne(cascade = { CascadeType.ALL })
    private GirlFriend21 girlFriend;

    public BoyFriend21(String name) {
        this.name = name;
    }

    public BoyFriend21() {
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public BoyFriend21(String name, GirlFriend21 girlFriend) {
        this.name = name;
        this.girlFriend = girlFriend;
    }

    public GirlFriend21 getGirlFriend() {
        return girlFriend;
    }

    public void setGirlFriend(GirlFriend21 girlFriend) {
        this.girlFriend = girlFriend;
    }
}

import org.hibernate.annotations.*;
import javax.persistence.*;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.ArrayList;
import java.util.List;

@Entity 
@Table(name = "GirlFriend21")
public class GirlFriend21 {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "Girl_ID")
    @SequenceGenerator(name = "Girl_ID", sequenceName = "Girl_ID_SEQUENCER", initialValue = 10,allocationSize = 1)
    private Integer id;

    @Column(name = "GIRL_NAME")
    private String name;

    @OneToOne(cascade = {CascadeType.ALL},mappedBy = "girlFriend")
    private BoyFriend21 boyFriends = new BoyFriend21();

    public GirlFriend21() {
    }

    public GirlFriend21(String name) {
        this.name = name;
    }


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public GirlFriend21(String name, BoyFriend21 boyFriends) {
        this.name = name;
        this.boyFriends = boyFriends;
    }

    public BoyFriend21 getBoyFriends() {
        return boyFriends;
    }

    public void setBoyFriends(BoyFriend21 boyFriends) {
        this.boyFriends = boyFriends;
    }
}


import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.Arrays;

public class Main578_DS {

    public static void main(String[] args) {
        final Configuration configuration = new Configuration();
         try {
             configuration.configure("hibernate.cfg.xml");
         } catch (HibernateException e) {
             throw new RuntimeException(e);
         }
        final SessionFactory sessionFactory = configuration.buildSessionFactory();
        final Session session = sessionFactory.openSession();
        session.beginTransaction();

        final BoyFriend21 clinton = new BoyFriend21("Bill Clinton");
        final GirlFriend21 monica = new GirlFriend21("monica lewinsky");

        clinton.setGirlFriend(monica);
        session.save(clinton);

        session.getTransaction().commit();
        session.close();
    }
}

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import java.util.List;

public class Main578_Modify {

    public static void main(String[] args) {
        final Configuration configuration = new Configuration();
        try {
            configuration.configure("hibernate.cfg.xml");
        } catch (HibernateException e) {
            throw new RuntimeException(e);
        }
        final SessionFactory sessionFactory = configuration.buildSessionFactory();
        final Session session1 = sessionFactory.openSession();
        session1.beginTransaction();

        GirlFriend21 monica = (GirlFriend21)session1.load(GirlFriend21.class,10);  // Monica lewinsky record has id  10.
        BoyFriend21 boyfriend = monica.getBoyFriends();
        System.out.println(boyfriend.getName()); // It will print  Clinton Name
        monica.setBoyFriends(null); // It will not impact relationship

        session1.getTransaction().commit();
        session1.close();

        final Session session2 = sessionFactory.openSession();
        session2.beginTransaction();

        BoyFriend21 clinton = (BoyFriend21)session2.load(BoyFriend21.class,10);  // Bill clinton record

        GirlFriend21 girlfriend = clinton.getGirlFriend();
        System.out.println(girlfriend.getName()); // It will print Monica name.
        //But if Clinton[Who owns the relationship as per "mappedby" rule can break this]
        clinton.setGirlFriend(null);
        // Now if Monica tries to check BoyFriend Details, she will find Clinton is no more her boyFriend
        session2.getTransaction().commit();
        session2.close();

        final Session session3 = sessionFactory.openSession();
        session1.beginTransaction();

        monica = (GirlFriend21)session3.load(GirlFriend21.class,10);  // Monica lewinsky record has id  10.
        boyfriend = monica.getBoyFriends();

        System.out.println(boyfriend.getName()); // Does not print Clinton Name

        session3.getTransaction().commit();
        session3.close();
    }
}
Hakuna Matata
la source
1

Relations de table et relations d'entité

Dans un système de base de données relationnelle, il ne peut y avoir que trois types de relations de table:

  • un-à-plusieurs (via une colonne de clé étrangère)
  • un-à-un (via une clé primaire partagée)
  • plusieurs-à-plusieurs (via une table de liens avec deux clés étrangères référençant deux tables parentes distinctes)

Ainsi, une one-to-manyrelation de table se présente comme suit:

Relation de table <code> un-à-plusieurs </code>

Notez que la relation est basée sur la colonne Clé étrangère (par exemple, post_id) dans la table enfant.

Il existe donc une seule source de vérité lorsqu'il s'agit de gérer une one-to-manyrelation de table.

Maintenant, si vous prenez une relation d'entité bidirectionnelle qui correspond à la one-to-manyrelation de table que nous avons vue précédemment:

Association d'entités bidirectionnelles <code> One-To-Many </code>

Si vous regardez le diagramme ci-dessus, vous pouvez voir qu'il existe deux façons de gérer cette relation.

Dans l' Postentité, vous avez la commentscollection:

@OneToMany(
    mappedBy = "post",
    cascade = CascadeType.ALL,
    orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();

Et, dans le PostComment, l' postassociation est mappée comme suit:

@ManyToOne(
    fetch = FetchType.LAZY
)
@JoinColumn(name = "post_id")
private Post post;

Donc, vous avez deux côtés qui peuvent changer l'association d'entité:

  • En ajoutant une entrée dans la commentscollection enfant, une nouvelle post_commentligne doit être associée à l' postentité parente via sonpost_id colonne.
  • En définissant la postpropriété de l' PostCommententité, la post_idcolonne doit également être mise à jour.

Étant donné qu'il existe deux façons de représenter la colonne de clé étrangère, vous devez définir quelle est la source de vérité lorsqu'il s'agit de traduire le changement d'état d'association en modification de la valeur de la colonne de clé étrangère équivalente.

MappedBy (aka le côté inverse)

L' mappedByattribut indique que le @ManyToOnecôté est en charge de la gestion de la colonne de clé étrangère, et la collection est utilisée uniquement pour récupérer les entités enfants et pour mettre en cascade les changements d'état de l'entité parente aux enfants (par exemple, la suppression du parent devrait également supprimer les entités enfants).

Il est appelé le côté inverse car il fait référence à la propriété d'entité enfant qui gère cette relation de table.

Synchronisez les deux côtés d'une association bidirectionnelle

Désormais, même si vous avez défini l' mappedByattribut et que l' @ManyToOneassociation côté enfant gère la colonne de clé étrangère, vous devez toujours synchroniser les deux côtés de l'association bidirectionnelle.

La meilleure façon de le faire est d'ajouter ces deux méthodes utilitaires:

public void addComment(PostComment comment) {
    comments.add(comment);
    comment.setPost(this);
}

public void removeComment(PostComment comment) {
    comments.remove(comment);
    comment.setPost(null);
}

Les méthodes addCommentet removeCommentgarantissent que les deux côtés sont synchronisés. Donc, si nous ajoutons une entité enfant, l'entité enfant doit pointer vers le parent et l'entité parente doit avoir l'enfant contenu dans la collection enfant.

Pour plus d'informations sur la meilleure façon de synchroniser tous les types d'association d'entités bidirectionnelles, consultez cet article .

Vlad Mihalcea
la source