Spring5 高级教程(五)
原文:Pro Spring 5
七、Spring 和 Hibernate
在前一章中,你看到了如何在 Spring 应用中使用 JDBC。然而,即使 Spring 在简化 JDBC 开发方面走了很长的路,您仍然有许多代码要写。在本章中,我们将介绍一个叫做 Hibernate 的对象关系映射(ORM)库。
如果您有使用 EJB 实体 beans(在 EJB 3.0 之前)开发数据访问应用的经验,您可能还记得这个痛苦的过程。在开发企业 Java 应用时,繁琐的映射配置、事务划分以及每个 bean 中管理其生命周期的大量样板代码极大地降低了生产率。
就像开发 Spring 是为了采用基于 POJO 的开发和声明性配置管理,而不是 EJB 笨重笨拙的设置一样,开发人员社区意识到一个更简单、轻量级和基于 POJO 的框架可以简化数据访问逻辑的开发。此后,出现了许多图书馆;它们通常被称为 ORM 库。ORM 库的主要目标是缩小关系数据库管理系统(RDBMS)中的关系数据结构和 Java 中的面向对象(OO)模型之间的差距,以便开发人员可以专注于使用对象模型进行编程,同时轻松执行与持久性相关的操作。
在开源社区提供的 ORM 库中,Hibernate 是最成功的一个。它的特性,比如基于 POJO 的方法、易于开发和支持复杂的关系定义,赢得了主流 Java 开发人员社区的青睐。
Hibernate 的流行也影响了 JCP,它开发了 Java 数据对象(JDO)规范,作为 Java EE 中的标准 ORM 技术之一。从 EJB 3.0 开始,EJB 实体 bean 甚至被 Java 持久性 API (JPA)所取代。JPA 的很多概念都受到了流行的 ORM 库的影响,比如 Hibernate、TopLink 和 JDO。Hibernate 和 JPA 的关系也很密切。Hibernate 的创始人 Gavin King 代表 JBoss 作为 JCP 专家组成员之一定义了 JPA 规范。从 3.2 版本开始,Hibernate 提供了 JPA 的实现。这意味着当您使用 Hibernate 开发应用时,您可以选择使用 Hibernate 自己的 API 或者使用 Hibernate 作为持久性服务提供者的 JPA API。
在简要介绍了 Hibernate 的历史之后,本章将介绍在开发数据访问逻辑时如何使用 Spring 和 Hibernate。Hibernate 是一个如此庞大的 ORM 库,以至于在一章中涵盖所有方面是不可能的,有许多书籍专门讨论 Hibernate。
本章涵盖了 Spring 中 Hibernate 的基本思想和主要用例。特别是,我们讨论以下主题:
- 配置 Hibernate session factory:Hibernate 的核心概念围绕着由
SessionFactory管理的Session接口。我们将向您展示如何配置 Hibernate 的会话工厂,以便在 Spring 应用中工作。 - 使用 Hibernate 的 ORM 的主要概念:我们讨论了如何使用 Hibernate 将 POJO 映射到底层关系数据库结构的主要概念。还讨论了一些常用的关系,包括一对多和多对多。
- 数据操作:我们展示了如何在 Spring 环境中使用 Hibernate 执行数据操作(查询、插入、更新、删除)的例子。使用 Hibernate 时,它的
Session接口是您将与之交互的主要接口。
在定义对象到关系的映射时,Hibernate 支持两种配置风格。一种是在 XML 文件中配置映射信息,另一种是在实体类中使用 Java 注释(在 ORM 或 JPA 世界中,映射到底层关系数据库结构的 Java 类称为实体类)。这一章着重于使用对象关系映射的注释方法。对于映射注释,我们使用 JPA 标准(例如,在javax.persistence包下),因为它们可以与 Hibernate 自己的注释互换,并将帮助您将来迁移到 JPA 环境。
示例代码的示例数据模型
图 7-1 显示了本章使用的数据模型。
图 7-1。
Sample data model
如该数据模型所示,添加了两个新表,即INSTRUMENT和SINGER_INSTRUMENT(连接表)。SINGER_INSTRUMENT对SINGER和INSTRUMENT表之间的多对多关系进行建模。在SINGER和ALBUM表中添加了一个VERSION列,用于乐观锁定,这将在后面详细讨论。在本章的示例中,我们将使用嵌入式 H2 数据库,因此数据库名称不是必需的。以下是创建本章示例所需表格的脚本:
CREATE TABLE SINGER (
ID INT NOT NULL AUTO_INCREMENT
, FIRST_NAME VARCHAR(60) NOT NULL
, LAST_NAME VARCHAR(40) NOT NULL
, BIRTH_DATE DATE
, VERSION INT NOT NULL DEFAULT 0
, UNIQUE UQ_SINGER_1 (FIRST_NAME, LAST_NAME)
, PRIMARY KEY (ID)
);
CREATE TABLE ALBUM (
ID INT NOT NULL AUTO_INCREMENT
, SINGER_ID INT NOT NULL
, TITLE VARCHAR(100) NOT NULL
, RELEASE_DATE DATE
, VERSION INT NOT NULL DEFAULT 0
, UNIQUE UQ_SINGER_ALBUM_1 (SINGER_ID, TITLE)
, PRIMARY KEY (ID)
, CONSTRAINT FK_ALBUM_SINGER FOREIGN KEY (SINGER_ID)
REFERENCES SINGER (ID)
);
CREATE TABLE INSTRUMENT (
INSTRUMENT_ID VARCHAR(20) NOT NULL
, PRIMARY KEY (INSTRUMENT_ID)
);
CREATE TABLE SINGER_INSTRUMENT (
SINGER_ID INT NOT NULL
, INSTRUMENT_ID VARCHAR(20) NOT NULL
, PRIMARY KEY (SINGER_ID, INSTRUMENT_ID)
, CONSTRAINT FK_SINGER_INSTRUMENT_1 FOREIGN KEY (SINGER_ID)
REFERENCES SINGER (ID) ON DELETE CASCADE
, CONSTRAINT FK_SINGER_INSTRUMENT_2 FOREIGN KEY (INSTRUMENT_ID)
REFERENCES INSTRUMENT (INSTRUMENT_ID)
);
以下 SQL 是数据填充的脚本:
insert into singer (first_name, last_name, birth_date)
values ('John', 'Mayer', '1977-10-16');
insert into singer (first_name, last_name, birth_date)
values ('Eric', 'Clapton', '1945-03-30');
insert into singer (first_name, last_name, birth_date)
values ('John', 'Butler', '1975-04-01');
insert into album (singer_id, title, release_date)
values (1, 'The Search For Everything', '2017-01-20');
insert into album (singer_id, title, release_date)
values (1, 'Battle Studies', '2009-11-17');
insert into album (singer_id, title, release_date)
values (2, 'From The Cradle ', '1994-09-13');
insert into instrument (instrument_id) values ('Guitar');
insert into instrument (instrument_id) values ('Piano');
insert into instrument (instrument_id) values ('Voice');
insert into instrument (instrument_id) values ('Drums');
insert into instrument (instrument_id) values ('Synthesizer');
insert into singer_instrument(singer_id, instrument_id) values (1, 'Guitar');
insert into singer_instrument(singer_id, instrument_id) values (1, 'Piano');
insert into singer_instrument(singer_id, instrument_id) values (2, 'Guitar');
配置 Hibernate 的会话工厂
正如本章前面提到的,Hibernate 的核心概念是基于从SessionFactory获得的Session接口。Spring 提供了一些类来支持将 Hibernate 的会话工厂配置为具有所需属性的 Spring bean。要使用 Hibernate,必须将 Hibernate 依赖项作为依赖项添加到项目中。以下是本章中项目使用的梯度配置:
//pro-spring-15/build.gradle
ext {
hibernateVersion = '5.2.10.Final'
...
hibernate = [
validator: "org.hibernate:hibernate-validator:5.1.3.Final",
ehcache : "org.hibernate:hibernate-ehcache:$hibernateVersion",
[ em] : "org.hibernate:hibernate-entitymanager:$hibernateVersion"
]
...
}
//chapter07.gradle
dependencies {
//we specify these dependencies for all submodules,
except the boot module, that defines its own
if !project.name.contains"boot" {
compile spring.contextSupport, spring.orm,
misc.slf4jJcl, misc.logback, db.h2, misc.lang3, [hibernate.em]
}
testCompile testing.junit
}
在下面的配置中,您可以看到为本章配置应用示例所需的 XML 元素:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:sql/schema.sql"/>
<jdbc:script location="classpath:sql/test-data.sql"/>
</jdbc:embedded-database>
<bean id="transactionManager"
class="org.springframework.orm.hibernate5.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory"/>
<tx:annotation-driven/>
<context:component-scan base-package=
"com.apress.prospring5.ch7"/>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"
p:dataSource-ref="dataSource"
p:packagesToScan="com.apress.prospring5.ch7.entities"
p:hibernateProperties-ref="hibernateProperties"/>
<util:properties id="hibernateProperties">
<prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">10</prop>
<prop key="hibernate.hbm2ddl.auto">create-drop</prop>
<prop key="hibernate.format_sql">true</prop>
<prop key="hibernate.use_sql_comments">true</prop>
</util:properties>
</beans>
接下来描述了等效的 Java 配置类,这两种配置的组件在代码片段之后并行解释:
package com.apress.prospring5.ch7.config;
import com.apress.prospring5.ch6.CleanUp;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
@Configuration
@ComponentScan(basePackages =
"com.apress.prospring5.ch7")
@EnableTransactionManagement
public class AppConfig {
private static Logger logger =
LoggerFactory.getLogger(AppConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder =
new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:sql/schema.sql",
"classpath:sql/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
private Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean public SessionFactory sessionFactory()
throws IOException {
LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource());
sessionFactoryBean.setPackagesToScan("com.apress.prospring5.ch7.entities");
sessionFactoryBean.setHibernateProperties(hibernateProperties());
sessionFactoryBean.afterPropertiesSet();
return sessionFactoryBean.getObject();
}
@Bean public PlatformTransactionManager transactionManager()
throws IOException {
return new HibernateTransactionManager(sessionFactory());
}
}
在前面的配置中,声明了几个 beans 能够支持 Hibernate 的会话工厂。这里列出了主要配置:
-
数据源 bean:使用的数据库是一个 H2 嵌入式数据库,如前面在第六章中解释的那样声明。
-
transaction manager bean:Hibernate 会话工厂需要一个事务管理器来进行事务数据访问。Spring 专门为包
org.springframework.orm.hibernate5.HibernateTransactionManager中声明的 Hibernate 5 提供了一个事务管理器。bean 是用分配的 IDtransactionManager声明的。默认情况下,当使用 XML 配置时,只要需要事务管理,Spring 就会在其ApplicationContext中查找名为transactionManager的 bean。当通过 bean 的类型而不是名称来搜索 bean 时,Java 配置会更灵活一些。我们将在第九章中详细讨论事务。此外,我们声明标签<tx:annotation-driven>,以支持使用注释声明事务划分需求。Java 配置的对等物是@EnableTransactionManagement注释。 -
组件扫描:这个标签和
@ComponentScan注释对您来说应该很熟悉。我们指示 Spring 扫描包com.apress.prospring5.ch7下的组件,以检测标注有@Repository的 beans。 -
Hibernate SessionFactory bean: The
sessionFactorybean is the most important part. Within the bean, several properties are provided. First, we need to inject thedataSourcebean into the session factory. Second, we instruct Hibernate to scan for the domain objects under the packagecom.apress.prospring5.ch.entities. Finally, thehibernatePropertiesproperty provides configuration details for Hibernate. There are many configuration parameters, and we define only a few important properties that should be provided for every application. Table 7-1 lists the main configuration parameters for the Hibernate session factory.表 7-1。
Hibernate Properties
| 财产 | 描述 | | --- | --- | | `hibernate.dialect` | 为 Hibernate 应该使用的查询指定数据库方言。Hibernate 支持许多数据库的 SQL 方言。那些方言是`org.hibernate.dialect.Dialect`的子类。主要方言有`H2Dialect`、`Oracle10gDialect`、`PostgreSQLDialect`、`MySQLDialect`、`SQLServerDialect`等。 | | `hibernate.max_fetch_depth` | 当映射对象与其他映射对象关联时,声明外部连接的“深度”。这个设置可以防止 Hibernate 获取太多嵌套关联的数据。一个常用的值是 3。 | | `hibernate.jdbc.fetch_size` | 指定 Hibernate 每次从数据库获取记录时应该使用的底层 JDBC `ResultSet`中的记录数。例如,向数据库提交了一个查询,而`ResultSet`包含 500 条记录。如果读取大小为 50,Hibernate 将需要读取 10 次才能获得所有数据。 | | `ibernate.jdbc.batch_` `size` | 指示 Hibernate 应该分组到一个批处理中的更新操作的数量。这对于在 Hibernate 中执行批处理作业非常有用。显然,当我们在做一个更新成千上万条记录的批处理作业时,我们希望 Hibernate 将查询分组,而不是一个接一个地提交更新。 | | `hibernate.show_sql` | 指示 Hibernate 是否应该将 SQL 查询输出到日志文件或控制台。您应该在开发环境中打开它,这在测试和故障排除过程中非常有帮助。 | | `hibernate.format_sql` | 指示日志或控制台中的 SQL 输出是否应该格式化。 | | `hibernate..use_sql_comments` | 如果设置为`true`,Hibernate 会在 SQL 内部生成注释,以便于调试。 |
关于 Hibernate 支持的属性的完整列表,请参考 Hibernate 的 ORM 用户指南,特别是第二十三部分,在 https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html 。
使用 Hibernate 注释的 ORM 映射
配置就绪后,下一步是对 Java POJO 实体类及其到底层关系数据结构的映射进行建模。
有两种映射方法。第一个是设计对象模型,然后基于对象模型生成数据库脚本。例如,对于会话工厂配置,您可以传入 Hibernate 属性hibernate.hbm2ddl.auto,让 Hibernate 自动将模式 DDL 导出到数据库。第二种方法是从数据模型开始,然后用期望的映射对 POJOs 建模。我们更喜欢后一种方法,因为我们可以对数据模型有更多的控制,这对于优化数据访问的性能很有用。但是第一个问题将在本章后面讨论,以描述用 Hibernate 配置 Spring 应用的另一种方式。基于数据模型,图 7-2 用类图展示了相应的 OO 模型。
图 7-2。
Class diagram for the sample data model
您可以看到Singer和Album之间是一对多的关系,而Singer和Instrument对象之间是多对多的关系。
简单映射
首先让我们从映射类的简单属性开始。下面的代码片段显示了带有映射注释的Singer类:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
private Long id;
private String firstName;
private String lastName;
private Date birthDate;
private int version;
public void setId(Long id) {
this.id = id;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
public Long getId() {
return this.id;
}
@Version
@Column(name = "VERSION")
public int getVersion() {
return version;
}
@Column(name = "FIRST_NAME")
public String getFirstName() {
return this.firstName;
}
@Column(name = "LAST_NAME")
public String getLastName() {
return this.lastName;
}
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
public Date getBirthDate() {
return birthDate;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public void setVersion(int version) {
this.version = version;
}
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate;
}
}
首先我们用@ Entity标注类型,这意味着这是一个映射的实体类。@Table注释定义了这个实体映射到的数据库中的表名。对于每个映射的属性,您可以用@Column注释对其进行注释,并提供列名。
如果类型和属性名称与表和列名称相同,则可以跳过表和列名称。
关于映射,我们想强调几点。
- 对于
birthDate属性,我们使用TemporalType.DATE值作为参数,用@Temporal对其进行注释。这意味着我们希望将数据类型从 Java 日期类型(java.util.Date)映射到 SQL 日期类型(java.sql.Date)。这允许我们像往常一样在应用中使用java.util.Date来访问Singer对象中的属性birthDate。 - 对于
id属性,我们用@Id对其进行注释。这意味着它是对象的主键。Hibernate 在管理其会话中的联系人实体实例时,将使用它作为唯一标识符。此外,@GeneratedValue注释告诉 Hibernate】值是如何生成的。IDENTITY策略意味着id值是在插入期间由后端生成的。 - 对于
version属性,我们用@Version对其进行注释。这指示 Hibernate 我们想要使用乐观锁定机制,使用version属性作为控制。每次 Hibernate 更新记录时,它都会将实体实例的版本与数据库中记录的版本进行比较。如果两个版本相同,说明之前没有人更新过数据,Hibernate 会更新数据并递增版本列。但如果版本不一样,说明之前有人更新过记录,Hibernate 会抛出StaleObjectStateException异常,Spring 会翻译成HibernateOptimisticLockingFailureException。我们使用整数进行版本控制的例子。除了整数,Hibernate 还支持使用时间戳。但是,建议使用整数进行版本控制,因为 Hibernate 在每次更新后都会将版本号加 1。当使用时间戳时,Hibernate 会在每次更新后更新最新的时间戳。时间戳稍微不太安全,因为两个并发事务可能在同一毫秒内加载和更新同一项。
另一个映射的对象是Album,如下图所示:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "album")
public class Album implements Serializable {
private Long id;
private String title;
private Date releaseDate;
private int version;
public void setId(Long id) {
this.id = id;
}
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
public Long getId() {
return this.id;
}
@Version
@Column(name = "VERSION")
public int getVersion() {
return version;
}
@Column
public String getTitle() {
return this.title;
}
@Temporal(TemporalType.DATE)
@Column(name = "RELEASE_DATE")
public Date getReleaseDate() {
return this.releaseDate;
}
public void setTitle(String title) {
this.title = title;
}
public void setReleaseDate(Date releaseDate) {
this.releaseDate = releaseDate;
}
public void setVersion(int version) {
this.version = version;
}
@Override
public String toString() {
return "Album - Id: " + id + ", Title: " +
title + ", Release Date: " + releaseDate;
}
}
以下是本章示例中使用的第三个实体类:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "instrument")
public class Instrument implements Serializable {
private String instrumentId;
@Id
@Column(name = "INSTRUMENT_ID")
public String getInstrumentId() {
return this.instrumentId;
}
public void setInstrumentId(String instrumentId) {
this.instrumentId = instrumentId;
}
@Override
public String toString() {
return "Instrument :" + getInstrumentId();
}
}
一对多映射
Hibernate 能够对多种关联进行建模。最常见的关联是一对多和多对多。每个Singer将有零个或多个相册,所以这是一个一对多的关联(在 ORM 术语中,一对多关联用于建模数据结构中的零对多和一对多关系)。下面的代码片段描述了定义Singer和Album实体之间的一对多关系所需的属性和方法:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
private Long id;
private String firstName;
private String lastName;
private Date birthDate;
private int version;
private Set<Album> albums = new HashSet<>();
...
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL,
orphanRemoval=true)
public Set<Album> getAlbums() {
return albums;
}
public boolean addAbum(Album album) {
album.setSinger(this);
return getAlbums().add(album);
}
public void removeAlbum(Album album) {
getAlbums().remove(album);
}
public void setAlbums(Set<Album> albums) {
this.albums = albums;
}
...
}
属性contactTelDetails的 getter 方法用@OneToMany注释,这表明与Album类的一对多关系。几个属性被传递给注释。mappedBy属性表示Album类中提供关联的属性(即通过FK_ALBUM_SINGER表中的外键定义链接起来)。属性意味着更新操作应该“级联”到子节点。orphanRemoval属性意味着在相册更新后,那些不再存在于相册集中的条目应该从数据库中删除。以下代码片段显示了关联映射的Album类中的更新代码:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "album")
public class Album implements Serializable {
private Long id;
private String title;
private Date releaseDate;
private int version;
private Singer singer;
@ManyToOne
@JoinColumn(name = "SINGER_ID")
public Singer getSinger() {
return this.singer;
}
public void setSinger(Singer singer) {
this.singer = singer;
}
...
}
我们用@ManyToOne注释singer属性的 getter 方法,这表明它是来自Singer的关联的另一方。我们还为底层外键列名指定了@JoinColumn注释。最后,在后面的示例代码中,toString()方法被覆盖,通过将其输出打印到控制台来方便测试。
多对多映射
每个歌手可以演奏零个或多个乐器,每个乐器也与零个或多个歌手相关联,这意味着这是一个多对多的映射。多对多映射需要一个连接表,就是SINGER_INSTRUMENT。以下代码示例显示了需要添加到Singer类中以实现这种关系的代码:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
public class Singer implements Serializable {
private Long id;
private String firstName;
private String lastName;
private Date birthDate;
private int version;
private Set<Instrument> instruments = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "SINGER_ID"),
inverseJoinColumns = @JoinColumn(name = "INSTRUMENT_ID"))
public Set<Instrument> getInstruments() {
return instruments;
}
public void setInstruments(Set<Instrument> instruments) {
this.instruments = instruments;
}
...
}
Singer类中属性instruments的 getter 方法用@ManyToMany注释。我们还提供了@JoinTable来指示 Hibernate 应该寻找的底层连接表。该名称是连接表的名称,joinColumns定义了作为SINGER表的外键的列,inverseJoinColumns定义了作为关联另一端(即INSTRUMENT表)的外键的列。下面是添加了实现这种关系的另一方的代码的Instrument类:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "instrument")
public class Instrument implements Serializable {
private String instrumentId;
private Set<Singer> singers = new HashSet<>();
@Id
@Column(name = "INSTRUMENT_ID")
public String getInstrumentId() {
return this.instrumentId;
}
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "INSTRUMENT_ID"),
inverseJoinColumns = @JoinColumn(name = "SINGER_ID"))
public Set<Singer> getSingers() {
return this.singers;
}
public void setSingers(Set<Singer> singers) {
this.singers = singers;
}
public void setInstrumentId(String instrumentId) {
this.instrumentId = instrumentId;
}
@Override
public String toString() {
return "Instrument :" + getInstrumentId();
}
}
该映射与Singer的映射大致相同,但是joinColumns和inverseJoinColumns属性被颠倒以反映关联。
Hibernate 会话接口
在 Hibernate 中,与数据库交互时,你需要处理的主要接口是Session接口,这个接口是从SessionFactory中获取的。
以下代码片段显示了本章示例中使用的SingerDaoImpl类,并将配置好的 Hibernate SessionFactory注入到该类中:
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private SessionFactory sessionFactory;
public SessionFactory getSessionFactory() {
return sessionFactory;
}
@Resource(name = "sessionFactory")
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
...
}
像往常一样,我们通过使用@Repository注释将 DAO 类声明为一个 Spring bean。注释定义了我们将在第九章中进一步讨论的事务需求。通过使用@Resource注释来注入sessionFactory属性。
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import java.util.List;
public interface SingerDao {
List<Singer> findAll();
List<Singer> findAllWithAlbum();
Singer findById(Long id);
Singer save(Singer contact);
void delete(Singer contact);
}
界面简单;它只有三个查找方法,一个保存方法和一个删除方法。save()方法将执行插入和更新操作。
使用 Hibernate 查询语言查询数据
Hibernate 和其他 ORM 工具,如 JDO 和 JPA,都是围绕对象模型设计的。因此,在定义了映射之后,我们不需要构造 SQL 来与数据库交互。相反,对于 Hibernate,我们使用 Hibernate 查询语言(HQL)来定义我们的查询。当与数据库交互时,Hibernate 会代表我们将查询翻译成 SQL 语句。
当编码 HQL 查询时,语法很像 SQL。但是,您需要从对象的角度而不是数据库的角度来考虑问题。在接下来的几节中,我们将带您看几个例子。
带延迟抓取的简单查询
让我们从实现findAll()方法开始,该方法简单地从数据库中检索所有联系人。以下代码示例显示了此功能的更新代码:
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private static final Log logger = LogFactory.getLog(SingerDaoImpl.class);
private SessionFactory sessionFactory;
@Transactional(readOnly = true)
public List<Singer> findAll() {
return sessionFactory.getCurrentSession()
.createQuery("from Singer s").list();
}
...
}
方法SessionFactory.getCurrentSession()获得 Hibernate 的Session接口。然后,调用Session.createQuery()方法,传入 HQL 语句。语句from Singer s简单地从数据库中检索所有联系人。该语句的另一种语法是select s from Singer s。@Transactional(readOnly=true)注释意味着我们希望将事务设置为只读。为只读方法设置该属性将获得更好的性能。
下面的代码片段显示了一个简单的SingerDaoImpl测试程序:
package com.apress.prospring5.ch7;
import com.apress.prospring5.ch7.config.AppConfig;
import com.apress.prospring5.ch7.dao.SingerDao;
import com.apress.prospring5.ch7.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
public class SpringHibernateDemo {
private static Logger logger =
LoggerFactory.getLogger(SpringHibernateDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
singerDao.delete(singer);
listSingers(singerDao.findAll());
ctx.close();
}
private static void listSingers(List<Singer> singers) {
logger.info(" ---- Listing singers:");
for (Singer singer : singers) {
logger.info(singer.toString());
}
}
}
运行前面的类会产生以下输出:
---- Listing singers:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
虽然找回了歌手唱片,但是专辑和乐器呢?让我们修改测试类来打印详细信息。在下面的代码片段中,您可以看到方法listSingers()被替换为listSingersWithAlbum():
package com.apress.prospring5.ch7;
import com.apress.prospring5.ch7.config.AppConfig;
import com.apress.prospring5.ch7.dao.SingerDao;
import com.apress.prospring5.ch7.entities.Album;
import com.apress.prospring5.ch7.entities.Instrument;
import com.apress.prospring5.ch7.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
public class SpringHibernateDemo {
private static Logger logger =
LoggerFactory.getLogger(SpringHibernateDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
Singer singer = singerDao.findById(2l);
singerDao.delete(singer);
listSingersWithAlbum(singerDao.findAllWithAlbum());
ctx.close();
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
for (Singer singer : singers) {
logger.info(singer.toString());
if (singer.getAlbums() != null) {
for (Album album :
singer.getAlbums()) {
logger.info("\t" + album.toString());
}
}
if (singer.getInstruments() != null) {
for (Instrument instrument : singer.getInstruments()) {
logger.info("\t" + instrument.getInstrumentId());
}
}
}
}
}
如果您再次运行该程序,您将看到以下异常:
---- Listing singers with instruments:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
org.hibernate.LazyInitializationException: failed to lazily initialize a
collection of role: com.apress.prospring5.ch7.entities.Singer.albums,
could not initialize proxy - no Session
当您试图访问关联时,您会看到 Hibernate 抛出了LazyInitializationException。
这是因为,默认情况下,Hibernate 会延迟获取关联,这意味着 Hibernate 不会连接记录的关联表(即ALBUM)。这背后的基本原理是为了性能;可以想象,如果一个查询要检索成千上万条记录,并且所有的关联都被检索到,那么大量的数据传输将会降低性能。
使用关联提取进行查询
要让 Hibernate 从关联中获取数据,有两个选项。首先可以定义与取模式EAGER的关联,例如@ManyToMany(fetch=FetchType.EAGER)。这告诉 Hibernate 在每个查询中获取相关的记录。然而,如前所述,这将影响数据检索性能。
另一种选择是在需要时强制 Hibernate 在查询中获取相关记录。如果使用Criteria查询,可以调用函数Criteria.setFetchMode()来指示 Hibernate 快速获取关联。使用NamedQuery时,可以使用fetch操作符指示 Hibernate 急切地获取关联。
让我们来看看findAllWithAlbum()方法的实现,它将检索所有联系信息以及他们的电话详细信息和爱好。这个例子将使用NamedQuery方法。NamedQuery可以外化到一个 XML 文件中,或者使用实体类上的注释来声明。在这里,您可以看到修改后的Singer域对象,带有使用注释定义的命名查询:
package com.apress.prospring5.ch7.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
...
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name="Singer.findAllWithAlbum",
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i")
})
public class Singer implements Serializable {
...
}
首先定义一个名为Singer.findAllWithAlbum的NamedQuery实例。然后我们在 HQL 定义查询。注意left join fetch子句,它指示 Hibernate 急切地获取关联。还需要使用select distinct;否则,Hibernate 将返回重复的对象(如果一个歌手有两张相关联的专辑,将返回两个歌手对象)。
下面是findAllWithAlbum()方法的实现:
package com.apress.prospring5.ch7.dao;
...
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
@Transactional(readOnly = true)
public List<Singer> findAllWithAlbum() {
return sessionFactory.getCurrentSession().
getNamedQuery("Singer.findAllWithAlbum").list();
}
}
这次我们使用Session.getNamedQuery()方法,传入NamedQuery实例的名称。修改测试程序(SpringHibernateDemo)来调用singerDao.findAllWithAlbum()将产生以下输出:
---- Listing singers with instruments:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Album - Id: 2, Singer id: 1, Title: Battle Studies, Release Date: 2009-11-17
Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
Instrument: Guitar
Instrument: Piano
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Album - Id: 3, Singer id: 2, Title: From The Cradle, Release Date: 1994-09-13
Instrument: Guitar
现在,所有带有详细信息的歌手都被正确检索到了。让我们看另一个带有参数的NamedQuery的例子。这一次,我们将实现findById()方法,并且也想获取关联。下面的代码片段显示了添加了新命名查询的Singer类:
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name="Singer.findById",
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i " +
"where s.id = :id"),
@NamedQuery(name="Singer.findAllWithAlbum",
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i")
})
public class Singer implements Serializable {
...
}
从名为Singer.findById的命名查询中,我们声明一个命名参数:id。这里你可以看到SingerDaoImpl中findById()方法的实现:
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private static final Log logger = LogFactory.getLog(SingerDaoImpl.class);
private SessionFactory sessionFactory;
@Transactional(readOnly = true)
public Singer findById(Long id) {
return (Singer) sessionFactory.getCurrentSession().
getNamedQuery("Singer.findById").
setParameter("id", id).uniqueResult();
}
...
}
在这个清单中,我们使用相同的Session.getNameQuery()方法。但是我们也调用了setParameter()方法,传入命名参数及其值。对于多个参数,可以使用Query接口的setParameterList()或setParameters()方法。
还有一些更高级的查询方法,比如原生查询和条件查询,我们将在下一章讨论 JPA 时讨论。为了测试这个方法,必须相应地修改SpringHibernateDemo类。
package com.apress.prospring5.ch7;
...
public class SpringHibernateDemo {
private static Logger logger =
LoggerFactory.getLogger(SpringHibernateDemo.class);
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
SingerDao singerDao = ctx.getBean(SingerDao.class);
Singer singer = singerDao.findById(2l);
logger.info(singer.toString());
ctx.close();
}
}
运行该程序会产生以下输出:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
插入数据
用 Hibernate 插入数据很简单。另一件有趣的事情是检索数据库生成的主键。在前面关于 JDBC 的章节中,我们需要显式声明我们想要检索生成的密钥,传入KeyHolder实例,并在执行 insert 语句后从中取回密钥。使用 Hibernate,所有这些操作都不是必需的。Hibernate 将检索生成的键,并在插入操作后填充域对象。下面的代码片段显示了save()方法的实现:
package com.apress.prospring5.ch7.dao;
import com.apress.prospring5.ch7.entities.Singer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.SessionFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private static final Log logger = LogFactory.getLog(SingerDaoImpl.class);
private SessionFactory sessionFactory;
public Singer save(Singer singer) {
sessionFactory.getCurrentSession().saveOrUpdate(singer);
logger.info("Singer saved with id: " + singer.getId());
return singer;
}
...
}
我们只需要调用Session.saveOrUpdate()方法,它可以用于插入和更新操作。我们还记录了保存的 singer 对象的 ID,该对象将在持久化后由 Hibernate 填充。下面的代码片段显示了在SINGER表中插入一个新的歌手记录,在ALBUM表中插入两个子记录,并测试插入是否成功的代码。此外,因为现在我们正在修改表的内容,所以 JUnit 类更适合单独测试每个操作。
package com.apress.prospring5.ch7;
import com.apress.prospring5.ch7.config.AppConfig;
import com.apress.prospring5.ch7.dao.SingerDao;
import com.apress.prospring5.ch7.entities.Album;
import com.apress.prospring5.ch7.entities.Instrument;
import com.apress.prospring5.ch7.entities.Singer;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerDaoTest {
private static Logger logger =
LoggerFactory.getLogger(SingerDaoTest.class);
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(AppConfig.class);
singerDao = ctx.getBean(SingerDao.class);
assertNotNull(singerDao);
}
@Test
public void testInsert(){
Singer singer = new Singer();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
Album album = new Album();
album.setTitle("My Kind of Blues");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1961, 7, 18)).getTime().getTime()));
singer.addAbum(album);
album = new Album();
album.setTitle("A Heart Full of Blues");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1962, 3, 20)).getTime().getTime()));
singer.addAbum(album);
singerDao.save(singer);
assertNotNull(singer.getId());
List<Singer> singers = singerDao.findAllWithAlbum();
assertEquals(4, singers.size());
listSingersWithAlbum(singers);
}
@Test
public void testFindAll(){
List<Singer> singers = singerDao.findAll();
assertEquals(3, singers.size());
listSingers(singers);
}
@Test
public void testFindAllWithAlbum(){
List<Singer> singers = singerDao.findAllWithAlbum();
assertEquals(3, singers.size());
listSingersWithAlbum(singers);
}
@Test
public void testFindByID(){
Singer singer = singerDao.findById(1L);
assertNotNull(singer);
logger.info(singer.toString());
}
private static void listSingers(List<Singer> singers) {
logger.info(" ---- Listing singers:");
for (Singer singer : singers) {
logger.info(singer.toString());
}
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
for (Singer singer : singers) {
logger.info(singer.toString());
if (singer.getAlbums() != null) {
for (Album album :
singer.getAlbums()) {
logger.info("\t" + album.toString());
}
}
if (singer.getInstruments() != null) {
for (Instrument instrument : singer.getInstruments()) {
logger.info("\tInstrument: " + instrument.getInstrumentId());
}
}
}
}
@After
public void tearDown(){
ctx.close();
}
}
如前面的代码所示,在testInsert()方法中,我们添加了两个相册,并保存了对象。之后,我们通过调用listSingersWithAlbum再次列出所有歌手。运行testInsert()方法会产生以下输出:
...
INFO o.h.d.Dialect - HHH000400:
Using dialect: org.hibernate.dialect.H2Dialect
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
Hibernate:
/* insert com.apress.prospring5.ch7.entities.Singer
*/ insert
into
singer
(ID, BIRTH_DATE, FIRST_NAME, LAST_NAME, VERSION)
values
(null, ?, ?, ?, ?)
Hibernate:
/* insert com.apress.prospring5.ch7.entities.Album
*/ insert
into
album
(ID, RELEASE_DATE, SINGER_ID, title, VERSION)
values
(null, ?, ?, ?, ?)
Hibernate:
/* insert com.apress.prospring5.ch7.entities.Album
*/ insert
into
album
(ID, RELEASE_DATE, SINGER_ID, title, VERSION)
values
(null, ?, ?, ?, ?)
INFO c.a.p.c.d.SingerDaoImpl - Singer saved with id: 4
...
INFO - ---- Listing singers with instruments:
INFO - Singer - Id: 4, First name: BB, Last name: King,
Birthday: 1940-09-16
INFO - Album - Id: 5, Singer id: 4, Title: A Heart Full of Blues,
Release Date: 1962-04-20
INFO - Album - Id: 4, Singer id: 4, Title: My Kind of Blues,
Release Date: 1961-08-18
INFO - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO - Album - Id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO - Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Instrument: Piano
INFO - Instrument: Guitar
INFO - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
INFO - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: 1945-03-30
INFO - Album - Id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
INFO - Instrument: Guitar
日志记录配置已修改,以便打印更详细的休眠信息。从INFO日志记录中,我们可以看到新保存的联系人的 ID 被正确填充。Hibernate 还会显示针对数据库执行的所有 SQL 语句,这样您就知道幕后发生了什么。
更新数据
更新记录就像插入数据一样简单。假设对于 ID 为 1 的歌手,我们希望更新其名字并删除一张专辑。为了测试更新操作,下面的代码片段显示了testUpdate()方法:
package com.apress.prospring5.ch7;
...
public class SingerDaoTest {
private GenericApplicationContext ctx;
private SingerDao singerDao;
...
@Test
public void testUpdate(){
Singer singer = singerDao.findById(1L);
//making sure such singer exists
assertNotNull(singer);
//making sure we got expected singer
assertEquals("Mayer", singer.getLastName());
//retrieve the album
Album album = singer.getAlbums().stream().filter(
a -> a.getTitle().equals("Battle Studies")).findFirst().get();
singer.setFirstName("John Clayton");
singer.removeAlbum(album);
singerDao.save(singer);
// test the update
listSingersWithAlbum(singerDao.findAllWithAlbum());
}
...
}
如前面的代码示例所示,我们首先检索 ID 为 1 的记录。后来,名字就改了。然后,我们遍历相册对象,检索标题为 Battle Studies 的相册,并将其从歌手的albums属性中删除。最后,我们再次调用singerDao.save()方法。当您运行该程序时,您将看到以下输出:
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
...
INFO - Singer saved with id: 1
Hibernate:
/* update
com.apress.prospring5.ch7.entities.Album */ update
album
set
RELEASE_DATE=?,
SINGER_ID=?,
title=?,
VERSION=?
where
ID=?
and VERSION=?
Hibernate:
/* delete com.apress.prospring5.ch7.entities.Album */ delete
from
album
where
ID=?
and VERSION=?
INFO ----- Listing singers with instruments:
INFO - Singer - Id: 1, First name: John Clayton, Last name: Mayer,
Birthday: 1977-10-16
INFO - Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Instrument: Guitar
INFO - Instrument: Piano
INFO - Singer - Id: 2, First name: Eric, Last name: Clapton,
Birthday: 1945-03-30
INFO - Album - Id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
INFO - Instrument: Guitar
INFO - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
你会看到名字被更新,而战史相册被移除。相册可以被删除是因为我们传递给一对多关联的orphanRemoval=true属性,该属性指示 Hibernate 删除所有存在于数据库中但在持久化时在对象中不再存在的孤立记录。
删除数据
删除数据也很简单。只需调用Session.delete()方法,传入联系对象。以下代码片段显示了要删除的代码:
package com.apress.prospring5.ch7.dao;
...
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private static final Log logger =
LogFactory.getLog(SingerDaoImpl.class);
private SessionFactory sessionFactory;
public void delete(Singer singer) {
sessionFactory.getCurrentSession().delete(singer);
logger.info("Singer deleted with id: " + singer.getId());
}
...
}
删除操作将删除歌手记录及其所有相关信息,包括专辑和乐器,正如我们在映射中定义的cascade=CascadeType.ALL。下面的代码片段显示了用于测试删除方法testDelete()的代码:
package com.apress.prospring5.ch7;
...
public class SingerDaoTest {
private static Logger logger =
LoggerFactory.getLogger(SingerDaoTest.class);
private GenericApplicationContext ctx;
private SingerDao singerDao;
@Test
public void testDelete(){
Singer singer = singerDao.findById(2l);
//making sure such singer exists
assertNotNull(singer);
singerDao.delete(singer);
listSingersWithAlbum(singerDao.findAllWithAlbum());
}
}
前面的清单检索 ID 为 2 的歌手,然后调用 delete 方法删除歌手信息。运行该程序将产生以下输出:
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
...
INFO c.a.p.c.d.SingerDaoImpl - Singer deleted with id: 2
Hibernate:
/* delete collection com.apress.prospring5.ch7.entities.
Singer.instruments */ delete
from
singer_instrument
where
SINGER_ID=?
Hibernate:
/* delete com.apress.prospring5.ch7.entities.Album */ delete
from
album
where
ID=?
and VERSION=?
Hibernate:
/* delete com.apress.prospring5.ch7.entities.Singer */ delete
from
singer
where
ID=?
and VERSION=?
INFO - ---- Listing singers with instruments:
INFO - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO - Album - Id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Album - Id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO - Instrument: Piano
INFO - Instrument: Guitar
INFO - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
您可以看到 ID 为 2 的歌手连同其在ALBUM和SINGER_INSTRUMENT表中的子记录一起被删除。
配置 Hibernate 从实体生成表
在使用 Hibernate 的启动应用中,常见的行为是首先编写实体类,然后根据它们的内容生成数据库表。这是通过使用hibernate.hbm2ddl.auto Hibernate 属性来完成的。当应用第一次启动时,该属性值被设置为create;这将使 Hibernate 扫描实体,并根据使用 JPA 和 Hibernate 注释定义的关系生成表和键(主、外来、唯一)。
如果实体配置正确,并且产生的数据库对象完全符合预期,那么属性的值应该更改为update。这将告诉 Hibernate 用以后在实体上执行的任何更改来更新现有的数据库,并保留原始数据库和插入其中的任何数据。
在生产应用中,编写在伪数据库上运行的单元和集成测试是可行的,该伪数据库在所有测试用例执行后被丢弃。通常测试数据库是内存数据库,Hibernate 被告知创建数据库,并在测试执行后通过将hibernate.hbm2ddl.auto值设置为create-drop来丢弃它。
您可以在 Hibernate 官方文档中找到hibernate.hbm2ddl.auto属性值的完整列表。1
下面的代码片段展示了 Java 配置AdvancedConfig类。正如您所看到的,引入了hibernate.hbm2ddl.auto,使用的数据源是一个 DBCP 池数据源。
package com.apress.prospring5.ch7.config;
import com.apress.prospring5.ch6.CleanUp;
import org.apache.commons.dbcp2.BasicDataSource;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.hibernate5.HibernateTransactionManager;
import org.springframework.orm.hibernate5.LocalSessionFactoryBuilder;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;
@Configuration
@ComponentScan(basePackages = "com.apress.prospring5.ch7")
@EnableTransactionManagement
@PropertySource("classpath:db/jdbc.properties")
public class AdvancedConfig {
private static Logger logger =
LoggerFactory.getLogger(AdvancedConfig.class);
@Value("${driverClassName}")
private String driverClassName;
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean(destroyMethod = "close")
public DataSource dataSource() {
try {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
} catch (Exception e) {
logger.error("DBCP DataSource bean cannot be created!", e);
return null;
}
}
private Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public SessionFactory sessionFactory() {
return new LocalSessionFactoryBuilder(dataSource())
.scanPackages("com.apress.prospring5.ch7.entities")
.addProperties(hibernateProperties())
.buildSessionFactory();
}
@Bean public PlatformTransactionManager transactionManager()
throws IOException {
return new HibernateTransactionManager(sessionFactory());
}
}
jdbc.properties文件包含访问内存数据库所需的属性。
driverClassName=org.h2.Driver
url=jdbc:h2:musicdb
username=prospring5
password=prospring5
但是在这种情况下,我们最初如何填充数据呢?您可以使用一个DatabasePopulator实例,一个类似 DbUnit、 2 的库,或者一个类似于DbIntializer bean 的自定义 populator bean,如下所示:
package com.apress.prospring5.ch7.config;
import com.apress.prospring5.ch7.dao.InstrumentDao;
import com.apress.prospring5.ch7.dao.SingerDao;
import com.apress.prospring5.ch7.entities.Album;
import com.apress.prospring5.ch7.entities.Instrument;
import com.apress.prospring5.ch7.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.GregorianCalendar;
@Service
public class DBInitializer {
private Logger logger =
LoggerFactory.getLogger(DBInitializer.class);
@Autowired SingerDao singerDao;
@Autowired InstrumentDao instrumentDao;
@PostConstruct
public void initDB(){
logger.info("Starting database initialization...");
Instrument guitar = new Instrument();
guitar.setInstrumentId("Guitar");
instrumentDao.save(guitar);
...
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singer.addInstrument(guitar);
singer.addInstrument(piano);
Album album1 = new Album();
album1.setTitle("The Search For Everything");
album1.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(2017, 0, 20)).getTime().getTime()));
singer.addAbum(album1);
Album album2 = new Album();
album2.setTitle("Battle Studies");
album2.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(2009, 10, 17)).getTime().getTime()));
singer.addAbum(album2);
singerDao.save(singer);
...
logger.info("Database initialization finished.");
}
}
DbIntializer只是一个简单的 bean,其中存储库作为依赖项被注入,并且有一个由注释@PostConstruct定义的初始化方法,在该方法中对象被创建并保存到数据库中。该 bean 用@Service注释进行了注释,以将其标记为提供初始化数据库内容服务的 bean。这个 bean 将在创建ApplicationContext时创建,初始化方法将被执行,这将确保在使用上下文之前填充数据库。
使用AdvancedConfig配置类,之前运行的相同测试集将通过。
注释方法或字段?
在前面的例子中,实体的 getters 上有 JPA 注释。但是 JPA 注释可以直接在字段上使用,这有几个优点。
- 实体配置更清晰,位于 fields 部分,而不是分散在整个类内容中。显然,只有当代码是按照干净代码的建议编写的,将一个类中的所有字段声明放在同一个连续的部分中时,这一点才是正确的。
- 注释实体字段不会强制提供 setter/getter。这对于
@Version带注释的字段很有用,它不应该被手动修改;只有 Hibernate 可以访问它。 - 注释字段允许在 setters 中进行额外的处理(例如,在从数据库加载值之后对其进行加密/计算)。属性访问的问题是,当对象被加载时,setters 也被调用。
网上有很多关于哪个更好的讨论。从性能的角度来看,没有任何区别。这个决定最终取决于开发人员,因为在一些有效的情况下,注释访问器可能更有意义。但是请记住,在数据库中,对象的状态是实际保存的,对象的状态是由其字段的值定义的,而不是由访问器返回的值。这也意味着可以从数据库中准确地重新创建一个对象,就像它被持久化时一样。因此,在某种程度上,在 getters 上设置注释可以被视为破坏封装。
在这里,您可以看到Singer实体类被重写为具有带注释的字段,并扩展了抽象类AbstractEntity,它包含应用中所有 Hibernate 实体类共有的两个字段:
// AbstractEntity.java
package com.apress.prospring5.ch7.entities;
import javax.persistence.*;
import java.io.Serializable;
@MappedSuperclass
public abstract class AbstractEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(updatable = false)
protected Long id;
@Version
@Column(name = "VERSION")
private int version;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
//Singer.java
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name=Singer.FIND_SINGER_BY_ID,
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i " +
"where s.id = :id"),
@NamedQuery(name=Singer.FIND_ALL_WITH_ALBUM,
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i")
})
public class Singer extends AbstractEntity {
public static final String FIND_SINGER_BY_ID = "Singer.findById";
public static final String FIND_ALL_WITH_ALBUM = "Singer.findAllWithAlbum";
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL,
orphanRemoval=true)
private Set<Album> albums = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "SINGER_ID"),
inverseJoinColumns = @JoinColumn(name = "INSTRUMENT_ID"))
private Set<Instrument> instruments = new HashSet<>();
...
}
使用 Hibernate 时的注意事项
如本章示例所示,一旦正确定义了所有对象到关系的映射、关联和查询,Hibernate 就可以为您提供一个环境,让您专注于使用对象模型编程,而不是为每个操作编写 SQL 语句。在过去的几年里,Hibernate 一直在快速发展,并被 Java 开发人员广泛采用为数据访问层库,无论是在开源社区还是在企业中。
然而,有几点你需要记住。首先,因为您无法控制生成的 SQL,所以在定义映射时应该小心,尤其是关联和它们的获取策略。其次,您应该观察 Hibernate 生成的 SQL 语句,以验证所有语句都按照您的预期执行。
理解 Hibernate 如何管理其会话的内部机制也很重要,尤其是在批处理作业操作中。Hibernate 将保持被管理对象在会话中,并定期刷新和清除它们。设计不良的数据访问逻辑可能会导致 Hibernate 过于频繁地刷新会话,从而严重影响性能。如果你想绝对控制查询,你可以使用本地查询,我们将在下一章讨论。
最后,设置(批处理大小、获取大小等等)在调优 Hibernate 的性能方面起着重要的作用。您应该在会话工厂中定义它们,并在对应用进行负载测试时调整它们,以确定最佳值。
毕竟,Hibernate 以及我们将在下一章讨论的它出色的 JPA 支持,对于寻找一种面向对象方法来实现数据访问逻辑的 Java 开发人员来说是一个自然的选择。
摘要
在本章中,我们讨论了 Hibernate 的基本概念以及如何在 Spring 应用中配置它。然后我们讲述了定义 ORM 映射的常用技术,讲述了关联以及如何使用HibernateTemplate类来执行各种数据库操作。关于 Hibernate,我们只讨论了它的一小部分功能和特性。对于那些对使用 Hibernate 和 Spring 感兴趣的人,我们强烈建议您学习 Hibernate 的标准文档。此外,许多书籍详细讨论了 Hibernate。我们推荐约瑟夫·奥廷格、杰夫·林伍德和戴夫·明特(Apress,2016 年)的《开始冬眠:为了 Hibernate 5》, 3 以及迈克·基思和梅里克·辛卡里奥尔的《Pro JPA 2》(Apress,2013 年)。 4 下一章,你就来看看 JPA,以及在使用 Spring 时如何使用。Hibernate 为 JPA 提供了出色的支持,对于下一章中的例子,我们将继续使用 Hibernate 作为持久性提供者。对于查询和更新操作,JPA act 喜欢 Hibernate。在下一章,我们将讨论一些高级主题,包括本地查询和标准查询,以及如何使用 Hibernate 和它的 JPA 支持。
Footnotes 1
https://docs.jboss.org/hibernate/orm/5.0/manual/en-US/html/ch03.html 见表 3.7。
2
我们将在 http://dbunit.sourceforge.net/ 找到官方的 DbUnit 站点。
3
从阿普瑞斯官方网站下载电子书: http://apress.com/us/book/9781484223185 。
4
从阿普瑞斯官方网站下载电子书: http://apress.com/us/book/9781430249269 。
八、使用 JPA2 在 Spring 中访问数据
在前一章中,我们讨论了当用 ORM 方法实现数据访问逻辑时,如何使用 Hibernate 和 Spring。我们演示了如何在 Spring 的配置中配置 Hibernate 的SessionFactory,以及如何使用Session接口进行各种数据访问操作。然而,这只是 Hibernate 的一种使用方式。在 Spring 应用中采用 Hibernate 的另一种方式是使用 Hibernate 作为标准 Java 持久性 API (JPA)的持久性提供者。
Hibernate 的 POJO 映射和它强大的查询语言(HQL)取得了巨大的成功,也影响了 Java 世界中数据访问技术标准的发展。在 Hibernate 之后,JCP 开发了 Java 数据对象(JDO)标准和 JPA。
在撰写本文时,JPA 已经到了 2.1 版,并提供了标准化的概念,如PersistenceContext、EntityManager和 Java 持久性查询语言(JPQL)。这些标准化为开发人员提供了一种在 Hibernate、EclipseLink、Oracle TopLink 和 Apache OpenJPA 等 JPA 持久性提供者之间切换的方式。因此,大多数新的 JEE 应用都采用 JPA 作为数据访问层。
Spring 也为 JPA 提供了出色的支持。例如,提供了许多EntityManagerFactoryBean实现来引导 JPA 实体管理器,支持前面提到的所有 JPA 提供者。Spring Data 项目还提供了一个名为 Spring Data JPA 的子项目,它为在 Spring 应用中使用 JPA 提供了高级支持。Spring Data JPA 项目的主要特性包括存储库和规范的概念,以及对查询领域特定语言(QueryDSL)的支持。
本章介绍了如何将 JPA 2.1 与 Spring 一起使用,使用 Hibernate 作为底层的持久性提供者。您将学习如何使用 JPA 的EntityManager接口和 JPQL 实现各种数据库操作。然后您将看到 Spring Data JPA 如何进一步帮助简化 JPA 开发。最后,我们将介绍与 ORM 相关的高级主题,包括本地查询和标准查询。
具体来说,我们讨论以下主题:
- Java 持久性 API (JPA)的核心概念:我们涵盖了 JPA 的一些主要概念。
- 配置 JPA 实体管理器:我们讨论 Spring 支持的
EntityManagerFactory类型,以及如何在 Spring 的 XML 配置中配置最常用的LocalContainerEntityManagerFactoryBean。 - 数据操作:我们展示了如何在 JPA 中实现基本的数据库操作,这非常类似于单独使用 Hibernate 时的概念。
- 高级查询操作:我们将讨论如何在 JPA 中使用原生查询,以及在 JPA 中使用强类型标准 API 来实现更灵活的查询操作。
- 介绍 Spring Data Java Persistence API(JPA):我们讨论 Spring Data JPA 项目,并演示它如何帮助简化数据访问逻辑的开发。
- 跟踪实体变更和审计:在数据库更新操作中,跟踪实体的创建日期或上次更新日期以及谁进行了变更是一个常见的需求。此外,对于像客户这样的关键信息,通常需要一个存储实体每个版本的历史表。我们将讨论 Spring DataJPA 和 Hibernate Envers (Hibernate 实体版本管理系统)如何帮助简化这种逻辑的开发。
与 Hibernate 一样,JPA 支持 XML 或 Java 注释中的映射定义。本章关注映射的注释类型,因为它的使用比 XML 风格更受欢迎。
JPA 2.1 简介
像其他 Java 规范请求(JSR)一样,JPA 2.1 规范(JSR-338)的目标是标准化 JSE 和 JEE 环境中的 ORM 编程模型。它定义了一组公共的概念、注释、接口和 JPA 持久性提供者应该实现的其他服务。当按照 JPA 标准编程时,开发人员可以选择随意切换底层提供者,就像为基于 JEE 标准开发的应用切换到另一个符合 JEE 标准的应用服务器一样。
在 JPA 中,核心概念是来自类型为EntityManagerFactory的工厂的EntityManager接口。EntityManager的主要工作是维护一个持久化上下文,在这个上下文中存储了它所管理的所有实体实例。EntityManager的配置定义为一个持久化单元,一个应用中可以有多个持久化单元。如果你正在使用 Hibernate,你可以把持久化上下文想成和Session接口一样的方式,而EntityManagerFactory和SessionFactory是一样的。在 Hibernate 中,受管实体存储在会话中,您可以通过 Hibernate 的SessionFactory或Session接口直接与之交互。然而,在 JPA 中,您不能直接与持久性上下文交互。相反,你需要依靠EntityManager来为你完成工作。
JPQL 类似于 HQL,所以如果你以前用过 HQL,JPQL 应该很容易上手。然而,在 JPA 2 中,引入了强类型标准 API,它依赖于映射实体的元数据来构造查询。鉴于此,任何错误都将在编译时而不是运行时被发现。
关于 JPA 2 的详细讨论,我们推荐 Mike Keith 和 Merrick Schincariol 的书 Pro JPA 2(a press,2013)。 1 在这一节中,我们讨论 JPA 的基本概念,本章将要用到的样本数据模型,以及如何配置 Spring 的ApplicationContext来支持 JPA。
示例代码的示例数据模型
在本章中,我们使用与第七章相同的数据模型。然而,当我们讨论如何实现审计特性时,我们将添加几个列和一个历史表进行演示。首先,我们将从上一章中使用的相同的数据库创建脚本开始。如果你跳过了第七章,看看那一章的“示例代码的示例数据模型”一节中给出的数据模型,这可以帮助你理解本章中的示例代码。
配置 JPA 的 EntityManagerFactory
正如本章前面提到的,要在 Spring 中使用 JPA,我们需要配置EntityManagerFactory,就像在 Hibernate 中使用的SessionFactory一样。Spring 支持三种类型的EntityManagerFactory配置。
第一个使用了LocalEntityManagerFactoryBean类。这是最简单的一种,只需要持久性单元名。然而,由于它不支持DataSource的注入,因此不能参与全局事务,它只适合于简单的开发目的。
第二种选择是在符合 JEE 标准的容器中使用,其中应用服务器根据部署描述符中的信息引导 JPA 持久性单元。这允许 Spring 通过 JNDI 查找来查找实体管理器。下面的配置片段描述了通过 JNDI 查找实体管理器所需的元素:
<beans ...>
<jee:jndi-lookup id="prospring5Emf"
jndi-name="persistence/prospring5PersistenceUnit"/>
</beans>
在 JPA 规范中,应该在配置文件META-INF/persistence.xml中定义一个持久性单元。但是,从 Spring 3.1 开始,增加了一个新特性,消除了这种需要;我们将在本章的后面向您展示如何使用它。
第三个选项是最常见的,也是本章使用的,是支持DataSource注入的LocalContainerEntityManagerFactoryBean类,它可以参与本地和全局事务。下面的配置片段显示了相应的 XML 配置文件(app-context-annotation.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:sql/schema.sql"/>
<jdbc:script location="classpath:sql/test-data.sql"/>
</jdbc:embedded-database>
<bean id="transactionManager" class=
"org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emf"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="emf" class=
"org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class=
"org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
<property name="packagesToScan" value="com.apress.prospring5.ch8.entities"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.H2Dialect
</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">10</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<context:component-scan base-package="com.apress.prospring5.ch8" />
</beans>
您可能期望有一个使用 Java 配置类的等效配置。有,如下图所示:
package com.apress.prospring5.ch8.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.apress.prospring5.ch8.service"})
public class JpaConfig {
private static Logger logger = LoggerFactory.getLogger(JpaConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder =
new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/schema.sql", "classpath:db/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch8.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
在前面的配置中,声明了几个 beans,以便能够使用 Hibernate 作为持久性提供者来支持LocalContainerEntityManagerFactoryBean的配置。主要配置如下:
- dataSource bean:我们使用 H2 声明了带有嵌入式数据库的数据源。因为它是一个嵌入式数据库,所以不需要数据库名称。
- transactionManager bean :
EntityManagerFactory需要一个事务管理器来进行事务数据访问。Spring 提供了专门针对 JPA 的事务管理器(org.springframework.orm.jpa.JpaTransactionManager)。该 bean 是用分配的 IDtransactionManager声明的。我们将在第九章中详细讨论事务。我们声明标签<tx:annotation-driven>来支持使用注释声明事务界定需求。它的对等注解是@EnableTransactionManagement,必须放在用@Configuration注解的类上。 - 组件扫描:标签应该是你熟悉的。我们指示 Spring 扫描包
com.apress.prospring5.ch8下的组件。 - JPA entitymanager factory bean:
emfbean 是最重要的部分。首先,我们声明 bean 使用LocalContainerEntityManagerFactoryBean。在 bean 中,提供了几个属性。首先,如您所料,我们需要注入DataSource豆。其次,我们用类HibernateJpaVendorAdapter配置属性jpaVendorAdapter,因为我们使用的是 Hibernate。第三,我们指示实体工厂在包com.apress.prospring5.ch8(由<property name="packagesToScan">标签指定)下扫描带有 ORM 注释的域对象。注意,这个特性是从 Spring 3.1 开始才有的,在域类扫描的支持下,你可以跳过META-INF/persistence.xml文件中持久性单元的定义。最后,jpaProperties属性提供了持久性提供者 Hibernate 的配置细节。您将看到配置选项与我们在第七章中使用的选项相同,因此我们可以跳过这里的解释。
使用 JPA 注释进行 ORM 映射
Hibernate 在很多方面影响了 JPA 的设计。对于映射注释,它们是如此的接近,以至于我们在第七章中使用的用于将域对象映射到数据库的注释在 JPA 中是相同的。如果你看一下第七章中域类的源代码,你会看到所有的映射注释都在包javax.persistence下,这意味着那些注释已经是 JPA 兼容的了。
一旦EntityManagerFactory被正确配置,将其注入到您的类中就很简单了。下面的代码片段显示了SingerServiceImpl类的代码,我们将用它作为使用 JPA 执行数据库操作的示例:
package com.apress.prospring5.ch8.service;
import com.apress.prospring5.ch8.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.apache.commons.lang3.NotImplementedException;
import java.util.List;
import javax.persistence.PersistenceContext;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public List<Singer> findAll() {
throw new NotImplementedException("findAll");
}
@Transactional(readOnly=true)
@Override
public List<Singer> findAllWithAlbum() {
throw new NotImplementedException("findAllWithAlbum");
}
@Transactional(readOnly=true)
@Override
public Singer findById(Long id) {
throw new NotImplementedException("findById");
}
@Override
public Singer save(Singer singer) {
throw new NotImplementedException("save");
}
@Override
public void delete(Singer singer) {
throw new NotImplementedException("delete");
}
@Transactional(readOnly=true)
@Override
public List<Singer> findAllByNativeQuery() {
throw new NotImplementedException("findAllByNativeQuery");
}
}
对该类应用了几个注释。@Service注释用于将类标识为向另一层提供业务服务的 Spring 组件,并将 Spring bean 命名为jpaSingerService。@Repository注释表明该类包含数据访问逻辑,并指示 Spring 将特定于供应商的异常转换为 Spring 的DataAccessException层次结构。正如您已经熟悉的,@Transactional注释用于定义事务需求。
为了注入EntityManager,我们使用了@PersistenceContext注释,这是实体管理器注入的标准 JPA 注释。关于我们为什么使用名称@PersistenceContext来注入实体管理器,这可能是有问题的,但是如果你考虑到持久化上下文本身是由EntityManager管理的,那么注释命名是完全有意义的。如果您的应用中有多个持久性单元,您还可以将unitName属性添加到注释中,以指定您想要注入哪个持久性单元。通常,一个持久性单元代表一个单独的后端DataSource。
用 JPA 执行数据库操作
本节介绍如何在 JPA 中执行数据库操作。下面的代码片段显示了SingerService接口,它表示我们将要提供的歌手信息服务:
package com.apress.prospring5.ch8.service;
import com.apress.prospring5.ch8.entities.Singer;
import java.util.List;
public interface SingerService {
List<Singer> findAll();
List<Singer> findAllWithAlbum();
Singer findById(Long id);
Singer save(Singer singer);
void delete(Singer singer);
List<Singer> findAllByNativeQuery();
}
界面非常简单;它只有三个查找方法,一个保存方法和一个删除方法。save 方法将同时服务于插入和更新操作。
使用 Java 持久性查询语言查询数据
JPQL 和 HQL 的语法是相似的,事实上,我们在第七章中使用的所有 HQL 查询都可以重用,以在SingerService接口中实现三个 finder 方法。要使用 JPA 和 Hibernate,您需要向项目添加以下依赖项:
//pro-spring-15/build.gradle
ext {
hibernateVersion = '5.2.10.Final'
hibernateJpaVersion = '1.0.0.Final'
..
hibernate = [
em :
"org.hibernate:hibernate-entitymanager:$hibernateVersion",
jpaApi :
"org.hibernate.javax.persistence:hibernate-jpa-2.1-api:$hibernateJpaVersion"
]
}
//chapter08.gradle
dependencies {
//we specify these dependencies for all submodules,
//except the boot module, that defines its own
if !project.name.contains"boot" {
compile spring.contextSupport, spring.orm, spring.context,
misc.slf4jJcl, misc.logback, db.h2, misc.lang3,
hibernate.em, hibernate.jpaApi
}
testCompile testing.junit
}
下面的代码片段概括了第七章中Singer域对象模型类的代码:
//Singer.java
package com.apress.prospring5.ch8.entities;
import static javax.persistence.GenerationType.IDENTITY;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.Column;
import javax.persistence.Version;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.OneToMany;
import javax.persistence.ManyToMany;
import javax.persistence.JoinTable;
import javax.persistence.JoinColumn;
import javax.persistence.CascadeType;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.EntityResult;
@Entity
@Table(name = "singer")
@NamedQueries({
@NamedQuery(name=Singer.FIND_ALL, query="select s from Singer s"),
@NamedQuery(name=Singer.FIND_SINGER_BY_ID,
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i " +
"where s.id = :id"),
@NamedQuery(name=Singer.FIND_ALL_WITH_ALBUM,
query="select distinct s from Singer s " +
"left join fetch s.albums a " +
"left join fetch s.instruments i")
})
@SqlResultSetMapping(
name="singerResult",
entities=@EntityResult(entityClass=Singer.class)
)
public class Singer implements Serializable {
public static final String FIND_ALL = "Singer.findAll";
public static final String FIND_SINGER_BY_ID = "Singer.findById";
public static final String FIND_ALL_WITH_ALBUM = "Singer.findAllWithAlbum";
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL,
orphanRemoval=true)
private Set<Album> albums = new HashSet<>();
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "SINGER_ID"),
inverseJoinColumns = @JoinColumn(name = "INSTRUMENT_ID"))
private Set<Instrument> instruments = new HashSet<>();
//setters and getters
@Override
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate;
}
}
// Album.java
package com.apress.prospring5.ch8.entities;
import static javax.persistence.GenerationType.IDENTITY;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.persistence.*;
@Entity
@Table(name = "album")
public class Album implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column
private String title;
@Temporal(TemporalType.DATE)
@Column(name = "RELEASE_DATE")
private Date releaseDate;
@ManyToOne
@JoinColumn(name = "SINGER_ID")
private Singer singer;
public Album() {
//needed byJPA
}
public Album(String title, Date releaseDate) {
this.title = title;
this.releaseDate = releaseDate;
}
//setters and getters
}
//Instrument.java
package com.apress.prospring5.ch8.entities;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.JoinTable;
import javax.persistence.JoinColumn;
import java.util.Set;
import java.util.HashSet;
@Entity
@Table(name = "instrument")
public class Instrument implements Serializable {
@Id
@Column(name = "INSTRUMENT_ID")
private String instrumentId;
@ManyToMany
@JoinTable(name = "singer_instrument",
joinColumns = @JoinColumn(name = "INSTRUMENT_ID"),
inverseJoinColumns = @JoinColumn(name = "SINGER_ID"))
private Set<Singer> singers = new HashSet<>();
//setters and getters
}
如果您分析使用@NamedQuery定义的查询,您会发现 HQL 和 JPQL 之间似乎没有区别。让我们从findAll()方法开始,它简单地从数据库中检索所有歌手。
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public List<Singer> findAll() {
return em.createNamedQuery(Singer.FIND_ALL, Singer.class)
.getResultList();
}
...
}
如清单所示,我们使用EntityManager.createNamedQuery()方法,传入查询名称和预期的返回类型。在这种情况下,EntityManager将返回一个TypedQuery<X>接口。然后调用方法TypedQuery.getResultList()来检索歌手。为了测试该方法的实现,我们将使用一个测试类,该类将包含每个将要实现的 JPA 方法的测试方法。
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.JpaConfig;
import com.apress.prospring5.ch8.entities.Singer;
import com.apress.prospring5.ch8.service.SingerService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerJPATest {
private static Logger logger = LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testFindAll(){
List<Singer> singers = singerService.findAll();
assertEquals(3, singers.size());
listSingers(singers);
}
private static void listSingers(List<Singer> singers) {
logger.info(" ---- Listing singers:");
for (Singer singer : singers) {
logger.info(singer.toString());
}
}
@After
public void tearDown(){
ctx.close();
}
}
如果assertEquals没有抛出异常(测试失败),运行testFindAll()测试方法将产生以下输出:
---- Listing singers:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
对于关联,JPA 规范规定,默认情况下,持久性提供者必须急切地获取关联。但是,对于 Hibernate 的 JPA 实现,默认的抓取策略仍然是 lazy。因此,当使用 Hibernate 的 JPA 实现时,您不需要显式地将关联定义为惰性抓取。Hibernate 的默认获取策略不同于 JPA 规范。
现在让我们实现findAllWithAlbum()方法,它将获取所有相关的专辑和乐器。实现如下所示:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public List<Singer> findAllWithAlbum() {
List<Singer> singers = em.createNamedQuery
(Singer.FIND_ALL_WITH_ALBUM, Singer.class).getResultList();
return singers;
}
...
}
findAllWithAlbum()与findAll()方法相同,但是它使用不同的命名查询,并启用了left join fetch。用于测试和打印条目的方法如下所示:
package com.apress.prospring5.ch8;
...
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testFindAllWithAlbum(){
List<Singer> singers = singerService.findAllWithAlbum();
assertEquals(3, singers.size());
listSingersWithAlbum(singers);
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
for (Singer singer : singers) {
logger.info(singer.toString());
if (singer.getAlbums() != null) {
for (Album album :
singer.getAlbums()) {
logger.info("\t" + album.toString());
}
}
if (singer.getInstruments() != null) {
for (Instrument instrument : singer.getInstruments()) {
logger.info("\tInstrument: " + instrument.getInstrumentId());
}
}
}
}
@After
public void tearDown(){
ctx.close();
}
}
如果assertEquals没有抛出异常(测试失败),运行testFindAllWithAlbum()测试方法将产生以下输出:
INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397:
Using ASTQueryTranslatorFactory
Hibernate:
/* Singer.findAllWithAlbum */ select
distinct singer0_.ID as ID1_2_0_,
albums1_.ID as ID1_0_1_,
instrument3_.INSTRUMENT_ID as INSTRUME1_1_2_,
singer0_.BIRTH_DATE as BIRTH_DA2_2_0_,
singer0_.FIRST_NAME as FIRST_NA3_2_0_,
singer0_.LAST_NAME as LAST_NAM4_2_0_,
singer0_.VERSION as VERSION5_2_0_,
albums1_.RELEASE_DATE as RELEASE_2_0_1_,
albums1_.SINGER_ID as SINGER_I5_0_1_,
albums1_.title as title3_0_1_,
albums1_.VERSION as VERSION4_0_1_,
albums1_.SINGER_ID as SINGER_I5_0_0__,
albums1_.ID as ID1_0_0 ,
instrument2_.SINGER_ID as SINGER_I1_3_1__,
instrument2_.INSTRUMENT_ID as INSTRUME2_3_1__
from
singer singer0_
left outer join
album albums1_
on singer0_.ID=albums1_.SINGER_ID
left outer join
singer_instrument instrument2_
on singer0_.ID=instrument2_.SINGER_ID
left outer join
instrument instrument3_
on instrument2_.INSTRUMENT_ID=instrument3_.INSTRUMENT_ID
INFO ----- Listing singers with instruments:
INFO - Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
INFO - Album - id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO - Album - id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Instrument: Guitar
INFO - Instrument: Piano
INFO - Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
INFO - Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
INFO - Album - id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
INFO - Instrument: Guitar
如果为 Hibernate 启用了日志记录,您还可以看到为从数据库中提取所有数据而生成的本地查询。
现在让我们看看findById()方法,它演示了如何在 JPA 中使用带有命名参数的命名查询。关联也将被提取。以下代码片段显示了实现:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public Singer findById(Long id) {
TypedQuery<Singer> query = em.createNamedQuery
(Singer.FIND_SINGER_BY_ID, Singer.class);
query.setParameter("id", id);
return query.getSingleResult();
}
...
}
调用了EntityManager.createNamedQuery(java.lang.String name, java.lang.Class<T> resultClass)来获取TypedQuery<T>接口的实例,这确保了查询结果必须是Singer类型的。然后用TypedQuery<T>.setParameter()方法设置查询中指定参数的值,并调用getSingleResult()方法,因为结果应该只包含一个具有指定 ID 的Singer对象。我们将把方法的测试作为一个练习留给您。
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Transactional(readOnly=true)
@Override
public Singer findById(Long id) {
TypedQuery<Singer> query = em.createNamedQuery
(Singer.FIND_SINGER_BY_ID, Singer.class);
query.setParameter("id", id);
return query.getSingleResult();
} ...
}
使用非类型化结果进行查询
在许多情况下,您希望向数据库提交一个查询并随意操作结果,而不是将它们存储在映射的实体类中。一个典型的例子是一个基于 web 的报表,它只列出多个表中一定数量的列。例如,假设您有一个显示歌手信息和他最近发行的专辑名称的网页。摘要信息包含歌手的全名和他最近发行的专辑名称。没有专辑的歌手不会被列出。在这种情况下,我们可以用一个查询实现这个用例,然后手动操作ResultSet对象。
让我们创建一个名为SingerSummaryUntypeImpl的新类,并将方法命名为displayAllSingerSummary()。下面的代码片段显示了方法的典型实现:
package com.apress.prospring5.ch8.service;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Iterator;
import java.util.List;
@Service("singerSummaryUntype")
@Repository
@Transactional
public class SingerSummaryUntypeImpl {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
public void displayAllSingerSummary() {
List result = em.createQuery(
"select s.firstName, s.lastName, a.title from Singer s "
+ "left join s.albums a "
+ "where a.releaseDate=(select max(a2.releaseDate) "
+ "from Album a2 where a2.singer.id = s.id)")
.getResultList();
int count = 0;
for (Iterator i = result.iterator(); i.hasNext(); ) {
Object[] values = (Object[]) i.next();
System.out.println(++count + ": " + values[0] + ", "
+ values[1] + ", " + values[2]);
}
}
}
如前面的代码示例所示,我们使用EntityManager.createQuery()方法创建Query,传入 JPQL 语句,然后得到结果列表。
当我们在 JPQL 中显式指定要选择的列时,JPA 将返回一个迭代器,迭代器中的每一项都是一个对象数组。我们循环遍历迭代器,对于对象数组中的每个元素,都会显示值。每个对象数组对应于ResultSet对象中的一条记录。以下代码片段显示了测试程序:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.JpaConfig;
import com.apress.prospring5.ch8.service.SingerSummaryUntypeImpl;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerSummaryJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerSummaryJPATest.class);
private GenericApplicationContext ctx;
private SingerSummaryUntypeImpl singerSummaryUntype;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerSummaryUntype = ctx.getBean(SingerSummaryUntypeImpl.class);
assertNotNull(singerSummaryUntype);
}
@Test
public void testFindAllUntype() {
singerSummaryUntype.displayAllSingerSummary();
}
@After
public void tearDown() {
ctx.close();
}
}
运行测试程序会产生以下输出:
1: John, Mayer, The Search For Everything
2: Eric, Clapton, From The Cradle
在 JPA 中,有一个更优雅的解决方案,而不是摆弄查询返回的对象数组,这将在下一节讨论。
使用构造函数表达式查询自定义结果类型
在 JPA 中,当查询像上一节中那样的定制结果时,您可以指示 JPA 从每个记录中直接构造一个 POJO。这个 POJO 也称为视图,因为它包含来自多个表的数据。对于上一节中的示例,让我们创建一个名为SingerSummary的 POJO,它存储歌手摘要的查询结果。下面的代码片段显示了该类:
package com.apress.prospring5.ch8.view;
import java.io.Serializable;
public class SingerSummary implements Serializable {
private String firstName;
private String lastName;
private String latestAlbum;
public SingerSummary(String firstName, String lastName,
String latestAlbum) {
this.firstName = firstName;
this.lastName = lastName;
this.latestAlbum = latestAlbum;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getLatestAlbum() {
return latestAlbum;
}
public String toString() {
return "First name: " + firstName + ", Last Name: " + lastName
+ ", Most Recent Album: " + latestAlbum;
}
}
前面的SingerSummary类有每个 singer 摘要的属性,有一个接受所有属性的构造函数方法。有了SingerSummary类,我们可以修改findAll()方法,并在查询中使用构造函数表达式来指示 JPA 提供者将ResultSet映射到SingerSummary类。让我们首先为SingerSummary服务创建一个接口。以下代码片段显示了该界面:
package com.apress.prospring5.ch8.service;
import com.apress.prospring5.ch8.view.SingerSummary;
import java.util.List;
public interface SingerSummaryService {
List<SingerSummary> findAll();
}
在这里,您可以看到SingerSummaryImpl.findAll()方法的实现,使用了用于ResultSet映射的构造函数表达式:
package com.apress.prospring5.ch8.service;
import com.apress.prospring5.ch8.view.SingerSummary;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Service("singerSummaryService")
@Repository
@Transactional
public class SingerSummaryServiceImpl implements SingerSummaryService {
@PersistenceContext
private EntityManager em;
@Transactional(readOnly = true)
@Override
public List<SingerSummary> findAll() {
List<SingerSummary> result = em.createQuery(
"select new com.apress.prospring5.ch8.view.SingerSummary("
+ "s.firstName, s.lastName, a.title) from Singer s "
+ "left join s.albums a "
+ "where a.releaseDate=(select max(a2.releaseDate):
+ "from Album a2 where a2.singer.id = s.id)",
SingerSummary.class).getResultList();
return result;
}
}
在 JPQL 语句中,指定了new关键字,以及 POJO 类的完全限定名,该类将存储结果并传入所选属性作为每个SingerSummary类的构造函数参数。最后,SingerSummary类被传入到createQuery()方法中以指示结果类型。以下代码片段显示了测试程序:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.JpaConfig;
import com.apress.prospring5.ch8.service.SingerSummaryService;
import com.apress.prospring5.ch8.view.SingerSummary;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerSummaryJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerSummaryJPATest.class);
private GenericApplicationContext ctx;
private SingerSummaryService singerSummaryService;
@Before
public void setUp() {
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerSummaryService = ctx.getBean(SingerSummaryService.class);
assertNotNull(singerSummaryService);
}
@Test
public void testFindAll() {
List<SingerSummary> singers = singerSummaryService.findAll();
listSingerSummary(singers);
assertEquals(2, singers.size());
}
private static void listSingerSummary(List<SingerSummary> singers) {
logger.info(" ---- Listing singers summary:");
for (SingerSummary singer : singers) {
logger.info(singer.toString());
}
}
@After
public void tearDown() {
ctx.close();
}
}
再次执行testFindAll方法类产生列表中每个SingerSummary对象的输出,如下所示(其他输出被省略):
INFO ---- Listing singers summary:
INFO - First name: John, Last Name: Mayer, Most Recent Album: The Search For Everything
INFO - First name: Eric, Last Name: Clapton, Most Recent Album: From The Cradle
正如您所看到的,构造函数表达式对于将定制查询的结果映射到 POJOs 以供进一步的应用处理非常有用。
插入数据
使用 JPA 插入数据很简单。和 Hibernate 一样,JPA 也支持检索数据库生成的主键。下面的代码片段显示了save()方法:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Override
public Singer save(Singer singer) {
if (singer.getId() == null) {
logger.info("Inserting new singer");
em.persist(singer);
} else {
em.merge(singer);
logger.info("Updating existing singer");
}
logger.info("Singer saved with id: " + singer.getId());
return singer;
}
...
}
如此处所示,save()方法首先通过检查id值来检查对象是否是新的实体实例。如果id是null(即尚未赋值),则该对象是一个新的实体实例,将调用EntityManager.persist()方法。当调用persist()方法时,EntityManager持久化实体并使其成为当前持久化上下文中的托管实例。如果id值存在,那么我们正在执行更新,而EntityManager.merge()方法将被调用。当调用merge()方法时,EntityManager将实体的状态合并到当前的持久化上下文中。
下面的代码片段显示了插入新歌手记录的代码。这都是在测试方法中完成的,因为我们想要测试插入是否成功。
package com.apress.prospring5.ch8;
...
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testInsert(){
Singer singer = new Singer();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
Album album = new Album();
album.setTitle("My Kind of Blues");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1961, 7, 18)).getTime().getTime()));
singer.addAbum(album);
album = new Album();
album.setTitle("A Heart Full of Blues");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1962, 3, 20)).getTime().getTime()));
singer.addAbum(album);
singerService.save(singer);
assertNotNull(singer.getId());
List<Singer> singers = singerService.findAllWithAlbum();
assertEquals(4, singers.size());
listSingersWithAlbum(singers);
}
...
@After
public void tearDown(){
ctx.close();
}
}
如此处所示,我们创建了一个新歌手,添加了两张专辑,并保存了对象。然后,我们再次列出所有的歌手,在我们测试了表中记录的正确数量之后。运行该程序会产生以下输出:
INFO - ---- Listing singers with instruments:
INFO - Singer - Id: 4, First name: BB, Last name: King, Birthday: 1940-09-16
INFO - Album - id: 5, Singer id: 4, Title: A Heart Full of Blues,
Release Date: 1962-04-20
INFO - Album - id: 4, Singer id: 4, Title: My Kind of Blues,
Release Date: 1961-08-18
INFO - Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
INFO - Album - id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
INFO - Album - id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO - Instrument: Piano
INFO - Instrument: Guitar
INFO - Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
INFO - Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
INFO - Album - id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
INFO - Instrument: Guitar
从INFO日志记录中,可以看到新保存的歌手的id被正确填充。Hibernate 还会显示所有被发送到数据库的 SQL 语句。
更新数据
更新数据就像插入数据一样简单。我们来看一个例子。假设对于一个 ID 为 1 的歌手,我们希望更新其名字并删除一张专辑。为了测试更新操作,下面的代码片段显示了testUpdate()方法:
package com.apress.prospring5.ch8;
...
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testUpdate(){
Singer singer = singerService.findById(1L);
//making sure such singer exists assertNotNull(singer);
//making sure we got expected record assertEquals("Mayer", singer.getLastName());
//retrieve the album
Album album = singer.getAlbums().stream()
.filter(a -> a.getTitle().equals("Battle Studies")).findFirst().get();
singer.setFirstName("John Clayton");
singer.removeAlbum(album);
singerService.save(singer);
listSingersWithAlbum(singerService.findAllWithAlbum());
}
...
@After
public void tearDown(){
ctx.close();
}
}
我们首先检索 ID 为 1 的记录,然后更改名字。然后,我们遍历专辑对象,检索带有标题战研究的对象,并将其从歌手的albums属性中删除。最后,我们再次调用SingerService.save()方法。当您运行该程序时,您将看到以下输出(其他输出被省略):
---- Listing singers with instruments:
Singer - Id: 1, First name: John Clayton, Last name: Mayer, Birthday: 1977-10-16
Album - id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
Instrument: Piano
Instrument: Guitar
Singer - Id: 2, First name: Eric, Last name: Clapton, Birthday: 1945-03-30
Album - id: 3, Singer id: 2, Title: From The Cradle,
Release Date: 1994-09-13
Instrument: Guitar
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
您将看到名字被更新,相册被移除。相册可以被删除,因为在一对多关联中定义了rphanRemoval=true属性,该属性指示 JPA provider (Hibernate)删除所有存在于数据库中但在持久化时在对象中不再找到的孤立记录。
@OneToMany(mappedBy = "singer", cascade=CascadeType.ALL, orphanRemoval=true)
删除数据
删除数据也一样简单。只需调用EntityManager.remove()方法并传入 singer 对象。以下代码片段显示了删除歌手的更新代码:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
@Override
public void delete(Singer singer) {
Singer mergedSinger = em.merge(singer);
em.remove(mergedSinger);
logger.info("Singer with id: " + singer.getId() + " deleted successfully");
}
...
}
首先调用EntityManager.merge()方法将实体的状态合并到当前的持久化上下文中。merge()方法返回托管实体实例。然后调用EntityManager.remove(),传入托管 singer 实体实例。remove 操作删除歌手记录及其所有相关信息,包括专辑和乐器,正如我们在映射中定义的cascade=CascadeType.ALL。为了测试删除操作,可以使用testDelete()方法,如下面的代码片段所示:
package com.apress.prospring5.ch8;
...
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testDelete(){
Singer singer = singerService.findById(2l);
//making sure such singer exists
assertNotNull(singer);
singerService.delete(singer);
listSingersWithAlbum(singerService.findAllWithAlbum());
}
...
@After
public void tearDown(){
ctx.close();
}
}
前面的清单检索 ID 为2的歌手,然后调用delete()方法删除歌手信息。运行该程序会产生以下输出:
---- Listing singers with instruments:
Singer - Id: 1, First name: John, Last name: Mayer, Birthday: 1977-10-16
Album - id: 1, Singer id: 1, Title: The Search For Everything,
Release Date: 2017-01-20
Album - id: 2, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
Instrument: Piano
Instrument: Guitar
Singer - Id: 3, First name: John, Last name: Butler, Birthday: 1975-04-01
可以看到 ID 为1的歌手被删除了。
使用本机查询
讨论了使用 JPA 执行简单的数据库操作之后,现在让我们继续讨论一些更高级的主题。有时,您可能希望对提交给数据库的查询拥有绝对控制权。一个例子是在 Oracle 数据库中使用分层查询。这种查询是特定于数据库的,称为本机查询。
JPA 支持本地查询的执行;EntityManager将按原样向数据库提交查询,不执行任何映射或转换。使用 JPA 原生查询的一个主要好处是将ResultSet映射回 ORM 映射的实体类。接下来的两节讨论了如何使用本地查询来检索所有歌手,并将ResultSet直接映射回Singer对象。
使用简单的本地查询
为了演示如何使用原生查询,让我们实现一个新方法来从数据库中检索所有歌手。下面的代码片段显示了必须添加到SingerServiceImpl中的新方法:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@Transactional(readOnly=true)
@Override
public List<Singer> findAllByNativeQuery() {
return em.createNativeQuery(ALL_SINGER_NATIVE_QUERY,
Singer.class).getResultList();
}
...
}
您可以看到,本地查询只是一个简单的 SQL 语句,用于从SINGER表中检索所有列。为了创建和执行查询,首先调用了EntityManager.createNativeQuery(),传递了查询字符串和结果类型。结果类型应该是一个映射的实体类(在本例中是Singer类)。createNativeQuery()方法返回一个Query接口,该接口提供getResultList()操作来获取结果列表。JPA 提供者将执行查询,并根据实体类中定义的 JPA 映射,将ResultSet对象转换成实体实例。执行前面的方法会产生与findAll()方法相同的结果。
使用 SQL 结果集映射的本机查询
除了映射的域对象之外,还可以传入一个字符串,该字符串表示 SQL ResultSet映射的名称。通过使用@SqlResultSetMapping注释,在实体类级别定义了 SQL ResultSet映射。一个 SQL ResultSet映射可以有一个或多个实体和列映射。
package com.apress.prospring5.ch8.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.EntityResult;
...
@Entity
@Table(name = "singer")
@SqlResultSetMapping(
name="singerResult",
entities=@EntityResult(entityClass=Singer.class)
)
public class Singer implements Serializable {
...
}
为实体类定义了一个名为singerResult的 SQL ResultSet映射,在Singer类本身中有entityClass属性。JPA 支持多个实体的更复杂的映射,并支持向下映射到列级映射。
在定义了 SQL ResultSet映射之后,可以使用ResultSet映射的名称来调用findAllByNativeQuery()方法。下面的代码片段显示了更新后的findAllByNativeQuery()方法:
package com.apress.prospring5.ch8.service;
...
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private static Logger logger =
LoggerFactory.getLogger(SingerServiceImpl.class);
@Transactional(readOnly=true)
@Override
public List<Singer> findAllByNativeQuery() {
return em.createNativeQuery(ALL_SINGER_NATIVE_QUERY,
"singerResult").getResultList();
}
...
}
如您所见,JPA 还为执行原生查询提供了强大的支持,提供了灵活的 SQL ResultSet映射工具。
使用 JPA 2 标准 API 进行标准查询
大多数应用都为用户提供了搜索信息的前端。最有可能的情况是,显示了大量可搜索的字段,用户只在其中的一些字段中输入信息并进行搜索。很难准备大量的查询,包括用户可能选择输入的每个可能的参数组合。在这种情况下,criteria API 查询特性可以帮上忙。
在 JPA 2 中,引入的一个主要新特性是强类型标准 API 查询。在这个新的 Criteria API 中,传递到查询中的标准基于映射的实体类的元模型。因此,指定的每个标准都是强类型的,错误将在编译时发现,而不是在运行时发现。
在 JPA Criteria API 中,实体类元模型由带有下划线后缀(_)的实体类名表示。例如,Singer实体类的元模型类是Singer_。下面的代码片段显示了Singer_类:
package com.apress.prospring5.ch8;
import java.util.Date;
import javax.annotation.Generated;
import javax.persistence.metamodel.SetAttribute;
import javax.persistence.metamodel.SingularAttribute;
import javax.persistence.metamodel.StaticMetamodel;
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Singer.class)
public abstract class Singer_ {
public static volatile SingularAttribute<Singer, String> firstName;
public static volatile SingularAttribute<Singer, String> lastName;
public static volatile SetAttribute<Singer, Album> albums;
public static volatile SetAttribute<Singer, Instrument> instruments;
public static volatile SingularAttribute<Singer, Long> id;
public static volatile SingularAttribute<Singer, Integer> version;
public static volatile SingularAttribute<Singer, Date> birthDate;
}
元模型类用@StaticMetamodel标注,属性是映射的实体类。在类中是每个属性及其相关类型的声明。
编码和维护那些元模型类将是乏味的。然而,工具可以帮助基于实体类中的 JPA 映射自动生成那些元模型类。Hibernate 提供的那个叫做 Hibernate 元模型生成器( www.hibernate.org/subprojects/jpamodelgen.html )。
您生成元模型类的方式取决于您使用什么工具来开发和构建您的项目。我们建议阅读文档的“用法”部分( http://docs.jboss.org/hibernate/jpamodelgen/1.3/reference/en-US/html_single/#chapter-usage )了解具体细节。作为本书一部分的示例代码使用 Gradle 来生成元类。元模型类生成所需的依赖项是hibernate-jpamodelgen库。这个依赖项是用它在pro-spring-15/build.gradle文件中的版本配置的。
ext {
...
//persistency libraries
hibernateVersion = '5.2.10.Final'
hibernateJpaVersion = '1.0.0.Final'
hibernate = [
...
jpaModelGen: "org.hibernate:hibernate-jpamodelgen:$hibernateVersion",
jpaApi : "org.hibernate.javax.persistence:hibernate-jpa-2.1-api:
$hibernateJpaVersion",
querydslapt: "com.mysema.querydsl:querydsl-apt:2.7.1"
]
...
这是生成元模型类的主要库。在编译模块之前,generateQueryDSL Gradle 任务在chapter08/jpa-criteria/build.gradle中使用它来生成元模型类。这里显示了chapter08/jpa-criteria/build.gradle的配置:
sourceSets {
generated
}
sourceSets.generated.java.srcDirs = ['src/main/generated']
configurations {
querydslapt
}
dependencies {
compile hibernate.querydslapt, hibernate.jpaModelGen
}
task generateQueryDSL(type: JavaCompile, group: 'build',
description: 'Generates the QueryDSL query types') {
source = sourceSets.main.java
classpath = configurations.compile + configurations.querydslapt
options.compilerArgs = [
"-proc:only",
"-processor", "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor"
]
destinationDir = sourceSets.generated.java.srcDirs.iterator.next
}
compileJava.dependsOn generateQueryDSL
设置好类生成策略后,让我们定义一个查询,该查询接受名字和姓氏来搜索歌手。下面的代码片段显示了在SingerService接口中新方法findByCriteriaQuery()的定义:
package com.apress.prospring5.ch8;
import java.util.List;
public interface SingerService {
List<Singer> findAll();
List<Singer> findAllWithAlbum();
Singer findById(Long id);
Singer save(Singer singer);
void delete(Singer singer);
List<Singer> findAllByNativeQuery();
List<Singer> findByCriteriaQuery(String firstName, String lastName);
}
下一个代码片段显示了使用 JPA 2 criteria API 查询的findByCriteriaQuery()方法的实现:
package com.apress.prospring5.ch8;
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import javax.persistence.PersistenceContext;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@Service("jpaSingerService")
@Repository
@Transactional
public class SingerServiceImpl implements SingerService {
final static String ALL_SINGER_NATIVE_QUERY =
"select id, first_name, last_name, birth_date, version from singer";
private Log log =
LogFactory.getLog(SingerServiceImpl.class);
@PersistenceContext
private EntityManager em;
...
@Transactional(readOnly=true)
@Override
public List<Singer> findByCriteriaQuery(String firstName, String lastName) {
log.info("Finding singer for firstName: " + firstName
+ " and lastName: " + lastName);
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Singer> criteriaQuery = cb.createQuery(Singer.class);
Root<Singer> singerRoot = criteriaQuery.from(Singer.class);
singerRoot.fetch(Singer_.albums, JoinType.LEFT);
singerRoot.fetch(Singer_.instruments, JoinType.LEFT);
criteriaQuery.select(singerRoot).distinct(true);
Predicate criteria = cb.conjunction();
if (firstName != null) {
Predicate p = cb.equal(singerRoot.get(Singer_.firstName),
firstName);
criteria = cb.and(criteria, p);
}
if (lastName != null) {
Predicate p = cb.equal(singerRoot.get(Singer_.lastName),
lastName);
criteria = cb.and(criteria, p);
}
criteriaQuery.where(criteria);
return em.createQuery(criteriaQuery).getResultList();
}
}
让我们来分解 API 使用的标准。
- 调用
EntityManager.getCriteriaBuilder()来检索CriteriaBuilder的实例。 - 使用
CriteriaBuilder.createQuery()创建类型化查询,将Singer作为结果类型传入。 - 调用
CriteriaQuery.from()方法,传入实体类。结果是一个对应于指定实体的查询根对象(Root<Singer>接口)。查询根对象构成了查询中路径表达式的基础。 - 两个
Root.fetch()方法调用强制执行与专辑和乐器相关的关联的快速获取。JoinType.LEFT参数指定了一个外部连接。用JoinType.LEFT作为第二个参数调用Root.fetch()方法相当于在 JPQL 中指定 left join fetch join 操作。 - 调用
CriteriaQuery.select()方法,并将根查询对象作为结果类型传递。带有 true 的distinct()方法意味着应该消除重复的记录。 - 通过调用
CriteriaBuilder.conjunction()方法获得一个Predicate实例,这意味着一个或多个限制的合取。一个Predicate可以是一个简单的或者复合的谓词,一个谓词是一个限制,表示由一个表达式定义的选择标准。 - 检查名字和姓氏参数。对于每个非
null参数,将使用CriteriaBuilder()方法(即CriteriaBuilder.and()方法)构造一个新的Predicate。方法equal()是指定一个相等的限制,在这个限制中Root.get()被调用,传递限制所应用的实体类元模型的相应属性。然后,通过调用CriteriaBuilder.and()方法,构造的谓词与现有的谓词(由变量 criteria 存储)进行“合取”。 - 通过调用
CriteriaQuery.where()方法,Predicate由所有标准和限制构成,并作为where子句传递给查询。 - 最后,
CriteriaQuery被传递给EntityManager. EntityManager,然后基于传入的CriteriaQuery值构造查询,执行查询,并返回结果。
为了测试条件查询操作,下面的代码片段显示了更新后的SingerJPATest类:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.JpaConfig;
import com.apress.prospring5.ch8.Album;
import com.apress.prospring5.ch8.Instrument;
import com.apress.prospring5.ch8.Singer;
import com.apress.prospring5.ch8.SingerService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class SingerJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(JpaConfig.class);
singerService = ctx.getBean("jpaSingerService", SingerService.class);
assertNotNull(singerService);
}
@Test
public void tesFindByCriteriaQuery(){
List<Singer> singers = singerService.findByCriteriaQuery("John", "Mayer");
assertEquals(1, singers.size());
listSingersWithAlbum(singers);
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
singers.forEach(s -> {
logger.info(s.toString());
if (s.getAlbums() != null) {
s.getAlbums().forEach(a -> logger.info("\t" + a.toString()));
}
if (s.getInstruments() != null) {
s.getInstruments().forEach(i -> logger.info
("\tInstrument: " + i.getInstrumentId()));
}
});
}
@After
public void tearDown(){
ctx.close();
}
}
运行该程序会产生以下输出(省略了其他输出,但保留了生成的查询):
INFO o.h.h.i.QueryTranslatorFactoryInitiator -
HHH000397: Using ASTQueryTranslatorFactory
INFO c.a.p.c.SingerServiceImpl -
Finding singer for firstName: John and lastName: Mayer
Hibernate:
select
distinct singer0_.ID as ID1_2_0_,
albums1_.ID as ID1_0_1_,
instrument3_.INSTRUMENT_ID as INSTRUME1_1_2_,
singer0_.BIRTH_DATE as BIRTH_DA2_2_0_,
singer0_.FIRST_NAME as FIRST_NA3_2_0_,
singer0_.LAST_NAME as LAST_NAM4_2_0_,
singer0_.VERSION as VERSION5_2_0_,
albums1_.RELEASE_DATE as RELEASE_2_0_1_,
albums1_.SINGER_ID as SINGER_I5_0_1_,
albums1_.title as title3_0_1_,
albums1_.VERSION as VERSION4_0_1_,
albums1_.SINGER_ID as SINGER_I5_0_0__,
albums1_.ID as ID1_0_0__,
instrument2_.SINGER_ID as SINGER_I1_3_1__,
instrument2_.INSTRUMENT_ID as INSTRUME2_3_1__
from
singer singer0_
left outer join
album albums1_
on singer0_.ID=albums1_.SINGER_ID
left outer join
singer_instrument instrument2_
on singer0_.ID=instrument2_.SINGER_ID
left outer join
instrument instrument3_
on instrument2_.INSTRUMENT_ID=instrument3_.INSTRUMENT_ID
where
1=1
and singer0_.FIRST_NAME=?
and singer0_.LAST_NAME=?
INFO c.a.p.c.SingerJPATest - ---- Listing singers with instruments:
INFO c.a.p.c.SingerJPATest - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO c.a.p.c.SingerJPATest - Album - id: 2, Singer id: 1,
Title: Battle Studies, Release Date: 2009-11-17
INFO c.a.p.c.SingerJPATest - Album - id: 1, Singer id: 1,
Title: The Search For Everything, Release Date: 2017-01-20
INFO c.a.p.c.SingerJPATest - Instrument: Guitar
INFO c.a.p.c.SingerJPATest - Instrument: Piano
您可以尝试不同的组合,或者向其中一个参数传递一个null值来观察输出。
Spring Data JPA 简介
Spring Data JPA 项目是 Spring Data 保护伞项目下的一个子项目。该项目的主要目标是提供额外的特性来简化 JPA 的应用开发。
Spring Data JPA 提供了几个主要特性。在本节中,我们讨论两个。第一个是Repository抽象,另一个是实体监听器,用于跟踪实体类的基本审计信息。
添加 Spring Data JPA 库依赖项
要使用 Spring Data JPA,我们需要向项目添加依赖项。在这里,您可以看到使用 Spring Data JPA 所需的 Gradle 配置:
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.M5'
bootVersion = '2.0.0.BUILD-SNAPSHOT'
springDataVersion = '2.0.0.M2'
...
spring = [
data : "org.springframework.data:spring-data-jpa:$springDataVersion",
...
]
...
}
//chapter08/spring-data-jpa/build.gradle
dependencies {
compile spring.aop, spring.data, misc.guava
}
使用 Spring Data JPA 存储库抽象进行数据库操作
Spring Data 及其所有子项目的一个主要概念是Repository抽象,它属于 Spring Data Commons 项目( https://github.com/spring-projects/spring-data-commons )。在撰写本文时,它的版本是 2.0.0 M2。在 Spring Data JPA 中,存储库抽象包装了底层 JPA EntityManager,并为基于 JPA 的数据访问提供了一个更简单的接口。Spring Data 中的中心接口是org.springframework.data.repository.Repository<T,ID extends Serializable>接口,这是一个属于 Spring Data Commons 发行版的标记接口。Spring Data 提供了Repository接口的各种扩展;其中之一是org.springframework.data.repository.CrudRepository接口(也属于 Spring Data 共享项目),我们将在本节中讨论。
CrudRepository接口提供了许多常用的方法。下面的代码片段显示了接口声明,它是从 Spring Data Commons 项目源代码中提取的:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable>
extends Repository<T, ID> {
long count();
void delete(ID id);
void delete(Iterable<? extends T> entities);
void delete(T entity);
void deleteAll();
boolean exists(ID id);
Iterable<T> findAll();
T findOne(ID id);
Iterable<T> save(Iterable<? extends T> entities);
T save(T entity);
}
尽管方法命名是不言自明的,但最好通过一个简单的例子来展示Repository抽象是如何工作的。让我们稍微修改一下SingerService接口,只剩下三个 finder 方法。下面的代码片段显示了修改后的SingerService界面:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Singer;
import java.util.List;
public interface SingerService {
List<Singer> findAll();
List<Singer> findByFirstName(String firstName);
List<Singer> findByFirstNameAndLastName(String firstName, String lastName);
}
下一步是准备SingerRepository接口,它扩展了CrudRepository接口。下面的代码片段显示了SingerRepository界面:
package com.apress.prospring5.ch8;
import java.util.List;
import com.apress.prospring5.ch8.entities.Singer;
import org.springframework.data.repository.CrudRepository;
public interface SingerRepository extends CrudRepository<Singer, Long> {
List<Singer> findByFirstName(String firstName);
List<Singer> findByFirstNameAndLastName(String firstName, String lastName);
}
我们只需要在这个接口中声明两个方法,因为CrudRepository.findAll()方法已经提供了findAll()方法。如前面的清单所示,SingerRepository接口扩展了CrudRepository接口,传入了实体类(Singer)和 ID 类型(Long)。Spring Data 的Repository抽象的一个有趣的方面是,当您使用findByFirstName和findByFirstNameAndLastName的通用命名约定时,您不需要为 Spring Data JPA 提供命名查询。相反,Spring Data JPA 将根据方法名为您“推断”和构造查询。例如,对于findByFirstName()方法,Spring Data JPA 会自动为您准备查询select s from Singer s where s.firstName = :firstName,并根据实参设置命名参数firstName。
要使用Repository抽象,您必须在 Spring 的配置中定义它。下面的代码片段显示了配置文件(app-context-annotation.xml):
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jdbc
http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<jdbc:embedded-database id="dataSource" type="H2">
<jdbc:script location="classpath:db/schema.sql"/>
<jdbc:script location="classpath:db/test-data.sql"/>
</jdbc:embedded-database>
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emf"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager" />
<bean id="emf"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
</property>
<property name="packagesToScan"
value="com.apress.prospring5.ch8.entities"/>
<property name="jpaProperties">
<props>
<prop key="hibernate.dialect">
org.hibernate.dialect.H2Dialect
</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
<prop key="hibernate.jdbc.fetch_size">50</prop>
<prop key="hibernate.jdbc.batch_size">10</prop>
<prop key="hibernate.show_sql">true</prop>
</props>
</property>
</bean>
<context:annotation-config/>
<context:component-scan base-package="com.apress.prospring5.ch8" >
<context:exclude-filter type="annotation"
expression="org.springframework.context.annotation.Configuration" />
</context:component-scan>
<jpa:repositories base-package="com.apress.prospring5.ch8"
entity-manager-factory-ref="emf"
transaction-manager-ref="transactionManager"/>
</beans>
首先,我们需要在配置文件中添加jpa名称空间。然后,<jpa:repositories>标签用于配置 Spring Data JPA 的Repository抽象。我们指示 Spring 扫描包com.apress.prospring5.ch8中的存储库接口,并分别传入EntityManagerFactory和事务管理器。
如果您还没有注意到的话,在<context:component-scan>定义中有一个<context:exclude-filter>指定了用@Configuration注释的类。引入该元素是为了排除对 Java 配置类的扫描,Java 配置类可以用来代替前面描述的 XML 配置。该类如下所示:
package com.apress.prospring5.ch8.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.apress.prospring5.ch8"})
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch8"})
public class DataJpaConfig {
private static Logger logger = LoggerFactory.getLogger(DataJpaConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/schema.sql",
"classpath:db/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch8.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getNativeEntityManagerFactory();
}
}
这里用来支持 Spring Data JPA 存储库的唯一配置元素是@ EnableJpaRepositories注释。使用basePackages属性,扫描定制的Repository扩展并创建存储库 beans 的包。其余的依赖项(emf和transactionManagerbean)由 Spring 容器自动注入。
下面的代码片段展示了SingerService接口的三个 finder 方法的实现:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Singer;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.google.common.collect.Lists;
@Service("springJpaSingerService")
@Transactional
public class SingerServiceImpl implements SingerService {
@Autowired
private SingerRepository singerRepository;
@Transactional(readOnly=true)
public List<Singer> findAll() {
return Lists.newArrayList(singerRepository.findAll());
}
@Transactional(readOnly=true)
public List<Singer> findByFirstName(String firstName) {
return singerRepository.findByFirstName(firstName);
}
@Transactional(readOnly=true)
public List<Singer> findByFirstNameAndLastName(
String firstName, String lastName) {
return singerRepository.findByFirstNameAndLastName(
firstName, lastName);
}
}
你可以看到,我们只需要将 Spring 基于SingerRepository接口生成的singerRepository bean 注入到服务类中,而不是EntityManager,Spring Data JPA 将为我们完成所有底层工作。在下面的代码片段中,您可以看到一个测试类,现在您应该已经熟悉了它的内容:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.DataJpaConfig;
import com.apress.prospring5.ch8.entities.Singer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class SingerDataJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerDataJPATest.class);
private GenericApplicationContext ctx;
private SingerService singerService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(DataJpaConfig.class);
singerService = ctx.getBean(SingerService.class);
assertNotNull(singerService);
}
@Test
public void testFindAll(){
List<Singer> singers = singerService.findAll();
assertTrue(singers.size() > 0);
listSingers(singers);
}
@Test
public void testFindByFirstName(){
List<Singer> singers = singerService.findByFirstName("John");
assertTrue(singers.size() > 0);
assertEquals(2, singers.size());
listSingers(singers);
}
@Test
public void testFindByFirstNameAndLastName(){
List<Singer> singers =
singerService.findByFirstNameAndLastName("John", "Mayer");
assertTrue(singers.size() > 0);
assertEquals(1, singers.size());
listSingers(singers);
}
private static void listSingers(List<Singer> singers) {
logger.info(" ---- Listing singers:");
for (Singer singer : singers) {
logger.info(singer.toString());
}
}
@After
public void tearDown() {
ctx.close();
}
}
运行测试类,所有的测试都应该通过,预期的数据将被打印在控制台中。
使用 JpaRepository
除了CrudRepository之外,还有一个更高级的 Spring 接口,可以更容易地创建定制库;它被称为JpaRepository接口,提供批处理、分页和排序操作。图 8-1 显示了JpaRepository与CrudRepository接口的关系。根据应用的复杂程度,可以选择使用CrudRepository或JpaRepository。从图 8-1 中可以看出JpaRepository延伸CrudRepository;因此,它提供了这个接口的所有功能。
图 8-1。
Spring Data JPA Repository interfaces hierarchy
带有自定义查询的 Spring Data JPA
在复杂的应用中,您可能需要 Spring 无法“推断”的定制查询。在这种情况下,必须使用@Query注释显式定义查询。让我们使用这个注释来搜索标题中包含的所有音乐专辑。下面的代码片段描述了AlbumRepository接口:
package com.apress.prospring5.ch8.repos;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.entities.Singer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface AlbumRepository extends JpaRepository<Album, Long> {
List<Album> findBySinger(Singer singer);
@Query("select a from Album a where a.title like %:title%")
List<Album> findByTitle(@Param("title") String t);
}
前面的查询有一个名为title的命名参数。当命名参数的名称与用@Query标注的方法中的参数名称相同时,不需要@Param标注。但是如果方法参数有不同的名称,就需要使用@Param注释来告诉 Spring 这个参数的值将被注入到查询中的命名参数中。
AlbumServiceImpl非常简单,只使用了albumRepository bean 来调用它的方法。
//AlbumService.java
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.entities.Singer;
import java.util.List;
public interface AlbumService {
List<Album> findBySinger(Singer singer);
List<Album> findByTitle(String title);
}
//AlbumServiceImpl.java
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.entities.Singer;
import com.apress.prospring5.ch8.repos.AlbumRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("springJpaAlbumService")
@Transactional
public class AlbumServiceImpl implements AlbumService {
@Autowired
private AlbumRepository albumRepository;
@Transactional(readOnly=true)
@Override public List<Album> findBySinger(Singer singer) {
return albumRepository.findBySinger(singer);
}
@Override public List<Album> findByTitle(String title) {
return albumRepository.findByTitle(title);
}
}
要测试findByTitle(..)方法,您可以使用下面的测试类:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.config.DataJpaConfig;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.services.AlbumService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class SingerDataJPATest {
private static Logger logger =
LoggerFactory.getLogger(SingerDataJPATest.class);
private GenericApplicationContext ctx;
private AlbumService albumService;
@Before
public void setUp(){
ctx = new AnnotationConfigApplicationContext(DataJpaConfig.class);
albumService = ctx.getBean(AlbumService.class);
assertNotNull(albumService);
}
@Test
public void testFindByTitle(){
List<Album> albums = albumService.findByTitle("The");
assertTrue(albums.size() > 0);
assertEquals(2, albums.size());
albums.forEach(a -> logger.info(a.toString() + ", Singer: "
+ a.getSinger().getFirstName() + " "
+ a.getSinger().getLastName()));
}
@After
public void tearDown() {
ctx.close();
}
}
如果您运行前面的测试类,testFindByTitle通过,并且两个相册细节被打印在控制台中。
INFO c.a.p.c.SingerDataJPATest - Album - id: 1, Singer id: 1,
Title: The Search For Everything, Release Date: 2017-01-20, Singer: John Mayer
INFO c.a.p.c.SingerDataJPATest - Album - id: 3, Singer id: 2,
Title: From The Cradle, Release Date: 1994-09-13, Singer: Eric Clapton
跟踪实体类的变化
在大多数应用中,我们需要跟踪用户维护的业务数据的基本审计活动。审计信息通常包括创建数据的用户、数据的创建日期、数据的最后修改日期以及最后修改数据的用户。
Spring Data JPA 项目以 JPA 实体监听器的形式提供了这个功能,它可以帮助您自动跟踪审计信息。要使用该特性,在 Spring 4 之前,实体类需要实现Auditable<U, ID extends Serializable, T extends TemporalAccessor> extends Persistable<ID>接口(属于 Spring Data Commons)或扩展任何实现该接口的类。下面的代码片段显示了从 Spring Data 的参考文档中提取的Auditable接口:
package org.springframework.data.domain;
import java.io.Serializable;
import java.time.temporal.TemporalAccessor;
import java.util.Optional;
public interface Auditable<U, ID extends Serializable,
T extends TemporalAccessor> extends Persistable<ID> {
Optional<U> getCreatedBy();
void setCreatedBy(U createdBy);
Optional<T> getCreatedDate();
void setCreatedDate(T creationDate);
Optional<U> getLastModifiedBy();
void setLastModifiedBy(U lastModifiedBy);
Optional<T> getLastModifiedDate();
void setLastModifiedDate(T lastModifiedDate);
}
为了展示它是如何工作的,让我们在数据库模式中创建一个名为SINGER_AUDIT的新表,它基于SINGER表,添加了四个与审计相关的列。下面的代码片段显示了表创建脚本(schema.sql):
CREATE TABLE SINGER_AUDIT (
ID INT NOT NULL AUTO_INCREMENT
, FIRST_NAME VARCHAR(60) NOT NULL
, LAST_NAME VARCHAR(40) NOT NULL
, BIRTH_DATE DATE
, VERSION INT NOT NULL DEFAULT 0
, CREATED_BY VARCHAR(20)
, CREATED_DATE TIMESTAMP
, LAST_MODIFIED_BY VARCHAR(20)
, LAST_MODIFIED_DATE TIMESTAMP
, UNIQUE UQ_SINGER_AUDIT_1 (FIRST_NAME, LAST_NAME)
, PRIMARY KEY (ID)
);
从前面显示的Auditable接口的定义中可以看出,日期类型列被限制为扩展java.time.temporal.TemporalAccessor的类型。
从 Spring 5 开始,实现Auditable<U,ID extends Serializable>不再是必要的,因为一切都可以被注释代替。四个带下划线的列表示与审计相关的列。请注意@CreatedBy、@CreatedDate、@LastModifiedBy和@LastModifiedDate注释。使用这些批注,日期列的类型限制不再适用。下一步是创建名为SingerAudit的实体类。下面的代码片段显示了SingerAudit类:
package com.apress.prospring5.ch8.entities;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Optional;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "singer_audit")
public class SingerAudit implements Serializable {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
@CreatedDate
@Column(name = "CREATED_DATE")
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@CreatedBy
@Column(name = "CREATED_BY")
private String createdBy;
@LastModifiedBy
@Column(name = "LAST_MODIFIED_BY")
private String lastModifiedBy;
@LastModifiedDate
@Column(name = "LAST_MODIFIED_DATE")
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
public Long getId() {
return this.id;
}
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Date getBirthDate() {
return this.birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public Optional<String> getCreatedBy() {
return Optional.of(createdBy);
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public Optional<Date> getCreatedDate() {
return Optional.of(createdDate);
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public Optional<String> getLastModifiedBy() {
return Optional.of(lastModifiedBy);
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Optional<Date> getLastModifiedDate() {
return Optional.of(lastModifiedDate);
}
public void setLastModifiedDate(Date lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate
+ ", Created by: " + createdBy + ", Create date: " + createdDate
+ ", Modified by: " + lastModifiedBy + ", Modified date: "
+ lastModifiedDate;
}
}
@Column注释应用于审计字段,以映射到表中的实际列。@EntityListeners(AuditingEntityListener.class)注释注册了AuditingEntityListener,仅用于持久化上下文中的这个实体。在更复杂的例子中,当需要不止一个实体类时,审计功能被隔离到一个用@MappedSuperclass标注的抽象类中,这个抽象类也用@EntityListeners(AuditingEntityListener.class)标注。如果SingerAudit是这样的层次结构的一部分,它将必须扩展下面的类:
package com.apress.prospring5.ch8.entities;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Optional;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AuditableEntity<U> implements Serializable {
@CreatedDate
@Column(name = "CREATED_DATE")
@Temporal(TemporalType.TIMESTAMP)
protected Date createdDate;
@CreatedBy
@Column(name = "CREATED_BY")
protected String createdBy;
@LastModifiedBy
@Column(name = "LAST_MODIFIED_BY")
protected String lastModifiedBy;
@LastModifiedDate
@Column(name = "LAST_MODIFIED_DATE")
@Temporal(TemporalType.TIMESTAMP)
protected Date lastModifiedDate;
public Optional<String> getCreatedBy() {
return Optional.of(createdBy);
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public Optional<Date> getCreatedDate() {
return Optional.of(createdDate);
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
public Optional<String> getLastModifiedBy() {
return Optional.of(lastModifiedBy);
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public Optional<Date> getLastModifiedDate() {
return Optional.of(lastModifiedDate);
}
public void setLastModifiedDate(Date lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
这将大大减小SingerAudit的大小,变成这样:
package com.apress.prospring5.ch8.entities;
import javax.persistence.*;
import java.util.Date;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "singer_audit")
public class SingerAudit extends AuditableEntity<SingerAudit> {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "ID")
private Long id;
@Version
@Column(name = "VERSION")
private int version;
@Column(name = "FIRST_NAME")
private String firstName;
@Column(name = "LAST_NAME")
private String lastName;
@Temporal(TemporalType.DATE)
@Column(name = "BIRTH_DATE")
private Date birthDate;
public Long getId() {
return this.id;
}
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Date getBirthDate() {
return this.birthDate;
}
public void setBirthDate(Date birthDate) {
this.birthDate = birthDate;
}
public String toString() {
return "Singer - Id: " + id + ", First name: " + firstName
+ ", Last name: " + lastName + ", Birthday: " + birthDate
+ ", Created by: " + createdBy + ", Create date: " + createdDate
+ ", Modified by: " + lastModifiedBy + ", Modified date: "
+ lastModifiedDate;
}
}
下面的代码片段显示了SingerAuditService接口,在这里我们只定义了几个方法来演示审计特性:
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.SingerAudit;
import java.util.List;
public interface SingerAuditService {
List<SingerAudit> findAll();
SingerAudit findById(Long id);
SingerAudit save(SingerAudit singer);
}
SingerAuditRepository接口只是扩展了CrudRepository,它已经实现了我们将要为SingerAuditService使用的所有方法。findById()方法由CrudRepository.findOne()方法实现。下面的代码片段显示了服务实现类SingerAuditServiceImpl:
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.SingerAudit;
import com.apress.prospring5.ch8.repos.SingerAuditRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import com.google.common.collect.Lists;
@Service("singerAuditService")
@Transactional
public class SingerAuditServiceImpl implements SingerAuditService {
@Autowired
private SingerAuditRepository singerAuditRepository;
@Transactional(readOnly=true)
public List<SingerAudit> findAll() {
return Lists.newArrayList(singerAuditRepository.findAll());
}
public SingerAudit findById(Long id) {
return singerAuditRepository.findOne(id).get();
}
public SingerAudit save(SingerAudit singer) {
return singerAuditRepository.save(singer);
}
}
我们还需要做一些配置工作。使用 XML 配置,需要将提供审计服务的AuditingEntityListener<T> JPA 实体监听器声明到项目根文件夹下的一个名为/src/main/resources/META-INF/orm.xml的文件中(必须使用这个文件名,这由 JPA 规范指示),并声明监听器,如下所示:
<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm
http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<description>JPA</description>
<persistence-unit-metadata>
<persistence-unit-defaults>
<entity-listeners>
<entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener" />
</entity-listeners>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>
使用注释配置,该文件被替换为@EntityListeners (AuditingEntityListener.class)注释。JPA 提供者将在审计字段处理的持久性操作(保存和更新事件)期间发现这个监听器。Spring 配置的其余部分几乎与您到目前为止看到的一样,只有一个例外:当然,是一个支持审计的新配置注释。
package com.apress.prospring5.ch8.com.apress.prospring5.ch8.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.apress.prospring5.ch8"})
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch8.repos"})
@EnableJpaAuditing(auditorAwareRef = "auditorAwareBean")
public class AuditConfig {
// same content as the configuration in the previous section
...
}
@EnableJpaAuditing(auditorAwareRef = "auditorAwareBean")声明相当于使用 XML 配置来启用 JPA 审计特性的<jpa:auditing auditor-aware-ref="auditorAwareBean"/>元素。auditorAwareBean bean 提供了用户信息。下面的代码片段显示了AuditorAwareBean类:
package com.apress.prospring5.ch8;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class AuditorAwareBean implements AuditorAware<String> {
public Optional<String> getCurrentAuditor() {
return Optional.of("prospring5");
}
}
AuditorAwareBean实现AuditorAware<T>接口,传入类型String。在实际情况下,这应该是用户信息的一个实例,例如一个User类,它表示正在执行数据更新操作的登录用户。为了简单起见,我们在这里使用String。在AuditorAwareBean类中,方法getCurrentAuditor()被实现,值被硬编码为prospring5。在实际情况下,应该从底层安全基础结构中获取用户。例如,在 Spring Security 中,可以从SecurityContextHolder类中检索用户信息。
现在所有的实现工作都完成了,下面的代码片段显示了SpringAuditJPADemo测试程序:
package com.apress.prospring5.ch8;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Date;
import com.apress.prospring5.ch8.config.AuditConfig;
import com.apress.prospring5.ch8.entities.SingerAudit;
import com.apress.prospring5.ch8.services.SingerAuditService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class SpringAuditJPADemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicationContext(AuditConfig.class);
SingerAuditService singerAuditService = ctx.getBean(SingerAuditService.class);
List<SingerAudit> singers = singerAuditService.findAll();
listSingers(singers);
System.out.println("Add new singer");
SingerAudit singer = new SingerAudit();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
singerAuditService.save(singer);
singers = singerAuditService.findAll();
listSingers(singers);
singer = singerAuditService.findById(4l);
System.out.println("");
System.out.println("Singer with id 4:" + singer);
System.out.println("");
System.out.println("Update singer");
singer.setFirstName("John Clayton");
singerAuditService.save(singer);
singers = singerAuditService.findAll();
listSingers(singers);
ctx.close();
}
private static void listSingers(List<SingerAudit> singerAudits) {
System.out.println("");
System.out.println("Listing singers without details:");
for (SingerAudit audit: singerAudits) {
System.out.println(audit);
System.out.println();
}
}
}
在main()方法中,我们列出了新歌手插入后和更新后的歌手审计信息。运行该程序会产生以下输出:
Add new singer
Listing singers without details:
// other singers ...
Singer - Id: 4, First name: BB, Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-07 14:19:02.96,
Modified by: prospring5, Modified date: 2017-05-07 14:19:02.96
Update singer
Listing singers without details:
// other singers ...
Singer - Id: 4, First name: Riley B., Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-07 14:33:15.645,
Modified by: prospring5, Modified date: 2017-05-07 14:33:15.663
在前面的输出中,您可以看到在新歌手创建之后,创建日期和最后修改日期是相同的。但是,在更新之后,会更新最后修改日期。审计是 Spring Data JPA 提供的另一个便利特性,因此您不需要自己实现逻辑。
通过使用 Hibernate 环境保持实体版本
在企业应用中,对于业务关键型数据,总是需要保留每个实体的版本。例如,在客户关系管理(CRM)系统中,每次插入、更新或删除客户记录时,以前的版本都应该保存在历史记录或审计表中,以满足公司的审计或其他合规性要求。
要实现这一点,有两种常见的选择。第一个是创建数据库触发器,它将在任何更新操作之前将更新前的记录克隆到历史表中。第二是开发数据访问层中的逻辑(例如,通过使用 AOP)。然而,这两种选择都有缺点。触发器方法依赖于数据库平台,而手动实现逻辑相当笨拙且容易出错。
Hibernate Envers(是“实体版本控制系统”的缩写)是一个 Hibernate 模块,专门用于自动控制实体的版本。在本节中,我们将讨论如何使用 Envers 来实现SingerAudit实体的版本控制。
Hibernate Envers 不是 JPA 的一个特性。我们之所以在这里提到它,是因为我们认为在我们讨论了一些可以与 Spring Data JPA 一起使用的基本审计特性之后,讨论这个问题更合适。
Envers 支持两种审计策略,如表 8-1 所示。
表 8-1。
Envers Auditing Strategies
| 审计策略 | 描述 | | --- | --- | | 默认 | Envers 保留了一个记录修订栏。每次插入或更新记录时,都会在历史记录表中插入一条新记录,并从数据库序列或表中检索修订号。 | | 有效性审计 | 该策略存储每个历史记录的开始和结束修订。每次插入或更新记录时,都会在历史记录表中插入一条新记录,其起始修订号为。同时,以前的记录将被更新为最终版本号。还可以配置 Envers 来记录时间戳,在该时间戳处,最终版本被更新到先前的历史记录中。 |在本节中,我们将演示有效性审计策略。尽管这将触发更多的数据库更新,但检索历史记录会变得更快。因为最终修订时间戳也被写入历史记录,所以在查询数据时更容易识别特定时间点的记录快照。
为实体版本控制添加表
为了支持实体版本控制,我们需要添加一些表。首先,对于实体(在本例中是SingerAudit实体类)将要进行版本控制的每个表,我们需要创建相应的历史表。为了对SINGER_AUDIT表中的记录进行版本控制,让我们创建一个名为SINGER_AUDIT_H的历史表。下面的代码片段显示了表创建脚本(schema.sql):
CREATE TABLE SINGER_AUDIT_H (
ID INT NOT NULL AUTO_INCREMENT
, FIRST_NAME VARCHAR(60) NOT NULL
, LAST_NAME VARCHAR(40) NOT NULL
, BIRTH_DATE DATE
, VERSION INT NOT NULL DEFAULT 0
, CREATED_BY VARCHAR(20)
, CREATED_DATE TIMESTAMP
, LAST_MODIFIED_BY VARCHAR(20)
, LAST_MODIFIED_DATE TIMESTAMP
, AUDIT_REVISION INT NOT NUL
, ACTION_TYPE INT
, AUDIT_REVISION_END INT
, AUDIT_REVISION_END_TS TIMESTAMP
, UNIQUE UQ_SINGER_AUDIT_H_1 (FIRST_NAME, LAST_NAME)
, PRIMARY KEY (ID, AUDIT_REVISION)
);
为了支持有效性审计策略,我们需要为每个历史表添加四列,在前面的脚本片段中显示为带下划线。表 8-2 显示了各列及其用途。Hibernate Envers 需要另一个表来跟踪修订号和每个修订创建时的时间戳。表名应该是REVINFO。
表 8-2。
Envers Auditing Strategies
| 审计策略 | 数据类型 | 描述 | | --- | --- | --- | | `AUDIT_REVISION` | `INT` | 历史记录的开始修订。 | | `ACTION_TYPE` | `INT` | 动作类型,可能值如下:0 表示添加,1 表示修改,2 表示删除 | | `AUDIT_REVISION_END` | `INT` | 历史记录的最终版本 | | `AUDIT_REVISION_END_TS` | `TIMESTAMP` | 更新结束修订的时间戳 |下面的代码片段显示了表创建脚本(schema.sql):
CREATE TABLE REVINFO (
REVTSTMP BIGINT NOT NULL
, REV INT NOT NULL AUTO_INCREMENT
, PRIMARY KEY (REVTSTMP, REV)
);
REV栏用于存储每个修订号,当创建新的历史记录时,修订号将自动增加。REVTSTMP列存储创建修订时的时间戳(以数字格式)。
为实体版本控制配置 EntityManagerFactory
Hibernate Envers 是以 EJB 监听器的形式实现的。我们可以在LocalContainerEntityManagerFactory bean 中配置这些监听器。这里你可以看到 Java 配置类。展示 XML 配置是没有意义的,因为这一节的唯一区别是我们有许多额外的特定于 Hibernate 的属性。
package com.apress.prospring5.ch8.config;
import org.hibernate.envers.boot.internal.EnversServiceImpl;
import org.hibernate.envers.event.spi.EnversPostUpdateEventListenerImpl;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {"com.apress.prospring5.ch8"})
@EnableJpaRepositories(basePackages = {"com.apress.prospring5.ch8.repos"})
@EnableJpaAuditing(auditorAwareRef = "auditorAwareBean")
public class EnversConfig {
private static Logger logger = LoggerFactory.getLogger(EnversConfig.class);
@Bean
public DataSource dataSource() {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:db/schema.sql", "classpath:db/test-data.sql").build();
} catch (Exception e) {
logger.error("Embedded DataSource bean cannot be created!", e);
return null;
}
}
@Bean
public PlatformTransactionManager transactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
return new HibernateJpaVendorAdapter();
}
@Bean
public Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.show_sql", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
//Properties for Hibernate Envers
hibernateProp.put("org.hibernate.envers.audit_table_suffix", "_H");
hibernateProp.put("org.hibernate.envers.revision_field_name",
"AUDIT_REVISION");
hibernateProp.put("org.hibernate.envers.revision_type_field_name",
"ACTION_TYPE");
hibernateProp.put("org.hibernate.envers.audit_strategy",
"org.hibernate.envers.strategy.ValidityAuditStrategy");
hibernateProp.put(
"org.hibernate.envers.audit_strategy_validity_end_rev_field_name",
"AUDIT_REVISION_END");
hibernateProp.put(
"org.hibernate.envers.audit_strategy_validity_store_revend_timestamp",
"True");
hibernateProp.put(
"org.hibernate.envers.audit_strategy_validity_revend_timestamp_field_name",
"AUDIT_REVISION_END_TS");
return hibernateProp;
}
@Bean
public EntityManagerFactory entityManagerFactory() {
LocalContainerEntityManagerFactoryBean factoryBean =
new LocalContainerEntityManagerFactoryBean();
factoryBean.setPackagesToScan("com.apress.prospring5.ch8.entities");
factoryBean.setDataSource(dataSource());
factoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
factoryBean.setJpaProperties(hibernateProperties());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter());
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
}
Envers 审计事件监听器org.hibernate.envers.event.AuditEventListener附加到各种持久性事件。侦听器拦截插入后、更新后或删除后事件,并将实体类的更新前快照克隆到历史表中。侦听器还附加到那些关联更新事件(预收集-更新、预收集-删除和预收集-重新创建),用于处理实体类关联的更新操作。Envers 能够保存关联中实体的历史记录(例如一对多或多对多)。还为 Hibernate Envers 定义了一些属性,这些属性汇总在表 8-3 中(为了清楚起见,省略了属性的前缀org.hibernate.envers)。22
表 8-3。
Hibernate Envers Properties Table
| 方法 | 描述 | | --- | --- | | `audit_table_suffix` | 版本化实体的表名后缀。例如,对于映射到表`SINGER_AUDIT`的实体类`SingerAudit`,Envers 将在表`SINGER_AUDIT_H`中保存历史,因为我们为属性定义了值`_H`。 | | `revision_field_name` | 历史表中用于存储每个历史记录的修订号的列。 | | `revision_type_field_name` | 用于存储更新操作类型的历史表列。 | | `audit_strategy` | 用于实体版本控制的审计策略。 | | `audit_strategy_validity_end_rev_field_name` | 历史表中用于存储每个历史记录的最终修订号的列。仅在使用有效性审计策略时需要。 | | `audit_strategy_validity_store_revend_timestamp` | 更新每个历史记录的结束修订号时是否存储时间戳。仅在使用有效性审计策略时需要。 | | `audit_strategy_validity_revend_timestamp_field_name` | 当每个历史记录的结束修订号被更新时,历史表中用于存储时间戳的列。仅在使用有效性审计策略且 previous 属性设置为`true`时需要。 |启用实体版本控制和历史检索
要启用实体的版本控制,只需用@Audited注释实体类。可以在类级别使用该注释,然后审计所有字段上的更改。如果您想逃避某些字段的审计,可以在这些字段上使用@NotAudited。这里您可以看到应用了注释的SingerAudit实体类的一个片段:
package com.apress.prospring5.ch8.entities;
import org.hibernate.envers.Audited;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
...
@Entity
@Table(name = "singer_audit")
@Audited
@EntityListeners(AuditingEntityListener.class)
public class SingerAudit implements Serializable {
...
}
entity 类用@ Audited注释,Envers 监听器将检查并执行更新实体的版本控制。默认情况下,Envers 还会尝试保存关联的历史记录;如果想避免这种情况,应该使用前面提到的@NotAudited注释。
为了检索历史记录,Envers 提供了org.hibernate.envers.AuditReader接口,该接口可以从AuditReaderFactory类中获得。让我们在SingerAuditService接口中添加一个名为findAuditByRevision()的新方法,用于通过修订号检索SingerAudit历史记录。下面的代码片段显示了SingerAuditService接口:
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.SingerAudit;
import java.util.List;
public interface SingerAuditService {
List<SingerAudit> findAll();
SingerAudit findById(Long id);
SingerAudit save(SingerAudit singerAudit);
SingerAudit findAuditByRevision(Long id, int revision);
}
要检索历史记录,一种方法是传入实体的 ID 和修订号。以下代码片段显示了提取修订的方法的实现:
package com.apress.prospring5.ch8.services;
import com.apress.prospring5.ch8.entities.SingerAudit;
import com.apress.prospring5.ch8.repos.SingerAuditRepository;
import com.google.common.collect.Lists;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Service("singerAuditService")
@Transactional
public class SingerAuditServiceImpl implements SingerAuditService {
@Autowired
private SingerAuditRepository singerAuditRepository;
@PersistenceContext
private EntityManager entityManager;
@Transactional(readOnly = true)
public List<SingerAudit> findAll() {
return Lists.newArrayList(singerAuditRepository.findAll());
}
public SingerAudit findById(Long id) {
return singerAuditRepository.findOne(id).get();
}
public SingerAudit save(SingerAudit singer) {
return singerAuditRepository.save(singer);
}
@Transactional(readOnly = true)
@Override
public SingerAudit findAuditByRevision(Long id, int revision) {
AuditReader auditReader = AuditReaderFactory.get(entityManager);
return auditReader.find(SingerAudit.class, id, revision);
}
}
EntityManager被注入到类中,该类被传递给AuditReaderFactory以检索AuditReader的一个实例。然后我们可以调用AuditReader.find()方法来检索特定版本的SingerAudit实体的实例。
测试实体版本
让我们看看实体版本控制是如何工作的。以下代码片段显示了测试代码片段;引导ApplicationContext和listSingers()函数的代码与SpringJpaDemo类中的代码相同。
package com.apress.prospring5.ch8;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Date;
import com.apress.prospring5.ch8.config.EnversConfig;
import com.apress.prospring5.ch8.entities.SingerAudit;
import com.apress.prospring5.ch8.services.SingerAuditService;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class SpringEnversJPADemo {
public static void main(String... args) {
GenericApplicationContext ctx =
new AnnotationConfigApplicati onContext(EnversConfig.class);
SingerAuditService singerAuditService =
ctx.getBean(SingerAuditService.class);
System.out.println("Add new singer");
SingerAudit singer = new SingerAudit();
singer.setFirstName("BB");
singer.setLastName("King");
singer.setBirthDate(new Date(
(new GregorianCalendar(1940, 8, 16)).getTime().getTime()));
singerAuditService.save(singer);
listSingers(singerAuditService.findAll());
System.out.println("Update singer");
singer.setFirstName("Riley B.");
singerAuditService.save(singer);
listSingers(singerAuditService.findAll());
SingerAudit oldSinger = singerAuditService.findAuditByRevision(4l, 1);
System.out.println("");
System.out.println("Old Singer with id 1 and rev 1:" + oldSinger);
System.out.println("");
oldSinger = singerAuditService.findAuditByRevision(4l, 2);
System.out.println("");
System.out.println("Old Singer with id 1 and rev 2:" + oldSinger);
System.out.println("");
ctx.close();
}
private static void listSingers(List<SingerAudit> singers) {
System.out.println("");
System.out.println("Listing singers:");
for (SingerAudit singer: singers) {
System.out.println(singer);
System.out.println();
}
}
}
我们首先创建一个新的歌手,然后更新它。然后我们分别检索修订版为 1 和 2 的SingerAudit实体。运行代码会产生以下输出:
Listing singers:
...//
Singer - Id: 4, First name: BB, Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-11 23:50:14.778,
Modified by: prospring5, Modified date: 2017-05-11 23:50:14.778
Old Singer with id 1 and rev 1:Singer - Id: 4,
First name: BB, Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-11 23:50:14.778,
Modified by: prospring5, Modified date: 2017-05-11 23:50:14.778
Old Singer with id 1 and rev 2:Singer - Id: 4,
First name: Riley B., Last name: King, Birthday: 1940-09-16,
Created by: prospring5, Create date: 2017-05-11 23:50:14.778,
Modified by: prospring5, Modified date: 2017-05-11 23:50:15.0
从前面的输出中,您可以看到在更新操作之后,SingerAudit的名字被更改为Riley B.。然而,当查看历史时,在修订版 1 中,第一个名字是BB。在修订版 2 中,名字变成了Riley B.。另请注意,修订版 2 的最后修改日期正确反映了更新的日期和时间。
Spring Boot JPA
到目前为止,我们已经配置了所有的东西,包括实体、数据库、存储库和服务。正如您现在可能期望的那样,应该有一个 Spring Boot 启动构件来帮助您更快地开发项目并最小化配置工作。一个 Spring Boot JPA 启动程序依赖于 Spring Boot JPA,所以它带有预配置的嵌入式数据库;所需要的只是依赖关系在类路径上。Hibernate 也提供了对持久层的抽象。SpringRepository接口被自动检测。因此,留给开发人员去做的就是提供实体、存储库扩展和一个Application类来一起使用它们。最后,您还可以开发一个类来填充数据库。所有这些都将在本节中完成和解释。
首先,我们需要添加 Spring Boot starter JPA 作为依赖项。这是像之前所有其他库一样完成的。在根build.gradle文件中添加版本、组 ID 和工件 ID,并在chapter08/boot-jpa/build.gradle文件中定义依赖关系。这里显示了两个配置片段:
//pro-spring-15/build.gradle
ext {
//spring libs
springVersion = '5.0.0.M5'
bootVersion = '2.0.0.BUILD-SNAPSHOT'
springDataVersion = '2.0.0.M2'
...
boot = [
...
starterJdbc :
"org.springframework.boot:spring-boot-starter-jdbc:$bootVersion",
starterJpa :
"org.springframework.boot:spring-boot-starter-data-jpa:$bootVersion"
]
testing = [
junit: "junit:junit:$junitVersion"
]
db = [
...
h2 : "com.h2database:h2:$h2Version"
]
}
//chapter08/boot-jpa/build.gradle
buildscript {
repositories {
mavenLocal
mavenCentral
maven { url "http://repo.spring.io/release" }
maven { url "http://repo.spring.io/milestone" }
maven { url "http://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/libs-snapshot" }
}
dependencies {
classpath boot.springBootPlugin
}
}
apply plugin: 'org.springframework.boot'
dependencies {
compile boot.starterJpa, db.h2
}
}
添加完这些依赖项并刷新项目后,整个spring-boot-starter-data-jpa依赖项树应该在 IntelliJ IDEA Gradle 项目视图中可见,如图 8-2 所示。
图 8-2。
Dependency tree for the Spring Boot Starter artifact
这些实体将与迄今为止使用的相同(Singer、Album和Instrument),因此没有必要再次描述它们。初始化它们的 bean 的类型是DBInitializer,它是一个使用SingerRepository和InstrumentRepositorybean 的服务类,由 Spring Boot 提供,用于将一组对象保存到数据库中。其内容如下所示:
package com.apress.prospring5.ch8.config;
import com.apress.prospring5.ch8.InstrumentRepository;
import com.apress.prospring5.ch8.SingerRepository;
import com.apress.prospring5.ch8.entities.Album;
import com.apress.prospring5.ch8.entities.Instrument;
import com.apress.prospring5.ch8.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.GregorianCalendar;
@Service
public class DBInitializer {
private Logger logger = LoggerFactory.getLogger(DBInitializer.class);
@Autowired
SingerRepository singerRepository;
@Autowired
InstrumentRepository instrumentRepository;
@PostConstruct
public void initDB(){
logger.info("Starting database initialization...");
Instrument guitar = new Instrument();
guitar.setInstrumentId("Guitar");
instrumentRepository.save(guitar);
Instrument piano = new Instrument();
piano.setInstrumentId("Piano");
instrumentRepository.save(piano);
Instrument voice = new Instrument();
voice.setInstrumentId("Voice");
instrumentRepository.save(voice);
Singer singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Mayer");
singer.setBirthDate(new Date(
(new GregorianCalendar(1977, 9, 16)).getTime().getTime()));
singer.addInstrument(guitar);
singer.addInstrument(piano);
Album album1 = new Album();
album1.setTitle("The Search For Everything");
album1.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(2017, 0, 20)).getTime().getTime()));
singer.addAbum(album1);
Album album2 = new Album();
album2.setTitle("Battle Studies");
album2.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(2009, 10, 17)).getTime().getTime()));
singer.addAbum(album2);
singerRepository.save(singer);
singer = new Singer();
singer.setFirstName("Eric");
singer.setLastName("Clapton");
singer.setBirthDate(new Date(
(new GregorianCalendar(1945, 2, 30)).getTime().getTime()));
singer.addInstrument(guitar);
Album album = new Album();
album.setTitle("From The Cradle");
album.setReleaseDate(new java.sql.Date(
(new GregorianCalendar(1994, 8, 13)).getTime().getTime()));
singer.addAbum(album);
singerRepository.save(singer);
singer = new Singer();
singer.setFirstName("John");
singer.setLastName("Butler");
singer.setBirthDate(new Date(
(new GregorianCalendar(1975, 3, 1)).getTime().getTime()));
singer.addInstrument(guitar);
singerRepository.save(singer);
logger.info("Database initialization finished.");
}
}
在这个例子中,SingerRepository和InstrumentRepository接口非常简单,如下所示:
//InstrumentRepository.java
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Instrument;
import org.springframework.data.repository.CrudRepository;
public interface InstrumentRepository
extends CrudRepository<Instrument, Long> {
}
//SingerRepository.java
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Singer;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface SingerRepository
extends CrudRepository<Singer, Long> {
List<Singer> findByFirstName(String firstName);
List<Singer> findByFirstNameAndLastName(String firstName, String lastName);
}
SingerRepository将被直接注入到 Spring Boot 注释的Application类中,用于从数据库中检索所有歌手记录及其子记录,并记录到控制台中。这里显示了Application类:
package com.apress.prospring5.ch8;
import com.apress.prospring5.ch8.entities.Singer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@SpringBootApplication(scanBasePackages = "com.apress.prospring5.ch8.config")
public class Application implements CommandLineRunner {
private static Logger logger = LoggerFactory.getLogger(Application.class);
@Autowired
SingerRepository singerRepository;
public static void main(String... args) throws Exception {
ConfigurableApplicationContext ctx =
SpringApplication.run(Application.class, args);
System.in.read();
ctx.close();
}
@Transactional(readOnly = true)
@Override public void run(String... args) throws Exception {
List<Singer> singers = singerRepository.findByFirstName("John");
listSingersWithAlbum(singers);
}
private static void listSingersWithAlbum(List<Singer> singers) {
logger.info(" ---- Listing singers with instruments:");
singers.forEach(singer -> {
logger.info(singer.toString());
if (singer.getAlbums() != null) {
singer.getAlbums().forEach(
album -> logger.info("\t" + album.toString()));
}
if (singer.getInstruments() != null) {
singer.getInstruments().forEach(
instrument -> logger.info("\t" + instrument.getInstrumentId()));
}
});
}
}
这个Application类引入了一些新的东西;它实现了CommandLineRunner接口。这个接口用来告诉 Spring Boot,当这个 bean 包含在一个 Spring 应用中时,应该执行run()方法。
scanBasePackages = "com.apress.prospring5.ch8.config"属性用于为作为参数指定的包(或多个包)启用组件扫描,以便创建其中包含的 beans 并将其添加到应用上下文中。当 beans 在不同于Application类的包中定义时,这是需要的。
您应该注意的另一件事是,不需要其他配置类;您不需要任何 SQL 脚本来初始化数据库,也不需要任何其他关于Application类的注释。显然,如果你想专注于应用的逻辑,Spring Boot 和它的启动依赖是非常方便的。
如果您运行前面的类,您将获得预期的结果。(请注意,应用在退出前会等待按键被按下)。
INFO c.a.p.c.c.DBInitializer - Starting database initialization...
INFO c.a.p.c.c.DBInitializer - Database initialization finished.
INFO c.a.p.c.Application - ---- Listing singers with instruments:
INFO c.a.p.c.Application - Singer - Id: 1, First name: John, Last name: Mayer,
Birthday: 1977-10-16
INFO c.a.p.c.Application - Album - id: 1, Singer id: 1, Title: Battle Studies,
Release Date: 2009-11-17
INFO c.a.p.c.Application - Album - id: 2, Singer id: 1,
Title: The Search For Everything, Release Date: 2017-01-20
INFO c.a.p.c.Application - Piano
INFO c.a.p.c.Application - Guitar
INFO c.a.p.c.Application - Singer - Id: 3, First name: John, Last name: Butler,
Birthday: 1975-04-01
INFO c.a.p.c.Application - Guitar
INFO c.a.p.c.Application - Started Application in 3.464 seconds (JVM running for 4.0)
使用 JPA 时的注意事项
尽管我们讨论了相当多的内容,但本章只讨论了 JPA 的一小部分。例如,使用 JPA 调用数据库存储过程就不在讨论范围之内。JPA 是一个完整而强大的 ORM 数据访问标准,借助 Spring Data JPA 和 Hibernate Envers 等第三方库,可以相对容易地实现各种横切关注点。
JPA 是一个 JEE 标准,受到大多数主要开源社区以及商业供应商(JBoss、GlassFish、WebSphere、WebLogic 等)的支持。因此,采用 JPA 作为数据访问标准是一个令人信服的选择。如果需要对查询进行绝对控制,可以使用 JPA 的原生查询支持,而不是直接使用 JDBC。
总之,为了用 Spring 开发 JEE 应用,我们建议使用 JPA 来实现数据访问层。如果需要,您仍然可以在 JDBC 混合一些特殊的数据访问需求。永远记住,Spring 允许您轻松地混合和匹配数据访问技术,事务管理是透明地为您处理的。如果您想进一步简化开发,Spring Boot 可以通过其预配置的 beans 和定制配置来实现。
摘要
在这一章中,我们介绍了 JPA 的基本概念,以及如何在 Spring 中使用 Hibernate 作为持久性服务提供者来配置 JPA 的EntityManagerFactory。然后我们讨论了使用 JPA 来执行基本的数据库操作。高级主题包括本地查询和强类型 JPA 标准 API。
此外,我们展示了 Spring Data JPA 的Repository抽象如何帮助简化 JPA 应用开发,以及如何使用其实体监听器来跟踪实体类的基本审计信息。对于实体类的完整版本,还讨论了使用 Hibernate Envers 来满足需求。
还讨论了 JPA 应用的 Spring Boot,因为它极大地简化了配置,并将重点放在开发所需的功能上。
在下一章,我们将讨论 Spring 中的事务管理。
Footnotes 1
从 www.apress.com/us/book/9781430249269 在线获取。
2
你可以在 https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#envers-configuration 的 Hibernate 官方文档中找到 Hibernate 属性的完整列表。