Skip to content

6. Spring Data JPA Hibernate

6.1. 简介

我们将基于由项目 [spring-jdbc-04] 管理的数据库 [dbproduitscategories],并实现该项目中定义的两个接口 [IDao<Categorie>, IDao<Produit>]。这将使我们能够:

  • 比较实现代码;
  • 使用相同的测试层;
  • 比较两个实现的性能;
  • [JDBC] 层由第 3.3 节中研究的 [mysql-config-jdbc] 项目实现;

现在我们转向其他层。

6.2. 搭建工作环境

使用 STS,导入位于 [<exemples>/spring-database-config/mysql/eclipse] [2] 文件夹中的 [mysl-config-jpa-hibernate] [1] 项目:

该项目配置了该项目的 [Spring JPA Hibernate] 层。每个 JPA 实现都有其独立的配置项目。

然后,导入位于 [<exemples>/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 配置

该项目是一个 Maven 项目,由以下 [pom.xml] 文件进行配置:


<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>
        <!-- 可变依赖项 ********************************************** -->
        <!-- JPA 提供者 -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- 常量依赖 ********************************************** -->
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Spring Context -->
        <!-- 继承自 JDBC 的配置 -->
        <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 层的配置项目,后者定义的内容包括: 所用 SGBD 的 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 {

    // JPA 提供程序
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        return hibernateJpaVendorAdapter;
    }

    // JPA 实体包
    public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };

    // 数据源
    @Bean
    public DataSource dataSource() {
        // 数据源 TomcatJdbc
        DataSource dataSource = new DataSource();
        // 访问配置JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
        // 初始打开的连接
        dataSource.setInitialSize(5);
        // 结果
        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();
    }

    // 事务管理器
    @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指定已连接的SGBD。 此配置至关重要。它使 Hibernate 能够使用 SGBD 的 SQL 方言,包括其专有部分。 此外,这还会告知 Hibernate 其可使用的 SQL 类型以及 SGBD 对象。 正是 JPA 实现能够适应特定 SGBD 的这一特性,使其在 SGBD 之间具有很强的可移植性;
  • 第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 行:通过 Bean [entityManagerFactory](第 53 和 57 行)将其连接到第 37 行的数据源;

只有第23至30行的Bean依赖于所使用的JPA实现。其他Bean随后基于它。

6.3.3. [JPA] 层的实体

  

目标数据库是 [dbproduitscategories],其中包含两个表:[CATEGORIES] 和 [PRODUITS]。 我们看到它还有另外三张表 [USERS, ROLES, USERS_ROLES],这些表将用于保障即将部署到 Web 上的 Web 服务的安全。目前我们暂不考虑这些表。为便于回顾,特此重述 [CATEGORIES] 和 [PRODUITS] 这两张表的结构:

[PRODUITS] 表的结构如下:

  • [ID]:表 [2] 的自增主键;
  • [NOM]:产品 [4] 的唯一名称;
  • [PRIX]:产品的价格;
  • [DESCRIPTION]:产品的描述;
  • [VERSIONING] 是产品的版本号。其初始版本为 1 [3]。每次修改产品时,操作该表的代码会递增其版本号;
  • [CATEGORIE_ID]:表[CATEGORIES]的外键,用于标识产品所属的类别;
  • 在 [1-3] 中,作为表 [PRODUITS] 的外键 [CATEGORIE_ID]。 该操作针对表 [CATEGORIES] 中的列 [ID] [4-5];
  • 当某分类被删除时,与其关联的所有产品也会被删除 [6]。这一点值得注意,因为它在构建利用 [dbproduitscategories] 数据库的 [DAO] 层时会被用到;

类别表 [CATEGORIES] 如下:

  • [ID]:自增主键;
  • [VERSIONING]:类别版本号;
  • [NOM]:类别的唯一名称;

接下来我们将描述实体 JPA、[Produit] 和 [Categorie],以及表 [PRODUITS] 和 [CATEGORIES] 的映像。

  

6.3.3.1. 接口 [AbstractCoreEntity]

接口 [AbstractCoreEntity] 由实体 JPA、[Categorie] 和 [Produit] 实现:


package generic.jpa.entities.dbproduitscategories;

public interface AbstractCoreEntity {

