本章主要通过建立一个简单的Hibernate程序“Hello World”,以介绍如何配置Hibernate的运行环境。与传统的Hibernate运行环境不同的是,本文提供了一套通用的配置程序来初始化JPA/Hibernate的运行环境。此外,本文使用了JTA数据源的方法来连接数据库,使得程序具有了分布式事务管理的能力。
1. Hibernate与JPA简介
1.1. 对象关系模型
现代编程语言大多都有对面向对象(Object-oriented, OO)的支持。其好处在于其抽象程度较高,使得问题域和开发域是一致的。也就是说,我们在描述一个软件系统的时候使用的一些概念,在设计和开发过程中,也同样存在。例如,我们在分析系统的时候提到“学生”和“课程”这个概念,那么我们在设计和开发面向对象系统的时候,同样也存在Student和Course这两个对象。在一些非面向对象的语言中需要将一些概念抽象为变量和地址(如C语言)或者寄存器(如汇编语言)。
正式由于面向对象的这些特点,它被广泛用于现代的软件系统中。然而对于软件系统必不可少的数据持久化(Data Persistent)却依旧停留在关系模型阶段。这使得程序员的思维要在两种不同的模型之间切换。
1.2. Hibernate
Hibernate是一种被称为“对象关系映射”(Object-Relationship Mapping, ORM)的工具。其主要功能就是将对象模型“自动”映射成关系模型,使得程序员可以一直停留在面向对象的思维方式中,而无需切换到关系模型。
Hibernate最早是为Java语言开发的,后来也被移植到其他的各种语言中,例如NHibernate就是为.net所做的移植。
Hibernate现在是一个庞大的体系,主要包括以下几部分:
- Hibernate ORM Core: 这是Hibernate实现ORM的核心组件,其他组件都依赖于它。
- Hibernate Entity Manager: 主要是适配标准的Java持久化API(Java Persistence API, JPA)。
- Hibernate Validator: 主要是支持对Java Bean做的验证,(JSR 380: Bean Validation 2.0和已经过时的JSR 303: bean Validation)。
- Hibernate Envers: 数据库的审计日志和版本追踪功能。
- Hibernate Search: 基于Hibernate的全文检索引擎。
- ……
1.3. JPA
在Hibernate之后,由于有大量的ORM框架涌现出来,使得ORM市场相对混乱。为了统一规范,Oracle(SUN)提出了一个标准的框架想统一ORM标准,这就是标准Java持久化API(JPA)。
使用JPA可以选择不同的底层实现,例如常见的有Hibernate、JDO、TopLink、EclipseLink等等。这些底层的实现也被称为JPA第三方提供程序。需要注意的是,JPA只提供了大部分的基础功能,不同的提供程序会有一些自己的增强。同时JPA也在不断地改进和增强中,每个版本都会提出一些新的功能。到目前为止,JPA的最高版本为2.1。
我在开发过程中尽量遵循JPA的标准。有时JPA不支持的功能,则使用Hibernate功能。
2. 创建第一个Hibernae项目
2.1. 工具说明
我开发时所使用的工具以及版本:
- Windows 10 Pro 64 bit
- JDK 1.8.0u152 64 bit
- Eclipse Oxygen.2 Release (4.7.2) 64 bit
- MariaDB 10.1.30 / MySql (通过Xampp使用)
- Gradle 4.5.1
MySQL可以说是最成功的开源关系数据库项目,它被大量的项目所使用。但SUN公司后来受够了MySQL,后来Oracle收购了Sun,也同时收购了MySQL。现在Oracle控制MySQL产品本身,并领导开发社区开发新的成品。由于Oracle已经有了一个商业数据库,因此人们担心他们可能没有足够的资源来使MySQL保持其领先地位。因此,许多开发者担心担心MySQL作为领先的免费开源数据库提供的功能可能太少、发布周期太慢并且支持费用更昂贵。因此大量的MySQL分支项目涌现出来,其中MaridaDB就是其中之一。MySQL之父Monty(Michael Wideneus为保证有一个始终开源的兼容MySQL的分支可用,创立了MariaDB,同时成立了非赢利组织 MariaDB 基金会。本文中,我主要使用Xampp工具集成的MariaDB项目。
MariaDB基本和MySQL是兼容的,但是考虑到两者未来的发展方向可能有些差异。Hibernate还是为两者定义了不同的方言。MariaDB也有自己的JDBC驱动包。
2.2. 添加Hibernate的依赖
Hibernate的运行依赖于很多不同的其他组件。在Java中,这些组件都以jar包的形式存在。做过Java开发的人都知道,手动维护jar包是一件非常痛苦的事情。因此,才出现了类似于Maven这样的工具,自动维护依赖关系。本文中,我主要使用Gradle工具来替代传统的Maven维护依赖关系,因为Gradle比Maven简介并且灵活。Maven和Gradle都有依赖传递功能,会自动加载其他被依赖的包。例如,hibernate-entitymanager包依赖于hibernate-core包,那么仅添加前者,即可自动下载后者。
首先通过Gradle建立一个Java工程(可以使用手动建立方式、Eclipse的Buildship插件或者通过ItelliJ自带的Gradle插件),添加Hibernate所依赖的jar包。请直接看代码:
1 | //为Gradle提供Java相关的构建任务 |
事务管理是保证程序数据正确的必要手段。传统JDBC程序的引擎管理必须在JDBC的连接上来操作。如果程序使用了跨几个连接的操作甚至跨了几台不同的服务器的数据库操作,则无法保证事务正常工作。因此需要一种更强大的事务管理引擎。Java Transaction API(JTA)标准化了系统事务管理和分布式事务管理。常用的支持JTA标准的独立事务管理引擎主要有Bitronix和Atomikos两个。程序中,我使用了Bitronix事务管理引擎,也就是我们在Gradle配置中加载的btm包。
2.3. 配置持久化单元
JPA工作的起点是持久化单元,持久化单元有多种配置方式,可以使用XML配置文件,或者直接在程序中使用Java代码构建。这里我使用XML配置文件的方式,使用这种方式可以方便的修改其中的参数,而不必重新编译整个项目。
持久化单元的配置文件名称为persistence.xml,并且必须放置在项目的META-INFO目录中。对于一个Gradle项目来说,其默认位置为:src/main/resources/META-INFO/persistence.xml。Hibernate启动时会自动去加载这个文件,并识别其中的持久化单元。请直接看代码:
1 | <persistence version="2.1" |
在程序中,只要通过持久化单元的名称即可引用持久化单元。注意其中和数据库连接的部分是通过JNDI查找名称为myDS的数据源来实现的。因此我们需要配置数据源。
2.4. 配置数据源
Bitronix中有多种方式可以配置数据源,例如直接在Java代码中配置或者在Properties文件中进行配置。这里我选择在Properties文件中配置,这种方式比较灵活,修改配置不需要重新编译程序。
首先在src/main/resources目录下建立一个datasource.properties文件。其内容如下:
1 | # 配置支持XA的MariaDB数据源 |
其中resource后面的名称无关紧要,不必和JNDI(Java Naming and Directory Interface)的名字一致,这里使用了同样的名称纯属巧合。其实resource后面紧跟的myDS名字的作用仅仅是为了分组,来区分不同的数据源配置。在程序中没有实际作用。但是uniqueName所指定的名字是有实际作用的,也就是可以通过这个名字在JNDI中查找到合适的数据源。 JNDI简单的理解就是可以在Java程序中使用一个简单的名字查找一个已经配置好的资源。在这个例子中,主要就是通过在uniqueName中配置的名字来查找我们所配置的数据源。
className参数必须制定一个支持XA协议的数据源类的全名。大部分的数据库都支持XA协议,例如:Oracle、Informix、DB2、Sql Server、MySQL、MariaDB和Sybase等。XA接口提供资源管理器与事务管理器之间进行通信的标准接口。是分布式事务管理的关键技术。对于应用来说,无需对XA有深入的了解,只需要知道哪些数据库系统支持XA协议即可。
这个程序没有并发竞争的问题,因此可以不对隔离锁进行任何设置。数据库的用户名和密码根据实际情况进行设置。
在Web应用中,一般Web容器都会提供JNDI功能。但如果和这次的程序一样脱离Web容器运行,则需要自己提供JNDI查找功能。好在Bitronix已经为我们提供了JNDI查找功能。只需要做一个简单的配置。在src/main/resource目录下建立jndi.properties文件,其内容如下:
1 | java.naming.factory.initial=bitronix.tm.jndi.BitronixInitialContextFactory |
当在程序中新建JNDI上下文的时候,程序即会自动读取改文件的内容,并且创建一个Bitronix所提供的JNDI容器,使得我们对数据源的查找可以顺利进行。
有了数据源的配置和JNDI容器,接下来就可以根据配置文件创建数据源,并将数据源放入JNDI容器中,供其他程序查找。在Bitronix中创建数据源的代码如下:
1 | package com.rainsia.hibernate.env; |
TransactionManagerSetup类提供了创建数据源的能力、通过JNDI查询数据源的能力以及创建事务管理器的能力。
2.5. 配置JPA和Hibernate
JPA实际上只是一个统一的标准,并没有任何具体实现,它最终的功能还是委托给第三方提供程序来实现的(本文中其实就是Hibernate)。我总是习惯使用JPA的功能来进行大部分的操作,只有在JPA不能实现某个功能,但是原生Hibernate才能实现的时候,我才会切换到Hibernate实现。因此接下来,为了使用Hibernate,需要配置JPA。代码如下:
1 | package com.rainsia.hibernate.env; |
JpaSetup类通过持久化单元的名字初始化一个EntityManagerFactory,并能够使用getEntityManager创建不同的EntityManager。EntityManager是JPA的核心接口,几乎所有JPA的功能都是通过这个接口完成的。JPA编程的重点也主要是在这个接口上。
JpaSetup类和TransactionManagerSetup类看似没有直接的关系,两者在代码上也没有直接的关联。其实两者是通过数据源关联在一起的。在创建TransactionManagerSetup的时候,会创建一个名称为myDS的数据源,并放置到JNDI上下文中:
1 | # 数据源的名称,可以用于JNDI查找 |
而创建JpaSetup的时候会加载持久化单元,其中配置的数据源的名称:
1 | <!-- 使用JNDI的方式提供一个数据源(数据源提供和数据库的连接),类型为java.sql.DataSource --> |
因此在JpaSetup创建EntityManagerFactory的时候会加载persistence.xml文件,并找到其中的数据源配置,Persistence类会试图通过JNDI查找名称为myDS的数据源。如果没有事先创建TransactionManagerSetup对象,则会产生异常提示找不到myDS数据源。
2.6. 创建实体
有了TransactionManagerSetup和JpaSetup之后,就可以持久化对象了。然而在JPA的世界中,并不是所有对象都能够持久化的。JPA把能够持久化的对象称为实体,必须在其类中使用@Entity注解标记。我创建了一个最简单的实体类Message:
1 | package com.rainsia.hibernate.model; |
2.7. 使用JPA持久化实体
有了实体之后,就可以使用JPA的功能(通过EntityManager)来把实体对象持久化进数据库。我甚至都无需去创建数据库表,Hibernate会根据我定义的实体,自动帮我创建包括表、字段、类型、主键、外键在内的所有元数据。通过一个简单的例子看看如何持久化实体:
1 | package com.rainsia.hibernate.app; |
可以看到,创建完实体对象之后,只要调用EntityManager的persist()方法即可将对象保存到数据库中。此处要注意的是,Hibernate不会在刚调用persist()方法的时候马上就执行insert语句来插入数据,而是先记录下用户想做的操作,然后等待事务提交的时候才执行插入。这其实也有利于任务的批量执行:先记录下用户要做的所有的操作,然后统一发送给数据库处理。
运行该程序之后,可以看到Message实体的一个实例的数据已经插入到数据库中:
并且在控制台有如下输出:
从输出结果中可以看出,在调用em.persist()方法的时候,实体并没有实际插入数据库,而仅仅生成了实体的主键值(注意:如果使用的是自增主键,此时实际还未生成主键值):
1 | …… |
而只有在事务提交的时候,才真正执行了插入的过程:
1 | …… |
最终我们实现了一个Hibernate的运行环境,这个环境其实非常的通用,可以在以后的程序中反复地重用。然而,在实际查看程序输出的时候可以发现,Hibernate自动生成的SQL语句当中,所有的参数都使用?占位符来表示。下一篇笔记中,我们将研究如何输出实际的参数。