Qual è "il lato inverso dell'associazione" in un'associazione bidirezionale JPA OneToMany / ManyToOne?


167

Nella sezione di esempio del @OneToManyriferimento all'annotazione JPA :

Esempio 1-59 @OneToMany - Classe cliente con generici

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

Esempio 1-60 @ManyToOne - Classe d'ordine con generici

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

Mi sembra che l' Customerentità sia il proprietario dell'associazione. Tuttavia, nella spiegazione mappedBydell'attributo nello stesso documento, è scritto che:

se la relazione è bidirezionale, imposta l'elemento mappedBy sul lato inverso (non proprietario) dell'associazione sul nome del campo o della proprietà che possiede la relazione come mostra l'Esempio 1-60.

Tuttavia, se non sbaglio, sembra che nell'esempio mappedBysia effettivamente specificato sul lato proprietario dell'associazione, piuttosto che sul lato non proprietario.

Quindi la mia domanda è sostanzialmente:

  1. In un'associazione bidirezionale (uno-a-molti / molti-a-uno), quale delle entità è il proprietario? Come possiamo designare One side come proprietario? Come possiamo designare il lato Many come proprietario?

  2. Cosa si intende per "il lato inverso dell'associazione"? Come possiamo designare il lato Uno come l'inverso? Come possiamo designare il lato Many come l'inverso?


1
il link fornito non è aggiornato. Per favore aggiornare.
MartinL

Risposte:


306

Per capirlo, devi fare un passo indietro. In OO, il cliente possiede gli ordini (gli ordini sono un elenco nell'oggetto cliente). Non può esserci un ordine senza un cliente. Quindi il cliente sembra essere il proprietario degli ordini.

Ma nel mondo SQL, un elemento conterrà effettivamente un puntatore all'altro. Poiché esiste 1 cliente per N ordini, ogni ordine contiene una chiave esterna per il cliente a cui appartiene. Questa è la "connessione" e questo significa che l'ordine "possiede" (o letteralmente contiene) la connessione (informazioni). Questo è esattamente l'opposto del mondo OO / modello.

Questo può aiutare a capire:

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

Il lato inverso è il "proprietario" OO dell'oggetto, in questo caso il cliente. Il cliente non ha colonne nella tabella per archiviare gli ordini, quindi è necessario indicare dove nella tabella degli ordini può salvare questi dati (che avviene tramite mappedBy).

Un altro esempio comune sono gli alberi con nodi che possono essere sia genitori che figli. In questo caso, i due campi vengono utilizzati in una 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;
}

Questo spiega le opere di progettazione multi-to-one "chiave esterna". C'è un secondo approccio che utilizza un'altra tabella per mantenere le relazioni. Ciò significa che, per il nostro primo esempio, hai tre tabelle: una con i clienti, una con gli ordini e una tabella a due colonne con coppie di chiavi primarie (customerPK, orderPK).

Questo approccio è più flessibile di quello sopra (può facilmente gestire uno a uno, molti a uno, uno a molti e persino molti a molti). Il prezzo è quello

  • è un po 'più lento (dover mantenere un'altra tabella e i join utilizzano tre tabelle anziché solo due),
  • la sintassi del join è più complessa (il che può essere noioso se devi scrivere manualmente molte query, ad esempio quando provi a eseguire il debug di qualcosa)
  • è più soggetto a errori perché improvvisamente puoi ottenere troppi o troppo pochi risultati quando qualcosa va storto nel codice che gestisce la tabella delle connessioni.

Ecco perché raccomando raramente questo approccio.


36
Giusto per chiarire: il lato più è il proprietario; il lato è l'inverso. Non hai scelta (praticamente parlando).
Giovanni,

11
No, Hibernate ha inventato questo. Non mi piace poiché espone parte dell'implementazione al modello OO. Preferirei una @Parento @Childl'annotazione al posto di "XtoY" allo stato quale sia la connessione mezzi (piuttosto che come viene implementata )
Aaron Digulla

4
@AaronDigulla ogni volta che devo passare attraverso una mappatura OneToMany sono venuto a leggere questa risposta, probabilmente la migliore in materia su SO.
Eugene,

