ORM的框架大致怎么玩知道了, 最基础的部分就是创建持久化类并且提供元数据, 这一个过程也就是ORM中的M, 即映射, 也就是我们实际要做的事情.

映射完成之后, 后边的工作就是与ORM框架交互, 等于是通过套了一层壳的JDBC去进行对数据库的操作. 所以这映射技术就是重中之重了. 映射做好了, 后边另一个重点就是操作框架.

  1. Entity与value types
  2. 唯一性映射注解 @Id
  3. @GeneratedValue的主键生成策略
  4. 自定义主键生成策略的属性
  5. Hibernate的主键生成策略
  6. Entity的其他映射配置 – 各种名称
  7. Entity的其他映射配置 – 是否允许动态生成SQL
  8. Entity的其他映射配置 – 不可变
  9. Entity的其他映射配置 – 持久化类映射为子查询/视图

Entity与value types

一上来又是Hibernate中的两个概念, 这两个概念其实不难理解.

Entity, 就是独立可以持久化的一个对象, 具有唯一性. 作为Entity的一个Java对象, 实际是一个引用, 对这个引用的持久化, 就是对数据库中一条记录的引用.

value types就是没有独立持久化身份的类, 比如String 和一些原始类型, 甚至是一个更复杂的类. 比如书里举的例子, 一个单纯依附于User对象的Address对象.

所以对于一个持久化类中的所有成员变量, 即使变量代表的是一个类, 也可以根据类之间的关系, 将其映射为Entity或者value types.

好比User与Address, 如果将User中的Address变量映射为value types, 其本质和对一条User记录添加三个Address相关的字段没区别.

如果将其映射为一个Entity, 那么在数据库中就要存在一个Address关系, 用来存储Address的数据, 既然存在于数据库中, 很显然, 这个对象也可以单独被取出使用, 不再依赖于User的生命周期.

在JPA中也有这两个概念, 只是称呼不同.对应关系如下:

JPA Hibernate
Entity Entity
value types basic property types
embeddable classes

后边就可以看到embeddable class 在实践中的应用, 这比单纯的扩大User类的成员变量数目, 显得要更加牛逼一些.

回到这UML类图上来, 很显然像ITEM, USER这种都是Entity, Address可以作为value types, 从图上可以发现User组合Address, UML类图中的组合关系意味着User要负责Address的生命周期, 单独的Address是没有意义的. Address对象也不需要User之外的引用.

Bid类就有点问题, 单从图上来看, ITEM组合BID, BID单向通过bidder变量指向User对象, 似乎可以将BID设置成为ITEM的value types.

但如果考虑未来domain model扩展, 要求知道一个User对应的所有bid, 很显然, 此时bid对象就被多个Entity对象共享, 根据一开始的定义, Bid需要成为一个Entity.

实践中经常会碰到这种复杂的问题, 也许会本能的想, 应该先将这些具有两面性的类都映射为value types, 仅仅只在必要的时候, 才将其提升为Entity.

一般来说, 增加整个映射的复杂度, 又不会带来任何好处的事情不要做. 通过Item和User查询Bid, 都可以通过SQL查询做到, UML类图中bid也都是单向关系, 所以Bid就可以被映射成Value Type. 但实际上, 对于Bid这种相对独立的内容, 映射成Entity是很好的做法.

我个人想了一下, 其实一对一的这种关系, 有一方映射成为value types会很方便., 而一对多这种关系, 双方当然还是映射成为Enttiy比较好.

总的来说,在选择将一个类映射成Entity或Value Type时候, 要考虑如下几点:

  1. Shared references, 共享引用. 查看Java类关系中是否有共享引用, 这个共享引用不仅是UML类图级别, 而且是Java代码级别的. 比如如果想Address类作为User类的value type, 那应该使用将Address做成一个不可变类, 禁止所有setUser()等公共方法, 通过构造器注入User对象, User对象中也必须加入address变量的判断来确保只拥有一个Address类的引用等等方法, 来确保这种Value type关系. 此外还需要一个无参构造器以让Hibernate也能创建Address实例.
  2. Life cycle dependencies, 生命周期的依赖关系. 如果一个User从数据库中被删除, 对应的地址有没有必要删除? 元数据中会包含对于级联规则, 使用何种级联规则实际上就取决于如何看待这个持久化类. 一旦确定, 无论在Java类的编写还是元数据的设置上, 都应该遵照同样的规则.
  3. Identity, 数据库理论里常讲的identity, 就是唯一性. Entity类一般都要有一个用于标志其唯一性的成员变量, 而不能依靠Java代码的比较相等性. value type则没有必要使用唯一性属性. 如果一个类需要唯一性, 那就需要将其作为Entity来映射.