    // 字段的 getter 和 setter[id]、[version]、[entityType]
    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 [Produit]

类 [Produit] 是与表 [PRODUITS] 中某行关联的实体 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 {
    // 属性
    @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();

    // 属性
    @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;

    // 类别
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
    private Categorie categorie;

    // 制造商
    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;
    }

    // 签名
    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);
    }

    // ------------------------------------------------------------
    // 重定义 [equals] 和 [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);
    }

    // 获取器和设置器
...
    public void setCategorie(Categorie categorie) {
        // 实体类型
        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] 将类 [Produit] 设为由层 [JPA] 管理的实体。 也可以写成 [@Entity(name="MonProduit")],这会给该实体命名 [MonProduit]。 若缺少此信息,实体的名称即为类名,此处即为 [Produit]。当实体中存在两个来自不同包且名称相同的类时,这种命名方式便变得必要;
  • 第 22 行:注释 [@Table(name = "PRODUITS")] 表示类 [Produit] 是数据库表 [PRODUITS] 中某行对应的对象;
  • 第 23 行:要应用于该实体的过滤器名称为 jSON。我们将看到,第 58 行中的属性 [categorie] 并非总是可用。 因此必须将其从对象的 jSON 表示中排除。为此我们需要一个过滤器。因此,我们将通过名为 [jsonFilterCategorie] 的过滤器来指定是否需要属性 [categorie];
  • 第 26 行:注释 [@Id] 将该注释字段设为第 19 行表的主键关联字段;
  • 第27行:注释[@GeneratedValue(strategy = GenerationType.IDENTITY)]确定了表[PRODUITS]中主键的自动生成模式。该模式由属性[strategy]设定。可选模式包括:

Image

