一对多、多对一和多对多的区别?

218

好的,这可能是一个简单的问题,但我很难想象和理解它们之间的差异以及何时使用每个选项。我还有一点不清楚的是,像单向和双向映射这样的概念如何影响一对多/多对多关系。我现在正在使用Hibernate,所以任何ORM相关的解释都会有帮助。

以以下示例为例:

public class Person {
    private Long personId;
    private Set<Skill> skills;
    //Getters and setters
}

public class Skill {
    private Long skillId;
    private String skillName;
    //Getters and setters
}

那么在这种情况下,我需要使用什么样的映射?非常感谢对这个具体例子的回答,但我也希望能够了解何时使用一对多和多对多,以及何时使用连接表或连接列,单向或双向。

8个回答

397

看起来每个人都在回答一对多多对多

一对多多对一多对多的区别是:

一对多 vs 多对一 是一种视角的问题单向 vs 双向 不会影响映射,但会影响如何访问数据。

  • 多对一中,many一侧将保留one一侧的引用。一个很好的例子是 “一个州拥有城市”。在这种情况下,State是一方,City是多方。在表cities中将有一个state_id列。

单向中,Person类将具有List<Skill> skills,但Skill不会有Person person。在双向中,两个属性都添加了,它允许你通过技能访问一个Person(即skill.person)。

  • 一对多中,我们的参考点将是一侧。例如,“一个用户有多个地址”。在这种情况下,我们可能会有三列address_1_idaddress_2_idaddress_3_id或带有user_id查找表以及多列唯一约束条件。在单向关系中,一个用户将拥有一个地址(Address address)。在双向关系中,Address类还将有一个额外的List<User> users成员。Many-to-Many模型中,每个参与方的成员可以引用另一方的任意数量成员。为了实现这一点,使用了一个查找表(look up table)。医生和患者之间的关系就是一个例子,一个医生可以有多个患者,反之亦然。在address_id上添加外键约束。

40
这应该是被接受的答案,大多数其他答案都没有回答问题。 - arg20
2
还有一个单向一对多的例子,当Person拥有List<Skill> skills时,实际上是多对多的,因为一个人可以有多个技能,而一个技能可以在许多List<Skill> skills列表中。我认为你想写的是“在单向关系中,Skill类将拥有Person person”。 - mixel
1
@mixel,我现在明白你的意思了,但我不认为Person中存在List<Skill> skills就自动使关系变成@Many-to-many,因为在生成的模式中,可能会对person_id设置unique约束,使得一个技能只属于一个人。明白我的意思吗? - Alexander Suraphel
1
为什么kleppmann(DDIA书籍作者)提到文档数据库容易支持一对多,但不支持多对一?如果这只是一个观点问题,那么对于其中一个的支持就意味着对于另一个的支持。 - piepi
1
@piepi 很好的发现。我认为这是因为文档数据库自然地从“一个”方面考虑问题。例如,描述一个人的文档可能会说他/她有很多帽子。但是,如果你转换到文档描述帽子的参考点上,很难表达一些帽子属于同一个人。然而,这个问题在关系型数据库管理系统中并不适用,因为经常使用交叉引用表。 - wlnirvana
显示剩余10条评论

220

一对多: 一个人拥有多种技能,技能不在人之间共用

  • 单向关联:一个人可以通过其集合直接引用技能
  • 双向关联:每个“子”技能都有一个指向上级人员的单一指针(代码中未显示)

多对多:一个人拥有多项技能,技能在多个人之间共用

  • 单向关联:一个人可以通过其集合直接引用技能
  • 双向关联:一个技能具有一组与之相关的人员集合。

在一对多关系中,一个对象是“父对象”,而另一个对象是“子对象”。父对象控制子对象的存在。在多对多关系中,两者的存在都取决于它们之外的某些因素(在更大的应用程序上下文中)。

您的主题(领域)应该决定关系是一对多还是多对多——然而,我发现使关系单向或双向是一项工程决策,需要平衡内存、处理、性能等方面。