唯一性映射注解 @Id

映射的第一个具体问题, 就是解决唯一性. 既然是Java框架, 先来了解一下几个唯一性的名词:

  1. Object identity,就是 == 操作符, JVM通过比较两个对象的内存地址来确定是否是同一个对象.
  2. .equals(), 如果说上一个比较重在”是不是同一个对象”, 这个方法在语义上重在”意义上是否等同”, 通过.equals()方法返回的结果判断是否是同一个对象, 一般用来比较两个对象的某些值是否相等进而判断意义上的相等关系.
  3. database identity, 数据库唯一性, 即一个持久化对象对应的数据库中的表和行是否相同. 由于一个持久化对象在内存中, 就对应着一个具体的数据库中的某个表的某一行. 即使两个Java对象的地址不同, 属性不同, 但如果代表的是同一个表的同一行, 持久化框架在持久化这个类的时候, 都会往同一行去写入.

这三种唯一性不是互斥的, 而是类似于三种属性, 可以同时存在于某(两个)对象上. 例如两个Java对象都有相同的数据库一致性, .equals()返回true, 但地址却并不相同.

数据库一致性在持久化类中的映射, 就是@Id注解. 当一个类加上@Entity注解的时候, 如果去启动框架, 就会报错, 提示至少需要一个属性被@Id注解.

通过前边的理论分析, 就知道了, Entity是必然要求有一个数据库唯一性的标记. @Id就是用于标记哪个成员变量作为唯一性的标记:

@Entity
public class Item {
    @Id
    @GeneratedValue(generator = "ID_GENERATOR")
    protected Long id;

    public Long getId() {
        return id;
    }
}

这是一个最简单的Item例子, 标记了@Entity属性, 相比原来, 只知道要加上这个注解, 现在对于@Entity代表的Entity理论也更清楚了.

然后这里使用了@Id注解加在id属性上, 意味着将使用这一个作为数据库唯一性的标记, 对应到关系型数据库中, 意味着将id属性作为主键.

除此之外@Id还有另外一个作用, 即如果@Id直接加在成员变量而不是访问器方法上, JPA会对这个类中所有的成员变量都采取通过反射直接访问的方式, 不会使用通过get方法访问的方式. 即使后边将@Column写在get方法上也没用. 这是JPA的标准, 当然也会有其他配置来覆盖这个配置, 此是后话.

还一个注意的要点是, 对于@Id标记的主键属性, 不需要设置set方法. Hibernate会自动替你管理. 既然不需要赋值, Hibernate如何知道给主键赋值呢, 当然就是要通过@GeneratedValue来指定如何生成主键了.

Hibernate的主键管理是基于主键不可变这一性质的, 如果数据库采取的是主键可变策略, 那么就不能简单的使用Hibernate.

@GeneratedValue的主键生成策略

数据库外键的知识已经了解过了, 采取surrogate keys也就是生成的, 不具有语义的, 也不对应用程序用户暴露的, 仅仅用于内部标识唯一性的主键是最好的.

这种主键的生成, 一般依赖于数据库或者应用. 良好的主键必须满足唯一性, 不可变性和不能是null, 加上Hibernate并不允许修改主键, 因此主键生成策略是紧跟着@Id马上要配置的.

如果在@Id之后没有@GeneratedValue注解, JPA会认为主键将在应用级别生成. 如今依赖应用生成的主键越来越多, 可以有效提高数据库效率, UUID就是典型的好实践, 这也是后话按下不表.

