6. Spring Data JPA Hibernate
6.1. 简介
我们将使用由 [spring-jdbc-04] 项目管理的 [dbproduitscategories] 数据库,并实现该项目中定义的两个接口 [IDao<Category>、IDao<Product>]。这将使我们能够实现以下功能:
- 比较实现代码;
- 使用相同的测试层;
- 比较两种实现的性能;
![]() |
- [JDBC] 层由第 3.3 节中讨论的 [mysql-config-jdbc] 项目实现;
接下来我们将探讨其他层。
6.2. 设置工作环境
使用 STS,导入位于 [<examples>/spring-database-config/mysql/eclipse] 文件夹 [2] 中的 [mysql-config-jpa-hibernate] 项目 [1]:
![]() |
该项目配置了项目的 [Spring JPA Hibernate] 层。每个 JPA 实现都有其独立的配置项目。
接下来,导入位于 [<examples>/spring-database-generic/spring-jpa] [2] 文件夹中的 [spring-jpa-generic] [1] 项目:
![]() |
完成此操作后,请为 [Package Explorer] 中的所有项目刷新 Maven 环境(Alt-F5):
![]() |
然后,为验证工作环境,请运行名为 [spring-jpa-generic-JUnitTestDao-hibernate] 的构建配置:
![]() |
此配置将运行 [JUnitTestDao] 测试。该测试应通过:
![]() |
6.3. JPA 层配置项目
![]() |
本项目的目的是配置下图所示架构的 JPA 层:
![]() |
6.3.1. Maven 配置
该项目是一个由以下 [pom.xml] 文件配置的 Maven 项目:
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>configuration mysql openjpa</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- dépendances variables ********************************************** -->
<!-- JPA provider -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
</dependency>
<!-- dépendances constantes ********************************************** -->
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<!-- Spring Context -->
<!-- configuration JDBC inherited -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- 第 5-7 行:该项目生成的 Maven 工件。其他 JPA 实现(Eclipselink 和 OpenJpa)的配置项目将使用同一个工件。这意味着在任何给定时间,这些项目中只能有一个处于活动状态。因此,应避免在 [Package Explorer] 中同时显示所有项目。只需其中一个即可;
- 第 10–14 行:父级 Maven 项目,用于指定该项目所需的大部分依赖项的版本;
- 第 19–22 行:Hibernate 库;
- 第 25–28 行:Spring Data 库;
- 第 32–34 行:JPA 层配置项目依赖于 JDBC 层配置项目,后者定义了所用 DBMS 的 JDBC 驱动程序以及数据库连接详细信息等内容;
- 第 35–39 行:JDBC 层配置项目包含 [Spring JDBC] 库,此处已被 [Spring Data JPA] 库取代。因此,我们指定不将其包含在项目依赖中。不过,即使保留该库,也不会引发任何错误;
最终,项目依赖项如下:
![]() |
6.3.2. Spring 配置
![]() |
[ConfigJpa] 类用于配置 Spring 项目:
package generic.jpa.config;
import javax.persistence.EntityManagerFactory;
import generic.jdbc.config.ConfigJdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {
// the provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
hibernateJpaVendorAdapter.setGenerateDdl(true);
return hibernateJpaVendorAdapter;
}
// JPA entity packages
public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
// data source
@Bean
public DataSource dataSource() {
// data source TomcatJdbc
DataSource dataSource = new DataSource();
// configuration access JDBC
dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
// initially open connections
dataSource.setInitialSize(5);
// result
return dataSource;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- 第 18 行:该类是一个 Spring 配置类;
- 第 19 行:它导入了由 [ConfigJdbc] 配置类定义的 Bean,该类用于配置 Spring 项目 [mysql-config-jdbc]。这些是 JSON 过滤器;
- 第 23–30 行:定义所使用的 JPA 实现,本例中为 Hibernate 实现(第 25 行);
- 第 26 行:您可以选择是否显示 Hibernate 实现所执行的 SQL 操作;
- 第 27 行:指定与 Hibernate 连接的数据库管理系统(DBMS)。此配置至关重要。它使 Hibernate 能够使用 MySQL DBMS 的 SQL 方言,包括其专有特性。此外,它还告知 Hibernate 可以使用的 SQL 类型和 DBMS 对象。正是 JPA 实现这种适应特定 DBMS 的能力,使其在不同 DBMS 之间具有高度的可移植性;
- 第 28 行:Hibernate 可能会根据其发现的 JPA 实体为目标数据库生成表,也可能不会。这种生成仅在表不存在时发生。如果表已存在,则不会执行任何操作。在演示本文档中使用的各种数据库的 SQL 生成脚本是如何创建时,我们将利用这一生成表的能力;
- 第 33 行:包含 [dbproduitscategories] 数据库 JPA 实体的包;
- 第 36–49 行:与数据库 [dbproduitscategories] 关联的数据源 [tomcat-jdbc];
- 第 52–60 行:名为 [entityManagerFactory] 的 Bean(必须以此命名)将创建 [EntityManager] 对象,该对象负责管理 JPA 持久化上下文。所有 JPA 操作均通过它进行。由于我们使用的是 [Spring Data JPA],因此我们自己永远不会直接使用该对象。但我们需要对其进行配置,它需要知道以下信息:
- 所使用的 JPA 实现(第 55 行);
- 所使用的数据源(第 57 行);
- 该数据源对应的 JPA 实体(第 56 行);
- 第 58 行:使用这些信息初始化 EntityManager;
- 第 59 行:返回单例 [entityManagerFactory];
- 第 63–68 行:定义事务管理器。其名称必须为 [transactionManager];
- 第 65 行:创建一个 JPA 事务管理器;
- 第 66 行:通过 [entityManagerFactory] Bean(第 53 行和第 57 行)将其连接到第 37 行的数据源;
仅第 23–30 行中的 Bean 依赖于所使用的 JPA 实现。其他 Bean 则依赖于它。
6.3.3. [JPA] 层中的实体
![]() |
![]() |
目标数据库是 [dbproduitscategories] 数据库,其中包含两个表 [CATEGORIES] 和 [PRODUITS]。我们已经看到,该数据库还包含另外三个表 [USERS、ROLES、USERS_ROLES],这些表将用于保障即将部署到 Web 上的 Web 服务的安全。 目前我们将忽略这些表。作为参考,以下是 [CATEGORIES] 和 [PRODUCTS] 表的结构:
[PRODUCTS] 表的结构如下:
![]() |
- [ID]:表的自增主键 [2];
- [NAME]:产品的唯一名称 [4];
- [PRICE]:产品的价格;
- [DESCRIPTION]:产品的描述;
- [VERSIONING] 是产品的版本号。其初始版本为 1 [3]。每次修改产品时,操作该表的代码会将其版本号递增;
- [CATEGORY_ID]:[CATEGORIES] 表中的外键,用于标识产品所属的类别;
![]() |
- 在 [1-3] 中,[PRODUITS] 表的外键 [CATEGORIE_ID]。它引用了 [CATEGORIES] 表的 [ID] 列 [4-5];
- 当删除一个类别时,与其关联的所有产品也会被删除 [6]。这一点值得注意,因为它用于构建使用 [dbproduitscategories] 数据库的 [DAO] 层;
[CATEGORIES] 表结构如下:
![]() |
- [ID]:自动递增的主键;
- [VERSIONING]:分类版本号;
- [NAME]: 类别的唯一名称;
接下来我们将描述 JPA 实体 [Product] 和 [Category],它们分别对应 [PRODUCTS] 和 [CATEGORIES] 表。
![]() |
6.3.3.1. [AbstractCoreEntity] 接口
[AbstractCoreEntity] 接口由 JPA 实体 [Category] 和 [Product] 实现:
package generic.jpa.entities.dbproduitscategories;
public interface AbstractCoreEntity {
// getters and setters for [id], [version], [entityType] fields
public Long getId();
public void setId(Long id);
public Long getVersion();
public void setVersion(Long version);
public enum EntityType {
PROXY, POJO
}
public EntityType getEntityType();
public void setEntityType(EntityType entityType);
}
该接口由两个 JPA 实体实现,仅列出了用于读写这些实体的 [id]、[version] 和 [entityType] 字段的方法。[entityType] 字段的作用将在后面解释;
6.3.3.2. JPA 实体 [Product]
[Product] 类是与 [PRODUCTS] 表中某行关联的 JPA 实体:
![]() |
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_PRODUITS)
@JsonFilter("jsonFilterProduit")
public class Produit implements AbstractCoreEntity {
// properties
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// properties
@Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
private String nom;
@Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
private Long idCategorie;
@Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
private double prix;
@Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
private String description;
// the category
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
private Categorie categorie;
// manufacturers
public Produit() {
}
public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
Categorie categorie) {
this.id = id;
this.version = version;
this.nom = nom;
this.idCategorie = idCategorie;
this.prix = prix;
this.description = description;
this.categorie = categorie;
}
// signature
public String toString() {
return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
description, idCategorie);
}
// ------------------------------------------------------------
// redefine [equals] and [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// getters and setters
...
public void setCategorie(Categorie categorie) {
// entity type
if (entityType == EntityType.PROXY) {
throw new ProxyException(1005, new RuntimeException(
"On ne peut changer la catégorie d'un produit de type [PROXY]"), simpleClassName);
}
this.categorie = categorie;
}
}
- 第 21 行:[@Entity] 注解将 [Product] 类定义为由 [JPA] 层管理的实体。您也可以写成 [@Entity(name="MyProduct")],这样会将该实体的名称设为 [MyProduct]。 若未指定此信息,则实体名称默认为类名,本例中即为 [Product]。当实体中存在来自不同包且名称相同的两个类时,此命名约定便显得尤为必要;
- 第 22 行:注解 [@Table(name = "PRODUCTS")] 表示 [Product] 类是数据库中 [PRODUCTS] 表中某一行数据的对象表示;
- 第 23 行:要应用于该实体的 JSON 过滤器名称。我们将看到第 58 行的 [categorie] 属性并非总是可用。因此必须将其从对象的 JSON 表示中排除。为此,我们需要一个过滤器。因此我们将使用名为 [jsonFilterCategorie] 的过滤器来指定是否包含 [categorie] 属性;
- 第 26 行:[@Id] 注解将标注的字段设为与第 19 行表的主键关联的字段;
- 第 27 行:[@GeneratedValue(strategy = GenerationType.IDENTITY)] 注解设置了 [PRODUITS] 表中主键的自动生成模式。这由 [strategy] 属性决定。可选模式包括:

