Co to jest „odwrotna strona asocjacji” w dwukierunkowym skojarzeniu JPA OneToMany / ManyToOne?

167

W sekcji przykładowej @OneToManyadnotacji JPA :

Przykład 1-59 @OneToMany - klasa klienta z typami generycznymi

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

Przykład 1-60 @ManyToOne - Order Class with Generics

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

Wydaje mi się, że Customerpodmiot jest właścicielem stowarzyszenia. Jednak w wyjaśnieniu mappedByatrybutu w tym samym dokumencie jest napisane, że:

jeśli relacja jest dwukierunkowa, ustaw element mappedBy po odwrotnej (nie będącej właścicielem) stronie powiązania na nazwę pola lub właściwości będącej właścicielem relacji, jak pokazano na przykładzie 1-60.

Jeśli się jednak nie mylę, w przykładzie wygląda na to, że w mappedByrzeczywistości jest on określony po stronie będącej właścicielem, a nie po stronie niebędącej właścicielem.

Więc moje pytanie jest w zasadzie:

  1. Która z jednostek jest właścicielem w powiązaniu dwukierunkowym (jeden do wielu / wiele do jednego)? Jak możemy wyznaczyć Stronę Jedną jako właściciela? Jak możemy wyznaczyć Stronę Wielu jako właściciela?

  2. Co należy rozumieć przez „odwrotną stronę skojarzenia”? Jak możemy wyznaczyć Jedną stronę jako odwrotną? Jak możemy wyznaczyć stronę Wielu jako przeciwieństwo?

Behrang Saeedzadeh
źródło
1
podany przez Ciebie link jest nieaktualny. Proszę zaktualizować.
MartinL

Odpowiedzi:

306

Aby to zrozumieć, musisz cofnąć się o krok. W OO klient jest właścicielem zamówień (zamówienia są listą w obiekcie klienta). Nie może być zamówienia bez klienta. Więc klient wydaje się być właścicielem zamówień.

Ale w świecie SQL jeden element będzie zawierał wskaźnik do drugiego. Ponieważ istnieje 1 klient dla zamówień N, każde zamówienie zawiera klucz obcy klienta, do którego należy. To jest „połączenie”, a to oznacza, że ​​zamówienie „jest właścicielem” (lub dosłownie zawiera) połączenie (informacje). To jest dokładnie odwrotne od świata OO / modelowego.

Może to pomóc zrozumieć:

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

Odwrotną stroną jest OO „właściciel” obiektu, w tym przypadku klient. Klient nie ma kolumn w tabeli do przechowywania zamówień, więc musisz wskazać mu, gdzie w tabeli zamówień może zapisać te dane (co dzieje się za pośrednictwem mappedBy).

Innym częstym przykładem są drzewa z węzłami, którymi mogą być zarówno rodzice, jak i dzieci. W tym przypadku te dwa pola są używane w jednej klasie:

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

Wyjaśnia to projekt „klucza obcego” typu „wiele do jednego”. Istnieje drugie podejście, które wykorzystuje inną tabelę do utrzymania relacji. Oznacza to, że w naszym pierwszym przykładzie masz trzy tabele: jedną z klientami, drugą z zamówieniami i dwukolumnową tabelę z parami kluczy podstawowych (customerPK, orderPK).

To podejście jest bardziej elastyczne niż to powyżej (może z łatwością obsługiwać jeden do jednego, wiele do jednego, jeden do wielu, a nawet wiele do wielu). Cena jest taka

  • jest trochę wolniejszy (konieczność utrzymywania innego stołu i łączenia wymaga trzech zamiast dwóch),
  • składnia łączenia jest bardziej złożona (co może być uciążliwe, jeśli musisz ręcznie pisać wiele zapytań, na przykład podczas próby debugowania)
  • jest bardziej podatny na błędy, ponieważ możesz nagle uzyskać zbyt wiele lub zbyt mało wyników, gdy coś pójdzie nie tak w kodzie, który zarządza tabelą połączeń.

Dlatego rzadko polecam to podejście.

