Apa itu "sisi terbalik dari asosiasi" dalam asosiasi dua arah JPA OneToMany / ManyToOne dua arah?

167

Di bagian contoh @OneToManyreferensi anotasi JPA :

Contoh 1-59 @OneToMany - Kelas Pelanggan dengan Generik

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

Contoh 1-60 @ManyToOne - Kelas Pemesanan Dengan Generik

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

Menurut saya Customerentitas itu adalah pemilik asosiasi. Namun, dalam penjelasan untuk mappedByatribut dalam dokumen yang sama, ada tertulis bahwa:

jika hubungan dua arah, maka setel elemen yang dipetakan oleh sisi terbalik (tidak memiliki) asosiasi ke nama bidang atau properti yang memiliki hubungan seperti Contoh 1-60 tunjukkan.

Namun, jika saya tidak salah, sepertinya dalam contoh, mappedBysebenarnya ditentukan pada sisi yang memiliki asosiasi, bukan sisi yang tidak memiliki.

Jadi pertanyaan saya pada dasarnya:

  1. Dalam asosiasi bidirectional (satu-ke-banyak / banyak-ke-satu), entitas mana yang merupakan pemilik? Bagaimana kita dapat menetapkan Satu sisi sebagai pemilik? Bagaimana kita dapat menunjuk sisi Banyak sebagai pemilik?

  2. Apa yang dimaksud dengan "sisi terbalik dari asosiasi"? Bagaimana kita dapat menetapkan Satu sisi sebagai kebalikannya? Bagaimana kita dapat menunjuk sisi Banyak sebagai kebalikannya?

Behrang Saeedzadeh
sumber
1
tautan yang Anda berikan sudah usang. Mohon perbarui.
MartinL

Jawaban:

306

Untuk memahami ini, Anda harus mundur selangkah. Di OO, pelanggan memiliki pesanan (pesanan adalah daftar di objek pelanggan). Tidak mungkin ada pesanan tanpa pelanggan. Jadi pelanggan tampaknya menjadi pemilik pesanan.

Tetapi di dunia SQL, satu item akan benar-benar berisi pointer ke item lainnya. Karena ada 1 pelanggan untuk pesanan N, setiap pesanan berisi kunci asing ke pelanggan miliknya. Ini adalah "koneksi" dan ini berarti urutan "memiliki" (atau secara harfiah berisi) koneksi (informasi). Ini persis kebalikan dari dunia OO / model.

Ini dapat membantu untuk memahami:

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

Sisi terbalik adalah OO "pemilik" objek, dalam hal ini pelanggan. Pelanggan tidak memiliki kolom di tabel untuk menyimpan pesanan, jadi Anda harus memberi tahu di mana di tabel pesanan dapat menyimpan data ini (yang terjadi melalui mappedBy).

Contoh umum lainnya adalah pohon dengan simpul yang dapat berupa orang tua dan anak-anak. Dalam hal ini, kedua bidang digunakan dalam satu kelas:

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

Ini menjelaskan untuk karya desain banyak "kunci asing". Ada pendekatan kedua yang menggunakan tabel lain untuk mempertahankan hubungan. Itu berarti, untuk contoh pertama kami, Anda memiliki tiga tabel: Satu dengan pelanggan, satu dengan pesanan dan tabel dua kolom dengan pasangan kunci utama (customerPK, orderPK).

Pendekatan ini lebih fleksibel daripada yang di atas (dapat dengan mudah menangani satu-ke-satu, banyak-ke-satu, satu-ke-banyak dan bahkan banyak-ke-banyak). Harganya adalah itu

  • itu sedikit lebih lambat (harus mempertahankan tabel lain dan bergabung menggunakan tiga tabel, bukan hanya dua),
  • sintaks join lebih kompleks (yang bisa membosankan jika Anda harus menulis banyak pertanyaan secara manual, misalnya ketika Anda mencoba men-debug sesuatu)
  • itu lebih rawan kesalahan karena Anda tiba-tiba bisa mendapatkan terlalu banyak atau terlalu sedikit hasil ketika ada kesalahan dalam kode yang mengelola tabel koneksi.

Itu sebabnya saya jarang merekomendasikan pendekatan ini.