[IDENTITY] 策略并非所有数据库管理系统(DBMS)都支持。在测试的六个 DBMS 中,[MySQL 5、PostgreSQL 9.4、SQL Server 2014、DB2 Express-C 10.5] 支持该策略。 对于其余两个 [Oracle Express 11g Release 2、Firebird 2.5.4],必须使用 [SEQUENCE] 策略。为确保 JPA 实现之间的可移植性,不应使用 [AUTO] 策略,因为它将主键生成策略的选择权交由 JPA 实现决定。因此,在 MySQL 5 且使用 [AUTO] 策略的情况下:
- Hibernate 会选择 [IDENTITY] 策略并采用 [AUTO_INCREMENT] 模式作为主键;
- EclipseLink 选择 [TABLE] 策略,该策略默认会创建一个名为 [SEQUENCE] 的表,必须通过查询该表来获取主键。
最终,这两个 JPA 实现所管理的数据库结构并不相同。如果由 Hibernate 生成的,则无法被 EclipseLink 使用,反之亦然。
- 第 28 行:[@Column(name="ID")] 注解将 [PRODUCTS] 表中与 [id] 字段关联的列名称设置为 "ID";
- 第 29 行:主键使用 [Long] 类型而非 [long]。这是因为 [null] 主键在 JPA 中具有特定含义。因此,此处最好使用对象类型而非简单类型;
- 第 31 行:[@Version] 注解表明 [version] 字段与版本控制列相关联。每次实体被修改时,JPA 实现都会递增该版本号。该数字用于防止两个不同用户同时更新同一实体:两个用户 U1 和 U2 读取版本号为 V1 的实体 E。 U1 修改了 E 并将此更改持久化到数据库中:此时版本号变为 V1+1。U2 随后也修改了 E 并尝试将此更改持久化到数据库中:由于其版本号(V1)与数据库中的版本号(V1+1)不一致,因此会抛出异常;
- 第 36 行:实体类型。共有两种类型:POJO 和 PROXY。默认情况下,Product 实例将是一个 POJO(普通 Java 对象)。 在某些情况下,从数据库检索到的 [Product] 实例将属于 [PROXY] 类型。当第 58 行的 [Category] 属性因第 56 行的 [fetch = FetchType.LAZY] 属性而未初始化为类别时,就会出现这种情况。在此情况下,待测试的 JPA 实现存在差异:
- [Hibernate, OpenJPA]:访问 [PROXY] 类型产品的类别会抛出异常。Hibernate 使用“proxy”一词来指代以 [LAZY] 模式获取的 JPA 实例。这就是我使用该术语来指代此类实体的原因;
- [EclipseLink]:访问 [PROXY] 类型产品的类别会触发对该类别在数据库中的检索,且不会抛出异常;
由于我希望构建一个与所用 JPA 实现独立的测试层,因此需要了解每个实体的类型:POJO 还是 PROXY。这就是我向 JPA 实体中添加 [entityType] 字段的原因;
- 第 35 行:[@Transient] 注解表明 JPA 实现必须忽略此字段。事实上,该字段在 DBMS 表中并不存在;
- 第 40 行:[Product] 类会抛出 [ProxyException],该异常要求提供类名;
- 第 38 行:与之前一样,我们指定 JPA 实现必须忽略此字段;
- 第 39 行:[@JsonIgnore] 注解表示 [Product] 实例的 JSON 序列化器/反序列化器必须忽略此字段;
- 第 43 行:[@Column] 注解将 [name] 字段与 [PRODUCTS] 表中的 [NAME] 列关联起来。当字段名称与关联列名称相同(不区分大小写)时,可以省略 [@Column] 注解。本例即属于这种情况。 属性 [unique = true, length = 30, nullable = false] 仅在 JPA 实现根据 [Product] 实体生成 [CATEGORIES] 表时使用。 这些属性将转换为 SQL 属性 [UNIQUE, VARCHAR(30), NOT NULL],确保 [NAME] 列长度不超过 30 个字符、在表中唯一且不能为 NULL;
- 第 46–47 行:[idCategorie] 字段与 [CATEGORIE_ID] 列相关联。稍后我们将再次讨论这些属性;
- 第 49–50 行:[price] 字段与 [PRICE] 列相关联;
- 第 52–53 行:[description] 字段与 [DESCRIPTION] 列相关联;
- 第 56–58 行:产品类别;
- 第 56 行:[@ManyToOne] 注解表明,第 57 行注解 [@JoinColumn(name = "CATEGORIE_ID")] 所引用的列是 [Product] 实体的 [PRODUITS] 表到第 58 行实体关联的 [CATEGORIES] 表的外键。此注解必须应用于 JPA 实体。 因此,第 58 行的类必须是 JPA 实体;
- 第 56 行:注解 [fetch = FetchType.LAZY] 指定当从 [PRODUCTS] 表中检索产品时,其类别(第 58 行)不会立即被检索(延迟加载)。该类别将在首次调用 [getCategory] 方法时被检索。 为实现此功能,在运行时,JPA 层会对初始的 [getCategory] 方法(该方法仅返回类别字段)进行增强,通过调用 DBMS 来获取类别——这种技术被称为“代理”。如前所述,不同 JPA 实现对该功能的处理方式各不相同。此属性并非强制要求。所使用的 JPA 实现可以忽略它。 正因为 [categorie] 属性可能存在也可能不存在,我们才在第 23 行引入了 JSON 过滤器。当插入或更新产品时,[PRODUITS] 表中的关联列 [CATEGORIE_ID] 会自动更新。它接收 [categorie.getId()] 的值,其中 [categorie] 是第 58 行中的字段。 JPA规范要求该连接列不得通过任何其他方式进行更新。因此,第46行强制执行了[insertable = false, updatable = false]属性,以确保与[idCategorie]字段关联的[CATEGORIE_ID]列(即连接列)无法被[idCategorie]字段修改。 仅允许将 [CATEGORIE_ID] 列传输到 [idCategorie] 字段;
- 第 91–104 行:[Product] 实体之间的相等性被定义为其主键 [id] 之间的相等性;
- 第 108–115 行:为确保测试层的可移植性,我们将统一管理三个 JPA 实现(Hibernate、EclipseLink、OpenJpa)中的 [PROXY] 实体。对于类型为 [PROXY] 的 [Product] 实体,我们将禁止修改 [category] 字段的值。[ProxyException] 类定义如下:
![]() |
package generic.jpa.infrastructure;
import generic.jdbc.infrastructure.UncheckedException;
public class ProxyException extends UncheckedException {
private static final long serialVersionUID = 7278276670314994574L;
public ProxyException() {
}
public ProxyException(int code, Throwable e, String simpleClassName) {
super(code, e, simpleClassName);
}
}
在总结对该实体的讨论时,需要注意的是,注解及其属性在两种不同的情况下使用:
- 创建数据库表;
- 查询这些表。在此情况下,JPA 实现期望找到的表必须与其自身生成的表完全一致。因此,我们不能将任意 [PRODUCTS] 表与前面的 [Product] 实体相关联。该表必须至少具备(可能还包含其他)JPA 会生成的 [PRODUCTS] 表的特征。 在使用 JPA 时,理想的做法是从一个空数据库开始,由 JPA 在其中生成表。我们稍后将讨论这种生成机制。为 MySQL 数据库管理系统提供的 SQL 脚本正是基于 JPA 生成的表生成的。
[Product] 实体的所有属性均用于生成 [PRODUCTS] 表。一旦生成完成,诸如 [unique = true, length = 30, nullable = false] 之类的生成属性在查询表时将不再被使用。
6.3.3.3. JPA [Category] 实体
[Category] 类是一个 JPA 实体,与 [CATEGORIES] 表中的一行相关联:
![]() |
其代码如下:
package generic.jpa.entities.dbproduitscategories;
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategorie")
public class Categorie implements AbstractCoreEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = ConfigJdbc.TAB_JPA_ID)
protected Long id;
@Version
@Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
protected Long version;
@Transient
protected EntityType entityType = EntityType.POJO;
@Transient
@JsonIgnore
protected String simpleClassName = getClass().getSimpleName();
// properties
@Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
private String nom;
// related products
@OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
private List<Produit> produits;
// manufacturers
public Categorie() {
}
public Categorie(Long id, Long version, String nom, List<Produit> produits) {
this.id = id;
this.version = version;
this.nom = nom;
this.produits = produits;
}
// signature
public String toString() {
return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
}
// methods
public void addProduit(Produit produit) {
// entity type
if (entityType == EntityType.PROXY) {
throw new ProxyException(1004, new RuntimeException(
"On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
}
// add a product
if (produits == null) {
produits = new ArrayList<Produit>();
}
if (produit != null) {
// we add the product
produits.add(produit);
// set your category
produit.setCategorie(this);
produit.setIdCategorie(this.id);
}
}
// ------------------------------------------------------------
// redefine [equals] and [hashcode]
@Override
public int hashCode() {
Long id = getId();
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractCoreEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractCoreEntity other = (AbstractCoreEntity) entity;
Long id = getId();
Long otherId = other.getId();
return id != null && otherId != null && id.equals(otherId);
}
// getters and setters
...
}
- 第 24 行:该类是一个 JPA 实体;
- 第 25 行:与 [CATEGORIES] 表相关联;
- 第 26 行:[Category] 实体的 JSON 表示形式由名为 [jsonFilterCategory] 的过滤器控制。在请求该实体的 JSON 表示形式之前,必须配置此过滤器。[jsonFilterCategory] 过滤器将用于确定是否在 [Category] 实体的 JSON 表示形式中包含第 40 行中的 [products] 字段;
- 第 29–32 行:[id] 字段与 [CATEGORIES] 表的主键 [ID] 相关联。所选的生成模式为 [IDENTITY],这对应于 MySQL 中的 [AUTO_INCREMENT];
- 第 34–36 行:[version] 字段与 [CATEGORIES] 表中的 [VERSIONING] 列相关联;
- 第 38–39 行:[Categorie] 实体的类型;
- 第 41–43 行:[Categorie] 类的简短名称;
- 第 46–47 行:[name] 字段与 [CATEGORIES] 表中的 [NAME] 列相关联。 我们为其分配了 JPA 属性 [unique = true, length = 30, nullable = false],这样在生成 [CATEGORIES] 表时,[NAME] 列将具有 SQL 属性 [UNIQUE, VARCHAR(30), NOT NULL];
- 第 50–51 行:属于该类别的商品;
- 第 50 行:[@OneToMany] 注解是我们在 [Product] 实体中遇到的 [@ManyToOne] 关系的逆向关系。[mappedBy = "category"] 属性指定了 [Product] 实体中被逆向 [@ManyToOne] 关系注解的字段。 属性 [cascade = { CascadeType.ALL }] 指定对 @Entity [Category] 执行的操作(persist、merge、remove)应级联到第 51 行的 [products]。可使用常量 [CascadeType.PERSIST、CascadeType.MERGE、CascadeType.REMOVE] 指定部分级联;
- 第 50 行:[fetch = FetchType.LAZY] 属性指定,当从 [CATEGORIES] 表中检索类别时,其关联的产品不会立即被检索。这些产品将在首次调用 [getProduits] 方法时被检索。 为实现此功能,在运行时,JPA 层会对初始的 [getProduits] 方法(该方法仅返回 products 字段)进行增强,通过调用 DBMS 来获取该类别的商品。此属性是必填的。 JPA 实现无法忽略它。由于 [products] 属性可能已初始化也可能未初始化,我们在第 26 行引入了 JSON 过滤器,这使我们能够指定是否需要该属性,并在第 39 行指定实体类型;
- 第 71–88 行:[addProduct] 方法允许向该类别添加产品;
- 第 73–76 行:为统一不同 JPA 实现中的代理处理机制,我们规定不能向类型为 PROXY 的 [Category] 实体添加产品;
- 第 92–112 行:如果两个 [Category] 实体的主键 [id] 相同,则视为相等;
6.3.4. [persistence.xml] 文件
![]() |
JPA 应用程序必须在应用程序类路径中的 [META-INF/persistence.xml] 文件中定义所用 JPA 提供程序的某些属性,以及将要使用的 JPA 实体。上文中,该文件被放置在 [src/main/resources] 文件夹中,该文件夹实际上是 Eclipse 项目类路径的一部分。 当将 JPA 与 Spring 结合使用时,原本应放在 [persistence.xml] 文件中的某些信息会被移至 Spring 配置类中的其他位置。在 Spring JPA 应用程序中,Spring 负责驱动 JPA。使用 Spring JPA Hibernate 时,[persistence.xml] 文件可以简化为最简形式:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
<persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
- 第 1-5 行:[persistence.xml] 文件必须包含一个根 <persistence> 标签。第 2 行中该标签的属性在本应用程序中不会被使用;
- 持久化文件可通过 <persistence-unit> 标签(第 4 行)定义一个或多个持久化单元。持久化单元负责管理对特定数据库的访问。若应用程序同时管理两个数据库,则需包含两个持久化单元;
- 第 4 行:持久化单元具有名称 [name 属性],支持事务类型 [transaction-type 属性],拥有属性,并定义了与该持久化单元所管理的数据库表相关的实体。在此,由于数据库访问将由 [Spring JPA Hibernate] 管理,因此最后这两项信息可置于其他位置。事务类型有两种:
- [RESOURCE_LOCAL]:事务由应用程序自身管理。本例即属此情况,即由 Spring 管理事务;
- [JTA](Java事务API):运行应用程序的EJB(企业级Java Bean)容器会根据代码中的Java注解自动管理事务。我们在此处未采用此配置;
稍后我们将看到,此 [persistence.xml] 文件的内容取决于所使用的 JPA 实现。
6.4. [spring-jpa-generic] 项目
让我们回顾一下我们的目标。我们希望实现以下架构:
![]() |
其中 [DAO] 层将实现第 4 章中学习的 [IDao<Product>, IDao<Category>] 接口。我们的目标是对比该接口的两种实现:
- 一种基于 Spring JDBC 构建;
- 另一种使用 Spring JPA 构建;
在上述架构中:
- [JDBC] 层由第 3.3 节中讨论的 [mysql-config-jdbc] 项目实现;
- [JPA] 层由第 6.3 节中讨论的 [mysql-config-jpa-hibernate] 项目实现;
[spring-jpa-generic] 项目负责实现 [DAO] 和 [Spring Data] 层。
![]() |
6.4.1. Maven 配置
[spring-jpa-generic] 项目是一个由以下 [pom.xml] 文件配置的 Maven 项目:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dvp.spring.database</groupId>
<artifactId>spring-jpa-generic</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-jpa-generic</name>
<description>démo spring data avec tables de catégories et de produits</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<!-- configuration JPA of SGBD -->
<dependency>
<groupId>dvp.spring.database</groupId>
<artifactId>generic-config-jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- 第 22–26 行:该项目仅有一个依赖项,即配置应用程序 [JPA] 层的项目(我们刚刚分析过)。这是一个通用应用程序:
- 通过修改 [JDBC] 层配置项目来更换数据库管理系统(DBMS);
- 通过修改 [JPA] 层配置项目来更换 JPA 实现;
最终,依赖关系如下:
![]() |
6.4.2. Spring 配置
![]() |
[AppConfig] 类用于配置 Spring 项目:
package spring.data.config;
import generic.jpa.config.ConfigJpa;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {
}
- 第 11 行:该类是一个 Spring 配置类;
- 第 10 行:使用 [@EnableJpaRepositories] 注解来指定包含 Spring Data [CrudRepository] 接口的包。这使得它们成为 Spring 组件,可以注入到其他 Spring 组件中;
- 第 12 行:[@ComponentScan] 注解指示必须扫描 [spring.data.dao] 包以查找 Spring 组件。将找到 [DaoCategory] 和 [DaoProduct] 组件;
- 第 13 行:导入来自 [ConfigJpa] 配置类的 Bean。这些包括所用 JPA 实现(Hibernate、Eclipselink、OpenJpa)的 Bean、将要使用的数据源、负责处理 JPA 操作的 EntityManager 以及事务管理器;
6.4.3. [Spring Data] 层
![]() |
![]() |
6.4.3.1. [CategoriesRepository] 接口
[CategoriesRepository] 接口负责管理对 [CATEGORIES] 表的访问:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Categorie;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
// categorie avec ses produits
@Query("select c from Categorie c left join fetch c.produits where c.id=?1")
public Categorie getLongCategorieById(Long id);
@Query("select c from Categorie c left join fetch c.produits where c.nom=?1")
public Categorie getLongCategorieByName(String nom);
@Query("select c from Categorie c where c.nom in ?1")
public List<Categorie> getShortCategoriesByName(Iterable<String> names);
@Query("select c from Categorie c where c.id in ?1")
public List<Categorie> getShortCategoriesById(Iterable<Long> ids);
@Query("select distinct c from Categorie c left join fetch c.produits where c.id in ?1")
public List<Categorie> getLongCategoriesById(List<Long> names);
@Query("select distinct c from Categorie c left join fetch c.produits where c.nom in ?1")
public List<Categorie> getLongCategoriesByName(List<String> names);
@Query("select c from Categorie c")
public List<Categorie> getAllShortCategories();
@Query("select distinct c from Categorie c left join fetch c.produits")
public List<Categorie> getAllLongCategories();
}
- 第 10 行:第 5.1.3 节中已使用并解释了 [CrudRepository] 接口。提醒一下:
- 该接口的第一个参数类型是用于 CRUD 操作(findOne、findAll、save、delete、deleteAll)的 JPA 实体,
- 该接口的第二个参数类型是 JPA 实体的主键,此处为整数 [Long];
该接口的方法是通过 JPQL(Java 持久化查询语言)查询实现的。这些查询针对 JPA 实体。在此类查询中:
- 表被替换为与其关联的 JPA 实体;
- “列”被替换为查询中使用的 JPA 实体的字段;
以第 31–32 行为例:第 32 行中的方法从数据库中检索所有类别的简短形式。它由第 31 行中的 JPQL(Java 持久化查询语言)查询实现,该查询与其对应的 SQL 语句非常相似。要更深入地了解 JPQL,请参阅 [ref2](参见第 1.2 节)。
[CategoriesRepository] 接口的方法如下:
- 第 13–14 行:[getLongCategoryById] 方法返回通过主键 [id] 引用的类别的完整版本,即包含其产品的类别。回顾 [Category] 实体中,[products] 字段具有 [fetch = FetchType.LAZY] 属性(延迟加载)。 在 JPQL 查询中,我们使用 [fetch] 关键字强制加载产品。查询中的 ?1 参数将在运行时被第 12 行方法的第一个参数值替换,即 [Long id] 参数;
- 第 16–17 行:[getLongCategoryByName] 方法返回通过名称 [name] 引用的类别的完整版本;
- 第 19–20 行:[getShortCategoriesByName] 方法返回通过名称引用的类别的简短版本。 这些类别的 [products] 字段不为空。它包含对一个代理(由 JPA 实现生成的类)的引用,该代理的作用是在被调用时检索类别中的产品。在 JPA 持久化上下文之外调用它会抛出异常(Hibernate 和 OpenJPA 如此,但 EclipseLink 除外)。因此,我们不会使用类别简短版本的 [products] 字段;
- 第 22–23 行:[getShortCategoriesById] 方法返回通过主键 [id] 引用的类别的简短版本;
- 第 25–26 行:[getLongCategoriesById] 方法根据主键 [id] 返回类别的长版本;
- 第 [28-29] 行:[getLongCategoriesByName] 方法根据名称返回所引用的类别的长版本;
- 第 31–32 行:[getAllShortCategories] 方法返回所有分类的简短版本;
- 第 34–35 行:[getAllLongCategories] 方法返回所有类别的长版本;
注意:并非所有 JPA 实现都支持相同的 JPQL 语法。因此,以下语法被 Hibernate 和 EclipseLink 接受,但不被 OpenJpa 接受:
@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
OpenJpa 不接受上文中的别名 [p]。
6.4.3.2. [ProductsRepository] 接口
[ProductsRepository] 接口负责管理对 [PRODUCTS] 表的访问:
package spring.data.repositories;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
@Transactional()
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
// un produit avec sa catégorie
@Query("select p from Produit p left join fetch p.categorie where p.id=?1")
public Produit getLongProduitById(Long id);
@Query("select p from Produit p left join fetch p.categorie where p.nom=?1")
public Produit getLongProduitByName(String nom);
@Query("select p from Produit p where p.id in ?1")
public List<Produit> getShortProduitsById(List<Long> ids);
@Query("select p from Produit p where p.nom in ?1")
public List<Produit> getShortProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie where p.id in ?1")
public List<Produit> getLongProduitsById(List<Long> ids);
@Query("select distinct p from Produit p left join fetch p.categorie where p.nom in ?1")
public List<Produit> getLongProduitsByName(List<String> names);
@Query("select distinct p from Produit p left join fetch p.categorie")
public List<Produit> getAllLongProduits();
@Query("select p from Produit p")
public List<Produit> getAllShortProduits();
}
- 第 [15-16] 行:[getLongProductById] 方法返回通过主键 [id] 标识的产品的完整版本,其中包含其类别。 回顾 [Product] 实体中,[category] 字段的属性 [fetch = FetchType.LAZY](延迟加载)。在 JPQL 查询中,我们使用 [fetch] 关键字强制加载类别;
- 第 18-19 行:[getLongProductByName] 方法返回通过名称标识的产品的详细版本;
- 第 21-22 行:[getShortProductsById] 方法返回通过主键 [id] 标识的产品的简短版本。在此简短版本中,[category] 字段不为空。它包含对 JPA 实现生成的代理的引用,若调用该代理,将获取产品的类别。此调用只能在 JPA 持久化上下文中进行。 在其他地方调用会引发异常(Hibernate 和 OpenJPA 会,但 EclipseLink 不会)。因此,在 [DAO] 层或其他地方,我们不会使用产品简短版本中的 [category] 字段。 在产品的简短版本中,[idCategorie] 字段已被初始化。其值为该产品所属类别的主键。这使我们能够稍后通过 [DaoCategorie.getShortCategoriesById(idCategorie)] 方法从 [DAO] 层检索该类别;
- 第 24–25 行:[getShortProduitsByName] 方法返回通过名称标识的产品的简短版本;
- 第 27–28 行:[getLongProduitsById] 方法返回通过主键标识的产品的详细版本;
- 第 30–31 行:[getLongProductsByName] 方法返回通过名称标识的产品的详细信息;
- 第 33–34 行:[getAllLongProducts] 方法返回所有产品的详细信息;
- 第 36–37 行:[getAllShortProducts] 方法返回所有产品的简短版本;
这些接口将由 JPA 实现于运行时生成的类来实现。此类类被称为 [代理] 类。默认情况下,[CrudRepository] 接口的方法在事务内执行。[ProductsRepository] 和 [CategoriesRepository] 接口继承自 [CrudRepository] 类,因此它们是 Spring 组件。因此,它们可以被注入到其他 Spring 组件中。
6.4.4. [DAO] 层
![]() |
![]() |
6.4.4.1. [IDao<T>] 接口
[IDao<T>] 接口即是在使用 Spring JDBC 实现 [DAO] 层时已讨论过的接口(参见第 4.7 节);
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
import java.util.List;
public interface IDao<T extends AbstractCoreEntity> {
// list of all T entities
public List<T> getAllShortEntities();
public List<T> getAllLongEntities();
// special entities - short version
public List<T> getShortEntitiesById(Iterable<Long> ids);
public List<T> getShortEntitiesById(Long... ids);
public List<T> getShortEntitiesByName(Iterable<String> names);
public List<T> getShortEntitiesByName(String... names);
// special entities - long version
public List<T> getLongEntitiesById(Iterable<Long> ids);
public List<T> getLongEntitiesById(Long... ids);
public List<T> getLongEntitiesByName(Iterable<String> names);
public List<T> getLongEntitiesByName(String... names);
// update of several entities
public List<T> saveEntities(Iterable<T> entities);
public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
// delete all entities
public void deleteAllEntities();
// deletion of multiple entities
public void deleteEntitiesById(Iterable<Long> ids);
public void deleteEntitiesById(Long... ids);
public void deleteEntitiesByName(Iterable<String> names);
public void deleteEntitiesByName(String... names);
public void deleteEntitiesByEntity(Iterable<T> entities);
public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
6.4.4.2. 抽象类 [AbstractDao]
![]() |
抽象类 [AbstractDao] 是实现 [DAO] 层的各类类的父类:
- [DaoProduit] 类,该类实现了 [IDao<Produit>] 接口,并管理对 [PRODUITS] 表的访问;
- [DaoCategorie] 类,它实现了 [IDao<Categorie>] 接口,并管理对 [CATEGORIES] 表的访问;
其代码如第4.8节所述,仅有一处细微差异:没有任何方法带有 [@Transactional] 属性,该属性会导致方法在事务内执行。在此,我们利用了 Spring Data 的 [CrudRepository] 接口默认在事务内执行这一特性。
6.4.4.3. [DaoCategorie] 类
![]() |
[DaoCategorie] 类如下所示实现了 [IDao<Categorie>] 接口:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Categorie> getAllShortEntities() {
try {
return setShortCategoriesType(categoriesRepository.getAllShortCategories());
} catch (Exception e) {
throw new DaoException(211, e, simpleClassName);
}
}
private List<Categorie> setShortCategoriesType(List<Categorie> categories) {
for (Categorie categorie : categories) {
categorie.setEntityType(EntityType.PROXY);
}
return categories;
}
@Override
public List<Categorie> getAllLongEntities() {
try {
return categoriesRepository.getAllLongCategories();
} catch (Exception e) {
throw new DaoException(202, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
categoriesRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(208, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesById(List<Long> ids) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
} catch (Exception e) {
throw new DaoException(203, e, simpleClassName);
}
}
@Override
protected List<Categorie> getShortEntitiesByName(List<String> names) {
try {
return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
} catch (Exception e) {
throw new DaoException(204, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesById(List<Long> ids) {
try {
return categoriesRepository.getLongCategoriesById(ids);
} catch (Exception e) {
throw new DaoException(205, e, simpleClassName);
}
}
@Override
protected List<Categorie> getLongEntitiesByName(List<String> names) {
try {
return categoriesRepository.getLongCategoriesByName(names);
} catch (Exception e) {
throw new DaoException(206, e, simpleClassName);
}
}
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
...
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
categoriesRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(209, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
categoriesRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(212, e, simpleClassName);
}
}
}
- 第 17 行:[@Component] 注解将 [DaoCategorie] 类定义为 Spring 组件;
- 第 18 行:[DaoCategorie] 类继承自 [AbstractDao<Categorie>] 类,这意味着它实现了 [IDao<Categorie>] 接口;
- 第 20–24 行:从 [Spring Data] 注入两个 [CrudRepository] 接口的引用。这种注入发生在 Spring 对象实例化时,通常是在 Spring 项目执行开始时;
- 该类的所有方法都将工作委托给 [CrudRepository] 接口中同名的方法;
- 所有返回简短形式实体的方法,均通过将实体类型设置为 [EntityType.PROXY] 来表明这一点(第 29、63、72 行);
[saveEntities] 方法需要特别说明:
@Override
protected List<Categorie> saveEntities(List<Categorie> categories) {
// on note les produits qui vont être insérés
List<Produit> insertedProduits = new ArrayList<Produit>();
for (Categorie categorie : categories) {
EntityType categorieType = categorie.getEntityType();
List<Produit> produits = null;
if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
for (Produit produit : produits) {
if (produit.getId() == null) {
insertedProduits.add(produit);
}
// on en profite pour rétablir (si besoin est) la relation produit --> categorie
produit.setCategorie(categorie);
}
}
}
// on persiste les catégories / produits
try {
categoriesRepository.save(categories);
} catch (Exception e) {
throw new DaoException(201, e, simpleClassName);
}
// on met à jour le champ [idCategorie] des produits insérés
for (Produit produit : insertedProduits) {
produit.setIdCategorie(produit.getCategorie().getId());
}
// résultat
return categories;
}
- 第 2 行:作为参数传递的类别既包括待插入的类别 [id==null],也包括待更新的类别 [id!=null];
- 第 20 行:我们使用 [categoriesRepository.save(entities)] 方法将分类持久化。 在测试过程中,我们发现已持久化的产品(id==null)的 [idCategorie] 字段未被填充。为解决此问题,我们在第 4–17 行标记待插入的产品,并在持久化后填充其 [idCategorie] 字段(第 25–27 行);
- 第 5–17 行:遍历类别列表;
- 第 8–16 行:针对每个类别,遍历其产品列表。此处存在一个难点。`saveEntities` 方法既用于持久化,也用于修改类别。在后一种情况下,类别可能是以简短版本检索的,因此其 `products` 字段中包含对代理方法的引用。 若在此处配合 Hibernate 使用,将引发异常,因为当前使用的类别已不在 JPA 持久化上下文中——该上下文已在检索类别简短版本的方法结束事务时被关闭。因此,我们在第 8 行通过 [Category] 实体的 [EntityType] 字段来判断是否能访问该类别的商品列表;
- 第14行:我们将产品与其所属类别建立关联。通常情况下,这种关联本应已存在。但我们无法确定该产品是如何创建的,也无法确定它是否已被关联到所属类别。因此,为避免任何 问题(为了管理[Product]实体,JPA要求其引用所关联的[Category]实体),我们自行建立了这一关联。
通过将此代码与 Spring JDBC 实现中的 [DaoProduit] 类代码进行对比(参见第 4.9 节),我们可以发现 Spring Data JPA 库极大地简化了 [DAO] 层的编写工作。
6.4.4.4. [ProductDao] 类
![]() |
[DaoProduct] 类如下所示实现了 [IDao<Product>] 接口:
package spring.data.dao;
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
import com.google.common.collect.Lists;
@Component
public class DaoProduit extends AbstractDao<Produit> {
@Autowired
private ProduitsRepository produitsRepository;
@Autowired
private CategoriesRepository categoriesRepository;
@Override
public List<Produit> getAllShortEntities() {
try {
return setShortProduitsType(produitsRepository.getAllShortProduits());
} catch (Exception e) {
throw new DaoException(102, e, simpleClassName);
}
}
private List<Produit> setShortProduitsType(List<Produit> produits) {
for (Produit produit : produits) {
produit.setEntityType(EntityType.PROXY);
}
return produits;
}
@Override
public List<Produit> getAllLongEntities() {
try {
return produitsRepository.getAllLongProduits();
} catch (Exception e) {
throw new DaoException(117, e, simpleClassName);
}
}
@Override
public void deleteAllEntities() {
try {
produitsRepository.deleteAll();
} catch (Exception e) {
throw new DaoException(112, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesById(List<Long> ids) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsById(ids));
} catch (Exception e) {
throw new DaoException(103, e, simpleClassName);
}
}
@Override
protected List<Produit> getShortEntitiesByName(List<String> names) {
try {
return setShortProduitsType(produitsRepository.getShortProduitsByName(names));
} catch (Exception e) {
throw new DaoException(104, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesById(List<Long> ids) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsById(ids));
} catch (Exception e) {
throw new DaoException(105, e, simpleClassName);
}
}
@Override
protected List<Produit> getLongEntitiesByName(List<String> names) {
try {
return linkLongProduitsToCategories(produitsRepository.getLongProduitsByName(names));
} catch (Exception e) {
throw new DaoException(106, e, simpleClassName);
}
}
private List<Produit> linkLongProduitsToCategories(List<Produit> produits) {
for (Produit produit : produits) {
Categorie categorie = produit.getCategorie();
if (categorie != null) {
produit.setCategorie(categorie);
produit.setIdCategorie(categorie.getId());
}
}
return produits;
}
@Override
protected List<Produit> saveEntities(List<Produit> entities) {
// re-establish (if necessary) the link between a product and its category
for (Produit produit : entities) {
if (produit.getEntityType() == EntityType.POJO) {
produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
}
}
// we persist products
try {
return Lists.newArrayList(produitsRepository.save(entities));
} catch (Exception e) {
throw new DaoException(111, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesById(List<Long> ids) {
try {
produitsRepository.delete(getShortEntitiesById(ids));
} catch (Exception e) {
throw new DaoException(113, e, simpleClassName);
}
}
@Override
protected void deleteEntitiesByName(List<String> names) {
try {
produitsRepository.delete(getShortEntitiesByName(names));
} catch (Exception e) {
throw new DaoException(118, e, simpleClassName);
}
}
}
该代码与 [DaoCategorie] 类的代码类似:
- 对于长版本的类别,测试表明产品的 [idCategorie] 字段未被填充。第 96–105 行中的 [linkLongProduitsToCategories] 方法解决了此问题;
- 第 108–121 行中的 [saveEntities] 方法用于插入新产品或修改现有产品。JPA 层要求每个 [Product] 实体都必须与一个 [Category] 实体建立关联。由于我们无法确定用户是否已执行此操作,因此我们在第 110–113 行中自行完成此关联。 我们只需将 [Product] 实体与主键与 [Product] 的 [idCategory] 字段匹配的 [Category] 实体建立关联即可。 在测试过程中,我们发现若将类别版本字段设为 null 会引发错误。因此这里将其设为 0,但实际可设为任意值。除主键外,JPA 层在插入或更新 [Product] 实体时并不要求 [Category] 实体的其他字段;
6.4.5. 测试层
![]() |
![]() |
上述测试与 Spring JDBC 实现中的测试完全一致。如有需要,请参阅以下页面:
- [JUnitTestCheckArguments]:第 4.11.1 节;
- [JUnitTestDao]:第 4.11.2 节;
- [JUnitTestPushTheLimits]: 第4.11.3节;
我们使用以下测试配置:
![]() | ![]() |
![]() | ![]() |
各项测试的结果如下:
![]() | ![]() |
![]() |
在[1]中,[JUnitTestPushTheLimits]测试采用了Spring Data JPA的Hibernate实现;而在[2]中,则采用了Spring JDBC的实现。我们可以看到,后者的性能表现更佳。因此,我们得出初步结论:使用Spring Data JPA开发[DAO]层要简单得多,但其性能表现不如Spring JDBC的实现。
[JUnitTestProxies] 测试是一个虚拟的 JUnit 测试。其目的是演示每种 JPA 实现处理代理(即实体的简化版本)时的行为:
package spring.data.tests;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import spring.data.config.AppConfig;
import spring.data.dao.IDao;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {
// layer [DAO]
@Autowired
private IDao<Produit> daoProduit;
@Autowired
private IDao<Categorie> daoCategorie;
@Before
public void clean() {
// the base is cleaned before each test
log("Vidage de la base de données", 1);
// we empty table [CATEGORIES] and cascade table [PRODUITS]
daoCategorie.deleteAllEntities();
}
@Test
public void doNothing() {
System.out.println("doNothing");
}
private List<Categorie> fill(int nbCategories, int nbProduits) {
// fill the tables
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < nbCategories; i++) {
Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
categorie.setProduits(new ArrayList<Produit>());
for (int j = 0; j < nbProduits; j++) {
Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
categorie.addProduit(produit);
}
categories.add(categorie);
}
// adding the category - by cascading the products will also be
// inserted
daoCategorie.saveEntities(categories);
// result
return categories;
}
@Test
public void getShortCategoriesByName1() {
// filling
fill(1, 1);
// test
log("getShortCategoriesByName1", 1);
Categorie categorie = daoCategorie.getShortEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getShortProduitsByName1() {
// filling
fill(1, 1);
// test
log("getShortProduitsByName1", 1);
Produit produit = daoProduit.getShortEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongCategoriesByName1() {
// filling
fill(1, 1);
// test
log("getLongCategoriesByName1", 1);
Categorie categorie = daoCategorie.getLongEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
System.out.println("Catégorie :");
try {
System.out.println(categorie.getProduits().size());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
@Test
public void getLongProduitsByName1() {
// filling
fill(1, 1);
// test
log("getLongProduitsByName1", 1);
Produit produit = daoProduit.getLongEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
System.out.println("Nom de la catégorie du produit :");
try {
System.out.println(produit.getCategorie().getNom());
} catch (Exception e) {
System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
}
}
private void log(String message, int mode) {
// poster message
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
}
结果如下:
Vidage de la base de données --------------------------------
doNothing
Vidage de la base de données --------------------------------
getShortCategoriesByName1 --------------------------------
Catégorie de type : PROXY
Catégorie :
Exception : org.hibernate.LazyInitializationException, Message : failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongCategoriesByName1 --------------------------------
Catégorie de type : POJO
Catégorie :
1
Vidage de la base de données --------------------------------
getShortProduitsByName1 --------------------------------
Produit de type : PROXY
Nom de la catégorie du produit :
Exception : org.hibernate.LazyInitializationException, Message : could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongProduitsByName1 --------------------------------
Produit de type : POJO
Nom de la catégorie du produit :
categorie[0]
在此我们可以看到,当访问 PROXY 类型类别的 [Categorie.produits] 字段以及 PROXY 类型产品的 [Produit.categorie] 字段时,这两种情况都会抛出 [org.hibernate.LazyInitializationException] 异常(第 7 行和第 17 行)。



