令人困惑的是,多对多双向关系不需要对称!也就是说,一堆人可以指向一个技能,但这个技能不一定只与这些人有关。通常会这样做,但这种对称性并不是必要的。以爱情为例——它是双向的(“我爱”、“爱我”),但通常是非对称的(“我爱她,但她不爱我”)!

所有这些都得到了Hibernate和JPA的很好支持。只要记住,Hibernate或任何其他ORM在管理双向多对多关系时不关心保持对称性......这完全取决于应用程序。


澄清一下,在您的BL或O/R映射中,任何关系都可以是单向或双向的(即使彼此独立!)。 - jyoungdev
5
“LOVE”这个例子刚好说明了问题。 ManyToMany是我的映射类型。 - Abdullah Khan
1
太好了。这样解释得非常清楚(并且在 OP 的例子中)。 - Anupam
1
回答不够恰当,您漏掉了"多对一"的部分。虽然"一对多"和"多对一"是一种感知问题,但这个回答没有提到那一点。 - Manzur Alahi

48

1)这些圆圈代表实体/POJO/Bean

2)deg是图形中度数(边数)的缩写

PK表示主键,FK表示外键

注意度数和方向名称之间的矛盾。Many对应于度数为1,而One对应于度数>1。

一对多 多对一的图示


1
真的很喜欢它如何将对象图与表双向绑定。 - Dmitry Minkovsky
13
看啊,这就是程序员的手写体 :D - Mehraj Malik

43

一对多

一对多的表关系如下所示:

One-to-many

在关系型数据库系统中,一对多的表关系基于子表中的Foreign Key列与父表中一个记录的Primary Key相关联来关联两个表。
在上面的表图中,post_comment表中的post_id列与post表的id列的Primary Key有Foreign Key关系。
    ALTER TABLE
        post_comment
    ADD CONSTRAINT
        fk_post_comment_post_id
    FOREIGN KEY (post_id) REFERENCES post

@ManyToOne注解

在JPA中,映射一对多表关系的最佳方法是使用@ManyToOne注解。

在我们的案例中,PostComment子实体使用@ManyToOne注解映射了post_id外键列:

    @Entity(name = "PostComment")
    @Table(name = "post_comment")
    public class PostComment {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String review;
    
        @ManyToOne(fetch = FetchType.LAZY)
        private Post post;
        
    }

使用JPA的@OneToMany注解

仅仅因为你有使用@OneToMany注解的选项,并不意味着它应该成为所有一对多数据库关系的默认选项。

JPA集合的问题在于,只有当元素数量相对较少时才能使用它们。

映射@OneToMany关联的最佳方式是依靠@ManyToOne方面来传播所有实体状态更改:

    @Entity(name = "Post")
    @Table(name = "post")
    public class Post {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String title;
    
        @OneToMany(
            mappedBy = "post", 
            cascade = CascadeType.ALL, 
            orphanRemoval = true
        )
        private List<PostComment> comments = new ArrayList<>();
    
        //Constructors, getters and setters removed for brevity
    
        public void addComment(PostComment comment) {
            comments.add(comment);
            comment.setPost(this);
        }
    
        public void removeComment(PostComment comment) {
            comments.remove(comment);
            comment.setPost(null);
        }
    }

父级Post实体具有两个实用方法(例如addCommentremoveComment),用于同步双向关联的两侧。

每当您使用双向关联时,应提供这些方法,否则,您会面临非常微妙的状态传播问题

应避免使用单向@OneToMany关联,因为它比使用@ManyToOne或双向@OneToMany关联效率低。

一对一

一对一表关系如下:

One-to-one

在关系型数据库系统中,一个一对一的表关系是基于子表中的一个主键列与父表行中相同的主键列关联的外键列来连接两个表的。因此,我们可以说子表与父表共享同一个主键。在上面的表图中,post_details表中的id列也与post表的id主键列有一个外键关系。
    ALTER TABLE
        post_details
    ADD CONSTRAINT
        fk_post_details_id
    FOREIGN KEY (id) REFERENCES post