7
Wow. Se solo la documentazione dei framework ORM avesse una spiegazione così buona, renderebbe il tutto più facile da ingoiare! Risposta eccellente!
NickJ,

2
@klausch: la documentazione di Hibernate è confusa. Ignoralo. Guarda il codice, l'SQL nel database e come funzionano le chiavi esterne. Se vuoi, puoi portare a casa un pezzo di saggezza: la documentazione è una bugia. Usa la fonte, Luke.
Aaron Digulla,

41

Incredibilmente, in 3 anni nessuno ha risposto alla tua eccellente domanda con esempi di entrambi i modi per mappare la relazione.

Come menzionato da altri, il lato "proprietario" contiene il puntatore (chiave esterna) nel database. È possibile designare una delle parti come proprietario, tuttavia, se si indica la parte Uno come proprietario, la relazione non sarà bidirezionale (la parte inversa nota come "molti" non avrà conoscenza del suo "proprietario"). Ciò può essere desiderabile per l'incapsulamento / accoppiamento libero:

// "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
}

L'unica soluzione di mappatura bidirezionale è avere il lato "molti" proprio puntatore a quello "uno" e usare l'attributo "mappedBy" di @OneToMany. Senza l'attributo "mappedBy" Hibernate si aspetterà una doppia mappatura (il database avrebbe sia la colonna join che la tabella join, che è ridondante (di solito indesiderabile)).

// "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;
}

2
Nel tuo esempio unidirezionale, JPA prevede l'esistenza di una tabella customer_order aggiuntiva. Con JPA2 è possibile utilizzare l'annotazione @JoinColumn (che mi sembra di utilizzare spesso) nel campo degli ordini del Cliente per indicare la colonna chiave esterna del database nella tabella Ordine che deve essere utilizzata. In questo modo si ha una relazione unidirezionale in Java pur avendo una colonna chiave esterna nella tabella Ordine. Quindi nel mondo dell'Oggetto, l'Ordine non è a conoscenza del Cliente mentre nel mondo del database, il Cliente non è a conoscenza dell'Ordine.
Henno Vermeulen,

1
Per essere assolutamente completo, è possibile mostrare il caso bidirezionale in cui il cliente è il lato proprietario della relazione.
HDave il

35

L'entità che ha la tabella con chiave esterna nel database è l'entità proprietaria e l'altra tabella, puntata su, è l'entità inversa.


30
ancora più semplice: il proprietario è il tavolo con la colonna FK
jacktrades,

2
Spiegazione semplice e buona. Qualsiasi lato può essere reso proprietario. Se utilizziamo mappedBy in Order.java, nel campo Cliente <Rimuovi mappedby da Customer.java>, verrà creata una nuova tabella simile a Order_Customer che avrà 2 colonne. ORDER_ID e CUSTOMER_ID.
HakunaMatata,

14

Regole semplici di relazioni bidirezionali:

1.Per le relazioni bidirezionali molti-a-uno, il lato multiplo è sempre il lato proprietario della relazione. Esempio: 1 stanza ha molte persone (una persona appartiene a una sola stanza) -> il lato proprietario è la persona

2.Per le relazioni bidirezionali uno a uno, il lato proprietario corrisponde al lato che contiene la chiave esterna corrispondente.

3.Per le relazioni bidirezionali molti-a-molti, ciascuna parte può essere la parte proprietaria.

La speranza può aiutarti.


Perché dobbiamo avere un proprietario e un contrario? Abbiamo già i concetti significativi di un lato e di molti lati e non importa chi è il proprietario in molte situazioni. Quali sono le conseguenze della decisione? È difficile credere che qualcuno così sinistro come un ingegnere di database abbia deciso di coniare questi concetti ridondanti.
Dan Cancro,

3

Per due classi di entità cliente e ordine, l'ibernazione creerà due tabelle.

