Skip to content

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] 属性决定。可选模式包括:

Image

[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.PERSISTCascadeType.MERGECascadeType.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 实现中的测试完全一致。如有需要,请参阅以下页面:

我们使用以下测试配置:

各项测试的结果如下:

在[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 行)。