Aaron Digulla
źródło
36
Dla wyjaśnienia: wiele stron jest właścicielem; jedna strona jest odwrotna. Nie masz wyboru (praktycznie rzecz biorąc).
Jan
11
Nie, Hibernate to wynalazł. Nie podoba mi się to, ponieważ eksponuje część implementacji modelowi OO. Wolałbym adnotację @Parentlub @Childzamiast „XtoY”, aby określić, co oznacza połączenie (a nie jak jest zaimplementowane )
Aaron Digulla
4
@AaronDigulla za każdym razem, gdy muszę przejść przez mapowania OneToMany, przyszedłem przeczytać tę odpowiedź, prawdopodobnie najlepszą na ten temat na SO.
Eugene
7
Łał. Gdyby tylko dokumentacja frameworka ORM miała tak dobre wyjaśnienie - ułatwiłoby to przełknięcie całej sprawy! Doskonała odpowiedź!
NickJ,
2
@klausch: Dokumentacja Hibernate jest zagmatwana. Zignoruj ​​to. Spójrz na kod, SQL w bazie danych i jak działają klucze obce. Jeśli chcesz, możesz zabrać do domu mądrość: Dokumentacja to kłamstwo. Użyj źródła, Luke.
Aaron Digulla
41

Niewiarygodne, że w ciągu 3 lat nikt nie odpowiedział na twoje doskonałe pytanie z przykładami obu sposobów odwzorowania relacji.

Jak wspominali inni, strona „właściciela” zawiera wskaźnik (klucz obcy) w bazie danych. Możesz wyznaczyć dowolną stronę jako właściciela, jednak jeśli wyznaczysz jedną stronę jako właściciela, relacja nie będzie dwukierunkowa (odwrotność, czyli „wiele” stron, nie będzie wiedziała o swoim „właścicielu”). Może to być pożądane w przypadku hermetyzacji / luźnego połączenia:

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

Jedynym rozwiązaniem do mapowania dwukierunkowego jest posiadanie wskaźnika „wiele” na stronie „jeden” i użycie atrybutu @OneToMany „mappedBy”. Bez atrybutu „mappedBy” Hibernate będzie oczekiwał podwójnego mapowania (baza danych będzie zawierała zarówno kolumnę złączenia, jak i tabelę złączeń, co jest zbędne (zwykle niepożądane).

// "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
źródło
2
W Twoim jednokierunkowym przykładzie JPA oczekuje, że będzie istniała dodatkowa tabela customer_orders. W JPA2 możesz użyć adnotacji @JoinColumn (której często używam) w polu zamówienia klienta, aby wskazać kolumnę klucza obcego bazy danych w tabeli zamówień, która powinna być używana. W ten sposób masz jednokierunkową relację w Javie, jednocześnie mając kolumnę klucza obcego w tabeli Order. Tak więc w świecie obiektów Zamówienie nie wie o Kliencie, podczas gdy w świecie bazy danych Klient nie wie o Zamówieniu.
Henno Vermeulen
1
Aby uzyskać kompletność, możesz pokazać przypadek dwukierunkowy, w którym klient jest właścicielem relacji.
HDave
35

Jednostka, która ma tabelę z kluczem obcym w bazie danych, jest jednostką będącą właścicielem, a wskazana druga tabela jest jednostką odwrotną.

Venu
źródło
30
Jeszcze prościej: właściciel jest przy stole z kolumną FK
jacktrades
2
Proste i dobre wyjaśnienie. Właścicielem może być każda strona. Jeśli użyjemy mappedBy w Order.java, w polu Customer <Remove mappedby from Customer.java> zostanie utworzona nowa tabela, podobna do Order_Customer, która będzie miała 2 kolumny. ORDER_ID i CUSTOMER_ID.
HakunaMatata,
14

Proste zasady relacji dwukierunkowych:

W przypadku relacji dwukierunkowych typu „wiele do jednego” strona wielu jest zawsze właścicielem relacji. Przykład: 1 pokój ma wiele osób (osoba należy tylko do jednego pokoju) -> stroną właściciela jest osoba

2. W przypadku relacji dwukierunkowych jeden do jednego strona będąca właścicielem odpowiada stronie zawierającej odpowiedni klucz obcy.

W przypadku relacji dwukierunkowych wiele-do-wielu, właścicielem może być każda ze stron.

Hope może ci pomóc.

Ken Block
źródło
Dlaczego w ogóle musimy mieć właściciela i odwrotność? Mamy już znaczące koncepcje jednostronne i wielostronne i nie ma znaczenia, kto jest właścicielem w wielu do wielu sytuacjach. Jakie są konsekwencje tej decyzji? Trudno uwierzyć, że ktoś tak leworęczny jak inżynier bazy danych zdecydował się wymyślić te zbędne koncepcje.
Dan Cancro
3

Dla dwóch klas jednostek Customer and Order, hibernacja utworzy dwie tabele.

Możliwe przypadki:

  1. mappedBy nie jest używany w Customer.java i Order.java Class then->

    Po stronie klienta zostanie utworzona nowa tabela [name = CUSTOMER_ORDER], która będzie nadal mapować CUSTOMER_ID i ORDER_ID. Są to klucze podstawowe tabel klientów i zamówień. Po stronie zamówienia wymagana jest dodatkowa kolumna, aby zapisać odpowiednie mapowanie rekordów Customer_ID.

  2. mappedBy jest używany w Customer.java [Jak podano w opisie problemu] Teraz dodatkowa tabela [CUSTOMER_ORDER] nie jest tworzona. Tylko jedna kolumna w tabeli zamówień

  3. mappedby jest używany w Order.java Teraz dodatkowa tabela zostanie utworzona przez hibernację. [name = CUSTOMER_ORDER] Tabela zamówień nie będzie miała dodatkowej kolumny [Customer_ID] do mapowania.

Właścicielem relacji może być dowolna strona. Ale lepiej wybrać stronę xxxToOne.

Efekt kodowania -> Tylko właściciel jednostki może zmienić status relacji. W poniższym przykładzie właścicielem relacji jest klasa BoyFriend. nawet jeśli Dziewczyna chce się rozstać, nie może.

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();
    }
}
HakunaMatata
źródło
1

