6个回答

327
为了理解这个问题,你需要退后一步。在面向对象编程中,顾客拥有订单(订单是顾客对象中的列表)。没有顾客就没有订单。因此,顾客似乎是订单的所有者。
但在SQL世界中,一个条目实际上会包含指向另一个条目的指针。由于每个顾客对应N个订单,因此每个订单都包含指向其所属顾客的外键。这就是"连接",这意味着订单“拥有”(或字面上包含)连接(信息)。这与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)的两列表。
这种方法比上面的方法更灵活(可以轻松处理一对一、多对一、一对多甚至多对多)。代价是:
  • 它稍微慢一点(需要维护另一个表和连接会使用三个表而不仅仅是两个),
  • 连接语法更复杂(如果您必须手动编写许多查询,例如在尝试调试时,可能会很繁琐),
  • 由于管理连接表的代码出现问题,您可能会突然获得太多或太少的结果,因此更容易出错。
这就是为什么我很少推荐这种方法的原因。

39
需要翻译的内容:Just to clarify: the many side is the owner; the one side is the inverse. You don't have a choice (practically speaking).为了澄清:多数方是所有者;单数方是反向。(从实际角度来看)你没有选择。 - John
13
不,这是Hibernate发明的。我不太喜欢它,因为它将实现的一部分暴露给了OO模型。我更喜欢使用@Parent@Child注解来表示连接的含义(而不是如何实现),而不是使用“XtoY”。 - Aaron Digulla
4
每次我要查看一对多映射的内容时,我都会阅读这个答案,很可能是关于该主题在 SO 上最好的答案。 - Eugene
9
哇,如果ORM框架的文档能有这样好的解释,那么整个使用过程会更加轻松易懂!非常出色的回答! - NickJ
2
@klausch:Hibernate 的文档很令人困惑。忽略它,看看代码、数据库中的 SQL 以及外键是如何工作的。如果你愿意,可以带走一条智慧:文档就是谎言。使用源码吧,卢克。 - Aaron Digulla
显示剩余6条评论

46

令人难以置信的是,在3年内,没有人用映射关系的两种方式举例回答您的问题。

正如其他人所提到的,“所有者”方在数据库中包含指针(外键)。您可以将任何一方指定为所有者,但是,如果您将One side指定为所有者,则关系将不是双向的(反向也就是“many” side将不知道它的“owner”)。这对封装/松耦合可能是有利的:

// "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希望存在一个额外的customer_orders表。使用JPA2,您可以在Customer的orders字段上使用@JoinColumn注释(我似乎经常使用)来表示应该使用Order表中的数据库外键列。这样,在Java中您就拥有了单向关系,同时在Order表中仍然有一个外键列。因此,在对象世界中,Order不知道Customer,而在数据库世界中,Customer不知道Order。 - Henno Vermeulen
1
为了完整无缺,您可以展示双向关系,其中客户是关系的拥有方。 - Dave

37

在数据库中,拥有带有外键表的实体被称为拥有实体,而被指向的其他表则被称为反向实体。


32
更简单地说,Owner 是具有外键列的表。 - jacktrades
2
简单明了的解释。任何一方都可以成为所有者。如果我们在Order.java中使用mappedBy,在Customer字段<从Customer.java中删除mappedBy>,那么将创建一个新表,类似于Order_Customer,其中将有2个列。ORDER_ID和CUSTOMER_ID。 - HakunaMatata

15

双向关系的简单规则:

1.对于多对一的双向关系,拥有方总是关系的拥有方。例如:一个房间有很多人(一个人只属于一个房间)-> 拥有方是人。

2.对于一对一的双向关系,拥有方对应包含相应外键的一侧。

3.对于多对多的双向关系,任何一侧都可以是拥有方。

希望能够帮到您。


我们为什么需要拥有所有者和反向关系呢?我们已经有了单侧和多侧的有意义的概念,在多对多的情况下,谁是所有者并不重要。这个决定的后果是什么?很难相信像数据库工程师这样左脑思维的人会创造这些多余的概念。 - Dan Cancro

4
对于两个实体类Customer和Order,Hibernate将创建两个表。
可能的情况:
1. 在Customer.java和Order.java类中未使用mappedBy,则在客户端创建一个新表[名称= CUSTOMER_ORDER],该表将保留CUSTOMER_ID和ORDER_ID的映射。这些是Customer和Order表的主键。在Order侧需要额外的列来保存相应的Customer_ID记录映射。
2. 在Customer.java中使用了mappedBy [如问题陈述中所示],现在不会创建额外的表[CUSTOMER_ORDER],只有一个列在Order表中。
3. 在Order.java中使用mappedby,现在Hibernate将创建额外的表[名称= CUSTOMER_ORDER]。Order表不会有额外的列[Customer_ID]用于映射。
任何一方都可以成为关系的所有者。但最好选择xxxToOne方。
编码效果 - 只有实体的拥有方才能改变关系状态。在下面的例子中,BoyFriend类是关系的所有者。即使Girlfriend想分手,她也不能。
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();
    }
}

2

表关系与实体关系

在关系型数据库系统中,只有三种类型的表关系:

  • 一对多(通过外键列)
  • 一对一(通过共享主键)
  • 多对多(通过连接表,具有两个外键分别引用两个父表)

因此,一对多 表关系如下所示:

一对多表关系

请注意,该关系基于子表中的外键列(例如,post_id)。

因此,在管理 一对多 表关系时,存在单一的真相来源。

现在,如果您采用映射到我们之前看到的 一对多 表关系的双向实体关系:

双向一对多实体关联

如果您查看上面的图表,您会发现有两种管理这种关系的方法。

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_id列将新的post_comment行与父post实体关联起来。
  • 通过设置PostComment实体的post属性,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);
}

addCommentremoveComment 方法确保双方同步。因此,如果我们添加一个子实体,则该子实体需要指向父实体,而父实体应该在子集合中包含该子实体。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接