Hibernate 06 映射集合

Hibernate 06 映射集合

之前映射了单个类, 映射了内嵌类, 还有类中间的各种属性. 现在要映射两个新东西, 一个是集合, 一个是类之间的关系, 有了这些就完整的映射知识了. 集合的映射又是类之间关系映射的基础, ORM的核心就是管理类之间的关系, 也是最为复杂的一部分, 这章估计会经常回来看. 映射集合的好处 映射SET

之前映射了单个类, 映射了内嵌类, 还有类中间的各种属性. 现在要映射两个新东西, 一个是集合, 一个是类之间的关系, 有了这些就完整的映射知识了. 集合的映射又是类之间关系映射的基础, 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类, 就是另外一个关键的映射, 即关系映射.
LICENSED UNDER CC BY-NC-SA 4.0
Comment