Що таке "зворотна сторона асоціації" у двосторонній асоціації JPA OneToMany / ManyToOne?


167

У прикладі розділу @OneToManyпосилання на анотацію JPA :

Приклад 1-59 @OneToMany - Клас клієнтів із дженериками

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

Приклад 1-60 @ManyToOne - Клас замовлення із дженериками

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

Мені здається, що Customerсуб’єкт господарювання є власником асоціації. Однак у поясненні mappedByатрибуту в тому ж документі написано, що:

якщо співвідношення є двонаправленим, то встановіть елемент mappedBy на зворотній (невласницькій) стороні асоціації на ім'я поля чи властивості, якій належить відношення, як показує Приклад 1-60.

Однак, якщо я не помиляюся, це виглядає, як у прикладі, mappedByфактично вказане на стороні асоціації, а не на стороні, що не володіє.

Отже, моє питання в основному:

  1. У двосторонній асоціації (один на багато / багато-до-одного) хто з суб'єктів є власником? Як ми можемо визначити Один бік як власника? Як ми можемо визначити "Багатього" як власника?

  2. Що означає "зворотна сторона асоціації"? Як ми можемо позначити одну сторону як зворотну? Як ми можемо позначити сторону "Багато" як зворотну?


1
надане вами посилання застаріле. Будь ласка, оновіть.
MartinL

Відповіді:


306

