Что такое «обратная сторона ассоциации» в двунаправленной ассоциации 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 клиент, каждый заказ содержит внешний ключ к клиенту, которому он принадлежит. Это «соединение», и это означает, что заказ «владеет» (или буквально содержит) соединение (информация). Это как раз противоположность мира ОО / модели.

Это может помочь понять:

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 это придумал. Мне это не нравится, так как он подвергает часть реализации ОО модели. Я предпочел бы, чтобы аннотация @Parentили @Child«XtoY» указывала, что означает соединение (а не как оно реализовано )
Аарон Дигулла,

4
@AaronDigulla каждый раз, когда мне приходится проходить сопоставления OneToMany, я приходил, чтобы прочитать этот ответ, вероятно, лучший по теме на SO.
Евгений

7
Вот это да. Если бы только документация по ORM-фреймворкам имела такое хорошее объяснение - все это было бы легче проглотить! Отличный ответ!
NickJ

2
@klausch: документация Hibernate сбивает с толку. Игнорируй это. Посмотрите на код, 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
}

Единственное решение двунаправленного отображения состоит в том, чтобы сторона «многие» имела свой указатель на «один» и использовала атрибут «mappedBy» @OneToMany. Без атрибута «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 ожидает существования дополнительной таблицы customer_orders. В JPA2 вы можете использовать аннотацию @JoinColumn (которую я, похоже, часто использую) в поле заказов Клиента, чтобы обозначить столбец внешнего ключа базы данных в таблице «Заказ», который следует использовать. Таким образом, у вас есть однонаправленное отношение в Java, при этом все еще имеется столбец внешнего ключа в таблице Order. Таким образом, в мире объектов Заказ не знает о Заказчике, а в мире баз данных Заказчик не знает о Заказе.
Хенно Вермёлен

1
Чтобы быть более полным, вы можете показать двунаправленный случай, когда клиент является владельцем отношений.
HDave

35

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


30
еще проще: владелец таблицы с колонкой FK
jacktrades

2
Простое и хорошее объяснение. Любая сторона может быть сделана владельцем. Если мы используем mappedBy в Order.java, в поле Customer <Remove mappedby from Customer.java>, то будет создана новая таблица, похожая на Order_Customer, которая будет иметь 2 столбца. ORDER_ID и CUSTOMER_ID.
HakunaMatata

14

Простые правила двунаправленных отношений:

1. Для двунаправленных отношений «многие-к-одному» сторона многих всегда является их владельцем. Пример: 1 комната имеет много человек (человек принадлежит только одной комнате) -> сторона, владеющая человеком

2. Для двунаправленных отношений «один к одному» сторона-владелец соответствует стороне, которая содержит соответствующий внешний ключ.

3. Для двунаправленных отношений «многие ко многим» любая сторона может быть принимающей стороной.

Надежда может помочь вам.


Зачем нам вообще нужен хозяин и обратный? У нас уже есть значимые понятия «односторонняя и многогранная», и не имеет значения, кто является владельцем в ситуациях «многие ко многим». Каковы последствия решения? Трудно поверить, что кто-то с таким же легкомыслием, как инженер по базам данных, решил придумать эти избыточные концепции.
Дэн Канкро

3

Для двух классов сущностей Customer и Order hibernate создаст две таблицы.

Возможные случаи:

  1. mappedBy не используется в классах Customer.java и Order.java then->

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

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

  3. mappedby используется в Order.java. Теперь hibernate создаст дополнительную таблицу. [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> один-ко-многим </ 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.