在 JPA 和Hibernate中映射多对多关系的 6 种方法

有几种方法可以通过使用@ManyToMany、@OneToMany和@ManyToOne来映射 JPA 和Hibernate中的多对多关系,包括

  • 使用单个主键、@OneToMany和@ManyToOne连接实体单向和双向映射

  • 使用复合主键、@OneToMany和@ManyToOne连接实体单向和双向映射

  • 不带连接实体的单向和双向映射,带@ManyToMany

本指南将向您展示如何绘制地图以及每种方法的优缺点

考虑出版商和书籍之间的关系。一个出版商可以出版许多书籍,一本书可以由多个出版商出版

1) 使用单个主键和@ManyToOne连接实体单向映射

连接的实体将使用单个@Id和 2 个@ManyToOne进行定义。父实体上没有子集合关联映射

@Entity
public class BookPublisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "publisher_id")
    private Publisher publisher;

    @Column(name = "published_date")
    private Date publishedDate;

    ...
}

@Entity
public class Book {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    ...
}

@Entity
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    ...
}

“获取类型”用于@ManyToOne,而不是默认的“获取类型”。.

@JoinColumn指定外键列。它是可选的,默认名称为关联字段名称及其主键列名称的下划线字符串连接

优点和缺点

  • 单主键映射比复合主键更简单

  • 避免@OneToMany的潜在性能问题

  • 父实体无法快速导航或级联 CRUD 操作到子集合。但是,您可以通过 JPQL 查询手动执行此操作

动手教程

2) 使用单个主键、@ManyToOne和@OneToMany连接实体双向映射

除了像上面这样定义连接实体的单向映射之外,我们还将定义子集合与父实体上@OneToMany的关联。

@Entity
public class BookPublisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "publisher_id")
    private Publisher publisher;

    @Column(name = "published_date")
    private Date publishedDate;

    ...
}

@Entity
public class Book {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
    private Set<BookPublisher> bookPublishers = new HashSet<>();

    ...
}

@Entity
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @OneToMany(mappedBy = "publisher", cascade = CascadeType.ALL)
    private Set<BookPublisher> bookPublishers = new HashSet<>();

    ...
}

@JoinColumn指定外键列。对于此映射,默认名称为关联字段名称及其主键列名称的下划线字符串连接是可选的

相反,需要为双向映射指定@OneToMany上的 mapBy 属性。如果映射 By 不存在,JPA 和休眠将自动创建冗余联接表,如下所示book_book_publishers

+--------------------+---------+------+-----+---------+-------+
| Field              | Type    | Null | Key | Default | Extra |
+--------------------+---------+------+-----+---------+-------+
| book_id            | int(11) | NO   | PRI | NULL    |       |
| book_publishers_id | int(11) | NO   | PRI | NULL    |       |
+--------------------+---------+------+-----+---------+-------+

优点和缺点

  • 单主键映射比复合主键更简单

  • 关系的双方可以快速访问和级联 CRUD 操作

  • @OneToMany可能会导致大型子集合出现性能问题

动手教程

3) 使用复合主键、@ManyToOne和@OneToMany连接实体单向和双向映射

复合主键将由自定义类定义,该类使用@Embeddable进行批注,并实现可序列化。还应定义其等式和哈希码方法

@Embeddable
public class BookPublisherId implements Serializable {  
    @Column(name = "book_id")
    private Integer bookId;

    @Column(name = "publisher_id")
    private Integer publisherId;

    ...
}

连接实体将使用 1 个用 @EmbeddedId 标记@Embeddable对象字段和 2 个用@MapsId标记的@ManyToOne来定义,这些字段指向@Embeddable类中定义的关键字段

@Entity
public class BookPublisher {  
    @EmbeddedId
    private BookPublisherId id;

    @ManyToOne
    @MapsId("bookId")
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne
    @MapsId("publisherId")
    @JoinColumn(name = "publisher_id")
    private Publisher publisher;

    @Column(name = "published_date")
    private Date publishedDate;

    public BookPublisher(Book book, Publisher publisher, Date publishedDate) {
        this.id = new BookPublisherId(book.getId(), publisher.getId());
        this.book = book;
        this.publisher = publisher;
        this.publishedDate = publishedDate;
    }

    ...
}

@Entity
public class Book {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    ...
}

@Entity
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    ...
}

要在此映射中保留联接的实体,需要手动填充@EmbededId值,因为 Hibernate 无法通过反射设置该值,否则,您将在控制台中收到以下错误

Caused by: org.hibernate.PropertyAccessException: Could not set field value by reflection

在 BookPubler 的上述实体映射中,我们通过其构造函数进行了配置。

@Entity
public class BookPublisher {  
    ...

    public BookPublisher(Book book, Publisher publisher, Date publishedDate) {
        this.id = new BookPublisherId(book.getId(), publisher.getId());
        ...
    }

    ...
}
这对于单向映射来说已经足够了。在双向映射的情况下,如果需要快速导航并将 CRUD 操作级联到子集合关联,请将@OneToMany放在父实体上,如上述方法 2

优点和缺点

  • 比单个主键映射更复杂

  • 具有@OneToMany的双向映射可能会导致大型子集合关联的性能问题

动手教程

4) 无连接实体单向和双向映射与@ManyToMany

此映射不会使用任何联接的实体。@ManyToMany将放置在一侧进行单向映射,或放置在两侧进行双向映射

@Entity
public class Book {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "book_publisher",
        joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
        inverseJoinColumns = @JoinColumn(name = "publisher_id", referencedColumnName = "id"))
    private Set<Publisher> publishers = new HashSet<>();

    ...
}

@Entity
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToMany(mappedBy = "publishers")
    private Set<Book> books = new HashSet<>();

    ...
}

优点和缺点

  • 通过双向映射,关系的双方可以快速访问和级联 CRUD 操作

  • 最简单的多对多映射。但是,它仅适用于没有额外列的联接表,并且与@OneToMany一样,@ManyToMany也可能导致大型子集合的性能问题

动手教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值