JPA标准规定了如下主键生成策略, 将其按照@GeneratedValue(strategy = ...)指明即可:

  1. GenerationType.AUTO, 让JPA提供商自行选择策略. 这等于什么参数都没有的@GeneratedValue注解, 在最开始的Message类中, 就是如此配置的. Hibernate则是根据所选择的数据库方言进行具体操作的, 可以看到对于PostgreSQL, Hibernate创建了一个序列并且设置从1开始, 然后将主键与该序列关联.
  2. GenerationType.SEQUENCE, 让JPA提供商创建并使用序列, 需要支持序列的数据库,比如Oracle或PgSQL. 对于Hibernate来说,会创建一个HIBERNATE_SEQUENCE, 并从中产生主键值. 对于PgSQL可以发现, 这和设置为AUTO是一样的效果.
  3. GenerationType.IDENTITY, 采用具有类似AUTO INCREMENT机制的主键生成策略, 需要具体数据库支持. 对于PgSQL来说, 使用之后, 创建Message表的语句会变成:
        create table Message (
            id int8 generated by default as identity,
            text varchar(255),
            primary key (id)
        )
        

    但由于PgSQL数据库的特性, 其实还是相当于创建了一个message_id_seq的序列, 然后指定给主键. 这二者的区别是, GenerationType.SEQUENCE如果不指定序列名称, 这个序列会被复用. 而GenerationType.IDENTITY就让PgSQL自己创建每个表关联的序列, 不会复用序列的值. 可见相比之下, 还是IDENTITY策略更好一些.

  4. GenerationType.TABLE, 使用一张表生成主键, 会创建一个叫做HIBERNATE_SEQUENCES的表, 有列名叫做SEQUENCE_NAME和SEQUENCE_NEXT_HI_VALUE. 经过PgSQL的试验, 确实也是如此, 每次Hibernate插入新数据,都会先从表中选出新产生的数字, 再将数字作为主键插入.

这四个都是JPA的标准, 实际上, 如果仅仅依赖这些标准, 在实践中是不够的, 就像前边的序列共用, 很显然会出问题. 因此JPA及Hibernate提供了一些可以自定义的选项.

自定义主键生成策略的属性

要使用自定义的策略属性, 对于JPA标准来说, 要写成@GeneratedValue(generator = "ID_GENERATOR"), 即不再直接指定策略, 而是指定一个生成器的名称.

然后就可以自定义生成器的具体属性了. 定义的方法可以使用@javax.persistence.SequenceGenerator或者@javax.persistence.TableGenerator,比如:

import javax.persistence.*;

@Entity
@SequenceGenerator(name = "my", initialValue = 20, allocationSize = 30)
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my")
    private Long id;

    private String text;

    ......
}

这里说一下的是, 一般只要有一个persistence.xml配置文件就可以, 不用同时使用persistence.xml和hibernate.cfg.xml, 如果同时有, 其中的属性不要重复设置.

将Hibernate的配置文件都删除, 按照上述配置之后, 尝试写入几十个Message对象, 可以看到, 序列从20开始, 每个增加1. 这里一定要注意allocationSize = 30这个奇怪的东西, 一定要设置成Hibernate的异常中报出来的数字. 这个表示插入多少个之后要重新查询一下数据库生成的序列值.

不过JPA的注解能设置的属性比较少, 所以一般都会使用Hibernate的注解, 反正我们JPA的提供商是Hibernate, 使用Hibernate特有的注解也一样OK:

import javax.persistence.*;

@Entity
@org.hibernate.annotations.GenericGenerator(
        name = "my",
        strategy = "enhanced-sequence",
        parameters = {
                @org.hibernate.annotations.Parameter(
                        name = "sequence_name",
                        value = "CONY_SEQUENCE"
                ),
                @org.hibernate.annotations.Parameter(
                        name = "initial_value",
                        value = "1000"
                ),
                @org.hibernate.annotations.Parameter(
                        name = "allocation_size",
                        value = "30"
                )}
)
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my")
    private Long id;

    private String text;

    ......
}

这里遵循了之前的规则, 将Hibernate的注解写全称, 这里配置了一个名称叫做”my”的生成器, 其生成策略strategy设置成"enhanced-sequence", 然后是参数部分, 设置了序列名称为CONY_SEQUENCE, 初始值为1000.