Relacje między tabelami a relacje encji

W systemie relacyjnej bazy danych mogą istnieć tylko trzy typy relacji między tabelami:

  • jeden do wielu (przez kolumnę klucza obcego)
  • jeden do jednego (przez wspólny klucz podstawowy)
  • wiele do wielu (poprzez tabelę łączącą z dwoma kluczami obcymi odwołującymi się do dwóch oddzielnych tabel nadrzędnych)

Tak więc one-to-manyrelacja między tabelami wygląda następująco:

Relacja tabeli <code> jeden do wielu </code>

Zauważ, że relacja jest oparta na kolumnie Foreign Key (np. post_id) W tabeli podrzędnej.

Tak więc istnieje jedno źródło prawdy, jeśli chodzi o zarządzanie one-to-manyrelacjami między stołami.

Teraz, jeśli weźmiesz dwukierunkową relację encji, która mapuje na one-to-manyrelację między tabelami, którą widzieliśmy wcześniej:

Dwukierunkowe powiązanie encji <code> jeden do wielu </code>

Jeśli spojrzysz na powyższy diagram, zobaczysz, że istnieją dwa sposoby zarządzania tą relacją.

W Postjednostce masz commentskolekcję:

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

A w PostCommentThe poststowarzyszenie jest odwzorowany w sposób następujący:

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

Masz więc dwie strony, które mogą zmienić powiązanie encji:

  • Dodając wpis do commentskolekcji podrzędnej, nowy post_commentwiersz powinien zostać powiązany z postjednostką nadrzędną za pośrednictwem jejpost_id kolumny.
  • Ustawiając postwłaściwość PostCommentjednostki, post_idnależy również zaktualizować kolumnę.

Ponieważ istnieją dwa sposoby przedstawiania kolumny klucza obcego, należy określić, który jest źródłem prawdy, jeśli chodzi o tłumaczenie zmiany stanu skojarzenia na odpowiadającą jej modyfikację wartości kolumny klucza obcego.

MappedBy (inaczej strona odwrotna)

mappedByAtrybut informuje, że @ManyToOnestrona jest odpowiedzialna za zarządzanie kolumny klucz obcy, a zbiór jest używany tylko do pobierania jednostek podrzędnych i kaskady zmian stanu podmiotem dominującym dla dzieci (na przykład usuwając rodzic powinien także usunąć podmioty podrzędne).

Nazywa się odwrotną stroną, ponieważ odwołuje się do właściwości jednostki podrzędnej, która zarządza tą relacją między tabelami.

Zsynchronizuj obie strony skojarzenia dwukierunkowego

Teraz, nawet jeśli zdefiniowałeś mappedByatrybut i @ManyToOnepowiązanie po stronie podrzędnej zarządza kolumną klucza obcego, nadal musisz zsynchronizować obie strony skojarzenia dwukierunkowego.

Najlepszym sposobem na to jest dodanie dwóch metod narzędziowych:

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

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

addCommentI removeCommentmetody zapewnienia, że obie strony są zsynchronizowane. Tak więc, jeśli dodamy jednostkę podrzędną, jednostka podrzędna musi wskazywać na jednostkę nadrzędną, a jednostka nadrzędna powinna zawierać element podrzędny w kolekcji podrzędnej.

Aby uzyskać więcej informacji na temat najlepszego sposobu synchronizacji wszystkich dwukierunkowych typów skojarzeń encji, zapoznaj się z tym artykułem .

Vlad Mihalcea
źródło