Aaron Digulla
sumber
36
Hanya untuk memperjelas: banyak pihak adalah pemilik; satu sisi adalah kebalikannya. Anda tidak punya pilihan (secara praktis).
John
11
Tidak, Hibernate menciptakan ini. Saya tidak suka karena mengekspos bagian dari implementasi ke model OO. Saya lebih suka satu @Parentatau @Childanotasi daripada "XtoY" untuk menyatakan apa artinya koneksi (daripada bagaimana penerapannya )
Aaron Digulla
4
@ AaronDigulla setiap kali saya harus melalui pemetaan OneToMany saya datang untuk membaca jawaban ini, mungkin yang terbaik pada subjek pada SO.
Eugene
7
Wow. Jika saja dokumentasi kerangka kerja ORM memiliki penjelasan yang begitu bagus - itu akan membuat semuanya lebih mudah untuk ditelan! Jawaban yang sangat bagus!
NickJ
2
@klausch: Dokumentasi Hibernate membingungkan. Abaikan itu. Lihatlah kode, SQL dalam database, dan cara kerja kunci asing. Jika mau, Anda dapat membawa pulang sepotong kebijaksanaan: Dokumentasi adalah dusta. Gunakan sumbernya, Luke.
Aaron Digulla
41

Luar biasa, dalam 3 tahun tidak ada yang menjawab pertanyaan luar biasa Anda dengan contoh kedua cara untuk memetakan hubungan.

Seperti yang disebutkan oleh orang lain, sisi "pemilik" berisi pointer (kunci asing) dalam database. Anda dapat menetapkan kedua sisi sebagai pemilik, namun, jika Anda menunjuk satu sisi sebagai pemilik, hubungan tidak akan bersifat dua arah (sisi terbalik "banyak" yang tidak akan memiliki pengetahuan tentang "pemilik" -nya). Ini dapat diinginkan untuk enkapsulasi / kopling longgar:

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

Satu-satunya solusi pemetaan dua arah adalah memiliki sisi "banyak" yang memiliki penunjuk ke "satu", dan menggunakan atribut @OneToMany "mappedBy". Tanpa atribut "mappedBy" Hibernate akan mengharapkan pemetaan ganda (database akan memiliki kolom bergabung dan tabel bergabung, yang berlebihan (biasanya tidak diinginkan)).

// "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
sumber
2
Dalam contoh searah Anda, JPA mengharapkan tabel customer_orders tambahan ada. Dengan JPA2 Anda dapat menggunakan anotasi @JoinColumn (yang sepertinya sering saya gunakan) pada bidang pesanan Pelanggan untuk menunjukkan basis data kolom kunci asing dalam tabel Pesanan yang harus digunakan. Dengan cara ini Anda memiliki hubungan searah di Jawa sementara masih memiliki kolom kunci asing di tabel pesanan. Jadi di dunia Object, Order tidak tahu tentang Pelanggan sementara di dunia database, Pelanggan tidak tahu tentang Order.
Henno Vermeulen
1
Agar lengkap, Anda dapat menunjukkan kasus dua arah di mana pelanggan adalah pihak yang memiliki hubungan.
HDave
35

Entitas yang memiliki tabel dengan kunci asing dalam database adalah entitas yang memiliki dan tabel lainnya, yang ditunjuk, adalah entitas terbalik.

Venu
sumber
30
lebih sederhana: Pemilik adalah meja dengan Kolom FK
jacktrades
2
Penjelasan sederhana dan bagus. Pihak mana pun bisa dijadikan Pemilik. Jika kita menggunakan mappedBy di Order.java, pada bidang Pelanggan <Hapus mappedby dari Customer.java> maka tabel baru akan dibuat seperti Order_Customer yang akan memiliki 2 kolom. ORDER_ID dan CUSTOMER_ID.
HakunaMatata
14

Aturan sederhana hubungan dua arah:

1.Untuk banyak hubungan dua arah, banyak pihak selalu merupakan pihak yang memiliki hubungan tersebut. Contoh: 1 Kamar memiliki banyak Orang (satu Orang hanya memiliki satu Kamar) -> pihak yang memiliki adalah Orang

2.Untuk hubungan dua arah satu-ke-satu, pihak yang memiliki sesuai dengan sisi yang berisi kunci asing yang sesuai.

3. Untuk hubungan dua arah banyak-ke-banyak, masing-masing pihak mungkin merupakan pihak yang memiliki.

Semoga bisa membantu Anda.

Ken Block
sumber
Mengapa kita perlu memiliki pemilik dan invers sama sekali? Kami sudah memiliki konsep yang bermakna dari satu sisi dan banyak sisi dan tidak masalah siapa pemiliknya dalam banyak situasi. Apa konsekuensi dari keputusan tersebut? Sulit untuk percaya bahwa seseorang yang berotak kiri sebagai insinyur basis data memutuskan untuk memanfaatkan konsep-konsep yang berlebihan ini.
Dan Cancro
3

Untuk dua Pelanggan dan Pesanan Kelas Entitas, hibernasi akan membuat dua tabel.

