hibernate
编者注:在本文中,我们提供了全面的Hibernate教程。 Hibernate ORM(简称Hibernate)是一个对象关系映射框架,它有助于将面向对象的域模型转换为传统的关系数据库。 Hibernate通过用高级对象处理功能代替直接持久性相关的数据库访问,解决了对象关系阻抗不匹配的问题。
Hibernate是目前最流行的Java框架之一。 由于这个原因,我们在Java Code Geeks上提供了很多教程,大多数教程可以在此处找到。
现在,我们希望创建一个独立的参考文章,以提供有关如何使用Hibernate的框架,并帮助您快速启动Hibernate应用程序。 请享用!
目录
介绍
Hibernate是Java世界中最流行的对象/关系映射(ORM)框架之一。 它允许开发人员将普通Java类的对象结构映射到数据库的关系结构。 借助ORM框架,将对象实例中的数据从内存中存储到持久数据存储并将它们加载回同一对象结构的工作变得非常容易。
同时,像Hibernate这样的ORM解决方案旨在从用于存储数据的特定产品中抽象出来。 这允许将相同的Java代码用于不同的数据库产品,而无需编写处理受支持产品之间细微差别的代码。
Hibernate还是JPA提供程序,这意味着它实现了Java Persistence API(JPA) 。 JPA是用于将Java对象映射到关系数据库表的独立于供应商的规范。 由于Ultimate系列的另一篇文章已经介绍了JPA,因此本文重点介绍Hibernate,因此不使用JPA批注,而是使用Hibernate特定的配置文件。
Hibernate由三个不同的组件组成:
- 实体:Hibernate映射到关系数据库系统表的类是简单的Java类(普通的旧Java对象)。
- 对象关系元数据:如何将实体映射到关系数据库的信息由注释(自Java 1.5开始)或基于XML的旧式配置文件提供。 这些文件中的信息在运行时用于执行到数据存储和Java对象的映射。
- Hibernate查询语言(HQL) :使用Hibernate时,不必使用本机SQL来编写发送到数据库的查询,而是可以使用Hibernate的查询语言进行指定。 由于这些查询在运行时转换为所选产品的当前使用的方言,因此以HQL编写的查询独立于特定供应商SQL方言。
在本教程中,我们将研究框架的各个方面,并将开发一个简单的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属性,并在以后的“依赖关系”部分中对其进行引用。
3.基础知识
SessionFactory和Session
现在,我们开始实施第一个O / R映射。 让我们从一个简单的类开始,该类提供在应用程序的main
方法中调用的run()
方法:
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文件的根目录中。 这样,可以在运行时在类路径上找到文件。
第二步, run()
方法构造一个使用先前加载的配置的ServiceRegistry
。 现在可以将此ServiceRegistry
的实例作为参数传递给Configuration
buildSessionFactroy()
方法。 现在,可以使用该SessionFactory
获取存储实体并将实体加载到基础数据存储所需的会话。
配置文件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
从上面的示例中可以看到,配置文件为会话工厂定义了一组属性。 第一个属性connection.driver_class
指定应使用的数据库驱动程序。 在我们的示例中,这是H2数据库的驱动程序。 通过属性connection.url
,可以指定JDBC-URL。 在我们的案例中,定义了我们要使用h2,并且H2存储其数据的单个数据库文件应位于用户的主目录中,并应命名为hibernate
( ~/hibernate
)。 因为我们要自己在示例代码中提交事务,所以我们还定义了特定于H2的配置选项AUTOCOMMIT=OFF
。
接下来,配置文件定义数据库连接的用户名和密码,以及连接池的大小。 我们的示例应用程序仅在一个线程中执行代码,因此我们可以将池大小设置为1。 如果应用程序必须处理多个线程和多个用户,则必须选择适当的池大小。
属性dialect
指定一个Java类,该类执行转换为数据库特定SQL方言。
从3.1版开始,Hibernate提供了一个名为SessionFactory.getCurrentSession()
的方法,该方法允许开发人员获取对当前会话的引用。 使用配置属性current_session_context_class
可以配置Hibernate从中获取此会话的位置。 此属性的默认值为jta
,表示Hibernate从基础Java事务API(JTA)获取会话。 由于在此示例中未使用JTA,因此我们使用配置值thread
指示Hibernate存储与当前线程之间的会话并从中检索会话。
为了简单起见,我们不想使用实体缓存。 因此,我们将属性cache.provider_class
为org.hibernate.cache.internal.NoCacheProvider
。
以下两个选项告诉Hibernate将每个SQL语句输出到控制台并对其进行格式化,以提高可读性。 为了减轻开发人员的负担,可以手动创建模式,我们指示Hibernate使用选项hbm2ddl.auto
设置为create
以在启动期间创建所有表。
最后但并非最不重要的一点是,我们定义了一个映射资源文件,其中包含我们应用程序的所有映射信息。 该文件的内容将在以下各节中说明。
如上所述,会话用于与数据存储进行通信,实际上代表了JDBC连接。 这意味着与连接的所有交互都通过会话完成。 它是单线程的,并为其迄今为止使用过的所有对象提供缓存。 因此,应用程序中的每个线程都应使用从会话工厂获取的自己的会话。
与会话相反,会话工厂是线程安全的,并为定义映射提供了不可变的缓存。 对于每个数据库,只有一个会话工厂。 可选地,会话工厂除了可以提供会话的第一级缓存外,还可以提供应用程序范围的第二级缓存。
交易次数
在hibernate.cfg.xml
配置文件中,我们已配置为自己管理事务。 因此,我们必须手动启动和提交或回滚每个事务。 以下代码演示了如何从会话中获取新事务以及如何启动和提交该事务:
try {
Transaction transaction = session.getTransaction();
transaction.begin();
...
transaction.commit();
} catch (Exception e) {
if (session.getTransaction().isActive()) {
session.getTransaction().rollback();
}
throw e;
}
第一步,我们调用getTransaction()
来检索新事务的引用。 通过在其上调用begin()
方法,可以立即开始此事务。 如果以下代码毫无例外地继续进行,则事务将被提交。 如果发生异常并且当前事务处于活动状态,则将回滚该事务。
由于上面显示的代码在所有即将出现的示例中都是相同的,因此不再以确切的形式重复一次。 读者可以将使用例如模板模式将代码重构为可重用形式的步骤留给读者。
桌子
既然我们已经了解了会话工厂,会话和事务,那么现在就该开始进行一流的映射了。 为了轻松起步,我们选择一个只有几个简单属性的简单类:
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
类带有两个属性来存储人名( firstName
和lastName
)。 字段id
用于将对象的唯一标识符存储为长值。 在本教程中,我们将使用映射文件而不是注释,因此,我们将此类映射到表T_PERSON
指定如下:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="hibernate.entity">
<class name="Person" table="T_PERSON">
<id name="id" column="ID">
<generator class="native"/>
</id>
<property name="firstName" column="FIRST_NAME"/>
<property name="lastName" column="LAST_NAME"/>
</class>
</hibernate-mapping>
XML元素hibernate-mapping
用于定义实体所在的包(此处为hibernate.entity
)。 在此元素内,为每个类提供了一个class
元素,应将其映射到数据库中的表。
id
元素指定包含唯一标识符的类的字段的名称( name
)以及该值存储在( ID
)中的列的名称。 Hibernate通过其子元素generator
了解如何为每个实体创建唯一标识符。 在上面显示的native
值旁边,Hibernate支持一长串不同的策略。
本native
策略只是为所使用的数据库产品选择最佳策略。 因此,该策略可以应用于不同的产品。 其他可能的值例如是: sequence
(使用数据库中的序列), uuid
(生成128位UUID)并进行assigned
(让应用程序自行分配值)。 除了预定义的策略之外,还可以通过实现org.hibernate.id.IdentifierGenerator
接口来实现自定义策略。
使用XML元素property
,将firstName
和lastName
字段映射到FIRST_NAME
和LAST_NAME
列。 属性name
和column
定义了类和列中的字段名称。
以下代码显示了如何在数据库中存储人员的示例:
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;
}
}
在处理事务的代码旁边,它创建了Person
类的新实例,并将两个值分配给firstName
和lastName
字段。 最后,它通过调用会话的方法save()
将人员存储在数据库中。
当我们执行上述代码时,控制台上将打印以下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
的三列ID
, FIRST_NAME
和LAST_NAME
以及主键的定义(此处为ID
)。
创建表后,对session.save()
的调用将向数据库发出一条insert
语句。 由于Hibernate在内部使用PreparedStatement
,因此在控制台上看不到这些值。 如果您还想查看绑定到PreparedStatement
参数的值,则可以将logger 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.继承
像Hibernate这样的O / R映射解决方案的一个有趣的功能是继承的使用。 用户可以选择如何将超类和子类映射到关系数据库的表。 Hibernate支持以下映射策略:
- 每个类一个表:超类和子类都映射到同一张表。 附加列标记该行是超类还是子类的实例,并且超类中不存在的字段保留为空。
- 联接的子类:此策略为每个类使用单独的表,而子类的表仅存储超类中不存在的字段。 要检索子类实例的所有值,必须在两个表之间执行联接。
- 每个类的表:该策略还为每个类使用一个单独的表,但在子类的表中还存储了超类的字段。 通过这种策略,子类表中的一行包含所有值,并且为了检索所有值,不需要join语句。
我们将要研究的方法是“每班单一表”方法。 作为人员的子类,我们选择Geek
类:
public class Geek extends Person {
private String favouriteProgrammingLanguage;
public String getFavouriteProgrammingLanguage() {
return favouriteProgrammingLanguage;
}
public void setFavouriteProgrammingLanguage(String favouriteProgrammingLanguage) {
this.favouriteProgrammingLanguage = favouriteProgrammingLanguage;
}
}
该类扩展了已知的Person
类,并添加了一个名为favouriteProgrammingLanguage
的附加字段。 此用例的映射文件如下所示:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="hibernate.entity">
<class name="Person" table="T_PERSON">
<id name="id" column="ID">
<generator class="native"/>
</id>
<discriminator column="PERSON_TYPE" type="string"/>
<property name="firstName" column="FIRST_NAME"/>
<property name="lastName" column="LAST_NAME"/>
<subclass name="Geek" extends="Person">
<property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
</subclass>
</class>
</hibernate-mapping>
第一个区别是引入了discriminator
列。 如上所述,此列存储当前实例属于哪种类型的信息。 在我们的例子中,我们将其PERSON_TYPE
,为了更好的可读性,字符串表示实际类型。 在这种情况下,默认情况下,Hibernate仅采用类名。 为了节省存储空间,也可以使用整数类型的列。
除了区分FAV_PROG_LANG
之外,我们还添加了subclass
元素,该元素告知Hibernate新的Java类Geek
及其字段favouriteProgrammingLanguage
,这些字段应映射到FAV_PROG_LANG
列。
以下示例代码显示了如何在数据库中存储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
现在包含两个新列PERSON_TYPE
和FAV_PROG_LANG
。 列PERSON_TYPE
包含用于怪胎的值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
存储实例的类型,而列FAV_PROG_LANG
包含超类Person
实例的值null
。
以类似于以下内容的方式更改映射定义,Hibernate将为超类和子类创建一个单独的表:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="hibernate.entity">
<class name="Person" table="T_PERSON">
<id name="id" column="ID">
<generator class="native"/>
</id>
<property name="firstName" column="FIRST_NAME"/>
<property name="lastName" column="LAST_NAME"/>
<joined-subclass name="Geek" table="T_GEEK">
<key column="ID_PERSON"/>
<property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
</joined-subclass>
</class>
</hibernate-mapping>
XML元素joined-subclass
告诉Hibernate创建表T_GEEK
的子类Geek
与其他列ID_PERSON
。 这种额外的键列存储外键表T_PERSON
要想在每一行分配T_GEEK
在其父行T_PERSON
。
使用上面显示的Java代码在数据库中存储一些怪胎,会在控制台上产生以下输出:
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
用于引用相应的人)和FAV_PROG_LANG
用于存储喜欢的编程语言)。
现在,将极客存储在数据库中包括两个插入语句:
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
引用父表中的相应行。
正在研究的下一个策略是“每个班级分配表”。 与最后一种策略类似,该策略也为每个类创建了一个单独的表,但是相比之下,子类的表也包含超类的所有列。 因此,此类表中的一行包含所有值,以构造此类型的实例,而无需联接父表中的其他数据。 在庞大的数据集上,这可以提高查询性能,因为联接需要在父表中另外查找对应的行。 这种额外的查找花费了这种方法所避免的时间。
为了在上述用例中使用此策略,可以像下面这样重写映射文件:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="hibernate.entity">
<class name="Person" table="T_PERSON">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<property name="firstName" column="FIRST_NAME"/>
<property name="lastName" column="LAST_NAME"/>
<union-subclass name="Geek" table="T_GEEK">
<property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
</union-subclass>
</class>
</hibernate-mapping>
XML元素union-subclass
提供实体的名称( Geek
)以及单独的表的名称( T_GEEK
)作为属性。 与其他方法一样,将字段favouriteProgrammingLanguage
声明为子类的属性。
关于其他方法的另一个重要更改包含在定义id生成器的行中。 由于其他方法使用的是native
生成器,该生成器在H2上退回到一个身份列,因此此方法需要一个ID生成器,该生成器创建两个表( T_PERSON
和T_GEEK
)唯一的身份。
标识列只是一种特殊的列,它会自动为每一行创建一个新的ID。 但是,两个表,我们也有两个标识列以及与其在该IDS T_PERSON
表可以是相同的T_GEEK
表。 这与仅通过读取表T_GEEK
一行就可以创建Geek
类型的实体以及所有人和Geek的标识符都是唯一的要求相矛盾。 因此,我们使用的是序列,而不是通过切换对所述值标识列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
不再是标识列,而是创建了一个序列。
一个人和一个怪胎的插入向数据库发出以下语句:
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
(?, ?, ?, ?, ?)
对于一个人和一个极客,我们显然只有两个insert语句。 T_GEEK
表完全由一次插入填充,并且包含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.关系
到目前为止,我们看到的两个表之间的唯一关系是“扩展”表。 除了单纯的继承,Hibernate还可以映射基于列表的关系,其中一个实体具有另一个实体的实例列表。 区分以下类型的关系:
- 一对一:这表示一种简单的关系,其中类型A的一个实体恰好属于类型B的一个实体。
- 多对一:顾名思义,这种关系涵盖了类型A的实体具有许多类型B的子实体的情况。
- 多对多:在这种情况下,可以有许多类型A的实体属于许多类型B的实体。
为了更好地理解这些不同类型的关系,我们将在下面进行研究。
一对一
作为“一对一”案例的示例,我们将以下类添加到实体模型中:
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;
}
}
身份证作为内部唯一标识符以及外部idNumber,签发日期和布尔值标志(指示该卡是否有效)。
在关系的另一侧,此人获得一个名为idCard
的新字段,该字段引用该人的卡:
public class Person {
...
private IdCard idCard;
...
public IdCard getIdCard() {
return idCard;
}
public void setIdCard(IdCard idCard) {
this.idCard = idCard;
}
要使用特定于Hibernate的映射文件映射此关系,我们可以通过以下方式对其进行更改:
<hibernate-mapping package="hibernate.entity">
<class name="IdCard" table="T_ID_CARD">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
</class>
<class name="Person" table="T_PERSON">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<property name="firstName" column="FIRST_NAME"/>
<property name="lastName" column="LAST_NAME"/>
<many-to-one name="idCard" column="ID_ID_CARD" unique="true"/>
<union-subclass name="Geek" table="T_GEEK">
<property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
</union-subclass>
</class>
</hibernate-mapping>
首先,我们为新class
添加一个新的class
元素,指定该类的名称及其对应的表名(此处为T_ID_CARD
)。 字段id
成为唯一标识符,应使用序列值填充。
另一方面, Person
映射现在包含新的XML元素many-to-one
并且其属性name
的引用是存储对IdCard
的引用的Person
类的字段。 可选的属性column
使我们可以在表T_PERSON
中指定链接到该人的身份证的外键列的确切名称。 由于此关系应为“一对一”类型,因此我们必须将unique
属性设置为true
。
执行此配置将导致以下DDL语句(请注意,为了减少表的数量,我们已切换回“每个类只有一个表”的方法,在该方法中,对于超类和子类只有一个表):
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
。
用于将人及其身份证插入的Java代码如下所示:
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
的关系的级联,我们可以简单地将属性cascade
添加到映射文件中的many-to-one
元素:
<many-to-one name="idCard" column="ID_ID_CARD" unique="true" cascade="all"/>
使用值all
告诉Hibernate级联所有类型的操作。 由于这并不是处理实体之间关系的首选方法,因此也只能选择特定的操作:
<many-to-one name="idCard" column="ID_ID_CARD" unique="true" cascade="save-update,refresh"/ ?-
>
上面的示例演示了如何配置映射,以便仅级联对save()
, saveOrUpdate()
和refresh
调用save()
从数据库中重新读取给定对象的状态)。 例如,不会转发对Hibernate方法delete()
或lock()
调用。
使用上述两种配置中的一种,可以将存储人员及其身份证的代码重写为以下代码:
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()
不会返回标识符,因为它假定实体已经存储到数据存储中,因此必须具有标识符。 尝试更新没有标识符的实体将引发异常:
org.hibernate.TransientObjectException: The given object has a null identifier: ...
因此,在需要省略用于确定是否已存储实体的代码的情况下, saveOrUpdate()
有所帮助。
一对多
在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
将参考保存回拥有此电话的人员。 由于一个人可以拥有多个电话,因此我们向Person
类添加了一个Set
以收集一个人的所有电话:
public class Person {
...
private Set phones = new HashSet();
...
public Set getPhones() {
return phones;
}
public void setPhones(Set phones) {
this.phones = phones;
}
}
映射文件必须相应地更新:
<hibernate-mapping package="hibernate.entity">
...
<class name="Phone" table="T_PHONE">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<property name="number" column="NUMBER"/>
<many-to-one name="person" column="ID_PERSON" unique="false" cascade="all"/>
</class>
<class name="Person" table="T_PERSON">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<discriminator column="PERSON_TYPE" type="string"/>
<property name="firstName" column="FIRST_NAME"/>
<property name="lastName" column="LAST_NAME"/>
<many-to-one name="idCard" column="ID_ID_CARD" unique="true" cascade="all"/>
<subclass name="Geek" extends="Person">
<property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
</subclass>
</class>
</hibernate-mapping>
上面的清单显示了Phone
类的映射的定义。 在使用序列和字段number
生成的常规标识符( id
)旁边,此定义还包含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
指向列ID
表的T_PERSON
。
为了向现有人员之一添加电话号码,我们首先加载特定人员,然后添加电话:
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)从数据存储中加载人员。 与SQL相似,此查询由from和where子句组成。 未使用其SQL名称引用FIRST_NAME
列。 而是使用Java字段/属性的名称。 可以使用setString()
方法将诸如名字之类的参数传递到查询中。
在下面的代码中,对找到的人(应该只是一个)进行迭代,并创建一个新的Phone
实例,该实例将添加到找到的人的电话集中。 从电话到该人的链接也已建立,然后再进行交易。 执行此代码后,数据库如下所示:
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
结果集的两个选择语句以上表明,在该行的T_PHONE
被连接到在所选择的行T_PERSON
因为它包含在它的第一列名“荷马”的人的ID ID_ID_PERSON
。
多对多
下一个有趣的关系是“多对多”关系。 在这种情况下,许多类型A的实体可以属于许多类型B的实体,反之亦然。 在实践中,例如极客和项目就是这种情况。 一个极客可以在多个项目中(同时或顺序)工作,而一个项目可以包含一个以上的极客。 因此,引入了新的实体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
)和一组怪胎。 在关系的另一侧, Geek
类具有一组项目:
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;
}
}
为了支持这种关系,必须以以下方式更改映射文件:
<hibernate-mapping package="hibernate.entity">
...
<class name="Project" table="T_PROJECT">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<property name="title" column="TITLE"/>
<set name="geeks" table="T_GEEKS_PROJECTS">
<key column="ID_PROJECT"/>
<many-to-many column="ID_GEEK" class="Geek"/>
</set>
</class>
<class name="Person" table="T_PERSON">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<discriminator column="PERSON_TYPE" type="string"/>
<property name="firstName" column="FIRST_NAME"/>
<property name="lastName" column="LAST_NAME"/>
<many-to-one name="idCard" column="ID_ID_CARD" unique="true" cascade="all"/>
<subclass name="Geek" extends="Person">
<property name="favouriteProgrammingLanguage" column="FAV_PROG_LANG"/>
<set name="projects" inverse="true">
<key column="ID_GEEK"/>
<many-to-many column="ID_PROJECT" class="Project"/>
</set>
</subclass>
</class>
</hibernate-mapping>
首先,我们看到映射到表T_PROJECT
的新类Project
。 其唯一标识符存储在字段id
,其字段title
存储在TITLE
列中。 XML元素set
定义了映射的一侧: geeks
集内的项目应存储在名为T_GEEKS_PROJECTS
的单独表中,该表的列ID_PROJECT
和ID_GEEK
。 在关系的另一侧,在Geek
的subclass
内set
的XML元素定义了逆关系( 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
的主键。
为了将可以使用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”作为其最喜欢的编程语言的怪胎。 然后,创建一个新的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的两个极客表示Java是他们最喜欢的编程语言。 因此,标题为“ Java Project”(ID:7)的项目由ID为3和5(最后选择语句)的两个怪胎组成。
零件
面向对象的设计规则建议将常用字段提取到单独的类中。 例如,上面的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为该期间创建一个单独的表,因为每个Project
应该仅具有一个开始日期和结束日期,并且我们希望避开其他联接。 在这种情况下,Hibernate可以将嵌入式类Period
的两个字段映射到与Project
类相同的表中:
<hibernate-mapping package="hibernate.entity">
...
<class name="Project" table="T_PROJECT">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<property name="title" column="TITLE"/>
<set name="geeks" table="T_GEEKS_PROJECTS">
<key column="ID_PROJECT"/>
<many-to-many column="ID_GEEK" class="Geek"/>
</set>
<component name="period">
<property name="startDate" column="START_DATE"/>
<property name="endDate" column="END_DATE"/>
</component>
</class>
...
</hibernate-mapping>
如何将此嵌入式类映射到表T_PROJECT
字段的T_PROJECT
是使用component
元素,并在Project
类中为name
属性提供字段的name
。 然后,将Period
类的两个字段声明为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 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
要将期间与项目一起加载,无需编写其他代码,该期间将自动加载并初始化:
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());
}
万一数据库中该期间的所有字段都已设置为NULL
,Hibernate还会将对Period
的引用设置为null
。
6.用户定义的数据类型
例如,在使用遗留数据库时,可能会发生某些列建模的方式不同于Hibernate映射它们的方式。 例如, Boolean
数据类型在H2数据库上映射为boolean
类型。 如果原始开发团队决定使用具有值“ 0”和“ 1”的字符串映射布尔值,则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
中提取布尔值的字符串表示ResultSet
。 如果返回的字符串等于“ 1”,则该方法返回“ true”,否则返回“ false”。
之前的insert
可以被执行的语句,在作为参数传递的布尔值value
必须被“解码”到任意字符串“1”或串“0”。 然后,方法nullSafeSet()
使用Hibernate的StringType
实现在PreparedStatement
上设置此字符串值。
最后,我们必须告诉Hibernate从nullSafeGet()
返回nullSafeGet()
对象,以及该类型应使用哪种列:
@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
is here our implementation of the UserType
interface, whereas the String
array defines how to reference this type in the mapping file:
<hibernate-mapping package="hibernate.entity">
<class name="IdCard" table="T_ID_CARD">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<property name="idNumber" column="ID_NUMBER"/>
<property name="issueDate" column="ISSUE_DATE"/>
<property name="valid" column="VALID" type="MyBooleanType"/>
</class>
...
</hibernate-mapping>
As can be seen from the snippet above, the new type “MyBooleanType” is used for the boolean property of the table T_ID_CARD
:
sql> select * from t_id_card;
ID | ID_NUMBER | ISSUE_DATE | VALID
2 | 4711 | 2015-03-27 11:49:57.533 | 1
7. Interceptors
A project may come with the requirement that for each entity/table the timestamp of its creation and its last update should be tracked. Setting these two values for each entity on all insert and update operations is a fairly tedious task. Therefore Hibernate offers the ability to implement interceptors that are called before an insert or update operation is performed. This way the code to set the creation and update timestamp can be extracted to a single place in the code base and does not have to be copied to all locations where it would be necessary.
As an example we are going to implement an audit trail that tracks the creation and update of the Project
entity. This can be done by extending the class 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;
}
}
As the class EmptyInterceptor
already implements all methods defined in the interface Interceptor
, we only have to override the methods onSave()
and onFlushDirty()
. In order to easily find all entities that have a field created
and lastUpdate
we extract the getter and setter methods for these entities into a separate interface called Auditable
:
public interface Auditable {
Date getCreated();
void setCreated(Date created);
Date getLastUpdate();
void setLastUpdate(Date lastUpdate);
}
With this interface it is easy to check whether the instance passed into the interceptor is of type Auditable
. Unfortunately we cannot modify the entity directly through the getter and setter methods but we have to use the two arrays propertyNames
and state
. In the array propertyNames
we have to find the property created
( lastUpdate
) and use its index to set the corresponding element in the array state
( currentState
).
Without the appropriate property definitions in the mapping file Hibernate will not create the columns in the tables. Hence the mapping file has to be updated:
<hibernate-mapping>
...
<class name="Project" table="T_PROJECT">
<id name="id" column="ID">
<generator class="sequence"/>
</id>
<property name="title" column="TITLE"/>
<set name="geeks" table="T_GEEKS_PROJECTS">
<key column="ID_PROJECT"/>
<many-to-many column="ID_GEEK" class="Geek"/>
</set>
<component name="period">
<property name="startDate" column="START_DATE"/>
<property name="endDate" column="END_DATE"/>
</component>
<property name="created" column="CREATED" type="timestamp"/>
<property name="lastUpdate" column="LAST_UPDATE" type="timestamp"/>
</class>
...
</hibernate-mapping>
As can be seen from the snippet above, the two new properties created
and lastUpdate
are of type 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. Download Hibernate Tutorial Source Code
This was a Hibernate Tutorial.
You can download the full source code of this tutorial here: hibernate-tutorial-sources .
翻译自: https://www.javacodegeeks.com/2015/03/hibernate-tutorial.html
hibernate