Hibernate教程-终极指南
编者按:Hibernate ORM(简称Hibernate)是一个对象-关系映射框架,用于将面向对象的域模型转换为传统的关系数据库。Hibernate通过将直接的持久性相关的数据库访问替换掉,而高级对象处理功能来替换,解决了对象-关系间不匹配的问题。
Hibernate是最流程的Java框架之一。为此,我们在Java Code Geeks提供了大量的教程,点击这里查看它们。
现在,我们想要创建一个独立的、具有参考性的文章,来提供一个关于如何使用Hibernate的框架,并且帮助你快速开启你的Hibernate应用开发。开始享受吧!
目录
简介
Hibernate是Java世界里最流程的对象/关系映射框架(ORM)之一。它允许开发者将普通的Java类的对象结构,映射到数据库中的关系结构。借助于ORM框架,我们的一些工作,比如将内存中的对象实例存储到持久化的数据库,和加载这些数据到原来的对象结构,都变得更加容易。
同时,这些ORM解决方案,比如Hibernate,旨在从那些用于存储数据的数据库产品中建立抽象层。这样,就能够面向不同的数据库产品使用相同的Java代码,而不需面向不同的数据库编写不同的代码,来处理其中细微的差别。
Hibernate也是一个JPA提供者,这意味着它实现了Java持久化API(JPA)。JPA是一个独立于厂商的规范,用于定义Java对象和关系型数据表的映射关系。在终结系列中另一篇文章已经详细解释了JPA,所以我们在本文将重点介绍Hibernate,因此,我们也不会使用JPA注解,而是使用Hibernate专有的配置文件。
Hibernate由三个不同的组件组成:
- 实体(Entities):Java类(普通Java对象),用于在Hibernate中映射到关系型数据库系统的表。
- 对象-关系元数据(Object-Relational metadata):实体和关系型数据库的映射信息,可以通过注解(Java 1.5开始支持)实现,或者使用传统的基于XML的配置文件。这些配置信息,用于在运行时执行Java对象和数据库数据的存储和恢复。
- Hibernate查询语言(HQL):使用Hibernate时,发送到数据库的查询,不必是原生的SQL格式,而是使用Hibernate专有的查询语言。这些查询语句,会在运行时被翻译成当前应用正在使用的数据库对应方言;所以说,HQL格式的语句是独立于特定数据库供应商的。
在本教程,我们将解释这个框架的各个方面,同时开发一个简单的Java SE应用程序,功能主要包括存储和检索关系型数据库的数据。我们将要使用到的类库/环境如下:
- maven >= 3.0 :编译环境
- Hibernate(4.3.8.Final)
- H2 (1.3.176):关系型数据库
项目搭建
第一步,我们通过命令行创建一个简单的Maven项目:
mvn archetype:create -DgroupId=com.javacodegeeks.ultimate -DartifactId=hibernate
这个命令会在文件系统中生成如下结构:
|-- src | |-- main | | `-- java | | `-- com | | `-- javacodegeeks | | `-- ultimate | `-- test | | `-- java | | `-- com | | `-- javacodegeeks | | `-- ultimate `-- pom.xml
我们项目中依赖的类库,通过如下方式添加到pom.xml中的dependencies部分:
1.3.176 4.3.8.Final com.h2database h2 ${h2.version} org.hibernate hibernate-core ${hibernate.version}
为了更清晰的管理不同类库的版本信息,我们使用Maven属性设定这些类库的版本信息,然后在pom.xml中的依赖设置部分引用这些属性。
3. 基础知识
3.1. SessionFactory和Session
现在我们可以开始第一个O/R映射功能了。首先我们创建一个简单的类,它有一个run()方法,主程序的main()方法会调用到它:
public class Main { private static final Logger LOGGER = Logger.getLogger("Hibernate-Tutorial"); public static void main(String[] args) { Main main = new Main(); main.run(); } public void run() { SessionFactory sessionFactory = null; Session session = null; try { Configuration configuration = new Configuration(); configuration.configure("hibernate.cfg.xml"); ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(configuration.getProperties()).build(); sessionFactory = configuration.buildSessionFactory(serviceRegistry); session = sessionFactory.openSession(); persistPerson(session); } catch (Exception e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } finally { if (session != null) { session.close(); } if (sessionFactory != null) { sessionFactory.close(); } } } ...
方法run()创建类org.hibernate.cfg.Configuration的一个实例,这个实例后面会在XML文件hibernate.cfg.xml中配置。这个文件位于我们项目的目录src/main/resources钟,这个目录中的文件在Maven编译时会将它们都放置到生成的jar文件的根目录。这样,在系统运行时就能够通过classpath访问这个文件。
第二步中,run()方法使用了前面已经加载到的配置信息来构建ServiceRegistry的实例。然后,ServiceRegistry的实例可以作为Configuration的buildSessionFactory()方法的参数进行后面的操作。现在SessionFactory就可以用来获取session了,session用来存储和加载数据库中的实体数据。
配置文件hibernate.cfg.xml含有如下内容:
org.h2.Driver jdbc:h2:~/hibernate;AUTOCOMMIT=OFF 1 org.hibernate.dialect.H2Dialect thread org.hibernate.cache.internal.NoCacheProvider true true create
我们从上例可以看到,这个配置文件中定义了Session Factory的大量属性。第一个属性connection.driver_class指明了使用的数据库驱动。我们的例子中设定的是H2数据库的驱动。JDBC_URL是通过属性connection.url设定的。本例的设定值,定义了我们会使用h2数据库,它对应的单数据文件位于当前用户的主目录,其名为hibernate(~/hibernate)。由于我们想要在示例代码中自己控制何时提交事务,所以这里我们也定义了H2的特有配置项AUTOCOMMIT=OFF。
接下来我们定义了数据库连接的用户名、密码和连接池的大小。我们的示例程序只在单线程中执行代码,所以我们这里设置连接池大小为1。如果在应用程序中需要处理多线程和多用户,这个配置项需要按需设定。
属性dialect指定了用来翻译HQL到特定数据库SQL方言的Java类。
在3.1版本中,Hibernate提供了一个名为SessionFactory.getCurrentSession()的方法,可以获取到当前session的引用。属性current_session_context_class,用来定义Hibernate可以从哪里获取到这个session。它的默认值是jta,意为Hibernate从底层的Java Transaction API(JTA)获取session。不过本例中我们不使用JTA,所以我们设定它的值为thread,意为从当前线程存储和获取session。
为了简单起见,我们不想使用实体缓存。所以这里我们设置属性cache.provider_class值为org.hibernate.cache.internal.NoCacheProvider。
后面的两个选项告诉Hibernate输出每一条SQL语句到控制台,并且格式化使之具有更好的可读性。为了减轻开发工作中手工创建schema的负担,我们这里设置属性hbm2ddl.auto=create,使之在应用启动时自动创建所有表。
最后我们定义一个含有应用程序的所有映射信息的资源文件。这个文件的具体内容会在后面的章节介绍。
如上所述,session用于和数据库系统的交流,它实质上代表一个JDBC连接。这意味着这个连接中的所有交互都是通过这个session完成。它是单线程的,同时含有一个缓存,其中保存了其从启动至今所有交互过的对象。所以,应用程序的每个线程都应该在自己通过session factory获取到的session中工作。
相对于session,session factory是线程安全的,它为定义的所有映射提供了一个不可变的缓存。每个数据库只有一个session factory。可选的,session factory除了提供基于session的一级缓存,还可以提供基于application的二级缓存。
3.2. 事务(Transactions)
我们在上一节中的hibernate.cfg.xml中配置了事务的管理方式为手动管理。因此,我们必须手动开始、提交或回滚每一个事务。下面的代码演示了如何从session中获取一个新的事务及如何开始和提交事务:
try { Transaction transaction = session.getTransaction(); transaction.begin(); ... transaction.commit(); } catch (Exception e) { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); } throw e; }
在第一步中我们调用getTransaction()来获取一个新的事务。调用这个事务的begin()方法后,它会立即启动。如果随后的代码运行正常且没有任何异常,这个事务会得到提交。如果有异常发生而且当前事务是活动着的话,这个事务会回滚。
上面的这段代码和我们后面章节使用到的例子是一模一样的,我们不会一遍又一遍的重复这些所有步骤。这些代码可以使用比如模板设计模式(Template Pattern)重构以得到可重用的样式,我们这里就不做演示了,请读者自行完成。
3.3. 表(Tables)
现在我们已经了解了会话工厂(session factory)、会话(session)和事务(transaction),是时候开始第一个类映射的实例了。简单起见,我们先创建一个只有几个简单属性的简单类:
public class Person { private Long id; private String firstName; private String lastName; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
Person类有2个用来存储人名的属性(firstName和lastName)。其中的long型的字段id用来存储对象的唯一标识符。在本教程中我们将使用映射文件而不是注解,因此我们按照如下方式来定义Person类和T_PERSON表的映射关系:
XML元素hibernate-mapping用来定义我们的实体类所在的包(这里是hibernate.entity)。它内部的class元素用来定义和数据库表有映射关系的类。
元素id用来指定类中保存唯一性标识符的字段的名字,和保存这个值的列的名字。通过它的子元素generator,Hibernate可以知道如何为每个实体生成唯一的标识符。除了上面示例所示的native,Hibernate还支持许多其它策略。
策略native会为所使用的数据库产品自动选择最佳策略。所以这个策略可以用在不同的数据库产品中。其它的可能值包括:sequence(使用数据库中的序列),uuid(生成的128位的 UUID),和assigned(让应用程序自己指定特定值)。另外,除了这些预定义的策略外,我们也可以通过实现接口org.hibernate.id.IdentifierGenerator来实现自己的自定义策略。
通过XML元素property,定义字段firstName和lastName分别映射到列FIRST_NAME和LAST_NAME。属性name和column分别指定类中的字段名和数据库中的列名。
下面的代码演示了如何存储Person数据到数据库中:
private void persistPerson(Session session) throws Exception { try { Transaction transaction = session.getTransaction(); transaction.begin(); Person person = new Person(); person.setFirstName("Homer"); person.setLastName("Simpson"); session.save(person); transaction.commit(); } catch (Exception e) { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); } throw e; } }
代码中获取了transaction之后,创建了一个类Person的新实例,并设置它的字段firstName和lastName的值。最后,通过调用session的方法save()存储这个Person数据到数据库中。
我们执行上面的代码后,控制台会输出如下SQL语句:
Hibernate: drop table T_PERSON if exists Hibernate: create table T_PERSON ( ID bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (ID) ) Hibernate: insert into T_PERSON (ID, firstName, lastName, ID_ID_CARD) values (null, ?, ?, ?)
我们在之前的配置中已经设定了Hibernate在启动时自动删除和创建表,所以这里输出的第一个语句是drop table和create table。我们可以看到表T_PERSON有4个列:ID, FIRST_NAME, LAST_NAME, 和主键(ID)。
完成表的创建之后,调用session.save()会生成一条数据库的insert语句。由于Hibernate内部使用的是PreparedStatement,所以我们在控制台看不到这些值。如果你想看到绑定到PreparedStatement的那些参数的值的话,可以设置日志记录器org.hibernate.type的日志级别为FINEST。具体设置如文件logging.properties中如下内容(这个文件的路径设定可以通过-Djava.util.logging.config.file=src/main/resources/logging.properties完成):
.handlers = java.util.logging.ConsoleHandler .level = INFO java.util.logging.ConsoleHandler.level = ALL java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter org.hibernate.SQL.level = FINEST org.hibernate.type.level = FINEST
在这里设置记录器的org.hibernate.SQL和在Hibernate配置文件中设置属性show_sql=true具有相同的作用。
现在你可以在控制台看到如下输出和绑定的具体值:
DEBUG: insert into T_PERSON (ID, FIRST_NAME, LAST_NAME, ID_ID_CARD) values (null, ?, ?, ?) TRACE: binding parameter [1] as [VARCHAR] - [Homer] TRACE: binding parameter [2] as [VARCHAR] - [Simpson] TRACE: binding parameter [3] as [BIGINT] - [null]
4. 继承(Inheritance)
O/R映射解决方案比如Hibernate中,一个很有意思的特性是继承用法。用户可以自己选择如何将父类和其子类映射到关系数据库的表。Hibernate支持下面几个映射策略:
- Single table per class:父类和子类都映射到同一张表。使用一个额外的标识列来标识当前记录行是父类或子类的实例;那些父类中没有的字段对应的列会置空。
- Joined subclass:这个策略中为每个类使用单独的表,不过子类对应的表中只存储那些父类中没有的字段的值。如果要获取某个子类实例的所有值,就必须要执行两个个表的连接操作。
- Table per class: 这个策略也是为每个类使用单独的表,不过这里的子类对应的表中存储的值包含了其父类的所有字段。这样的话,子类的表中的记录包含了其所有的值,获取这些值的时候不再需要连接操作。
我们下面来探究一下“Single Table per class”策略。我们构建一个Person类的子类Geek:
public class Geek extends Person { private String favouriteProgrammingLanguage; public String getFavouriteProgrammingLanguage() { return favouriteProgrammingLanguage; } public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) { this.favouriteProgrammingLanguage = favouriteProgrammingLanguage; } }
这个类继承自已有的类Person,它额外增加了一个字段favoriteProgrammingLanguage。 这个用例的映射文件如下所示:
第一个不同点是引入的discriminator列。如上所示,这个列存储的信息指明当前实例是哪种类型。本例中我们称它为PERSON_TYPE,而且为了更好的可读性,设置它的值是表示实际类型的字符串。默认情况下Hibernate会使用类名。为了节省空间的话也可以使用integer型的列值。
除了discriminator外我们还增加了subclass元素,用来告知Hibernate新加入的Java类Geek和它的映射到列FAV_PROG_LANG的字段favoriteProgrammingLanguage。
下面的代码演示了如何在数据库中存储Geek的实例:
session.getTransaction().begin(); Geek geek = new Geek(); geek.setFirstName("Gavin"); geek.setLastName("Coffee"); geek.setFavouriteProgrammingLanguage("Java"); session.save(geek); geek = new Geek(); geek.setFirstName("Thomas"); geek.setLastName("Micro"); geek.setFavouriteProgrammingLanguage("C#"); session.save(geek); geek = new Geek(); geek.setFirstName("Christian"); geek.setLastName("Cup"); geek.setFavouriteProgrammingLanguage("Java"); session.save(geek); session.getTransaction().commit();
执行上面的代码,可以看到如下输出:
Hibernate: drop table T_PERSON if exists Hibernate: create table T_PERSON ( ID bigint generated by default as identity, PERSON_TYPE varchar(255) not null, FIRST_NAME varchar(255), LAST_NAME varchar(255), FAV_PROG_LANG varchar(255), primary key (ID) ) Hibernate: insert into T_PERSON (ID, FIRST_NAME, LAST_NAME, FAV_PROG_LANG, PERSON_TYPE) values (null, ?, ?, ?, 'hibernate.entity.Geek')
相较于前例,表T_PERSON现在含有了2个新列:PERSON_TYPE和FAV_PROG_LANG。列PERSON_TYPE对应于Geek类型的值是hibernate.entity.Geek。
为了研究表T_PERSON中的内容,我们利用H2的jar文件附带提供的一个Shell应用:
> java -cp h2-1.3.176.jar org.h2.tools.Shell -url jdbc:h2:~/hibernate ... sql> select * from t_person; ID | PERSON_TYPE | FIRST_NAME | LAST_NAME | FAV_PROG_LANG 1 | hibernate.entity.Person | Homer | Simpson | null 2 | hibernate.entity.Geek | Gavin | Coffee | Java 3 | hibernate.entity.Geek | Thomas | Micro | C# 4 | hibernate.entity.Geek | Christian | Cup | Java
和之前讨论的一样,列PERSON_TYPE存储实例的类型值;对于父类Person的实例,列FAV_PROG_LANG的对应值是null。
如果我们按照如下所示更改映射定义,Hibernate将会为父类和子类创建各自单独的表:
XML元素joined-subclass告诉Hibernate,要为子类Geek创建多了额外列ID_PERSON的表T_GEEK。这个额外的键列存储了表T_PERSON的外键,用来关联T_GEEK中每一行数据到它的T_PERSON中的父记录。
使用上面的Java代码存储一些Geek数据到数据库,会在控制台产生如下输出:
Hibernate: drop table T_GEEK if exists Hibernate: drop table T_PERSON if exists Hibernate: create table T_GEEK ( ID_PERSON bigint not null, FAV_PROG_LANG varchar(255), primary key (ID_PERSON) ) Hibernate: create table T_PERSON ( ID bigint generated by default as identity, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (ID) ) Hibernate: alter table T_GEEK add constraint FK_p2ile8qooftvytnxnqtjkrbsa foreign key (ID_PERSON) references T_PERSON
现在Hibernate创建了两个表,而不是之前的一个表,而且表T_GEEK中定义了指向表T_PERSON的外键。表T_GEEK包含两列:ID_PERSON指向对应的Person,FAV_PROG_LANG用来存储最喜爱的编程语言。
现在在数据库中存储Geek数据的话需要两个insert语句:
Hibernate: insert into T_PERSON (ID, FIRST_NAME, LAST_NAME, ID_ID_CARD) values (null, ?, ?, ?) Hibernate: insert into T_GEEK (FAV_PROG_LANG, ID_PERSON) values (?, ?)
第一个语句往表T_PERSON中插入一条新纪录,第二个语句往表T_GEEK中插入一条新纪录。于是现在两个表的内容看起来是这样:
sql> select * from t_person; ID | FIRST_NAME | LAST_NAME 1 | Homer | Simpson 2 | Gavin | Coffee 3 | Thomas | Micro 4 | Christian | Cup sql> select * from t_geek; ID_PERSON | FAV_PROG_LANG 2 | Java 3 | C# 4 | Java
显然表T_PERSON只存储父类所拥有的那些属性,而表T_GEEK只存储子类的字段。列ID_PERSON指向父表中对应记录的引用。
下一个要研究的策略是“table per class”。与上一个策略类似,这个策略依然为每个类创建单独的表,所不同的是,子类对应的表同时包含了其父类中的所有列。于是这样的表中的一条记录,包含了所有的用来构建本类型实例的值,而不需要从父表中获取额外的数据。在大数据环境下,这样可以提高查询性能,因为连接操作需要去额外查找父表中的对应记录。这样的策略就规避了这种额外的查找时间。
为了在上述用例中使用这种策略,可以改写映射文件如下:
XML元素union-subclass提供了实体(Geek)的名字和此实体对应的表(T_GEEK)。和其它处理方案一样,字段favoriteProgrammingLanguage声明为subclass的一个属性。
相较于其它处理方案的另一个重要变化,位于定义id生成器的配置行。曾经提到过的一个方案使用的是native生成器,它依赖于H2生成标识列;而本方案设定的生成器会生成在所有表中(T_PERSON和T_GEEK)都独一无二的标识符。
标识列只是一种特殊类型的、可以自动为每个记录行生成新id的列。但是在这两个表中我们都设置了标识列,这就可能导致表T_PERSON中的id值和表T_GEEK中的id值相同。这就和我们的需求就有了冲突:我们的需求是,只读取表T_GEEK中的一条记录就能构造Geek的实体,而且要保证所有的Person和Geek都是独一无二的。所以这里我们改而使用sequence,相应的配置是更改属性class的值native为sequence。
现在Hibernate生成的DDL语句看起来如下:
Hibernate: drop table T_GEEK if exists Hibernate: drop table T_PERSON if exists Hibernate: drop sequence if exists hibernate_sequence Hibernate: create table T_GEEK ( ID bigint not null, FIRST_NAME varchar(255), LAST_NAME varchar(255), FAV_PROG_LANG varchar(255), primary key (ID) ) Hibernate: create table T_PERSON ( ID bigint not null, FIRST_NAME varchar(255), LAST_NAME varchar(255), primary key (ID) ) Hibernate: create sequence hibernate_sequence
上面的输出,清晰的显示了表T_GEEK中除了列FAV_PROG_LANG同时也包含了父类的所有列(FIRST_NAME和LAST_NAME)。上面的语句并没有创建两个表之间的外键。请注意,现在ID列不再是标识列,同时创建了一个sequence。
往数据库中写入一个Person和一个Geek会生成如下语句:
Hibernate: call next value for hibernate_sequence Hibernate: insert into T_PERSON (FIRST_NAME, LAST_NAME, ID) values (?, ?, ?, ?) Hibernate: call next value for hibernate_sequence Hibernate: insert into T_GEEK (FIRST_NAME, LAST_NAME, FAV_PROG_LANG, ID) values (?, ?, ?, ?, ?)
显然在存储一个Person和一个Geek数据时我们只有两个insert语句。表T_GEEK中的记录只需一条insertion操作完成,即包括了一个Geek实例的所有值:
sql> select * from t_person; ID | FIRST_NAME | LAST_NAME 1 | Homer | Simpson sql> select * from t_geek; ID | FIRST_NAME | LAST_NAME | FAV_PROG_LANG 3 | Gavin | Coffee | Java 4 | Thomas | Micro | C# 5 | Christian | Cup | Java
5. 关系(Relationships)
目前为止我们只看到了表间的“扩展(extends)”关系。在继承关系之外,Hibernate还提供了基于集合的映射关系,即在一个实体内含有另一个实体实例的集合。主要有如下三种关系:
- 一对一: 这是比较简单的关系,其中A类型的实体仅输入B类型的实体。
- 多对一: 顾名思义,这个关系包括的情况是,A类型的实体含有许多B类型的实体集合。
- 多对多: 这种关系下,A类型的多个实体可以属于B类型的多个实体。
为了更好的理解这些实体关系,我们在后面会详细讲解。
5.1. 一对一
为了演示“one to one”,我们在实体模型中加入下面这个类:
public class IdCard { private Long id; private String idNumber; private Date issueDate; private boolean valid; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getIdNumber() { return idNumber; } public void setIdNumber(String idNumber) { this.idNumber = idNumber; } public Date getIssueDate() { return issueDate; } public void setIssueDate(Date issueDate) { this.issueDate = issueDate; } public boolean isValid() { return valid; } public void setValid(boolean valid) { this.valid = valid; } }
每个IdCard都是一个内部的具有唯一性的标识,它含有外部的idNumber、发行日期,和一个表示此卡是否有效的布尔标志位。
这个关系的另一端的Person会有一个新的字段idCard指向当前Person所拥有的卡的引用:
public class Person { ... private IdCard idCard; ... public IdCard getIdCard() { return idCard; } public void setIdCard(IdCard idCard) { this.idCard = idCard; }
在Hibernate的具体映射文件中定义这个关系的映射,如下所示:
首先我们为这个新的类增加一个新的class元素,它指定了这个新类的名字和它所对应的表名(这里是T_ID_CARD)。字段id是具有唯一性的标识符,它的值定义为序列(sequence)。
另外一点,Person的映射新增了一个XML元素many-to-one,其中的name属性定义了类Person中存储其所对应的IdCard的引用。可选的属性column用来定义表T_PERSON中指向idCard的外键列的确切名字。我们这里所定义的关系类型是“one to one”,所以设置此处的属性unique值为true。
执行如上配置会产生如下的DDL语句(请注意,为了减少所产生的表的数量, 我们这里已经切换到了“Single table per class”策略,这个策略将为父类和子类生成一个相同的表):
Hibernate: drop table T_ID_CARD if exists Hibernate: drop table T_PERSON if exists Hibernate: drop sequence if exists hibernate_sequence Hibernate: create table T_ID_CARD ( ID bigint not null, ID_NUMBER varchar(255), ISSUE_DATE timestamp, VALID boolean, primary key (ID) ) Hibernate: create table T_PERSON ( ID bigint not null, PERSON_TYPE varchar(255) not null, FIRST_NAME varchar(255), LAST_NAME varchar(255), ID_ID_CARD bigint, FAV_PROG_LANG varchar(255), primary key (ID) ) Hibernate: alter table T_PERSON add constraint UK_96axqtck4kc0be4ancejxtu0p unique (ID_ID_CARD) Hibernate: alter table T_PERSON add constraint FK_96axqtck4kc0be4ancejxtu0p foreign key (ID_ID_CARD) references T_ID_CARD Hibernate: create sequence hibernate_sequence
在前面的列子中所发生的变化是:表T_PERSON现在包含了一个额外的列ID_ID_CARD,它用来定义表T_ID_CARD的外键。表T_ID_CARD自身包含预期的三个列:ID_NUMBER, ISSUE_DATE, 和 VALID。
来看一段演示代码,看如何插入一条含有IdCard的Person记录:
Person person = new Person(); person.setFirstName("Homer"); person.setLastName("Simpson"); session.save(person); IdCard idCard = new IdCard(); idCard.setIdNumber("4711"); idCard.setIssueDate(new Date()); person.setIdCard(idCard); session.save(idCard);
创建IdCard的实例比较简单直接,不过这里需要注意的是,从Person到IdCard的引用是在最后一步完成的,不过这些操作都处于同一个事务中。所有的实例都要通过Hibernate的save()方法进行持久化。
如果再深究上述代码的细节的话,有人可能会质疑为什么我们要把所有的实例都传递到当前会话的save()方法。这是事出有因的,因为Hibernate有如下要求:当处理完整的实体图时,某些特定操作要“级联”执行。为了建立和IdCard的级联关系,我们只需要在映射文件中在元素many-to-one里增加一个属性cascade:
设置其值为all,是告诉Hibernate级联所有类型的操作。不过这并不总是处理实体间关系时的首选方式,你也可以选择设定一些特定的操作:
上面的例子演示了如何配置映射关系,才能只级联save()、saveOrUpdate()和refresh(重新从数据库读取给定对象的状态)操作。其它Hibernate操作比如delete()或lock()则不会被转发。
使用上述两种配置方式之一,来演示一下存储一个Person和他对应IdCard数据的代码可以重写如下:
Person person = new Person(); person.setFirstName("Homer"); person.setLastName("Simpson"); IdCard idCard = new IdCard(); idCard.setIdNumber("4711"); idCard.setIssueDate(new Date()); person.setIdCard(idCard); session.save(person);
在本例中,除了使用方法save(),也可以使用saveOrUpdate()来替代。方法saveOrUpdate()的作用在于,它可以用于更新一个已存在的实体信息。二者之间的微小区别是,save()方法会返回新创建的实体的标识符:
Long personId = (Long) session.save(person);
有一种情况下这个方法会很有用:当我们在服务端代码中,需要返回给方法调用者当前对象的标识符。另一方面,方法update()不会返回这个标识符,因为它假设当前实体已经存储在数据库中了所以它必定已经有了标识符。如果试图在一个没有标识符的实体上执行update操作会抛出异常:
org.hibernate.TransientObjectException: The given object has a null identifier: ...
所以,saveOrUpdate()的好处就在这里,它能够操作于所有实体对象,而不用考虑当前实体是否已经存在于数据库中。
5.2. 一对多
另一个O/R映射中经常出现的关系是“一对多”关系。在这种情况下,一组实体属于另一种类型的实体。为了建模这种关系,我们添加类Phone:
public class Phone { private Long id; private String number; private Person person; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public Person getPerson() { return person; } public void setPerson(Person person) { this.person = person; } }
通常情况下Phone实体有一个内部标识符(ID)和一个用来存储实际电话号码的字段。字段person则存储着实际拥有这个Phone的Person的引用。一个Person可以拥有一个以上的Phone,所以我们在Person类中增加一个集合(Set),用于存储此人拥有的所有手机:
public class Person { ... private Set phones = new HashSet(); ... public Set getPhones() { return phones; } public void setPhones(Set phones) { this.phones = phones; } }
相应的,如下所示更新映射文件:
...
上面所示配置信息显示了类Phone对应的映射信息。其中含有标识符(id)和字段number,id设置为使用sequence产生;另外还含有many-to-one元素。相较于我们之前介绍的“一对一”关系,这里的属性unique设置为false。除此之外,属性column定义了外键对应列的名字,属性cascade定义了Hibernate在此种关系中如何做级联操作。
执行以上这些配置文件后,可以看到如下DDL语句:
... Hibernate: drop table T_PERSON if exists Hibernate: drop table T_PHONE if exists ... Hibernate: create table T_PERSON ( ID bigint not null, PERSON_TYPE varchar(255) not null, FIRST_NAME varchar(255), LAST_NAME varchar(255), ID_ID_CARD bigint, FAV_PROG_LANG varchar(255), primary key (ID) ) Hibernate: create table T_PHONE ( ID bigint not null, NUMBER varchar(255), ID_PERSON bigint, primary key (ID) ) ... Hibernate: alter table T_PHONE add constraint FK_dvxwd55q1bax99ibyw4oxa8iy foreign key (ID_PERSON) references T_PERSON ...
除了表T_PERSON外,Hibernate同时创建了新表T_PHONE,它有三个列:ID,NUMBER和ID_PERSON。最后面的列用来存储对Person的引用,所以Hibernate也为表T_PHONE增加了一个外键约束,限定了此列指向表T_PERSON中的ID列。
为了给一些已存在的Person添加一个电话号码,首先我们查询出一个Person,然后向其添加Phone:
session.getTransaction().begin(); List resultList = session.createQuery("from Person as person where person.firstName = ?").setString(0, "Homer").list(); for (Person person : resultList) { Phone phone = new Phone(); phone.setNumber("+49 1234 456789"); session.persist(phone); person.getPhones().add(phone); phone.setPerson(person); } session.getTransaction().commit();
这个例子展示了如何使用Hibernate查询语言(HQL)从数据库中加载一个Person数据。与SQL类似,这个查询也是由一个from子句和一个where子句组成。这里的列FIRST_NAME的引用使用的不是它的SQL名字,而是它对应的Java字段/属性的名字。这些参数比如firstName,可以通过setString()方法传递到当前查询中。
其后的代码遍历所有查询到的Person(应该只有一个),然后创建一个Phone的新实例,并将其添加到当前Person的Phone集合中。在事务提交之前,也要设置从Phone指向Person的连接。执行上述代码后,数据库中结果集如下所示:
sql> select * from t_person where first_name = 'Homer'; ID | PERSON_TYPE | FIRST_NAME | LAST_NAME | ID_ID_CARD | FAV_PROG_LANG 1 | hibernate.entity.Person | Homer | Simpson | 2 | null sql> select * from t_phone; ID | NUMBER | ID_PERSON 6 | +49 1234 456789 | 1
从这两个select语句的执行结果可以看到,T_PHONE中的记录行和T_PERSON中的记录行具有连接关系,它的列ID_PERSON中的存储值就是firstName=“Homer”的Person的id值。
5.3. 多对多
我们来看另一个很有意思的关系-“多对多”。在这种情况下,多个类型A的实体可以属于多个类型B的实体,反之亦然。在实践中可以以Geek和Project为例。一个Geek可以工作于多个Project(同步执行或顺序执行),一个Project也可以包括多于一个的Geek。我们来引入一个新的实体Project:
public class Project { private Long id; private String title; private Set geeks = new HashSet(); public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Set getGeeks() { return geeks; } public void setGeeks(Set geeks) { this.geeks = geeks; } }
它含有标识符(id),title字段和一个Geek集合。这个关系的另一端的类Geek含有一个Project集合:
public class Geek extends Person { private String favouriteProgrammingLanguage; private Set projects = new HashSet(); public String getFavouriteProgrammingLanguage() { return favouriteProgrammingLanguage; } public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) { this.favouriteProgrammingLanguage = favouriteProgrammingLanguage; } public Set getProjects() { return projects; } public void setProjects(Set projects) { this.projects = projects; } }
关系对应的映射文件配置如下:
...
首先设置的是映射到表T_PROJECT的类Project。它的唯一标识符存储在字段id中,字段title存储在列TITLE中。XML元素set定义了映射关系的一端:Geek集合中的所有项应存储在一个单独的表T_GEEKS_PROJECTS中,表中有两个列ID_PROJECT和ID_GEEK。关系的另一端,Geek的配置位于subclass元素,其中又包含了元素set,这里定义了反转关系(inverse=”true”)。这一端的类Geek中的集合字段是projects,引用的类是Project。
如上配置会生成如下的建表语句:
... Hibernate: drop table T_GEEKS_PROJECTS if exists Hibernate: drop table T_PROJECT if exists ... Hibernate: create table T_GEEKS_PROJECTS ( ID_PROJECT bigint not null, ID_GEEK bigint not null, primary key (ID_PROJECT, ID_GEEK) ) Hibernate: create table T_PROJECT ( ID bigint not null, TITLE varchar(255), primary key (ID) ) ... Hibernate: alter table T_GEEKS_PROJECTS add constraint FK_2kp3f3tq46ckky02pshvjngaq foreign key (ID_GEEK) references T_PERSON Hibernate: alter table T_GEEKS_PROJECTS add constraint FK_36tafu1nw9j5o51d21xm5rqne foreign key (ID_PROJECT) references T_PROJECT ...
这些语句会创建新表T_PROJECT和T_GEEKS_PROJECTS。表T_PROJECT含有列ID和TITLE,其中ID列的值对应于表T_GEEKS_PROJECTS中ID_PROJECT列的值。这个表的第二个外键指向T_PERSON的主键。
举个例子,如果要往数据库中写入一个含有多个geek的Project信息,可以使用如下的Java代码:
session.getTransaction().begin(); List resultList = session.createQuery("from Geek as geek where geek.favouriteProgrammingLanguage = ?").setString(0, "Java").list(); Project project = new Project(); project.setTitle("Java Project"); for (Geek geek : resultList) { project.getGeeks().add(geek); geek.getProjects().add(project); } session.save(project); session.getTransaction().commit();
开始时查询出所有以“Java”作为最喜爱的编程语言的Geek们。然后创建一个Project的新实例,并将之前结果集中的所有Geek添加到这个Project实例的Geek集合中。在关系的另一端,这个Project实例也被添加到每个Geek的Project集合中。最后保存这个Project实例,提交当前事务。
执行这些操作后,数据库中含有如下结果:
sql> select * from t_person; ID | PERSON_TYPE | FIRST_NAME | LAST_NAME | ID_ID_CARD | FAV_PROG_LANG 1 | hibernate.entity.Person | Homer | Simpson | 2 | null 3 | hibernate.entity.Geek | Gavin | Coffee | null | Java 4 | hibernate.entity.Geek | Thomas | Micro | null | C# 5 | hibernate.entity.Geek | Christian | Cup | null | Java sql> select * from t_project; ID | TITLE 7 | Java Project sql> select * from t_geeks_projects; ID_PROJECT | ID_GEEK 7 | 5 7 | 3
第一个查询结果显示只有id为3和5的Geek选择Java作为他们最喜欢的编程语言。因此名字为“Java Project”的项目(id:7)含有两个id分别为3和5的Geek(最后一个查询结果)。
5.4. 组件(Component)
面向对象的设计规则建议提取通用的字段到一个单独的类中。上面中的类Project还缺少一个开始和结束日期属性。但是这样的日期段属性也可能用于其它实体,所以这里我们创建一个新的名为Period的类用于封装这两个字段startDate和endDate:
public class Period { private Date startDate; private Date endDate; public Date getStartDate() { return startDate; } public void setStartDate(Date startDate) { this.startDate = startDate; } public Date getEndDate() { return endDate; } public void setEndDate(Date endDate) { this.endDate = endDate; } } public class Project { ... private Period period; ... public Period getPeriod() { return period; } public void setPeriod(Period period) { this.period = period; } }
不过我们并不想Hibernate为Peroid创建一个单独的表,因为每一个Project只会有一个确切的开始和结束日期,我们不想要引入额外的连接查询操作。在本例中,Hibernate可以将嵌入类Peroid的两个字段映射到和Project同一张表:
... ...
将嵌入类映射到表T_PROJECT的字段的方法就是,使用component元素,并在name属性上设置Project类的字段的名字。类Peroid的两个字段就被设定在了component的属性里。
执行后将生成如下DDL语句:
... Hibernate: create table T_PROJECT ( ID bigint not null, TITLE varchar(255), START_DATE timestamp, END_DATE timestamp, primary key (ID) ) ...
虽然字段START_DATE和END_DATE位于一个单独的类,Hibernate还是将它们添加到了表T_PROJECT。下面的代码,创建了一个新的Project,而且为它添加了Period:
Project project = new Project(); project.setTitle("Java Project"); Period period = new Period(); period.setStartDate(new Date()); project.setPeriod(period); ... session.save(project);
这将生成如下结果:
sql> select * from t_project; ID | TITLE | START_DATE | END_DATE 7 | Java Project | 2015-01-01 19:45:12.274 | null
不需要增加另外的代码去加载Project的Peroid信息,这些信息会自动加载并初始化:
List projects = session.createQuery("from Project as p where p.title = ?") .setString(0, "Java Project").list(); for (Project project : projects) { System.out.println("Project: " + project.getTitle() + " starts at " + project.getPeriod().getStartDate()); }
为了防止数据库中Period的所有字段设置为NULL,Hibernate也设置了Peroid的引用为null。
6. 用户自定义数据类型
我们工作中有时会使用到遗留数据库,而有可能发现其中某些列使用到的建模方式和Hibernate中的方式不一样。例如, Boolean数据类型映射为H2数据库中boolean类型。如果原来的开发团队当初决定使用字符类型的“0”和“1”来映射boolean值得花,Hibernate中可以实现用户自定义类型来实现这样的映射关系。实现中需要用到Hibernate的接口org.hibernate.usertype.UserType:
public interface UserType { int[] sqlTypes(); Class returnedClass(); boolean equals(Object var1, Object var2) throws HibernateException; int hashCode(Object var1) throws HibernateException; Object nullSafeGet(ResultSet var1, String[] var2, SessionImplementor var3, Object var4) throws HibernateException, SQLException; void nullSafeSet(PreparedStatement var1, Object var2, int var3, SessionImplementor var4) throws HibernateException, SQLException; Object deepCopy(Object var1) throws HibernateException; boolean isMutable(); Serializable disassemble(Object var1) throws HibernateException; Object assemble(Serializable var1, Object var2) throws HibernateException; Object replace(Object var1, Object var2, Object var3) throws HibernateException; }
我们来看一个简单的实现类代码:
@Override public boolean equals(Object x, Object y) throws HibernateException { if (x == null) { return y == null; } else { return y != null && x.equals(y); } } @Override public int hashCode(Object o) throws HibernateException { return o.hashCode(); } @Override public Object deepCopy(Object o) throws HibernateException { return o; } @Override public boolean isMutable() { return false; } @Override public Serializable disassemble(Object o) throws HibernateException { return (Serializable) o; } @Override public Object assemble(Serializable cached, Object owner) throws HibernateException { return cached; } @Override public Object replace(Object original, Object target, Object owner) throws HibernateException { return original; }
来看一下UserType中很有意思的两个方法:nullSafeGet()和nullSafeSet():
@Override public Object nullSafeGet(ResultSet resultSet, String[] strings, SessionImplementor sessionImplementor, Object o) throws HibernateException, SQLException { String str = (String) StringType.INSTANCE.nullSafeGet(resultSet, strings[0], sessionImplementor, o); if ("1".equals(str)) { return Boolean.TRUE; } return Boolean.FALSE; } @Override public void nullSafeSet(PreparedStatement preparedStatement, Object value, int i, SessionImplementor sessionImplementor) throws HibernateException, SQLException { String valueToStore = "0"; if (value != null) { Boolean booleanValue = (Boolean) value; if (booleanValue.equals(Boolean.TRUE)) { valueToStore = "1"; } } StringType.INSTANCE.nullSafeSet(preparedStatement,valueToStore, i, sessionImplementor); }
方法nullSafeGet()实际是使用了Hibernate的StringType的实现逻辑来从数据库查询返回的ResultSet中抽取出boolean值的字符串形式。如果返回值等于“1”,那么这个方法返回“true”,否则返回“false”。在insert语句执行之前,参数中包含的boolean值首先会被“解码”为字符串“1”或“0”。然后方法nullSafeSet()使用Hibernate的StringType实现逻辑去设定PreparedStatement中的字符值。
最后,我们还要告诉HibernatenullSafeGet()返回的对象类型和此类型所对应的数据列的类型:
@Override public int[] sqlTypes() { return new int[]{ Types.VARCHAR }; } @Override public Class returnedClass() { return Boolean.class; }
完成UserType接口的实现类所有逻辑后,可以在Configuration中注册这个实现类的实例了:
Configuration configuration = new Configuration(); configuration.configure("hibernate.cfg.xml"); configuration.registerTypeOverride(new MyBooleanType(), new String[]{"MyBooleanType"}); ...
这里的MyBooleanType就是我们自定义的UserType接口的实现类,另一个参数String数组定义了在映射文件中如何引用这个类型:
...
从上面的配置文件可以看到,新定义的类型MyBooleanType用来定义表T_ID_CARD的boolean属性:
sql> select * from t_id_card; ID | ID_NUMBER | ISSUE_DATE | VALID 2 | 4711 | 2015-03-27 11:49:57.533 | 1
7. 拦截器(Interceptors)
有些项目可能会有这样的需求:对于每一个实体/表,它的所有记录的创建和最后更新的时间戳应该可以追踪。在每个实体的所有insert和update操作中都设置这两个值,确实是个单调的、繁琐的工作。因此,Hibernate提供了拦截器(interceptor)功能,它可以在每个insert或update操作之前调用执行。这样的话,就可以在我们的代码库中,将设置创建和更新时间戳的代码抽取到一个单独的类中,而不用在每个需要这个功能的地方拷贝一次。
下面我们来看一个示例,来追踪Project实体的创建和更新的时间戳。通过继承类EmptyInterceptor来实现:
public class AuditInterceptor extends EmptyInterceptor { @Override public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { if (entity instanceof Auditable) { for ( int i=0; i < propertyNames.length; i++ ) { if ( "created".equals( propertyNames[i] ) ) { state[i] = new Date(); return true; } } return true; } return false; } @Override public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) { if (entity instanceof Auditable) { for ( int i=0; i < propertyNames.length; i++ ) { if ( "lastUpdate".equals( propertyNames[i] ) ) { currentState[i] = new Date(); return true; } } return true; } return false; } }
因为类EmptyInterceptor已经实现了接口Interceptor中的所有方法,我们这里只需要重写方法onSave()和onFlushDirty()。为了方便的获得所有拥有字段created和lastUpdated的实体,我们将这些实体的getter和setter方法抽取到一个单独的接口Auditable:
public interface Auditable { Date getCreated(); void setCreated(Date created); Date getLastUpdate(); void setLastUpdate(Date lastUpdate); }
有了这个接口,就很容易检查当前interceptor所拦截的实例是不是Auditable类型。不幸的是我们不能直接通过getter和setter方法来修改实体,不过我们可以通过两个数组propertyNames和state来完成。在数组propertyNames中,我们需要找到属性created(lastUpdate)然后使用它的所处index来设置数组state(currentState)中的对应元素。
如果映射文件中没有定义好这些属性的话,Hibernate就不能在表中创建这些列。因此,需要如下更新映射文件:
... ...
从上面的示例可以看到,两个新增加的属性created和lastUpdate都是timestamp类型的:
sql> select * from t_person; ID | PERSON_TYPE | FIRST_NAME | LAST_NAME | CREATED | LAST_UPDATE | ID_ID_CARD | FAV_PROG_LANG 1 | hibernate.entity.Person | Homer | Simpson | 2015-01-01 19:45:42.493 | null | 2 | null 3 | hibernate.entity.Geek | Gavin | Coffee | 2015-01-01 19:45:42.506 | null | null | Java 4 | hibernate.entity.Geek | Thomas | Micro | 2015-01-01 19:45:42.507 | null | null | C# 5 | hibernate.entity.Geek | Christian | Cup | 2015-01-01 19:45:42.507 | null | null | Java
8. 源码下载
以上就是我们讲解的一个关于JBoss Hibernate的入门教程。
Translated by: Vincent Jia |
This post is a translation of Hibernate Tutorial – The ULTIMATE Guide from Martin Mois |