Kemungkinan Kasus:

  1. mappedBy tidak digunakan di kelas Customer.java dan Order.java->

    Di sisi pelanggan, tabel baru akan dibuat [nama = CUSTOMER_ORDER] yang akan menyimpan pemetaan CUSTOMER_ID dan ORDER_ID. Ini adalah kunci utama Tabel Pelanggan dan Pesanan. Di sisi Pesanan, kolom tambahan diperlukan untuk menyimpan pemetaan catatan Customer_ID yang sesuai.

  2. mappedBy digunakan di Customer.java [Seperti yang diberikan dalam pernyataan masalah] Sekarang tabel tambahan [CUSTOMER_ORDER] tidak dibuat. Hanya satu kolom di Tabel Pesanan

  3. mappedby digunakan di Order.java Sekarang tabel tambahan akan dibuat oleh hibernate. [name = CUSTOMER_ORDER] Tabel Pesanan tidak akan memiliki kolom [Customer_ID] tambahan untuk pemetaan.

Sisi mana pun bisa dijadikan Pemilik hubungan. Tapi lebih baik memilih sisi xxxToOne.

Efek pengkodean -> Hanya sisi yang dimiliki entitas yang dapat mengubah status hubungan. Dalam contoh di bawah ini, kelas BoyFriend adalah pemilik hubungan. bahkan jika Pacar ingin putus, dia tidak bisa.

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
sumber
1

Hubungan tabel vs hubungan entitas

Dalam sistem basis data relasional, hanya ada tiga jenis hubungan tabel:

  • satu-ke-banyak (melalui kolom Kunci Asing)
  • satu-ke-satu (melalui Kunci Utama bersama)
  • many-to-many (melalui tabel tautan dengan dua Kunci Asing merujuk dua tabel induk terpisah)

Jadi, one-to-manyhubungan tabel terlihat sebagai berikut:

hubungan tabel <code> satu-ke-banyak </code>

Perhatikan bahwa hubungan didasarkan pada kolom Kunci Asing (misalnya, post_id) di tabel anak.

Jadi, ada satu sumber kebenaran ketika datang untuk mengelola one-to-manyhubungan tabel.

Sekarang, jika Anda mengambil hubungan entitas dua arah yang memetakan pada one-to-manyhubungan tabel yang kita lihat sebelumnya:

Asosiasi entitas <code> Satu-Untuk-Banyak </code> dua arah

Jika Anda melihat diagram di atas, Anda dapat melihat bahwa ada dua cara untuk mengelola hubungan ini.

Di Postentitas, Anda memiliki commentskoleksi:

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

Dan, dalam PostComment, postasosiasi dipetakan sebagai berikut:

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

Jadi, Anda memiliki dua sisi yang dapat mengubah asosiasi entitas:

  • Dengan menambahkan entri dalam commentskoleksi anak, post_commentbaris baru harus dikaitkan dengan postentitas induk melalui post_idkolomnya.
  • Dengan mengatur postproperti PostCommententitas, post_idkolom juga harus diperbarui.

Karena ada dua cara untuk mewakili kolom Kunci Asing, Anda harus menentukan mana sumber kebenaran ketika harus menerjemahkan perubahan status asosiasi menjadi modifikasi nilai kolom Kunci Asing yang setara.

MappedBy (alias sisi terbalik)

The mappedByatribut mengatakan bahwa @ManyToOnesisi bertugas mengelola kolom Foreign Key, dan koleksi hanya digunakan untuk mengambil entitas anak dan untuk kaskade perubahan induk entitas negara untuk anak-anak (misalnya, menghapus orang tua juga harus menghapus entitas anak).

Ini disebut sisi terbalik karena mereferensikan properti entitas anak yang mengelola hubungan tabel ini.

Sinkronkan kedua sisi dari asosiasi dua arah

Sekarang, bahkan jika Anda mendefinisikan mappedByatribut dan @ManyToOneasosiasi sisi anak mengelola kolom Kunci Asing, Anda masih perlu menyinkronkan kedua sisi dari asosiasi dua arah.

Cara terbaik untuk melakukannya adalah dengan menambahkan dua metode utilitas ini:

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

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

Metode addCommentdan removeCommentmemastikan kedua belah pihak disinkronkan. Jadi, jika kita menambahkan entitas anak, entitas anak perlu menunjuk ke induk dan entitas induk harus memiliki anak yang terkandung dalam koleksi anak.

Untuk detail lebih lanjut tentang cara terbaik untuk menyinkronkan semua jenis asosiasi entitas dua arah, lihat artikel ini .

Vlad Mihalcea
sumber