下边的allcation_size是我自己加的, 其实删去也没事. 另外我发现如果将这个序列移动到其他类上边, 依然会报allocationSize的错误, 注解在当前类上就没有问题.

实际运行JPA的API去操作, 可以发现正常. 在JPA提供商是Hibernate的情况下, 使用Hibernate的配置方式更好, 有如下优点:

  1. "enhanced-sequence"是一个可移植的特性, 如果DBMS原生支持SEQUENCE, 会使用SEQUENCE, 如果不支持, 会使用一个表来生成序列. 在INSERT之前会进行序列的生成工作.插入失败的话生成的序列值会被抛弃.
  2. 可以自定义序列的名称, 相比JPA更灵活, JPA只能依赖于提供商的默认名称
  3. 可以重用. 但是这里如果在其他地方也指定相同名称的生成器, 又会报INCREMENT SIZE不匹配的错误, 即使在GenericGenerator中设置了也不行, 所以很是奇怪.

这里着重要说的是"enhanced-sequence", 除了这个之外, 还有很多的Hibernate的策略可选, 这些和JPA标准并不完全相同. 在persistence.xml中有一个配置可以控制使用JPA的策略(称为老策略)还是Hibernate的策略(称为新策略):

<properties>
    ......
    <property name="hibernate.id.new_generator_mappings" value="true"/>

</properties>

默认是true, 表示可以使用新策略.

Hibernate的主键生成策略

来看一下Hibernate的策略:

  1. native, 相当于使用JPA的AUTO策略, 会自动选择SEQUENCE或者IDENTITY.
  2. sequence, 使用默认名称为HIBERNATE_SEQUENCE的序列, 在每次INSERT之前获取序列, 当然, 可以像上边一样自定义名称和其他属性, 这个sequence背后的类是org.hibernate.id.SequenceGenerator.
  3. sequence-identity, 在插入的时候生成值, 是在INSERT之后去序列中获取值, 可配置的属性和sequence一样,背后的类是org.hibernate.id.SequenceIdentityGenerator.
  4. enhance-sequence, 特性在前边介绍过, 相当于JPA的 GenerationType.SEQUENCE, 或者是打开了新策略下的AUTO, 这也是Hibernate内置策略中最推荐使用的策略. 还支持额外的优化器
  5. seqhilo, 使用数据库的序列加上高低位来进行操作. 了解即可. 对应老策略下的JPA SEQUENCE策略.
  6. hilo, 使用额外的一个表加上高低位算法来进行操作. 了解即可.
  7. enhance-table, 使用叫做Hibernate_SEQUENCES的表, 等于打开新策略下的JPA TABLE策略.
  8. identity, 使用以SQL Server和MySQL为代表的自增列, 奇怪的是不能直接在@GenericGeneric中配置该策略. 使用老策略映射的时候, JPA的IDENTITY就对应这个策略
  9. increment, Hibernate启动的时候读取所有数据表的主键列的最大值, 在每个表插入新行的时候, Hibernate生成最大值+1来更新. 除非Hibernate对某个数据库有完全而且非并发的独占, 否则不要使用该策略.
  10. select, Hibernate完全不生成任何主键值, 在插入的时候也不会将主键列包含在INSERT语句中, 而是预期DBMS会在插入的时候通过触发器等指定一个值, 仅用于旧式的无法返回生成键的JDBC数据库程序中.
  11. uuid2, 在应用层生成一个128位的UUID, 使用这个需要@Id注解的属性是String, byte[16] 或者java.util.UUID类型.背后的类是org.hibernate.id.UUIDGenerationStrategy和org.hibernate.id.UUIDGenerator.
  12. guid, 使用Oracle , MySQL等数据库的GUID生成程序创建GUID, 映射到String属性. 背后的类是org.hibernate.id.IdentityGenerator

一般来说, 使用插入前生成主键值的策略, 然后选择可以支持原生序列的enhance-sequence. 此外UUID也是可用的好策略之一. 所以最终的结论是:

  1. JPA标准: 打开新策略然后配置enerationType.SEQUENCE
  2. Hibernate原生: enhance-sequence
  3. Hibernate原生: uuid2