Щоб зрозуміти це, потрібно зробити крок назад. В ОО замовник є власником замовлень (замовлення - це список в об'єкті замовника). Замовлення не може бути без замовника. Тож замовник, здається, є власником замовлень.

Але у світі SQL один елемент фактично буде містити вказівник на інший. Оскільки для N замовлень є 1 клієнт, кожне замовлення містить іноземний ключ для того замовника, якому він належить. Це "з'єднання", і це означає, що замовлення "володіє" (або буквально містить) з'єднання (інформацію). Це якраз протилежне світу OO / модель.

Це може допомогти зрозуміти:

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

Зворотною стороною є ОО "власник" об'єкта, в даному випадку замовник. У клієнта немає стовпців у таблиці для зберігання замовлень, тому ви повинні сказати йому, де в таблиці замовлень він може зберігати ці дані (що відбувається через mappedBy).

Ще один поширений приклад - дерева з вузлами, які можуть бути як батьками, так і дітьми. У цьому випадку два поля використовуються в одному класі:

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

Це пояснює "багатогранні" дизайнерські роботи "іноземного ключа". Існує другий підхід, який використовує іншу таблицю для підтримки відносин. Це означає, що для нашого першого прикладу у вас є три таблиці: одна з клієнтами, друга із замовленнями та двоколонкова таблиця з парами первинних ключів (customerPK, orderPK).

Цей підхід є більш гнучким, ніж вищевказаний (він може легко впоратися один на один, багато в одного, один на багато і навіть багато-на-багато). Ціна така

  • це трохи повільніше (потрібно підтримувати іншу таблицю і приєднується, використовує три таблиці замість лише двох),
  • синтаксис приєднання є більш складним (що може бути нудно, якщо вам доведеться вручну писати багато запитів, наприклад, коли ви намагаєтеся щось налагодити)
  • це більше схильне до помилок, тому що ви можете раптом отримати занадто багато або занадто мало результатів, коли щось піде не так у коді, який керує таблицею з'єднань.

Ось чому я рідко рекомендую такий підхід.


36
Просто для уточнення: багато сторін є власником; одна сторона - обернена. У вас немає вибору (практично кажучи).
Джон

11
Ні, Hibernate це винайшов. Мені це не подобається, оскільки він виставляє частину реалізації моделі OO. Я вважаю за краще замість "XtoY" @Parentабо @Childпримітки зазначити, що означає з'єднання (а не як його реалізувати )
Aaron Digulla

4
@AaronDigulla щоразу, коли мені доводиться переглядати відображення OneToMany, я прийшов прочитати цю відповідь, мабуть, найкраще в цьому питанні.
Євген

7
Ого. Якби лише документація з фреймворків ORM мала таке добре пояснення - це полегшило б проковтнути всю справу! Відмінна відповідь!
NickJ

2
@klausch: Документація до сплячого режиму заплутана. Ігноруйте це. Подивіться на код, SQL у базі даних та як працюють зовнішні ключі. Якщо хочете, можете взяти частину мудрості додому: Документація - брехня. Скористайтеся джерелом, Лука.
Аарон Дігулла

41

Неймовірно, що за 3 роки ніхто не відповів на ваше відмінне запитання із прикладами обох способів скласти карту відносин.

Як зазначають інші, сторона "власника" містить вказівник (зовнішній ключ) у базі даних. Ви можете призначити будь-яку сторону як власника, однак, якщо ви визначите Один бік як власника, відносини не будуть двосторонніми (зворотна ака "багато" сторона не матиме знань про свого "власника"). Це може бути бажано для інкапсуляції / сипучої муфти:

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

Єдине двонаправлене рішення для картографування полягає у тому, щоб сторона "багатьох" володіла своїм покажчиком на "один" та використовувала атрибут @OneToMany "mappedBy". Без атрибута "mappedBy" Hibernate очікує подвійного відображення (база даних матиме як стовпець об'єднання, так і таблицю приєднання, що є зайвим (зазвичай небажаним)).

// "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
У вашому односпрямованому прикладі JPA очікує існування додаткової таблиці________подібних таблиць. За допомогою JPA2 ви можете використовувати анотацію @JoinColumn (яку я, як правило, використовую часто) в полі Замовлення Клієнта, щоб позначити стовпчик із зовнішнім ключем бази даних у таблиці Замовлення, яку слід використовувати. Таким чином у вас є однонаправлене відношення в Java, зберігаючи стовпчик із зовнішнім ключем у таблиці Порядок. Тож у світі Об'єктів Замовлення не знає про Клієнта, перебуваючи у світі баз даних, Клієнт не знає про Замовлення.
Генно Вермеулен

1
Щоб бути неповноцінним, ви можете показати двосторонній випадок, коли клієнт є власною стороною відносин.
HDave

35

Суб'єкт, який має таблицю із зовнішнім ключем у базі даних, є власником, а інша таблиця, на яку вказують, - обернена сутність.


30
ще простіше: власник таблиці з колоною FK
джек-треди

2
Просте і хороше пояснення. Власником може бути будь-яка сторона. Якщо ми використовуємо mappedBy у Order.java, у полі Клієнт <Видалити mappedby з Customer.java> тоді буде створена нова таблиця на зразок Order_Customer, яка матиме 2 стовпці. ORDER_ID та CUSTOMER_ID.
HakunaMatata

14

Прості правила двонаправлених відносин:

1.Для двосторонніх відносин багато в один, багато сторін завжди є власною стороною відносин. Приклад: 1 кімната має багато осіб (особі належить лише одна кімната) -> власником є ​​особа

2.Для двосторонніх відносин один на один власна сторона відповідає стороні, що містить відповідний зовнішній ключ.

3. Для двосторонніх відносин між багатьма людьми може бути будь-яка сторона.

Надія може вам допомогти.


Чому нам взагалі потрібно мати власника та обернену? У нас вже є змістовні поняття одностороння та багатостороння, і не важливо, хто є власником у багатьох ситуаціях. Які наслідки прийнятого рішення? Важко повірити, що хтось настільки мізерний, як інженер баз даних, вирішив придумати ці зайві концепції.
Ден Кенкро

3

Для двох клієнтських класів клієнтів та замовлення сплячий режим створить дві таблиці.

Можливі випадки:

  1. mappedBy не використовується в класах Customer.java та Order.java тоді->

    На стороні замовника буде створена нова таблиця [name = CUSTOMER_ORDER], яка буде зберігати відображення CUSTOMER_ID та ORDER_ID. Це основні ключі таблиць клієнтів та замовлень. На стороні замовлення необхідний додатковий стовпець, щоб зберегти відповідне відображення записів Customer_ID.

  2. mappedBy використовується в Customer.java [Як зазначено в заяві про проблему] Тепер додаткова таблиця [CUSTOMER_ORDER] не створена. У таблиці замовлень лише один стовпець

  3. mappedby використовується в Order.java Тепер додаткова таблиця буде створена сплячим режимом. [name = CUSTOMER_ORDER] Таблиця замовлень не матиме додаткового стовпця [Customer_ID] для відображення.

Власником відносин може бути будь-яка сторона. Але краще вибрати сторону xxxToOne.

Ефект кодування -> Тільки власна сторона сутності може змінити статус відносин. Нижче на прикладі клас BoyFriend є власником відносин. навіть якщо подруга хоче розлучитися, вона не може.

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

Табличні відносини проти відносин сутності

У системі реляційних баз даних може бути лише три типи зв’язків таблиць:

  • один на багато (через стовпець із зовнішнім ключем)
  • один на один (за допомогою спільного первинного ключа)
  • багато-до-багатьох (через таблицю посилань з двома іноземними ключами, що посилаються на дві окремі батьківські таблиці)

Отже, співвідношення one-to-manyтаблиці виглядає так:

<code> співвідношення таблиці «один до багатьох» </code>

Зауважте, що взаємозв'язок заснований на стовпці "Зовнішній ключ" (наприклад, post_id) у дочірній таблиці.

Отже, є єдине джерело істини, коли мова йде про управління one-to-manyвідносинами таблиці.

Тепер, якщо ви берете двонаправлене відношення сутності, яке відображає на one-to-manyтаблиці відносини, які ми бачили раніше:

Двонаправлена ​​асоціація <code> One-To-Many </code>

Якщо ви подивитеся на діаграму вище, ви побачите, що є два способи управління цим взаємозв'язком.

У Postсутності ви маєте commentsколекцію:

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

І, в PostComment, то postоб'єднання відображається наступним чином :

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

Отже, у вас є дві сторони, які можуть змінити асоціацію об'єктів:

  • Додавши запис у commentsдочірню колекцію, новий post_commentрядок повинен бути пов’язаний із материнським postоб'єктом через його post_idстовпець.
  • Встановивши postвластивість PostCommentсутності, post_idстовпець також повинен бути оновлений.

Оскільки є два способи подання стовпця "Зовнішній ключ", ви повинні визначити, що є джерелом істини, коли мова заходить про перетворення зміни стану асоціації у його еквівалентну модифікацію значення стовпця "Зовнішній ключ"

MappedBy (він же зворотний бік)

mappedByАтрибут говорить про те , що @ManyToOneсторона відповідає за управління стовпцем зовнішнього ключа, а колекція використовується тільки для вилучення дочірніх об'єктів і каскадних батьківської сутність змін стану дітей (наприклад, видалення батька повинен також видалити дочірні об'єкти).

Його називають зворотною стороною, оскільки вона посилається на властивість дочірньої сутності, яка управляє цим співвідношенням таблиці.

Синхронізуйте обидві сторони двостороннього об'єднання

Тепер, навіть якщо ви визначили mappedByатрибут і @ManyToOneасоціація на стороні дитини керує стовпцем Іноземний ключ, вам все одно потрібно синхронізувати обидві сторони двонаправленої асоціації.

Найкращий спосіб зробити це - додати ці два корисні методи:

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

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

Методи addCommentта removeCommentметоди забезпечують синхронізацію обох сторін. Отже, якщо ми додамо дочірнє ціле, то дочірнє сутність повинно вказувати на батьків, і материнське підприємство повинно мати дитину, що міститься в дочірній колекції.

Щоб отримати докладнішу інформацію про найкращий спосіб синхронізувати всі двонаправлені типи асоціацій, перегляньте цю статтю .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.