之前映射了单个类, 映射了内嵌类, 还有类中间的各种属性. 现在要映射两个新东西, 一个是集合, 一个是类之间的关系, 有了这些就完整的映射知识了.

集合的映射又是类之间关系映射的基础, ORM的核心就是管理类之间的关系, 也是最为复杂的一部分, 这章估计会经常回来看.

  1. 映射集合的好处
  2. 映射SET
  3. 映射Bag类型
  4. 映射List类型
  5. 映射MAP类型
  6. 映射排序的集合 – sorted
  7. 映射排序的集合 – ordered
  8. 映射Embedded对象的集合

映射集合的基础概念

可以映射集合, 有如下好处:

  1. someItem.getImages()这种方法, 可以自动被转换成SELECT * from IMAGE where ITEM_ID = ?,
    而且如果持久化类处于managed的状态下, 只要执行这个, 就可以得到关联的所有对象, 而不用使用EntityManager去加载数据.
  2. 不用一个一个去持久化集合中的所有对象, 只要将其添加到集合中, 然后持久化集合即可. 这种方便级联的操作极大的提高了操作效率.
  3. 可以设置Entity之间的级联关系, 比如删除一个Item之后自动删除其所属的所有Image.

知道了能够映射集合的好处, 接下来的首要问题就是选择何种集合接口

如果不提供泛型, 则可以使用@ElementCollection(targetClass=String.class)或者@MapKeyClass来提供泛型信息. 但最好还是使用泛型.

Hibernate支持所有的Java collection类型, 对于每种类型都有一个对应的默认映射方法, 并且在映射时保留这些类型的语义. 在不扩展Hibernate的类型的情况下, 可以选择如下集合类型:

  1. java.util.Set接口,实际类型是java.util.HashSet. 顺序不重要, 不允许重复元素, 是JPA标准支持的.
  2. java.util.SortedSet接口,实际类型是java.util.Treeset. 不是JPA标准, Hibernate支持, 会在Hibernate取出值之后在内存中进行排序.
  3. java.util.List接口,实际类型是java.util.ArrayList. 会将其中的内容和对应的元素(额外一列)都持久化, 是JPA标准.
  4. java.util.Collection接口,实际类型是java.util.ArrayList.. 这个的语义是Bag类型, 即允许重复元素, 顺序无所谓. 也是JPA标准.
  5. java.util.Map接口,实际类型是java.util.HashMap. 键和值都存在数据库中, 也是JPA标准.
  6. java.util.SortedMap接口,实际类型是java.util.TreeMap. 支持排序的Map, 也是内存中排序, 不是JPA标准, 是Hibernate支持.
  7. 数组也是集合, 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<>();
}

解释如下:

  1. @ElementCollection用在一个value type的集合上, 这里要注意, 是value type的集合哦.
  2. @CollectionTable用来覆盖默认的ITEM类型对应的表名, 默认是ITEM_IMAGES. 其中的@JoinColumn控制的是IMAGE表的外键列名称.
  3. 通过集合生成的表, 主键是联合主键, 由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类的特点

  1. 一定要实现.equals()/.hashCode()方法
  2. 既然是Embedded类型, 在集合属性上可以使用@AttributeOverride来重新命名内嵌类的列名和其他属性
  3. 在Embedded类型中, 可以设置一个对包含类的引用, 采用@org.hibernate.annotations.Parent注解
  4. 可排序的集合依然可以用排序功能, 使用JPA标准的排序和Hibernate的排序注解都可以
  5. Bag依然要通过注解给一个额外的标识列
  6. Map支持Embedded类作为键, 这时候不需要@MapKeyColumn注解, 只要泛型中给出键的类型就可以.
  7. Embedded类中可以再嵌套value type的集合(本篇文章提到集合就是指value type的集合), 基础注解一样只需要@ElementCollection.

可以看到, 虽然集合也会被映射成为数据库中的表, 但和@Entity还是有本质的区别, 即不会作为独立的关系被我们管理.

现在如果把集合中的value type换成Entity类, 就是另外一个关键的映射, 即关系映射.