看到这里就尝试了一下uuid2, 简单配置了一下发现确实可用:

@Entity
@org.hibernate.annotations.GenericGenerator(
        name = "my",
        strategy = "uuid2"
        }
)
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "my")
    private String id;

    private String text;

    ......
}

就这样直接配置, 发现就可以自动生成uuid的主键了. 不过由于没有指定长度, id列依然是用的255长度的varchar.

到这里, 映射策略解决了第一步也是很重要的一步, 即一个Entity对象的主键映射方法.

Entity的其他映射配置 – 各种名称

只使用@Entity和@Id系列属性配置的持久化类, 除了前边说的意味着所有成员变量都由Hibernate直接读取之外, 很多配置都是采取默认的, 比如表名, 字段名称等. 这里就来看看如下三种更精细的配置:

  1. 各种命名及相关策略
  2. 动态SQL的生成
  3. 可变性

先来看各种名称, 当仅使用@Entity的时候, 该持久化类的表名就是类名, 由于SQL大小写不敏感, 所以数据库中的表名实际上就是java类名的全小写.

需要注意的是, 如果Java类名小写之后是SQL的关键字, 那么将报错. 为了能够具体指定表名, 可以采用@Tables(name = "table_name")来指定表名, 最常见的User表, 其小写user就是SQL关键字, 一般会将其指定为users表.

如果数据库是按照catalog或者schema组织的, @Table的设置中还可以设置相关属性, 默认是采用数据库的当前schema.

对于SQL关键字, 像PgSQL中就提供了双引号用来表名这不是一个关键字, 只是一个变量名称, 从而避免占用关键字或者需要转义. persistence.xml可以配置一个属性, 用来让Hibernate 5 自动通过配置的SQL方言在关键字上添加双引号.

Hibernate 5有一个功能, 可以自动为关键字添加引号, 有如下两种方式操作:

  1. 在注解的地方用反引号包围关键字
  2. 在persistence.xml中配置<property name="hibernate.auto_quote_keyword" value="true"/>

这里我就去掉了User类上边的@Table, 然后进行了XML配置, 之后查看Hibernate创建表的语句, 是这样的:

create table "User" (
   id int8 not null,
    firstname varchar(255),
    lastname varchar(255),
    primary key (id)
)

JPA对此的规定与Hibernate不同, 而是要求使用转义字符: @Table(name = "\"USER\"").

虽然可以使用上边这些技巧, 但是依然建议如果表和列的名称与SQL关键字相同, 首先考虑的应该是修改这些表和列的映射名称, 而不是使用上述技巧.

自定义表名

实际上仅仅使用@Table还不够, 需要手工一个一个的指定, 如果所有的表名都符合某个规范, 可以使用Hibernate提供的PhysicalNamingStrategy接口, 继承Hibernate提供的PhysicalNamingStrategyStandardImpl这个标准实现, 来编写自定义的名称处理器.

如果想把所有表的名称都加上一个前缀, 可以编写如下的类:

import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;

public class MyNamingStrategy extends org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl {

    @Override
    public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) {
        return new Identifier("cony_" + name.getText(), name.isQuoted());
    }
}

查看这个类的文档可以知道, 提供了对于catalog, schema, table, sequence和column的重命名方法, 显然配置在Table中, 被调用的就是这个更改物理表名的方法.

将其配置到persistence.xml中:

<persistence-unit name="HelloWorldPU">
    <properties>
    <!--            自动为关键字添加引号-->
        <property name="hibernate.auto_quote_keyword" value="true"/>
    <!--            自定义生成表名的规则-->
        <property name="hibernate.physical_naming_strategy" value="cc.conyli.model.helloworld.MyNamingStrategy"/>
    ......

运行程序, 可以看到建表语句和查询语句, 都变成了带有cony_前缀的SQL语句.

还有一种隐式命名策略, 也就是不使用@Table的时候, 不过最终还是PhysicalNamingStrategy策略其作用, 这个了解即可, 在持久化类上使用@Table是一个良好的习惯, 尤其是针对带有schema的数据库.

名称重复的持久化类

