value type的集合就是集合映射, 如果集合中的元素是Entity类型, 那就不是简单的集合映射, 而是类的关系了. 如果Image是一个Entity对象, 这个映射代表的就是不是一系列值. 而是Item和Image这两个类之间的关系.
实际上通过UML类图也可以知道, 集合映射的还是类内部的那些东西以及要组合的其他类型; 而关系映射, 就是要映射UML类图之间的关系线, 也是比较核心的映射.
逐渐接触到ORM的核心内容了, 昨天年会开完, 今天还没有怎么醒酒, 先更新一篇短的, 复杂的关系映射慢慢整理.
- 关系映射
- 单向多对一关系
- 把单向多对一关系转变成双向
- 级联关系
关系映射
在刚才的集合中, 实际上是一种父子或者说组合关系, 集合中的值类型都依赖所属类的生命周期.
在关系映射中, 就不一样了, 映射的是两个Entity之间的关系, 通过Entity定义我们可以知道, Entity之间不会有互相依赖的生命周期, 都可以独立操作. 一个Entity对象可以独立的增删改查.
因此对于Entity,就没有父子关系, 而是要用新的术语来定义Entity之间的关系. 整体上来说一共有三种关系:
- 多对一, 站在Bid类的角度, 描述Bid类与Item类的关系, 就是多对一, 即存在多个(0到任意)Bid对象对应一个Item. 从Java角度来说, 每个Bid类中只有一个Item属性.
- 一对多, 站在Item类的角度描述Bid类与Item的关系, 一个Item对象, 会包含多个(0到任意)的Bid对象, 这叫做一对多关系. 从Java角度来说, 一个Item中有一个Bid集合.
- 多对多, 比如学生选课, Student类可以选择多个Lecture对象, Lecture对象也对应多个Student对象. 从Java角度来说, Student类中有一个Lecture集合, Lecture类中有一个Student集合.
单向多对一关系
单向多对一关系是最简单的一种关系, Bid#Item的关系中, 如果仅仅只有Bid类有一个外键关联到Item, 只映射这个外键关系, 就是最基础的单向多对一关系.
看一下如何映射这个外键关系:
@Entity
public class Bid {
@Id
@GeneratedValue
protected Long id;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
protected Item item;
public Item getItem() {
return item;
}
public void setItem(Item item) {
this.item = item;
}
......
}
@ManyToOne
注解一个属性, 让这个属性会成为一个关系映射, 而且这个属性也变成必须提供的属性. @ManyToOne
还能控制细节, optional=false表示not null约束. 默认的策略是FetchType.EAGER, 可以更改成LAZY.
标记为@ManyToOne
的属性在数据库中会被转换成一个外键列, 在JPA标准中, 外键列被称为join column. 对于外键列, 必须要使用@JoinColumn
注解来控制名称和其他属性. 外键列默认的名称是外键关联到的表名_id, 这里的ITEM_ID其实就是默认名称.
在@JoinColumn
中也可以使用nullable来控制是不是可以允许为null. 这不允许为null实际上带有一种生命周期的依赖, 即不允许Bid脱离Item存在.
对于外键列, 唯一需要的是@ManyToOne
注解, 其他注解可以不添加.
写个小测试来试试:
@Test
public void test() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("HelloWorldPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
Item item = new Item();
item.getImages().add("home");
item.getImages().add("kindergarden");
item.getImages().add("games");
Bid bid = new Bid();
bid.setAmount(new BigDecimal("3.93"));
bid.setItem(item);
tx.begin();
em.persist(item);
em.persist(bid);
tx.commit();
}
代码好懂,这里红色的部分要注意, 一定要先持久化item, 才能持久化bid, 否则会看到如下错误:
Attempting to save one or more entities that have a non-nullable association with an unsaved transient entity.
The unsaved transient entity must be saved in an operation prior to saving these dependent entities.
意思就是如果有不能为null的关联关系, 关联到一个尚未持久化的Entity, 一定要把尚未持久化的Entity先于外键所在类给持久化了才行.
创建表的语句如下:
Hibernate:
create table Bid (
id int8 not null,
amount numeric(19, 2),
ITEM_ID int8 not null,
primary key (id)
)
alter table if exists Bid
add constraint FK884gyguvo3jcbca0v78w95l3k
foreign key (ITEM_ID)
references Item
可见添加了一个外键.还要来看看查询怎么操作:
List<Bid> bids = em.createQuery("SELECT b FROM Bid b WHERE b.item = item", Bid.class).getResultList();
可以看到, 不是使用SQL语句中的用id来查, 而是先要获得一个Item对象, 然后查出哪个bid对象等于这个item, 这样就把数据库里实际的外键是否相等, 转换成了比较Java对象是否相等.
把单向多对一关系转变成双向
变成双向, 在Java角度就意味这我们是不是会经常使用item.getBids()
这种方法来获取一个Item对象对应的全部Bid.
所以很显然, 需要在Item中设置一个Bid集合属性, 当然肯定少不了标记:
@OneToMany(mappedBy = "item", fetch = FetchType.LAZY)
private Set<Bid> bids = new HashSet<>();
其实可以发现, 从Item角度来说, 就是一对多关系, 所以需要使用@OneToMany
这个注解. 这个注解对应的集合, 注意可不是值类型的集合了, 而是一个Entity的集合, 这个bids属性是不会保存在数据库中的, 只是一个关系映射.
@OneToMany
的mappedBy
属性很重要, 不能缺少, 表示"对方类中的属性", 对于Item来说, 就是Bid类中的表示Item类的属性名称, 也就是小写的item. 这个一定不能错.
对于集合类型, 默认的FetchType就是LAZY. 所以这里也可以不用设置. 一般都会使用LAZY, 因为不太想一下就将对应的全部Bids都载入内存.
同时, 既然有了双向关系, Item中也应该添加一个向bids中新增Bid对象的方法:
public void addBid(Bid bid) {
// Be defensive
if (bid == null)
throw new NullPointerException("Can't add null Bid");
if (bid.getItem() != null)
throw new IllegalStateException("Bid is already assigned to an Item");
getBids().add(bid);
bid.setItem(this);
}
这样在将Item与Bid关联的时候, 只要执行这个方法就可以了. 新的测试如下:
public void test() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("HelloWorldPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
//创建一个Item和两个Bid对象
Item item = new Item();
item.getImages().add("home");
item.getImages().add("kindergarden");
item.getImages().add("games");
Bid bid = new Bid();
bid.setAmount(new BigDecimal("3.93"));
item.addBid(bid);
Bid bid1 = new Bid();
bid1.setAmount(new BigDecimal("4.01"));
item.addBid(bid1);
tx.begin();
em.persist(item);
em.persist(bid);
em.persist(bid1);
tx.commit();
}
测试其实很容易, 和之前类似, 也是要先持久化item对象, 再持久化bid对象, 否则bid会找不到外键.
在最后.commit()之前, Hibernate会执行dirty checking. 最后会以最新的状态写入.
级联关系
Item和Bid类的关系, 在数据库中是外键, 在Java代码中, 使用了getBids().add(bid); bid.setItem(this);
, 这是为了保持数据的完整性. 这样无论在数据库层面, 还是Java层面, 关系都是一致的.
但是在上边的例子中, 必须要先持久化item, 再持久化bid, 这是因为插入bid的时候, 外键必须关联到一个已经存在的对象上, 不先持久化item, 外键就是null, 不符合约束条件.
但是仔细一想, 既然已经有了关系, 外键关系反应到Java上, 也是一种依赖关系, 是不是可以设置一下关联关系会自动的反映在如何操作上.
这就是为两个类的关系定义级联操作, 即遇到一些特定的操作, 该如何具体的从两个类的关系中生成相应的操作.
CascadeType.PERSIST 级联保存
先看一下级联操作的第一个种类, CascadeType.PERSIST, 表示级联保存. 注意看前边的例子,需要先保存一对多关系中的那个"一", 剩下的"多"才有对应关系. 将CascadeType.PERSIST加在一对多的"一"那边:
@OneToMany(mappedBy = "item", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private Set<Bid> bids = new HashSet<>();
如此设置之后, 就可以只持久化item, 其中的bid集合中的所有对象, 会跟着一起持久化:
item.addBid(bid);
item.addBid(bid1);
tx.begin();
em.persist(item);
tx.commit();
@ManyToOne
也可以设置级联保存, 但是一般不会使用.
CascadeType.REMOVE 级联删除
先来看看, 在数据库的层面要删除一个Item对象如何操作呢, 很显然分为两步, 第一步是先删除这个Item的所有Bid表中的关联内容(不能先删除Item表,否则外键为空), 然后删除Item表.
如果换成Hibernate操作, 要这么写:
@Test
public void test2() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("HelloWorldPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
List<Item> items = em.createQuery("SELECT i FROM Item i", Item.class).getResultList();
//对于每个item
for (Item i : items) {
//删除item对应的所有bid
for (Bid b : i.getBids()) {
em.remove(b);
}
//删除bid对应的item
em.remove(i);
}
tx.commit();
}
设置了级联删除之后, 只需要删除item, 就会自动完成这个过程:
@OneToMany(mappedBy = "item", fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private Set<Bid> bids = new HashSet<>();
级联删除有一个比较重大的问题, 就是Hibernate会认为其中的bid仅仅只有和item关联, 实际上未必, 如果bid还和其他类有关联关系, 未必可以如此简单的将其删除.
因此在级联删除的时候需要小心, 尤其是UML类图的映射, 如果只是单向关系, 就没有必要将单向关系映射为双向, 这样就可以避免不必要的级联操作. 此外级联删除是一条条查询后再删除, 效率会比较低下.
Hibernate提供了特殊的@org.hibernate.annotations.OnDelete
用于控制行为, 可以具体指定行为方式, 使用Hibernate的特性改写的级联删除如下:
@Entity
public class Item {
@OneToMany(mappedBy = "item", cascade = CascadeType.PERSIST)
@org.hibernate.annotations.OnDelete(action = org.hibernate.annotations.OnDeleteAction.CASCADE)
protected Set<Bid> bids = new HashSet<>();
......
}
这种简单的@OneToMany和@ManyToOne算是比较简单的, 后边再逐渐看这三种关系的深入剖析. 这级联操作也还有很多种类要看.