使用JPA的@OneToOne@MapsId注解

映射@OneToOne关系的最佳方法是使用@MapsId。这样,您甚至不需要双向关联,因为您始终可以通过使用Post实体标识符来获取PostDetails实体。

映射如下:

@Entity(name = "PostDetails")
@Table(name = "post_details")
public class PostDetails {

    @Id
    private Long id;

    @Column(name = "created_on")
    private Date createdOn;

    @Column(name = "created_by")
    private String createdBy;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    @JoinColumn(name = "id")
    private Post post;

    public PostDetails() {}

    public PostDetails(String createdBy) {
        createdOn = new Date();
        this.createdBy = createdBy;
    }

    //Getters and setters omitted for brevity
}

这样,id属性就同时充当了主键和外键。您会注意到,@Id列不再使用@GeneratedValue注释,因为标识符是使用post关联的标识符填充的。

多对多

多对多表关系如下所示:

Many-to-many

在关系型数据库系统中,多对多表关系通过一个子表连接两个父表,该子表包含两个外键列,这些列引用了两个父表的主键列。
在上面的表格图中,post_tag表中的post_id列也与post表的id主键列具有外键关系。
    ALTER TABLE
        post_tag
    ADD CONSTRAINT
        fk_post_tag_post_id
    FOREIGN KEY (post_id) REFERENCES post

此外,post_tag表中的tag_id列与tag表的idPrimary Key列有一个Foreign Key关系:

    ALTER TABLE
        post_tag
    ADD CONSTRAINT
        fk_post_tag_tag_id
    FOREIGN KEY (tag_id) REFERENCES tag

使用JPA的@ManyToMany映射

以下是如何使用JPA和Hibernate映射多对多表关系的方法:

    @Entity(name = "Post")
    @Table(name = "post")
    public class Post {

        @Id
        @GeneratedValue
        private Long id;

        private String title;

        @ManyToMany(cascade = { 
            CascadeType.PERSIST, 
            CascadeType.MERGE
        })
        @JoinTable(name = "post_tag",
            joinColumns = @JoinColumn(name = "post_id"),
            inverseJoinColumns = @JoinColumn(name = "tag_id")
        )
        private Set<Tag> tags = new HashSet<>();

        //Getters and setters ommitted for brevity

        public void addTag(Tag tag) {
            tags.add(tag);
            tag.getPosts().add(this);
        }

        public void removeTag(Tag tag) {
            tags.remove(tag);
            tag.getPosts().remove(this);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Post)) return false;
            return id != null && id.equals(((Post) o).getId());
        }

        @Override
        public int hashCode() {
            return getClass().hashCode();
        }
    }

    @Entity(name = "Tag")
    @Table(name = "tag")
    public class Tag {

        @Id
        @GeneratedValue
        private Long id;

        @NaturalId
        private String name;

        @ManyToMany(mappedBy = "tags")
        private Set<Post> posts = new HashSet<>();

        //Getters and setters ommitted for brevity

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Tag tag = (Tag) o;
            return Objects.equals(name, tag.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name);
        }
    }
  1. Post实体中的tags关联仅定义了PERSISTMERGE级联类型。对于@ManyToMany JPA关联,REMOVE实体状态转换没有任何意义,因为它可能触发一次链式删除,最终会清除关联的双方。
  2. 如果使用双向关联,则添加/删除实用程序方法是必需的,以确保关联的双方保持同步。
  3. Post实体使用实体标识符作为相等性,因为缺少任何唯一业务键。只要确保在所有实体状态转换中保持一致,就可以使用实体标识符作为相等性。
  4. Tag实体具有带有Hibernate特定的@NaturalId注释的唯一业务键。在这种情况下,唯一业务键是相等性检查的最佳候选
  5. Tag实体中posts关联的mappedBy属性标记着在这个双向关系中,Post实体拥有该关联。这是必需的,因为只有一侧可以拥有关系,并且更改仅从这个特定侧面传播到数据库。
  6. 使用Set更佳,因为使用带有@ManyToManyList效率较低。