在ORM框架启动的时候, 会将类名加入到查询引擎的公共命名空间中, 这样当编写JPQL/HQL语句的时候, 就可以直接使用类名, 比如:

SELECT m FROM Message m;

这其中的Message只使用了类名, ORM的查询引擎通过命名空间内的Message名称知道Message实际对应的类, 不会有问题

目前仅仅只有一个叫做Message的持久化类, 如果在同一个程序的另外一个路径下也存在一个叫做Message的持久化类, 直接使用类名就会出现冲突, 要解决这个问题,有两个办法:

  1. 在JPQL/HQL中使用类的完整包名
  2. 在@Entity注解中指定name属性, 比如@Entity(name = "Message2")

使用第二种方法的话, 可以看到很有意思的现象, 如果没有配置@Table, 则物理表名会变成cony_message2, 如果配置了@Table(name= "saner"), 最后的物理表名会变成cony_saner, 这就是前边所说的显示和隐式表名都受最后物理表名的处理.

使用第二种方法的话, 在编写JPQL/HQL的时候, 就可以使用Message2这个名称了. 当然, 硬是要写成"select m from cc.conyli.model.helloworld.Message m"也是没有问题的, 但是谁不喜欢简单一点呢.

Entity的其他映射配置 – 是否允许动态生成SQL

说到底, ORM就是一个高级一点的壳, 到最后执行的还是SQL. 稍加了解就知道, 一般都不会生成一条SQL就执行一次, 而是可以缓存一些SQL然后一起提交.

Hibernate为了提高效率, 在框架启动的时候, 会为每一个持久化类创建一些简单的静态SQL语句, 这些语句是用于单行的CRUD. 很显然, 预先生成并缓存这些语句要比运行的时候现生成会快一些.

不过这带来一个问题, 就是Hibernate在启动的时候根本不知道会更新哪些列, 如何生成对应的SQL语句呢?. Hibernate这里用了笨但是可以包括所有情况的办法, 就是生成更新所有列的SQL语句. 在仅更新某些列的时候, 会先读出数据, 然后UPDATE的时候, 不更新的列就直接采用原值.

对于简单的表来说, 这样没有什么问题, 但对于大型数据库, 一行有很多列的时候, 读出的内容占据的内存空间就会很大. 而且Hibernate的启动时间也会变长.

如果确实遇到这种情况, 通过添加两个注解可以启用动态生成SQL功能:

@Entity
@org.hibernate.annotations.DynamicInsert
@org.hibernate.annotations.DynamicUpdate
public class Message {
    ......
}

启用了这两个注解之后, 在更新和插入的时候, 就会动态生成SQL语句, 到底使用哪种情况, 就根据数据大小来权衡利弊吧.

Entity的其他映射配置 – 不可变

在前边的例子中已经知道, Hibernate查询出来的结果, 如果进行修改, 会直接反映到数据库中, 这也是ORM映射的本来要做的, 就是类的引用等于对数据库中一行的引用.

这个方法虽然方便, 但很多时候可能不希望查询结果被改变, 以免出现不必要的错误. 比如UML图中的BID数据, 一旦在业务层面生成并且持久化之后, 没有任何理由再去UPDATE.

对于这种类, 可以在其上进行注解如下:

@Entity
@org.hibernate.annotations.Immutable
public class Bid {
    ......
}

如此注解之后, 取出来之后Hibernate不会进行dirty check, 随你修改, 也不会将数据更新到数据库中.

这里要注意的是, 你可能会想, 既然BID已经如此注解, 干脆在Java代码中取消Bid类全部的public set方法, 只剩构造器, 这样在Java层面POJO类也是不可变. 对于Hibernate来说, 如果@Id配置在成员变量上, 会采用反射直接读取所有成员变量, 有没有set方法无所谓. 但如果搭配其他一些框架使用, 要注意这些框架对于POJO的要求.

Entity的其他映射配置 – 持久化类映射为子查询/视图

没想到这才刚开始, 就已经有高级一点的内容了.

在现实中, 很多时候数据库管理和开发者的工作是互相分离的, 创建数据库和维护的工作由DBA完成, 开发者一般不被允许修改数据库结构, 甚至是创建视图.