Casi possibili:

  1. mappedBy non viene utilizzato in Customer.java e Order.java Class quindi->

    Sul lato cliente verrà creata una nuova tabella [nome = CUSTOMER_ORDER] che manterrà la mappatura di CUSTOMER_ID e ORDER_ID. Queste sono le chiavi primarie delle tabelle clienti e ordini. Sul lato Ordine è necessaria una colonna aggiuntiva per salvare il corrispondente mapping dei record Customer_ID.

  2. mappedBy viene utilizzato in Customer.java [Come indicato nella dichiarazione del problema] Ora la tabella aggiuntiva [CUSTOMER_ORDER] non viene creata. Solo una colonna nella tabella degli ordini

  3. mappedby viene utilizzato in Order.java Ora la tabella aggiuntiva verrà creata da ibernazione. [name = CUSTOMER_ORDER] La tabella degli ordini non avrà una colonna aggiuntiva [Customer_ID] per la mappatura.

Qualsiasi parte può essere resa proprietaria della relazione. Ma è meglio scegliere il lato xxxToOne.

Effetto di codifica -> Solo il lato proprietario dell'entità può modificare lo stato della relazione. Nell'esempio seguente la classe BoyFriend è proprietaria della relazione. anche se Girlfriend vuole sciogliersi, non può.

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

1

Relazioni tra tabelle e relazioni tra entità

In un sistema di database relazionale, possono esserci solo tre tipi di relazioni tra tabelle:

  • uno-a-molti (tramite una colonna chiave esterna)
  • one-to-one (tramite una chiave primaria condivisa)
  • molti-a-molti (tramite una tabella di collegamenti con due chiavi esterne che fanno riferimento a due tabelle padre separate)

Quindi, una one-to-manyrelazione di tabella appare come segue:

Relazione tabella <code> one-to-many </code>

Si noti che la relazione si basa sulla colonna Chiave esterna (ad esempio, post_id) nella tabella figlio.

Quindi, c'è una sola fonte di verità quando si tratta di gestire una one-to-manyrelazione tra tavoli.

Ora, se prendi una relazione di entità bidirezionale che mappa sulla one-to-manyrelazione di tabella che abbiamo visto in precedenza:

Associazione bidirezionale <code> One-To-Many </code>

Se dai un'occhiata al diagramma sopra, puoi vedere che ci sono due modi per gestire questa relazione.

Nella Postentità, si ha la commentscollezione:

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

E, in PostComment, l' postassociazione è mappata come segue:

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

Quindi, hai due lati che possono cambiare l'associazione entità:

  • Aggiungendo una voce nella commentsraccolta figlio, una nuova post_commentriga dovrebbe essere associata postall'entità padre tramite la sua post_idcolonna.
  • Impostando la postproprietà PostCommentdell'entità, anche la post_idcolonna dovrebbe essere aggiornata.

Poiché esistono due modi per rappresentare la colonna Chiave esterna, è necessario definire qual è la fonte della verità quando si tratta di tradurre la modifica dello stato dell'associazione nella modifica equivalente del valore della colonna Chiave esterna.

MappedBy (aka il lato inverso)

L' mappedByattributo indica che la @ManyToOneparte è incaricata di gestire la colonna Chiave esterna e la raccolta viene utilizzata solo per recuperare le entità figlio e per trasferire in cascata le modifiche dello stato dell'entità padre ai figli (ad esempio, la rimozione del genitore dovrebbe anche rimuovere le entità figlio).

Si chiama il lato inverso perché fa riferimento alla proprietà dell'entità figlio che gestisce questa relazione di tabella.

Sincronizza entrambi i lati di un'associazione bidirezionale

Ora, anche se hai definito l' mappedByattributo e l' @ManyToOneassociazione lato figlio gestisce la colonna Chiave esterna, devi comunque sincronizzare entrambi i lati dell'associazione bidirezionale.

Il modo migliore per farlo è aggiungere questi due metodi di utilità:

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

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

I metodi addCommente removeCommentassicurano che entrambe le parti siano sincronizzate. Pertanto, se aggiungiamo un'entità figlio, l'entità figlio deve puntare al genitore e l'entità genitore dovrebbe avere il figlio contenuto nella raccolta figlio.

Per ulteriori dettagli sul modo migliore per sincronizzare tutti i tipi di associazione di entità bidirezionali, consulta questo articolo .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.