14
我会这样解释:
一对一 - 一对一 (一个人有一个鼻子 - 一个鼻子属于一个人)
@OneToOne
Person person;

@OneToOne
Nose nose;

OneToMany - ManyToOne (一个牧羊人有很多只羊 - 一只羊只有一个牧羊人)

@OneToMany
Shepherd shepherd;

@ManyToOne
List<Sheep> sheeps;

多对多 - 多对多 (许多旅行者有许多目的地 - 许多目的地有许多旅行者)

@ManyToMany
List<Traveler> travelers;

@ManyToMany
List<Destination> destinations;

1
你用三个简单的例子解释了。我不知道为什么人们不给这个点赞! - Ranjithkumar

9
请看这篇文章:映射对象关系

在映射时,您需要关注两类对象关系。第一类基于多重性,包括三种类型:

*One-to-one relationships.  This is a relationship where the maximums of each of its multiplicities is one, an example of which is holds relationship between Employee and Position in Figure 11.  An employee holds one and only one position and a position may be held by one employee (some positions go unfilled).
*One-to-many relationships. Also known as a many-to-one relationship, this occurs when the maximum of one multiplicity is one and the other is greater than one.  An example is the works in relationship between Employee and Division.  An employee works in one division and any given division has one or more employees working in it.
*Many-to-many relationships. This is a relationship where the maximum of both multiplicities is greater than one, an example of which is the assigned relationship between Employee and Task.  An employee is assigned one or more tasks and each task is assigned to zero or more employees. 

第二类基于方向性,包含两种类型:单向关系和双向关系。
*Uni-directional relationships.  A uni-directional relationship when an object knows about the object(s) it is related to but the other object(s) do not know of the original object.  An example of which is the holds relationship between Employee and Position in Figure 11, indicated by the line with an open arrowhead on it.  Employee objects know about the position that they hold, but Position objects do not know which employee holds it (there was no requirement to do so).  As you will soon see, uni-directional relationships are easier to implement than bi-directional relationships.
*Bi-directional relationships.  A bi-directional relationship exists when the objects on both end of the relationship know of each other, an example of which is the works in relationship between Employee and Division.  Employee objects know what division they work in and Division objects know what employees work in them. 

6
当一个重数的最大值为1而另一个重数大于1时,就会出现这种情况。 - serg

1

这可能需要一个多对多的关系,如下所示



public class Person{

    private Long personId;
    @manytomany

    private Set skills;
    //Getters and setters
}

public class Skill{
    private Long skillId;
    private String skillName;
    @manyToMany(MappedBy="skills,targetClass="Person")
    private Set persons; // (people would not be a good convenion)
    //Getters and setters
}

你可能需要定义一个joinTable + JoinColumn,但即使没有也有可能工作...


0
首先,仔细阅读所有的细则。请注意,NHibernate(因此,我认为Hibernate也是如此)关系映射与DB和对象图映射有一个有趣的对应关系。例如,一对一关系通常被实现为多对一关系。
其次,在我们告诉您应该如何编写您的O/R映射之前,我们必须看到您的DB。特别是,一个技能是否可以被多个人拥有?如果是这样,那么您就有了一个多对多的关系;否则,它是多对一的关系。
第三,我更喜欢不直接实现多对多关系,而是在您的领域模型中建模“连接表”-即将其视为实体,像这样:
class PersonSkill 
{
    Person person;
    Skill skill;    
}

那么你看到了什么?你有两个一对多的关系。(在这种情况下,Person可能拥有一个PersonSkills集合,但不会拥有一个Skills集合。)然而,有些人更喜欢使用多对多的关系(Person和Skill之间的关系);这是有争议的。

第四,如果您有双向关系(例如,Person不仅拥有Skills集合,而且Skill也拥有Persons集合),NHibernate不会为您强制执行BL中的双向性;它只理解关系的双向性以进行持久化。

第五,在NHibernate(我假设Hibernate也是如此)中,多对一比一对多(集合映射)更容易正确使用。

祝你好运!


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