之前映射了单个类, 映射了内嵌类, 还有类中间的各种属性. 现在要映射两个新东西, 一个是集合, 一个是类之间的关系, 有了这些就完整的映射知识了.
集合的映射又是类之间关系映射的基础, ORM的核心就是管理类之间的关系, 也是最为复杂的一部分, 这章估计会经常回来看.
- 映射集合的好处
- 映射SET
- 映射Bag类型
- 映射List类型
- 映射MAP类型
- 映射排序的集合 - sorted
- 映射排序的集合 - ordered
- 映射Embedded对象的集合
映射集合的基础概念
可以映射集合, 有如下好处:
someItem.getImages()
这种方法, 可以自动被转换成SELECT * from IMAGE where ITEM_ID = ?
,
而且如果持久化类处于managed的状态下, 只要执行这个, 就可以得到关联的所有对象, 而不用使用EntityManager去加载数据.
- 不用一个一个去持久化集合中的所有对象, 只要将其添加到集合中, 然后持久化集合即可. 这种方便级联的操作极大的提高了操作效率.
- 可以设置Entity之间的级联关系, 比如删除一个Item之后自动删除其所属的所有Image.
知道了能够映射集合的好处, 接下来的首要问题就是选择何种集合接口
如果不提供泛型, 则可以使用@ElementCollection(targetClass=String.class)
或者@MapKeyClass
来提供泛型信息. 但最好还是使用泛型.
Hibernate支持所有的Java collection类型, 对于每种类型都有一个对应的默认映射方法, 并且在映射时保留这些类型的语义. 在不扩展Hibernate的类型的情况下, 可以选择如下集合类型:
java.util.Set
接口,实际类型是java.util.HashSet
. 顺序不重要, 不允许重复元素, 是JPA标准支持的.
java.util.SortedSet
接口,实际类型是java.util.Treeset
. 不是JPA标准, Hibernate支持, 会在Hibernate取出值之后在内存中进行排序.
java.util.List
接口,实际类型是java.util.ArrayList
. 会将其中的内容和对应的元素(额外一列)都持久化, 是JPA标准.
java.util.Collection
接口,实际类型是java.util.ArrayList.
. 这个的语义是Bag类型, 即允许重复元素, 顺序无所谓. 也是JPA标准.
java.util.Map
接口,实际类型是java.util.HashMap
. 键和值都存在数据库中, 也是JPA标准.
java.util.SortedMap
接口,实际类型是java.util.TreeMap
. 支持排序的Map, 也是内存中排序, 不是JPA标准, 是Hibernate支持.
- 数组也是集合, JPA标准不支持集合, Hibernate支持. 但是很少使用.
这里有个小知识点, 就是Image中只存储文件名称, 但是Java的文件读写并不支持事务, 无法回滚. 现在也有一些支持事务的文件操作库, 比如XADisk.
注意这些集合映射, 现在说的都是value type的集合映射, 如果映射的是一批其他Entity, 那就不是单纯的集合映射, 而是表关系.
映射SET
下边使用的一个Item对应多个Image, 还没有使用Image类, 一个Image就是一个String文件名, 所以Item对应一个文件名的集合, 由于文件名相等代表是同一个文件, 因此应该使用Set:
@Entity
public class Item {
@Id
@GeneratedValue
protected Long id;
@ElementCollection
@CollectionTable(
name = "IMAGE",
joinColumns = @JoinColumn(name = "ITEM_ID")
)
@Column(name = "FILENAME")
protected Set<String> images = new HashSet<>();
}
解释如下:
@ElementCollection
用在一个value type的集合上, 这里要注意, 是value type的集合哦.
@CollectionTable
用来覆盖默认的ITEM类型对应的表名, 默认是ITEM_IMAGES. 其中的@JoinColumn控制的是IMAGE表的外键列名称.
- 通过集合生成的表, 主键是联合主键, 由String类型和外键列共同组成, 这意味对于同一个ITEM无法插入重复的图片文件名.
写个小测试运行一下看看实际生成的代码吧:
tx.begin();
Item item = new Item();
item.addImage("image1");
item.addImage("image2");
item.addImage("image3");
item.addImage("image4");
item.addImage("image5");
Item item2 = new Item();
item2.addImage("image10");
item2.addImage("image11");
item2.addImage("image12");
em.persist(item);
em.persist(item2);
tx.commit();
实际创建了两个表:
Hibernate:
create table Item (
id int8 not null,
primary key (id)
)
create table IMAGE (
ITEM_ID int8 not null,
FILENAME varchar(255)
)
alter table if exists IMAGE
add constraint FK81w867q86d41yp2romymdpbvi
foreign key (ITEM_ID)
references Item
就是一个外键关系, 这里放上@CollectionTable
不带任何参数的生成语句, 可以方便的看到注解中控制了哪些内容:
Hibernate:
create table Item (
id int8 not null,
primary key (id)
)
create table Item_images (
Item_id int8 not null,
FILENAME varchar(255)
)
alter table if exists Item_images
add constraint FKnt0u91fi0efuy5ug9qq9ua2jt
foreign key (Item_id)
references Item
看到这里觉得真是妙, 原来可以value type类型的集合也能够持久化, 以前用Hibernate, 上来就是持久化类之间的关系, 却没意识到这值类型的集合也能够持久化成一个表. 对于TresSet也是完全相同的操作.
映射Bag类型
Java的集合类型并没有一个Bag类型, 但是算法中经常有背包一说, 在Hibernate中, 只要使用Collection多态, 具体实现类是ArrayList, 就会被解析映射成一个背包类型, 背包类型是无序, 允许重复的集合类型.
映射背包的方式如下:
@Entity
@org.hibernate.annotations.GenericGenerator(
name = "ID_GENERATOR",
strategy = "enhanced-sequence",
parameters = {
@org.hibernate.annotations.Parameter(
name = "sequence_name",
value = "cony_sequence"
),
@org.hibernate.annotations.Parameter(
name = "initial_value",
value = "1000"
)
})
public class Item {
@Id
@GeneratedValue
protected Long id;
@ElementCollection
@CollectionTable(
name = "IMAGE"
)
@Column(name = "FILENAME")
@org.hibernate.annotations.CollectionId(
columns = @Column(name = "IMAGE_ID"),
type = @org.hibernate.annotations.Type(type = "long"),
generator = "ID_GENERATOR"
)
protected Collection<String> images = new ArrayList<>();
}
类上边定义了一个GenericGenerator, 是为了后边用. 这里依然使用了@ElementCollection
和@CollectionTable
两个注解. 为什么不能像上边的SET一样在@CollectionTable内使用column属性, 是因为这么做会生成id和文件名的联合主键, 导致无法放入重复元素.
所以在下边加了一个Hibernate的注解, 专门用来注解集合表中的Id, 其中指定了IMAGE表的主键名称是IMAGE_ID, 类型是long, 生成器是刚刚注解出来的生成器, 三个属性缺一不可.
有意思的是生成的表:
Hibernate:
create table IMAGE (
Item_id int8 not null,
FILENAME varchar(255),
IMAGE_ID int8 not null,
primary key (IMAGE_ID)
)
create table Item (
id int8 not null,
primary key (id)
)
alter table if exists IMAGE
add constraint FKfmjenilsjv7utxi4500ytgc5j
foreign key (Item_id)
references Item
可以发现, IMAGE类变成了三列, ITEM_ID关联到ITEM类, 此外主键是IMAGE_ID, 这样即使相同的FILENAME都可以关联到同一个ITEM_ID上, 也就是同一个ITEM上.
映射List类型
前边已经看过了两种情况, 都是无序的,一个允许重复, 一个不允许重复. 现在来看看有序的List, 同时List集合语义上也是允许重复的.
关于List, 一个最大的诱惑就是有序, 究竟有序怎么处理, 看映射:
@Entity
public class Item {
@Id
@GeneratedValue
protected Long id;
@ElementCollection
@CollectionTable(name = "IMAGE")
@Column(name = "FILENAME")
@OrderColumn
protected List<String> images = new ArrayList<>();
}
看上去似乎简单了不少, 前边已经知道, 只要使用了前两个注解, 生成的IMAGE有主键和FILENAME两列, BAG会额外添加一列, List既然是有序, 也通过@OrderColumn
添加了一列. 生成的语句是:
Hibernate:
create table IMAGE (
Item_id int8 not null,
FILENAME varchar(255),
images_ORDER int4 not null,
primary key (Item_id, images_ORDER)
)
create table Item (
id int8 not null,
primary key (id)
)
alter table if exists IMAGE
add constraint FKfmjenilsjv7utxi4500ytgc5j
foreign key (Item_id)
references Item
一看语句就一目了然了, 不会有属于同一个ITEM并且序号重复的内容, 但是FILENAME可以重复. 查询其实也是如此, 比较不智能的是删除, 如果删除一个序号是2的元素, Hibernate会从3开始直到末尾挨个UPDATE序号为当前序号减1.
映射MAP类型
有了前边的经验, MAP映射其实心里也应该有数了, 就是一列KEY, 一列VALUE, 外加一个ID:
@Entity
public class Item {
@Id
@GeneratedValue
protected Long id;
@ElementCollection
@CollectionTable(name = "IMAGE")
@Column(name = "FILENAME")
@MapKeyColumn(name = "IMAGENAME")
protected Map<String,String> images = new HashMap<>();
}
新东西是@MapKeyColumn
, 存放文件名的列相当于value, 现在要加上一个KEY列, 就用这个注解来指定, 其他都不变, value列名依然是FILENAME, 表名叫IMAGE, 跑一下测试看语句:
tx.begin();
Item item = new Item();
item.getImages().put("cony", "d:\\cony.jpg");
item.getImages().put("saner", "c:\\owl.jpg");
item.getImages().put("kiki", "e:\\kiwi.jpg");
em.persist(item);
tx.commit();
建表语句是:
Hibernate:
create table IMAGE (
Item_id int8 not null,
FILENAME varchar(255),
IMAGENAME varchar(255) not null,
primary key (Item_id, IMAGENAME)
)
create table Item (
id int8 not null,
primary key (id)
)
alter table if exists IMAGE
add constraint FKfmjenilsjv7utxi4500ytgc5j
foreign key (Item_id)
references Item
可以看到, 联合主键是Item_id和IMAGENAME, 对应一个ITEM就不会有重复的key, 也符合MAP的语义.
这里有一点要注意的是,如果键是基本类型或者BigDecimal这种, 无需额外注解,如果键是一个枚举类型, 需要使用@MapKeyEnumerated
注解, 如果是时间类型, 则需要@MapKeyTemporal
.
MAP实际上是不允许重复的, 是无序的.
映射排序的集合 - sorted
这个是Hibernate特有的功能, 不是JPA标准.
说到排序, 有两个词, 一个是sorted, 一个是ordered, sorted表示在内存中使用Java的排序, 而ordered表示Hibernate存取时候使用ORDER BY的排序.
排序功能目前能用于之前提到的两个支持类型:TreeMap和TreeSet. 先来看TreeMap, 写了一个完整的类和测试放在这里:
import org.junit.Test;
import javax.persistence.*;
import java.util.Comparator;
import java.util.SortedMap;
import java.util.TreeMap;
@Entity
public class SortedMapItem {
@Id
@GeneratedValue
private long id;
@ElementCollection
@CollectionTable(name = "IMAGE")
@MapKeyColumn(name = "FILENAME")
@Column(name = "IMAGENAME")
@org.hibernate.annotations.SortComparator(ReverseIntegerComparator.class)
protected SortedMap<Integer, String> images =
new TreeMap<>();
public static class ReverseIntegerComparator implements Comparator<Integer> {
@Override
public int compare(Integer a, Integer b) {
return b - a;
}
}
public SortedMap<Integer, String> getImages() {
return images;
}
public void setImages(SortedMap<Integer, String> images) {
this.images = images;
}
@Override
public String toString() {
return "SortedMapItem{" +
", images=" + images.toString() +
'}';
}
//TreeMap自动排序
@Test
public void test1() {
SortedMap<Integer, String> stringSortedMap = new TreeMap<>();
stringSortedMap.put(3, "fsa");
stringSortedMap.put(4, "123fsa");
stringSortedMap.put(7, "32123fsa");
stringSortedMap.put(1, "iouv");
stringSortedMap.put(2, "bkj");
System.out.println(stringSortedMap);
}
//测试放入然后取出
@Test
public void test2() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("HelloWorldPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
SortedMapItem sortedMapItem = new SortedMapItem();
sortedMapItem.getImages().put(7, "fsa");
sortedMapItem.getImages().put(1, "fs1sta");
sortedMapItem.getImages().put(3, "3333");
sortedMapItem.getImages().put(2, "534");
sortedMapItem.getImages().put(8, "534");
System.out.println(sortedMapItem.getImages());
em.persist(sortedMapItem);
tx.commit();
// 测试取出
tx.begin();
SortedMapItem item = em.createQuery("select i FROM SortedMapItem i", SortedMapItem.class).getSingleResult();
System.out.println(item);
item.getImages().put(4, "4tong");
item.getImages().put(0, "dazhuan");
System.out.println(item);
tx.commit();
}
@Test
public void test3() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("HelloWorldPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
SortedMapItem item = em.createQuery("select i FROM SortedMapItem i", SortedMapItem.class).getSingleResult();
System.out.println(item);
System.out.println("插入序号");
item.getImages().put(9, "9bxkj");
System.out.println(item);
tx.commit();
}
}
这个排序的原理是Hibernate仅仅加载数据, TreeMap数据类型每次插入的时候会自动排序, 因此自然得到了排序的结果. SortComparator
可以传入一个Java Comparator接口的实现类, 用于给键排序.
也可以使用@org.hibernate.annotations.SortNatural
来进行自然排序.
除了这里的TreeMap用来排序键值对, 还有存储单个元素的SortedSet, 这些数据类型因为是Java在内存中进行排序, 所以都可以使用SortComparator
或者@SortNatural
来排序.
映射排序的集合 - ordered
除了上边的Java排序, 还有一些数据类型, 可以让Hibernate通过ORDER BY来装载特定的顺序, 而不是通过Java数据类型让其排序, 可以说与上边的不同之处就是控制权交给了Hibernate而不是Java.
这里也写一个完整的例子就可以了, Hibernate会按照指定的排序子句来装载集合:
import org.junit.Test;
import javax.persistence.*;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
@Entity
public class LinkedHashSetItem {
@Id
@GeneratedValue
private long id;
@ElementCollection
@CollectionTable(name = "LINK_IMAGE")
@Column(name = "IMAGE_NAME")
@org.hibernate.annotations.OrderBy(clause = "IMAGE_NAME")
protected Set<String> images = new LinkedHashSet<>();
public Set<String> getImages() {
return images;
}
public void setImages(Set<String> images) {
this.images = images;
}
@Override
public String toString() {
return "LinkedHashSetItem{" +
"id=" + id +
", images=" + images +
'}';
}
@Test
public void test() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("HelloWorldPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
LinkedHashSetItem item = new LinkedHashSetItem();
item.getImages().add("home");
item.getImages().add("kingergarden");
item.getImages().add("bed");
item.getImages().add("room");
//此时的打印, 打印的是插入顺序
System.out.println(item);
em.persist(item);
tx.commit();
}
@Test
public void test2() {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("HelloWorldPU");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
LinkedHashSetItem item = em.createQuery("SELECT ll FROM LinkedHashSetItem ll",LinkedHashSetItem.class).getSingleResult();
//此时打印, 是排序后的顺序
System.out.println(item);
tx.commit();
}
}
两个测试中, 一个是写入, 一个是取出, 可以看到取出之后的排序, 就和写入时候的代码不同, 而是经过了排序.
与LinkedHashSet同样可以排序的是Bag类型, 记得Bag类型要生成一个额外的id哦.
最后总结一下:
集合映射指的是映射value type的集合
类型 |
映射为 |
HashSet |
一张表, 一个外键关联到所属类的id, 联合主键, 不允许表有行重复 |
Bag(Collection+ArrayList) |
一张表, 需要额外设置一个主键, 然后另外一列关联到所属类的id, 可以存重复的值. 可以使用@OrderBy 排序 |
ArrayList |
有id, 顺序, 内容三列, id和顺序是联合主键, 这样保证没有重复的序列, 但可以有重复的值. |
HashMap |
有id, 键, 值三列, id和键是联合主键, 不能有重复的键 |
TreeMap, TreeSet |
与Map和Set一样, 但可以加上@SortComparator/@SortNatural 排序 |
LinkedHashMap |
与Map和Set一样, 但可以加上@OrderBy 排序 |
映射Embedded对象的集合
现实中不大可能直接映射一个字符串, 根据UML类图, Image其实应该是一个类.
不过之前我们知道, 既然映射value type, 那么一个基本类型和一个Embedded类对Hibernate来说都是value type, 并没有本质的区别.
实际上相比原来的基本类型, Embedded类只不过增加了几列, 哪怕还有继续内嵌的Embedded类也一样, 可见ORM真的绝妙.
相比基本类型, 唯一要注意的就是要编写Embedded类的判断相等的方法, 包括, 因为Set集合类型需要检测重复.
映射Map的时候, 键除了基本类型, 也可以是其他的Embedded类.
具体代码不放了,简单总结一下映射Emb类的特点
- 一定要实现
.equals()/.hashCode()
方法
- 既然是Embedded类型, 在集合属性上可以使用@AttributeOverride来重新命名内嵌类的列名和其他属性
- 在Embedded类型中, 可以设置一个对包含类的引用, 采用
@org.hibernate.annotations.Parent
注解
- 可排序的集合依然可以用排序功能, 使用JPA标准的排序和Hibernate的排序注解都可以
- Bag依然要通过注解给一个额外的标识列
- Map支持Embedded类作为键, 这时候不需要
@MapKeyColumn
注解, 只要泛型中给出键的类型就可以.
- Embedded类中可以再嵌套value type的集合(本篇文章提到集合就是指value type的集合), 基础注解一样只需要@ElementCollection.
可以看到, 虽然集合也会被映射成为数据库中的表, 但和@Entity还是有本质的区别, 即不会作为独立的关系被我们管理.
现在如果把集合中的value type换成Entity类, 就是另外一个关键的映射, 即关系映射.