ManyToManyView
经典ORM关联的不足
在基础映射/关联映射中,我们学习了ORM中经典的关联映射,包括一对一、多对一、一对多和多对多。
然而,有一种场景,映射模式的选择让人很纠结。为了展示这种场景,从熟悉的场景开始。
无争议的多对多关联
让我们来看一段DDL
create table book(
...略...
)engine=innodb;;
create table author(
...略...
) engine=innodb;
create table book_author_mapping(
book_id bigint unsigned not null,
author_id bigint unsigned not null
) engine=innodb;
alter table book_author_mapping
add constraint pk_book_author_mapping
primary key(book_id, author_id)
;
alter table book_author_mapping
add constraint fk_book_author_mapping__book
foreign key(book_id)
references book(id)
on delete cascade
;
alter table book_author_mapping
add constraint fk_book_author_mapping__author
foreign key(author_id)
references author(id)
on delete cascade
;
这段DDL中,book_author_mapping
表很特殊,只有两个外键,一个指向book
表,一个指向author
表。这种只有两个外键的子表用于表达两个父表之间的多对多关联。
ORM的多对多映射会隐藏中间表,即,中间表没有对应的Java/Kotlin实体类型。因此,中间表并不需要独立主键,而是直接使用两个外键作为联合主键。
除了两个关联外键外,中间表不得具备其他任何字段,这是ORM中多对多关联的限制
ORM中对应的多对多关联如下:
-
主动方:
Book.authors
- Java
- Kotlin
Book.java@Entity
public interface Book {
@ManyToMany
List<Author> authors();
...省略其他代码...
}Book.kt@Entity
interface Book {
@ManyToMany
val authors: List<Author>
...省略其他代码...
} -
从动方 (可选):
Author.books
- Java
- Kotlin
Author.java@Entity
public interface Author {
@ManyToMany(mappedBy = "authors")
List<Book> books();
...省略其他代码...
}Author.kt@Entity
interface Author {
@ManyToMany(mappedBy = "authors")
val books: List<Book>
...省略其他代码...
}
无争议的双重多对一关联
让我们再来看第二个场景,首先,还是看一段DDL
create table order_(
...略...
) engine=innodb;
create table product(
...略...
) engine=innodb;
create table order_item(
id bigint unsigned not null auto_increment primary key,
order_id bigint unsigned not null,
product_id bigint unsigned not null,
quantity int not null,
unit_price numeric(10, 2) not null
) engine=innodb;
alter table order_item
add constraint business_key_order_item
unique(order_id, product_id)
;
alter table order_item
add constraint fk_order_item__order
foreign key(order_id)
references order_(id)
;
alter table order_item
add constraint fk_order_item__product
foreign key(product_id)
references product(id)
;
这是一个经典的订单-订单明细-产品
关联。
其中,order-item
看起来有点像多对多的中间表,因为它存在两个外键:指向order_
表的order_id
和指向product
表的product_id
。
然而,order-item
并不是中间表,因为它具备其他业务字段,表示商品数量的quantity
,以及表示下单这一刻商品的价格快照的unit_price
。
幸运的是,对于经典的订单-订单明细-产品
关联,更适合人们直觉的思维方式是认为order_item
是一个独立的实体,持有分别指向order_
和product
的两个多对一关联;
而不是将order-item
视为中间表,认为order_
和product
之间存在多对多关系。
正是因为把order_item
作为一个独立的实体来看待,所以order_item
采用了独立主键。
我们可以在ORM中两个多对一关联来映射这三张表
-
OrderItem.order
和OrderItem.product
- Java
- Kotlin
OrderItem.java@Entity
public interface OrderItem {
@ManyToOne
Order order();
@ManyToOne
Product product();
int quantity();
BigDecimal unitPrice();
...省略其他代码...
}OrderItem.kt@Entity
interface OrderItem {
@ManyToOne
val order: Order
@ManyToOne
val product: Product
val quantity: Int
val unitPrice: BigDecimal
...省略其他代码...
} -
Order.items
这类系统中,人们常常需要根据订单获取其明细列表,所以我们定义一个一对多属性
Order.items
,作为OrderItem.order
的镜像。- Java
- Kotlin
Order.java@Entity
@Table(name = "ORDER_")
public interface Order {
@OneToMany(mappedBy = "order")
List<OrderItem> items();
...省略其他代码...
}Order.kt@Entity
@Table(name = "ORDER_")
interface Order {
@OneToMany(mappedBy = "order")
val items: List<OrderItem>
...省略其他代码...
} -
不提供
Product.items
这类系统中,人们基本不会需要根据产品获取其明细列表 (如果从产品端开始分析,一般都是复杂的查询,而非简单的关联),所以并不提供
Product.items
,一个单向的OrderItem.product
关联足够。因此,不必展示
Product
实体的代码。