在这种时候, ORM框架面向对象的特点就凸显出来了, 相比干巴巴用JDBC查完之后再处理, Hibernate可以让一个被@Entity标注的持久化类, 映射到一个应用程序级别的子查询, 或者说是一个视图, 而不是一个真正的数据库表.

以后使用这个类的时候, Hibernate就替你执行了指定的查询, 然后将结果设置到对象上, 果然够高级.

比如Message目前已经有若干个数据, 想取出Message对象的id, text, text+id 组成的字符串的结果. 如果不能在数据库添加视图, 那么可能的操作是先查出来Message对象, 然后创建一个具有这三个属性的类, 再从结果集中把每一个属性设置到类上去.

现在用这个高级办法, 既然查询结果有三列, 就直接创建一个类, 然后映射到这个查询上去:

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
@org.hibernate.annotations.Immutable
@org.hibernate.annotations.Subselect(
        value = "Select m.id as MID, m.text as TEXT, concat(cast(m.id as varchar), m.text) as IDTEXT FROM Message m"
)
@org.hibernate.annotations.Synchronize("Message")
public class MessagePart {

    @Id
    protected int mid;

    protected String text;

    protected String idtext;

    @Override
    public String toString() {
        return "MessagePart{" +
                "mid=" + mid +
                ", text='" + text + '\'' +
                ", idtext='" + idtext + '\'' +
                '}';
    }

    public MessagePart() {

    }
}

这里有几个要点:

  1. @org.hibernate.annotations.Subselect注解就是将这个@Entity映射为一个子查询/视图的关键注解. 其value就是子查询的SQL语句
  2. SQL语句使用了PgSQL的concat函数和cast转换, 将id和text拼成一个字符串. 注意其中每一行AS的别名, 要与MessagePart类中的属性名称对应.
  3. @org.hibernate.annotations.Synchronize("Message"), @Synchronize注解要和上边的@SubSelect搭配使用, 其中要把所有SQL语句中提到的表名=关系名都列入进来. 这其中的名称, 必须是能在查询框架命名空间中能找到的表名.
  4. 类的设置要有一个@ID,此外各个属性名, 必须与SQL中的各个列的别名一致.

然后要在persistence.xml中的class中加入这个MessagePart. 现在暂时删去自定义的名称处理器和Message类上的其他自定义名称的注解, 就默认采用message表名.

然后来编写一段查询, 看看着究竟是怎么一回事:

@Test
public void test() {
    EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("HelloWorldPU");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    MessagePart messagePart = em.find(MessagePart.class, 80);
    System.out.println(messagePart);

    tx.commit();
}

执行程序, 查看Hibernate的SQL语句:

select
        messagepar0_.mid as mid1_0_0_,
        messagepar0_.idtext as idtext2_0_0_,
        messagepar0_."text" as text3_0_0_
    from
        ( Select
            m.id as MID,
            m.text as TEXT,
            concat(cast(m.id as varchar),
            m.text) as IDTEXT
        FROM
            Message m ) messagepar0_
    where
        messagepar0_.mid=?

可以看到, 实际就是将MessagePart上注解的SQL语句当成了一个子查询或者说是视图, 在这个视图里, 自动再查询id=80的结果.

最后查询得到的结果, 直接就映射到一个MessagePart对象上.

想想就可以发现这个功能的本质, 无论是视图, 子查询还是实际存在于关系数据库中的表, 都是关系. 而一个持久化类, 实际上对应的应该就是一个关系, 无论这个关系是实际存储在数据库中, 还是以视图或者临时的子查询存在, 都没有关系.

通过这个技巧, Hibernate就可以把一个持久化类映射到虚拟的视图或者子查询上, 等于提供了一种灵活的过渡, 确实有趣.

这里我遇到的一个问题就是, 如果使用了之前的处理表名, 或者更改Message类的命名空间的名称等问题, 将新的名称写到子查询的SQL中, 会报找不到xxx关系的错误. 看来这个命名查询里边可能还有一些坑, 尤其是@Synchronize注解. 而且目前自己的所有查询API还没学过, 看看后边能不能搞的再清楚一些.

目前使用默认名称能够运行起来这个高阶功能, 已经感觉可以了.