策略 [IDENTITY] 并非适用于所有 SGBD。 在测试的六个 SGBD 中,该策略适用于 SGBD 和 [MySQL 5, PostgreSQL 9.4, SQL Server 2014, DB2 Express-C10.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"] 确定了表 [PRODUITS] 中与字段 [id] 关联的列名;
  • 第 29 行:主键使用类型 [Long] 而非 [long]。这是因为主键 [null] 对 JPA 具有特殊含义。 因此,此处建议使用对象类型而非简单类型;
  • 第 31 行:注释 [@Version] 表明字段 [version] 关联了一个版本控制列。实现 JPA 将在实体每次被修改时递增该版本号。 该版本号用于防止两个不同用户同时更新该实体:用户 U1 和 U2 读取的实体 E 具有版本号 V1。 U1 修改了 E 并将该修改保存到数据库中:此时版本号变为 V1+1。 U2 随后修改 E 并将该修改保存到数据库中:它将引发异常,因为其版本号(V1)与数据库中的版本号(V1+1)不一致;
  • 第36行:实体的类型。将有两种类型:POJO 和 PROXY。默认情况下,生成的实例将是一个 POJO(普通Java对象)。 在某些情况下,从数据库中检索到的 [Produit] 实例将属于 [PROXY] 类型。 这种情况发生在第 58 行中的 [Categorie categorie] 属性因第 56 行中的 [fetch = FetchType.LAZY] 属性而未被初始化为类别时。 在此情况下,待测试的 JPA 实现存在差异:
    • [Hibernate, OpenJPA]:访问 [PROXY] 类型产品的类别会引发异常。 Hibernate 使用“代理”一词来指代在 [LAZY] 模式下获取的 JPA 实例。因此,我使用该术语来指代此类实体;
    • [EclipseLink]:访问类型为 [PROXY] 的产品的类别时,系统会在数据库中搜索该类别,且不会抛出异常;

由于我希望建立一个与所用 JPA 实现方案独立的测试层,因此需要明确每个实体的类型:是 POJO 还是 PROXY。 因此,我在 JPA 实体中添加了 [entityType] 字段;

  • 第 35 行:注释 [@Transient] 指出,实现 JPA 必须忽略该字段。因为该字段在 SGBD 的表中并不存在;
  • 第 40 行:类 [Produit] 抛出类型为 [ProxyException] 的异常,该异常需要类名;
  • 第 38 行:与前文相同,此处指出 JPA 实现应忽略该字段;
  • 第 39 行:注解 [@JsonIgnore] 指出,[Produit] 实例的序列化器/反序列化器 jSON 应忽略该字段;
  • 第 43 行:注释 [@Column] 将字段 [nom] 与表 [PRODUITS] 的列 [NOM] 关联起来。 当字段名称与关联列名称相同(不区分大小写)时,可省略注释 [@Column]。本例即属此情况。 [unique = true, length = 30, nullable = false] 属性仅在 JPA 实现需要根据实体 [Produit] 生成表 [CATEGORIES] 时使用。 这些属性将由 SQL 和 [UNIQUE, VARCHAR(30), NOT NULL] 属性转换,从而确保 [NOM] 列的长度不超过 30 个字符,在表中唯一,且不能取值 NULL;
  • 第 46-47 行:字段 [idCategorie] 与该表的 [CATEGORIE_ID] 列相关联。稍后我们将详细讨论其属性;
  • 第 49-50 行:字段 [prix] 与列 [PRIX] 相关联;
  • 第 52-53 行:字段 [description] 与列 [DESCRIPTION] 相关联;
  • 第 56-58 行:产品类别;
  • 第 56 行:注释 [@ManyToOne] 表示第 57 行注释 [@JoinColumn(name = "CATEGORIE_ID")] 的列是表 [PRODUITS] 的外键,该表属于[Produit] 实体的注释列,该列关联于第 58 行所对应的 [CATEGORIES] 表。该注释必须标注 JPA 实体。 因此,第 58 行中的类必须是实体 JPA;
  • 第56行:注解[fetch = FetchType.LAZY]要求,当从表[PRODUITS]中检索产品时,其类别(第58行)不会立即被检索(延迟加载)。 该类别将在首次调用方法 [getCategorie] 时获取。为此,在运行时, JPA 层会通过调用 SGBD 方法来获取类别,从而增强原始的 [getCategorie] 方法(该方法仅返回 categorie 字段),这种技术被称为“代理”。如前所述,JPA 的各实现版本在该特性的实现上存在差异。此属性不具有强制性。 所使用的 JPA 实现有权忽略它。正是因为 [categorie] 属性可能存在也可能不存在,我们才在第 23 行引入了 jSON 过滤器。 在插入或更新产品时,表 [PRODUITS] 的连接列 [CATEGORIE_ID] 会自动更新。 它接收来自 [categorie.getId()] 的值,其中 [categorie] 是第 58 行中的字段。JPA 规范规定,该连接列不能通过其他方式进行更新。 因此,它还强制执行第46行的[insertable = false, updatable = false]属性,这些属性确保与字段[idCategorie]关联的列[CATEGORIE_ID] (即该连接列)与字段 [idCategorie] 相关联,且该列不能被字段 [idCategorie] 修改。仅允许将列 [CATEGORIE_ID] 的值传输至字段 [idCategorie];
  • 第 91-104 行:[Produit] 实体之间的相等关系被定义为其主键 [id] 之间的相等关系;
  • 第 108-115 行:为了使我们的测试层具有可移植性,我们将统一处理三个实现(JPA、[Hibernate, EclipseLink, OpenJpa])中的 [PROXY] 实体。 对于类型为 [PROXY] 的 [Produit] 类型,将禁止更改字段 [categorie] 的值。[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 期望找到的表应与其自身生成的表一致。因此,不能将任意 [PRODUITS] 表与前面的 [Produit] 实体相关联。 该表必须至少具备(也可包含其他属性)其本应生成的 [PRODUITS] 表的特征。 在使用 JPA 时,最佳做法是从一个空数据库开始,让 JPA 在其中生成表。我们稍后将详细讨论这种生成方式。 为 SGBD 和 MySQL 提供的脚本 SQL 是基于 JPA 生成的表生成的。

实体 [Produit] 的所有属性均用于生成表 [PRODUITS]。完成此操作后,在处理表时将不再使用诸如 [unique = true, length = 30, nullable = false] 之类的生成属性。

6.3.3.3. 实体 JPA [Categorie]

类 [Categorie] 是与表 [CATEGORIES] 中某行关联的实体 JPA:

其代码如下:


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();

    // 属性
    @Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
    private String nom;

    // 相关产品
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    private List<Produit> produits;

    // 构造函数
    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;
    }

    // 签名
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
    }

    // 方法
    public void addProduit(Produit produit) {
        // 实体类型
        if (entityType == EntityType.PROXY) {
            throw new ProxyException(1004, new RuntimeException(
                    "On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
        }
        // 添加产品
        if (produits == null) {
            produits = new ArrayList<Produit>();
        }
        if (produit != null) {
            // 正在添加产品
            produits.add(produit);
            // 设置其类别
            produit.setCategorie(this);
            produit.setIdCategorie(this.id);
        }
    }

    // ------------------------------------------------------------
    // 重新定义 [equals] 和 [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);
    }

    // 获取器和设置器
...
}
  • 第 24 行:该类是一个 JPA 实体;
  • 第 25 行:关联到表 [CATEGORIES];
  • 第26行:实体[Categorie]的表示jSON由名为[jsonFilterCategorie]的过滤器控制。 在请求该实体的 jSON 表示形式之前,必须先配置该过滤器。 将使用过滤器 [jsonFilterCategorie] 来决定是否从实体 [Categorie] 的表示 jSON 中排除第 40 行中的字段 [produits];
  • 第29-32行:字段[id]与表[CATEGORIES]的主键[ID]相关联。 所选的生成模式是 [IDENTITY] 模式,因此 MySQL 的生成模式为 [AUTO_INCREMENT];
  • 第34-36行:字段[version]与表[CATEGORIES]中的版本控制列[VERSIONING]相关联;
  • 第 38-39 行:实体 [Categorie] 的类型;
  • 第 41-43 行:类 [Categorie] 的简短名称;
  • 第 46-47 行:字段 [nom] 与表 [CATEGORIES] 的列 [NOM] 相关联。 为其赋予属性 JPA [unique = true, length = 30, nullable=false],以便在生成表 [CATEGORIES] 时, [NOM]列将具有SQL和[UNIQUE, VARCHAR(30), NOT NULL]的属性;
  • 第50-51行:属于该类别的商品;
  • 第50行:注释[@OneToMany]是我们在实体[Produit]中遇到的关系[@ManyToOne]的反向关系。 属性 [mappedBy = "categorie"] 指明实体 [Produit] 中被反向关系 [@ManyToOne] 标注的字段。 属性 [cascade = { CascadeType.ALL }] 要求对 @Entity [Categorie] 执行的操作(persist、merge、remove)应级联到第 51 行中的 [produits]。 可以通过常量 [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE] 指定部分级联;
  • 第50行:属性[fetch = FetchType.LAZY]要求,当从表[CATEGORIES]中检索某类别时,其关联产品不会立即被检索。这些产品将在首次调用方法[getProduits]时被检索。 为此,在运行时, JPA 层会通过调用 SGBD 方法来获取该类别的商品,从而扩展初始的 [getProduits] 方法(该方法仅返回 produits 字段)。 该属性具有约束力。JPA 实现不能忽略它。 由于属性 [produits] 可能已初始化也可能未初始化,我们在第 26 行引入了过滤器 jSON,以便指定是否需要该属性,并在第 39 行指定实体类型;
  • 第 71-88 行:方法 [addProduit] 用于将产品添加到类别中;
  • 第73-76行:为统一不同JPA实现之间的代理管理,我们决定不能向类型为PROXY的[Categorie]实体添加产品;
  • 第 92-112 行:如果两个 [Categorie] 实体具有相同的主键 [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 行:一个持久化单元名为 [attribut name],支持事务类型 [attribut 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<Produit>, IDao<Categorie>] 接口。我们的任务是比较该接口的两个实现:

  • 一个使用 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>
        <!-- SGBD 的 JPA 配置 -->
        <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] 层的配置项目,即可切换 SGBD;
    • 通过更改 [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 组件。将找到 [DaoCategorie] 和 [DaoProduit] 组件;
  • 第 13 行:导入配置类 [ConfigJpa] 中的 Bean。其中包含所用实现 JPA 的 Bean (Hibernate、Eclipselink、OpenJpa),待使用的数据源 EntityManager,以及事务管理器 JPA;

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> {

    // 产品及其所属类别
    @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 行:接口 [CrudRepository] 已在第 5.1.3 节中使用并说明。需要提醒的是:
    • 该接口的第一个参数类型是实体 JPA,用于管理对 CRUD(findOne、findAll、 save、delete、deleteAll),
    • 接口的第二个参数类型是实体 JPA 的主键,此处为整数 [Long];

该接口的方法通过 JPQL 查询(Java Persistence Query Language)实现。该查询针对 JPA 实体。在此类查询中:

  • 表被替换为与其关联的实体 JPA;
  • 列被替换为查询中使用的 JPA 实体的字段;

以第 31-32 行为例:第 32 行中的方法将数据库中的所有类别以简短形式返回。 该方法由第31行的JPQL查询(Java持久化查询语言)实现,其结构与对应的SQL非常相似。 若要深入了解 JPQL,可参阅 [ref2](参见第 1.2 节)。

接口 [CategoriesRepository] 的方法如下:

  • 第13-14行:方法[getLongCategorieById]返回由主键[id]引用的类别的详细版本,即包含其产品的类别。 需要提醒的是,在实体 [Categorie] 中,字段 [produits] 具有属性 [fetch = FetchType.LAZY](延迟加载)。 在查询 JPQL 中,我们使用关键字 [fetch] 强制加载产品。 查询中的参数 ?1 在执行时将被第 12 行方法的第一个参数值替换,即参数 [Long id];
  • 第16-17行:方法[getLongCategorieByName]返回通过名称[nom]引用的类别的长版本;
  • 第19-20行:方法[getShortCategoriesByName]返回通过名称引用的类别的简短版本。这些类别的字段[produits]并非null。 该字段包含一个代理的引用(由实现类 JPA 创建),其作用是在被调用时返回该类别的商品。在 JPA 持久化上下文之外调用该方法会引发异常 (Hibernate 及 OpenJpa 会抛出,但 EclipseLink 不会)。因此,我们不会使用类别简短版本中的 [produits] 字段;
  • 第 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. 接口 [ProduitsRepository]

接口 [ProduitsRepository] 管理对表 [PRODUITS] 的访问:


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> {

    // 带分类的产品
    @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] 行:方法 [getLongProduitById] 返回由主键 [id] 标识的产品的详细信息,即包含其类别。 需要提醒的是,在实体 [Produit] 中,字段 [categorie] 具有属性 [fetch = FetchType.LAZY](延迟加载)。 在查询 JPQL 中,我们使用关键字 [fetch] 强制加载类别;
  • 第 18-19 行:方法 [getLongProduitByName] 返回通过名称标识的产品的详细信息;
  • 第21-22行:方法[getShortProduitsById]返回通过主键[id]标识的产品的简短版本。 在此简短版本中,字段 [categorie] 的值并非 null。它包含由实现 JPA 生成的代理引用,若调用该代理,将获取产品的类别。 该调用只能在持久化上下文 JPA 中进行。在其他地方进行该调用会引发异常(Hibernate 和 OpenJpa,但不包括 EclipseLink)。 因此,在 [DAO] 层或其他地方,我们不会在产品的简版中使用字段 [categorie]。 在产品的简短版本中,字段 [idCategorie] 已被初始化。其值为该产品所属类别的主键。这使得后续可以通过方法 [DaoCategorie. getShortCategoriesById(idCategorie)] 向 [DAO] 层查询该类别
  • 第24-25行:方法[getShortProduitsByName]返回通过名称标识的产品的简短版本;
  • 第27-28行:方法[getLongProduitsById]返回通过主键标识的产品的详细信息;
  • 第 30-31 行:方法 [getLongProduitsByName] 返回按名称标识的产品的详细信息;
  • 第 33-34 行:方法 [getAllLongProduits] 返回所有产品的长描述;
  • 第 36-37 行:方法 [getAllShortProduits] 返回所有产品的简短版本;

这些接口将在项目运行时由 JPA 实现生成的类来实现。此类类被称为 [proxy] 类。 默认情况下,[CrudRepository] 接口的方法在事务中执行。由于 [ProduitsRepository, CategoriesRepository] 接口继承了 [CrudRepository] 类,因此它们属于 Spring 组件。因此,它们可以被注入到其他 Spring 组件中。

6.4.4. [DAO] 层

  

6.4.4.1. [IDao<T>] 接口

接口 [IDao<T>] 即为在基于 Spring 实现的 [DAO] 层中已探讨过的接口(参见第 4.7 节);


package spring.data.dao;

import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;

import java.util.List;

public interface IDao<T extends AbstractCoreEntity> {

    // 所有 T 实体的列表
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // 特定实体的列表 - 简短版本
    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);

    // 特定实体 - 长版
    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);

    // 更新多个实体
    public List<T> saveEntities(Iterable<T> entities);

    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);

    // 删除所有实体
    public void deleteAllEntities();

    // 删除多个实体
    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] 层的类的父类:

  • 实现接口 [IDao<Produit>] 并管理对表 [PRODUITS] 访问的类 [DaoProduit];
  • 类 [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行:将[CrudRepository]和[Spring Data]这两个接口的引用注入。该注入将在Spring对象实例化时进行,通常发生在Spring项目执行开始时;
  • 该类的所有方法都将工作委托给 [CrudRepository] 接口中同名的方法;
  • 所有将实体转换为简短版本的方法,都会通过将实体的类型设置为 [EntityType.PROXY] 来表明这一点(第 29、63、72 行);

方法 [saveEntities] 需要特别说明:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // 标注待插入的产品
        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);
                    }
                    // 借此机会(如有必要)恢复产品 --> 类别关系
                    produit.setCategorie(categorie);
                }
            }
        }
        // 保存类别/产品
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // 更新已插入产品的字段 [idCategorie]
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // 结果
        return categories;
    }
  • 第 2 行:作为参数传递的类别既包括需要插入的类别([id==null]),也包括需要修改的类别([id!=null]);
  • 第20行:使用方法[categoriesRepository.save(entities)]持久化分类。测试发现,持久化商品(id==null)的字段[idCategorie]未被填充。 为解决此问题,在第4-17行记录待插入的产品,并在数据持久化后,填充其[idCategorie]字段(第25-27行);
  • 第5-17行:遍历类别列表;
  • 第8-16行:遍历每个类别的商品列表。这里存在一个难点。 方法 [saveEntities] 既用于持久化,也用于修改类别。在后一种情况下,类别可能以简短版本获取,因此字段 [produits] 中包含代理方法的引用。 若在 Hibernate 中使用该字段,将引发异常,因为所操作的类别已不在持久化上下文 JPA 中——该上下文已在返回类别短版本的方法事务结束时被关闭。 因此,我们使用实体 [Categorie] 第 8 行中的字段 [EntityType] 来判断是否可以访问该类别的商品列表;
  • 第14行:将产品与其类别建立关联。通常情况下,该关联应已存在。但我们无法确定该产品是如何构建的,以及是否已与其类别建立关联。 因此,为避免任何问题(处理实体 [Produit] 时,JPA 需要该实体引用其关联的实体 [Categorie]),我们自行建立此关联。

通过将此代码与 Spring 实现 JDBC 中的 [DaoProduit] 类代码进行对比(参见第 4.9 节) 可以发现,Spring Data 库 JPA 极大地简化了 [DAO] 层的编写工作。

6.4.4.4. [DaoProduit]类

  

类 [DaoProduit] 以如下方式实现了接口 [IDao<Produit>]:


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) {
        // (如有必要)恢复商品与其分类之间的关联
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // 保存产品
        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 层要求每个 [Produit] 实体都与一个 [Categorie] 实体相关联。由于无法确定用户是否已执行此操作,因此我们在第 110-113 行自行完成此操作。 只需将 [Produit] 与一个 [Categorie] 实体建立关联,该实体的主键需等于 [Produit] 中的 [idCategorie] 字段。 测试表明,若将类别版本设为 null 将会报错。因此此处将其设为 0,但实际可设置任意值。 除主键外,实体 [Categorie] 的任何字段均非 JPA 层插入/修改实体 [Produit] 所必需;

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 开发 [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 {

    // 层[DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

    @Before
    public void clean() {
        // 每次测试前清理数据库
        log("Vidage de la base de données", 1);
        // 清空表 [CATEGORIES],并级联清空表 [PRODUITS]
        daoCategorie.deleteAllEntities();
    }

    @Test
    public void doNothing() {
        System.out.println("doNothing");
    }

    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // 填充表
        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);
        }
        // 添加类别 - 相关产品也将随之
        // 插入
        daoCategorie.saveEntities(categories);
        // 结果
        return categories;
    }

    @Test
    public void getShortCategoriesByName1() {
        // 填充
        fill(1, 1);
        // 测试
        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() {
        // 填充
        fill(1, 1);
        // 测试
        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() {
        // 填充
        fill(1, 1);
        // 测试
        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() {
        // 填充
        fill(1, 1);
        // 测试
        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) {
        // 显示消息
        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] 时, 这两种情况(第7行和第17行)都会抛出类型为[org.hibernate.LazyInitializationException]的异常。