Skip to content

11. [Cours]:使用 Spring Data 管理关系型数据库

关键词:多层架构、Spring、依赖注入、API JPA(Java Persistence API)、Spring Data。

我们将使用 Spring 生态系统的一个分支 [Spring Data] 来实现 TD 的 [DAO] 层。 [Spring Data] 基于 JPA (Java Persistence API),该层使 [DAO] 层能够操作对象,而非 SQL 命令。 最终,[DAO]层并不知道它正在与数据库进行交互。它只了解[Spring Data]层的接口。

我们将首先通过两个示例来了解 [Spring Data]。

11.1. Support

  • 在 [1] 中,文件夹 [support / chap-11] 包含三个 Eclipse 项目;
  • 在 [2] 中,脚本 SQL 用于创建本章的示例数据库;

11.2. 示例 1

Spring 官网上有许多关于 Spring 入门教程([http://spring.io/guides])。我们将使用其中一个来介绍 Spring Data。为此,我们将使用 Spring Tool Suite(STS)。

  • 在 [1] 中,我们导入 [spring.io/guides] 中的一个教程;
  • 在 [2] 中,我们选择教程 [Accessing Data Jpa],该教程演示了如何使用 Spring Data 访问数据库;
  • 在 [3] 中,选择一个由 Maven 配置的项目;
  • 在 [4] 中,教程可提供两种形式:[initial] 是一个空模板,需根据教程逐步填充;[complete] 则是教程的最终版本。我们选择后者;
  • 在 [5] 中,可以选择在浏览器中查看教程;
  • 在 [6] 中,是最终项目。

11.2.1. 项目的 Maven 配置

项目的 Maven 依赖项配置在文件 [pom.xml] 中:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- 所有操作均使用 UTF-8 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • 第 5-9 行:定义了一个父级 Maven 项目。该项目定义了项目的大部分依赖。如果这些依赖已足够,则无需添加;否则,需补充缺失的依赖;
  • 第 12-15 行:定义了对 [spring-boot-starter-data-jpa] 的依赖。该工件包含 Spring Data 的类;
  • 第 16-19 行:定义了对 SGBD 和 H2 的依赖,这些依赖用于创建和管理内存数据库。

让我们看看这些依赖引入的类:

这些类数量众多:

  • 部分属于 Spring 生态系统(以 spring 开头的);
  • 另一些属于 Hibernate 生态系统(如 hibernate、jboss),其中我们在此使用的是 JPA 这一实现;
  • 还有的是测试库(junit、hamcrest);
  • 还有日志库(log4j、logback、slf4j);

我们将保留所有这些库。对于生产环境中的应用程序,应仅保留必要的库。

在文件 [pom.xml] 的第 26 行,可以看到以下代码:


<start-class>hello.Application</start-class>

该行与以下几行相关:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

第6-9行,[spring-boot-maven-plugin]插件用于生成应用程序的可执行jar文件。因此,[pom.xml]文件的第26行指定了该jar文件的可执行类。

11.2.2. [JPA] 层

通过 [JPA] 层访问数据库,Java Persistence API:

  

该应用程序较为基础,用于管理客户 [Customer]。类 [Customer] 属于 [JPA] 层,具体如下:


package hello;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }

}

一位客户拥有ID [id]、名字 [firstName] 和姓氏 [lastName]。每个 [Customer] 实例代表数据库表中的一行。

  • 第8行:注解JPA,该注解使得实例[Customer]的持久化操作(创建、读取、更新、删除)将由实现类JPA进行管理。 根据 Maven 依赖关系,可以看到实际使用的是 JPA / Hibernate 实现;
  • 第 11-12 行:JPA 注解将字段 [id] 关联到 [Customer] 表的主键。 第 12 行表明,JPA 实现将使用所用 SGBD 特有的主键生成方法,此处为 H2;

没有其他关于 JPA 的注释。因此将使用默认值:

  • [Customer] 表将采用类名,即 [Customer];
  • 该表的列名将采用类字段的名称:[id, firstName, lastName](需注意表列名不区分大小写);

需要注意的是,所使用的 JPA 实现从未被命名。

11.2.3. [Spring Data] 层

类 [CustomerRepository] 实现了表访问层 [Customer]。其代码如下:

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

因此这是一个接口而非类(第 7 行)。它继承了 Spring Data 接口 [CrudRepository](第 5 行)。 该接口由两个类型进行参数化:第一个是所管理元素的类型,此处为类型 [Customer];第二个是所管理元素的主键类型,此处为类型 [Long]。接口 [CrudRepository] 如下所示:


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

该接口定义了可在类型 JPA T 上执行的操作 CRUD(创建 – 读取 – 更新 – 删除):

  • 第 8 行:save 方法用于将实体 T 持久化到数据库中。它使用 SGBD 赋予的主键将实体持久化。该方法还允许更新由主键 id 标识的实体 T。 具体执行哪种操作取决于主键 id 的值:若该值为 null,则执行持久化操作;否则执行更新操作;
  • 第 10 行:同上,但针对实体列表;
  • 第12行:方法findOne用于根据主键id检索实体T;
  • 第22行:delete方法用于删除通过主键id标识的实体T;
  • 第24-28行:方法[delete]的变体;
  • 第16行:方法[findAll]可用于检索所有持久化实体T;
  • 第18行:同上,但仅限于已传入标识符列表的实体;

让我们回到 [CustomerRepository] 接口:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • 第 9 行可通过名称 [lastName] 检索 [Customer];

关于 [DAO] 层的内容就到此为止。该接口没有实现类,它由 [Spring Data] 在运行时自动生成。 接口 [CrudRepository] 的方法会被自动实现。至于在接口 [CustomerRepository] 中添加的方法,则视情况而定。让我们回到 [Customer] 的定义:


private long id;
private String firstName;
private String lastName;

第 9 行中的方法由 [Spring Data] 自动实现,因为它引用了 [Customer] 的字段 [lastName](第 3 行)。 当在待实现的接口中遇到 [findBySomething] 方法时,Spring Data 会通过以下 JPQL(Java 持久化查询语言)查询来实现它:

select t from T t where t.something=:value

因此,类型 T 必须包含一个名为 [something] 的字段。这样,方法

List<Customer> findByLastName(String lastName);

将通过类似以下代码实现:

return [em].createQuery("select c from Customer c where  c.lastName=:value").setParameter("value",lastName).getResultList()

其中 [em] 指代持久化上下文 JPA。这仅在类 [Customer] 拥有名为 [lastName] 的字段时才可行,而实际情况正是如此。

综上所述,在简单情况下,Spring Data 允许我们通过一个简单的接口来实现 [DAO] 层。

11.2.4. [console] 层

  

[Application] 类如下:


package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired
    CustomerRepository repository;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Override
    public void run(String... strings) throws Exception {
        // 保存部分客户
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // 获取所有客户
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        // 通过 ID 获取单个客户
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        // 按姓氏检索客户
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }

}
  • 第 9 行:该类实现了接口 [CommandLineRunner],该接口是 [Spring Boot] 的子接口(第 4 行)。该接口仅有一个方法,即第 19 行中的方法;
  • 第 8 行:@SpringBootApplication 是一个聚合了多个 @[Spring Boot] 注解的注解:
    • @Configuration:表示该类是配置类;
    • @EnableAutoConfiguration:要求 [Spring Boot] 根据各种属性(特别是项目 Classpath 的内容)自行创建一定数量的 Bean。 由于 Hibernate 库位于 Classpath 中,因此 [entityManagerFactory] Bean 将通过 Hibernate 进行实现。 由于 SGBD 的库位于 Classpath 中,因此 [dataSource] Bean 将使用 H2 进行实现。 在 [dataSource] Bean 中,还需定义用户及其密码。 此时 Spring Boot 将使用 H2 的默认管理员,该管理员无密码。由于类路径中包含 [spring-tx] 库,因此将使用 Spring 的事务管理器;
    • @EnableWebMvc:如果类路径中包含 [spring-mvc] 库。此时,将为 Web 应用程序进行自动配置;
    • @ComponentScan:该注解告知 Spring 应从何处查找其他 Bean、配置及服务。 默认情况下,系统会在包含该标记类所在的包中进行查找,即 [hello] 包。因此,系统将找到 [Customer] 和 [CustomerRepository] 这两个类。 由于第一个类带有 [@Entity] 注解,它将被归类为由 Hibernate 管理的实体。由于第二个类继承了 [CrudRepository] 接口,它将被注册为 Spring Bean;
  • 第 11-12 行:[CustomerRepository] Bean 被注入到主类的代码中;
  • 第 15 行:执行 Spring Boot 项目中 [SpringApplication] 类的静态方法 [run]。其参数是带有 [Configuration] 或 [EnableAutoConfiguration] 注解的类。 随后将执行上述所有说明的内容。结果是一个 Spring 应用上下文,即一组由 Spring 管理的 Bean;

后续操作仅调用实现 [CustomerRepository] 接口的 Bean 中的方法。控制台输出结果如下:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-10 15:35:43.661  INFO 5784 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 5784 (started by ST in C:\Users\Serge Tahé\Documents\workspace-sts-3.6.3.RELEASE\gs-accessing-data-jpa-complete)
2015-03-10 15:35:43.708  INFO 5784 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:45.230  INFO 5784 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:45.254  INFO 5784 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-10 15:35:45.331  INFO 5784 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-10 15:35:45.332  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-10 15:35:45.334  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-10 15:35:45.651  INFO 5784 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-10 15:35:45.754  INFO 5784 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2015-03-10 15:35:45.877  INFO 5784 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-10 15:35:46.154  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:46.169  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2015-03-10 15:35:46.779  INFO 5784 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2015-03-10 15:35:47.040  INFO 5784 --- [           main] hello.Application                        : Started Application in 3.623 seconds (JVM running for 4.324)
2015-03-10 15:35:47.042  INFO 5784 --- [       Thread-1] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:47.044  INFO 5784 --- [       Thread-1] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2015-03-10 15:35:47.046  INFO 5784 --- [       Thread-1] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:47.047  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:47.051  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • 第 1-8 行:Spring Boot 项目的徽标;
  • 第 9 行:执行 [hello.Application] 类;
  • 第 10 行:[AnnotationConfigApplicationContext] 是实现 Spring 的 [ApplicationContext] 接口的类。这是一个 Bean 容器;
  • 第 11 行:Bean [entityManagerFactory] 通过 Spring 类 [LocalContainerEntityManagerFactory] 实现;
  • 第 12 行:出现了 [Hibernate]。最终选定的是该实现类 JPA;
  • 第 19 行:Hibernate 方言为 SQL,需与 SGBD 配合使用。 此处的 [H2Dialect] 方言表明,Hibernate 将与 SGBD 和 H2 配合使用;
  • 第 21-22 行:数据库已创建。 创建了表 [CUSTOMER]。这意味着 Hibernate 已被配置为根据 JPA 定义生成表,此处为类 [Customer] 的定义 JPA;
  • 第 26-30 行:接口中 [findAll] 方法的执行结果;
  • 第 34 行:接口中 [findOne] 方法的返回结果;
  • 第 38-39 行:方法 [findByLastName] 的结果;
  • 第 41 行及后续行:Spring 上下文关闭日志。

11.2.5. Spring Data 项目的手动配置

我们将前一个项目复制到 [gs-accessing-data-jpa-02] 项目中:

  

在这个新项目中,我们将不依赖 Spring Boot 的自动配置,而是手动进行配置。如果默认配置不符合我们的需求,这将非常有用。

首先,我们在文件 [pom.xml] 中显式声明所需的依赖项:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa-02</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- H2 数据库 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- TomcatJDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- 所有内容均使用 UTF-8 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>

</project>
  • 第 10-14 行:父级 Maven 项目,我们将使用其中定义的库;
  • 第 18-21 行:用于访问数据库的 Spring Data;
  • 第 23-26 行:JPA 规范的 Hibernate 实现;
  • 第 28-31 行:SGBD H2;
  • 第33-36行:数据库通常与开放连接池配合使用,以避免反复建立和关闭连接。此处采用的实现是[tomcat-jdbc];

在新项目中,实体 [Customer] 和接口 [CustomerRepository] 保持不变。我们将修改类 [Application],将其拆分为两个类:

  • [Config],作为配置类:
  • [Main],作为可执行类;
  

可执行类 [Application] 现内容如下:


package console;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import repositories.CustomerRepository;
import config.AppConfig;
import entities.Customer;

public class Application {
    public static void main(String[] args) {
        // Spring 上下文实例化
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // 保存几位客户
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        ...

        // 关闭上下文
        context.close();
    }
}
  • 第 9 行:类 [Application] 不再包含配置注解;
  • 第 3-7 行:请注意,不再导入 [Spring Boot] 包;
  • 第 12 行:实例化 Spring Bean。获取包含所创建 Bean 引用的 Spring 上下文;
  • 第 13 行:请求 [CustomerRepository] 类型 Bean 的引用;

用于配置项目的 [Config] 类( )如下所示:


package config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
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;
import org.springframework.transaction.annotation.EnableTransactionManagement;

//@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "repositories" })
@Configuration
// @ComponentScan(basePackages={"package1","package2"})
public class AppConfig {

    // 数据库 H2
    @Bean
    public DataSource dataSource() {
        // 数据源 TomcatJdbc
        DataSource dataSource = new DataSource();
        // 访问配置 JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // 最初打开的连接
        dataSource.setInitialSize(1);
        // 结果
        return dataSource;
    }

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

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // 事务管理器
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • 第 17 行:注解 [@EnableTransactionManagement] 表示 [CrudRepository] 接口的方法必须在事务内执行。由于这是默认行为,因此该注解已被注释掉;
  • 第 18 行:注解 [@EnableJpaRepositories] 用于指定 Spring Data 接口 [CrudRepository] 的所在目录。这些接口将作为 Spring 组件,并在 Spring 上下文中可用;
  • 第 19 行:注解 [@Configuration] 将类 [Config] 设为 Spring 配置类;
  • 第 20 行:注解 [@ComponentScan] 用于列出需要搜索 Spring 组件的文件夹。 Spring组件是指带有Spring注解(如@Service@Component@Controller等)的类。此处除了[AppConfig]类中定义的组件外没有其他组件,因此该注解已被注释掉;
  • 第 24-37 行:定义数据源,即数据库 H2。正是第 25 行的 @Bean 注解,使得该方法创建的对象成为由 Spring 管理的组件。 此处方法名称可任意设定。但若第51行的EntityManagerFactory不存在且通过自动配置定义,则该方法必须命名为[dataSource];
  • 第 30 行:数据库将命名为 [demo],并生成在项目文件夹中;
  • 第40-47行:定义所使用的实现JPA,此处为Hibernate实现。此处方法名可任意设定;
  • 第 43 行:不生成 SQL 日志;
  • 第 44 行:若数据库不存在,则创建数据库;
  • 第50-58行:定义了将管理JPA持久化的EntityManagerFactory。该方法必须命名为[entityManagerFactory];
  • 第 51 行:该方法接收两个参数,其类型与之前定义的两个 Bean 相同。Spring 将构建这些 Bean,并将其作为方法参数注入;
  • 第 53 行:指定使用的 JPA 实现;
  • 第 54 行:指定查找 JPA 实体的文件夹;
  • 第 55 行:指定要管理的数据源;
  • 第 61-66 行:事务管理器。该方法必须命名为 [transactionManager]。它接收第 51-58 行中的 Bean 作为参数;
  • 第 64 行:将事务管理器关联至 EntityManagerFactory;

上述方法可以按任意顺序定义。

运行该项目将得到相同的结果。项目文件夹中会出现一个新文件,即数据库文件 H2:

  

11.2.6. 创建可执行归档

要创建该项目的可执行归档文件,可按以下步骤操作:

  • 在 [1] 中:创建一个运行配置;
  • 在 [2] 中:类型为 [Java Application]
  • 在 [3] 中:指定要执行的项目(使用 Browse 按钮);
  • 在 [4] 中:指定要执行的类;
  • 在 [5] 中:运行配置的名称——可以是任意名称;
  • 在 [6] 中:导出项目;
  • 在 [7] 中:以可执行存档的形式;
  • 在 [8] 中:指定要创建的可执行文件的路径和名称;
  • 在 [9] 中:在 [5] 中创建的运行配置名称;
  • 在 [10] 中,生成的归档文件;

完成上述操作后,在包含可执行归档文件的文件夹中打开一个控制台:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-02.jar

按以下方式运行该程序:


.....\dist>java -jar gs-accessing-data-jpa-02.jar

控制台显示的结果如下:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder 了解更多详情。
mars 10, 2015 5:27:20 PM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
mars 10, 2015 5:27:20 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.8.Final}
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
mars 10, 2015 5:27:22 PM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
mars 10, 2015 5:27:22 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
mars 10, 2015 5:27:22 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

11.3. 示例 2

11.3.1. 简介

我们将沿用之前介绍 API 和 JDBC 时所用的产品表示例,并构建以下架构:

数据库 [dbintrospringjpa] 包含两个表:[PRODUITS] 和 [CATEGORIES]。表 [CATEGORIES] 的内容如下:

 
  • [ID]:主键为 AUTO_INCREMENT;
  • [VERSION]:记录版本号;
  • [NOM]:类别名称——唯一;

表 [PRODUITS] 如下:

 
  • [ID]:主键,模式为 AUTO_INCREMENT;
  • [VERSION]:记录版本号;
  • [NOM]:产品名称——唯一标识;
  • [ID_CATEGORIE]:其类别编号——[CATEGORIES.ID]字段的外键;
  • [PRIX]:其价格;
  • [DESCRIPTION]:产品描述;

待完成任务:使用支持文档中的脚本 SQL [dbintrospringdata.sql] 创建数据库 [dbintrospringdata]:


11.3.2. 创建 Maven 项目

要创建 Spring Data 项目骨架,可以按照以下步骤操作:

  • 在 [1] 中,创建一个新项目;
  • 在 [2] 中:类型为 [Spring Starter Project];
  • 生成的项目将是一个Maven项目。在[3]中,指定项目组的名称;
  • 在 [4] 中:指定构建项目时将生成的工件名称(此处为 jar);
  • 在 [5] 中:指定项目的 Eclipse 名称——可以是任意名称(不必与 [4] 相同);
  • 在 [7] 中:指定将创建一个包含 [JPA] 层的项目,该层包含 SGBD 和 MySQL。 此类项目所需的依赖项将被包含在文件 [pom.xml] 中;
  • 在 [8] 中,输入项目文件夹名称;
  • 在 [9] 中,完成向导;
  • 在 [10] 中:生成的项目;

文件 [pom.xml] 包含项目所需的依赖项 JPA:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
        <relativePath/> <!-- 从存储库中查找父项 -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.IntroSpringData01Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 第 14-19 行:父级 Maven 项目——定义了大量库及其版本——我们使用这些库作为 Maven 依赖项,但未指定其版本;
  • 第 28-31 行:JPA 所需的依赖项——将包含 [Spring Data];
  • 第 32-36 行:MySQL 对驱动程序 JDBC 的依赖;
  • 第 37-41 行:JUnit 测试所需的依赖项(已集成 Spring);

可执行类 [Application] 本身不执行任何操作,但已预先配置:


package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class IntroSpringData01Application {

    public static void main(String[] args) {
        SpringApplication.run(IntroSpringData01Application.class, args);
    }
}
  • 注解 [@SpringBootApplication] 将该类设为项目的自动配置类;

测试类 [ApplicationTests] 无实际功能,但已预配置:


package demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = IntroSpringData01Application.class)
public class IntroSpringData01ApplicationTests {

    @Test
    public void contextLoads() {
    }

}
  • 第 9 行:注解 [@SpringApplicationConfiguration] 允许使用配置文件 [Application]。因此,该测试类将受益于该文件中定义的所有 Bean;
  • 第 8 行:注解 [@RunWith] 实现了 Spring 与 JUnit 的集成:该类将能够作为 JUnit 测试执行。 [@RunWith] 是一个 JUnit 注解(第 4 行),而 [SpringJUnit4ClassRunner] 类是一个 Spring 类(第 6 行);

现在我们已经有了一个 JPA 应用程序框架,可以对其进行补充,编写与产品数据库相关的持久层项目。

11.3.3. Eclipse 项目

我们将前一个项目按以下方式进行扩展:

  
  • [AppConfig.java]:Spring 项目的配置类;
  • [Main.java]:项目的可执行类;
  • [IDao.java]:[DAO] 层的接口;
  • [Dao.java]:[DAO] 层的实现类;
  • [AbstractEntity.java]:[Produit] 和 [Categorie] 类的父类;
  • [Produit.java]:与数据库表 [PRODUITS] 中某行关联的类;
  • [Categorie.java]:与数据库表 [CATEGORIES] 中某行关联的类;
  • [ProduitsRepository]:用于访问表 [PRODUITS] 的 Spring Data 接口;
  • [CategoriesRepository]:用于访问表 [CATEGORIES] 的 Spring Data 接口;
  • [pom.xml]:Maven 项目的配置文件;

该项目实现了以下架构:

[DAO] 层仅可见由 [Spring Data] 实现的层。

11.3.4. Maven 配置

Maven 项目的 [pom.xml] 文件如下:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- MySQL 数据库 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- 库 jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- Spring Boot 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 日志库 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</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>

此配置即第 11.2.5 节中使用并解释的配置。我们在此添加以下库:

  • 第 42-49 行:jSON 库,由 [Produit] 类的 [toString] 方法调用;
  • 第51-55行:[Google Guava]库,该库提供了用于管理元素集合的实用方法。它将被[Dao]类所使用,该类实现了[DAO]层;
  • 第 56-67 行:测试所需的库 JUnit;
  • 第69-72行:一个日志库;
  • 第 81-86 行:项目所需的 Maven 插件;

11.3.5. [JPA] 层的实体

[DAO]

[console]

图层

[JPA]

飞行员

[JDBC]

夹克

[Spring Data]

Spring 4

SGBD

  

11.3.5.1. 类 [AbstractEntity]

[AbstractEntity]类如下:


package spring.data.entities;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@MappedSuperclass
public abstract class AbstractEntity {
    // 属性
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;

    // 构造函数
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // 重定义 [equals] 和 [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id.longValue() == other.id.longValue();
    }

    // 签名 jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // 获取器和设置器
....
}

该类的目的是为 JPA 实体提供一个父类,通过将 [id, version] (第19、22行)的属性,这些属性与两个关联数据库的实体[Produit]和[Categorie]共同拥有。这些属性与表中的[ID, VERSION]列相关联(第18、21行)。

  • 第13行:注释[@MappedSuperclass]表明该类是实体JPA的父类;
  • 第 16 行:注释 [@Id] 表示字段 [id](可能有其他名称)与某张表的主键相关联;
  • 第 17 行:注解 [@GeneratedValue(strategy=GenerationType.IDENTITY)] 确定了主键的生成模式。模式 [GenerationType.IDENTITY] 将与 MySQL 一起使用模式 [AUTO_INCREMENT]。 若使用另一个 SGBD,该模式将采用另一种方法。其优势在于开发人员无需为此操心,且无论使用哪个 SGBD,其代码均保持有效;
  • 第 18 行:注释 [@Column] 指明了与字段关联的列。当该注释缺失时,JPA 会默认该列与字段同名。此处即为这种情况。因此,本可以省略该注释;
  • 第20行:注释[@Version]表明字段[version]关联了一个版本控制列。实现JPA将在实体每次被修改时递增该版本号。 该版本号用于防止两个不同用户同时更新该实体:用户 U1 和 U2 读取实体 E 时,其版本号均为 V1。 U1 修改了 E 并将该修改保存到数据库中:此时版本号变为 V1+1。 U2 随后修改 E 并将该修改保存到数据库中:它将引发异常,因为其版本号(V1)与数据库中的版本号(V1+1)不一致;
  • 第 35-52 行:重定义方法 [hashCode] 和 [equals]。 默认情况下,若 [obj1==obj2] 为 true(即 ob1 和 obj2 是两个相等的指针),则 [obj1.equals(obj2)] 也为 true。 如果要比较被指针指向的对象而非指针本身,则需重定义方法 [equals] 和 [hashCode]。后者对于两个被方法 [equals] 判定为相等的对象,应返回相同的值;
  • 第 42-51 行:两个 [AbstractEntity] 类型或其派生类型的对象,若其主键 [id] 相等,则被视为相等;
  • 第 35-38 行:方法 [hashCode] 确实为两个相同的 [AbstractEntity] 对象返回相同的值,因此它们具有相同的主键 [id];
  • 第 55-63 行:方法 [toString] 返回对象 [this] 的字符串 jSon。 如果该对象指向一个子类,则该方法将返回子类的字符串 jSON。这使我们无需在子类中创建 [toString] 方法;

11.3.5.2. 实体 JPA [Produit]

类 [Produit] 是实体 JPA,该实体与表 [PRODUITS] 中的某一行相关联:

 

package spring.data.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFilter;

@Entity
@Table(name = "PRODUITS")
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {

    // 属性
    @Column(name = "NOM")
    private String nom;

    @Column(name = "CATEGORIE_ID", insertable = false, updatable = false)    
    private Long idCategorie;

    @Column(name = "PRIX")
    private double prix;

    @Column(name = "DESCRIPTION")
    private String description;

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

    // 构造函数
    public Produit() {

    }

    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }

    // 获取器和设置器
...
}
  • 第 12 行:注释 [@Entity] 将类 [Produit] 设为由层 [JPA] 管理的实体;
  • 第 13 行:注释 [@Table(name = "PRODUITS")] 表示类 [Produit] 是数据库中表 [PRODUITS] 某行对应的对象;
  • 第 14 行:要应用于该实体的过滤器名称 jSON。我们将看到,第 13 行中的属性 [categorie] 并非总是可用。 此时需将其从对象的 jSON 表示中排除。为此我们需要一个过滤器。因此,我们将通过名为 [jsonFilterCategorie] 的过滤器来指定是否包含属性 [categorie];
  • 第 18 行:注释 [@Column] 将字段 [nom] 与表 [PRODUITS] 中的列 [NOM] 关联起来。 当字段名称与关联列名称相同时,可省略注释 [@Column]。本例即属此情况;
  • 第 31-33 行:产品类别;
  • 第 31 行:注释 [@ManyToOne] 表示第 32 行注释 [@JoinColumn(name = "CATEGORIE_ID")] 的列是[Produit] 实体的注释列,该实体关联于第 33 行中的实体。该注释应标注 JPA 实体。 因此,第 33 行中的类必须是实体 JPA;
  • 第31行:注释[fetch = FetchType.LAZY]要求,当从表[PRODUITS]中检索产品时,其类别(第33行)不会立即被检索(延迟加载)。 该属性将在首次调用方法 [getCategorie] 时获取。此属性不具强制性。 所使用的 JPA 实现有权忽略该属性。正因为 [categorie] 属性可能存在也可能不存在,我们才在第 14 行引入了 jSON 过滤器。 现有的 JPA 实现(Hibernate、Eclipselink、OpenJPA)对该注解的处理方式并不一致。 Hibernate 通过调用 SGBD 来获取类别,从而增强了最初的 [getCategorie] 方法(该方法仅返回 categorie 字段)。 为了实现这一点,最初用于获取产品的 SGBD 连接必须保持打开状态,否则会引发异常。

11.3.5.3. 实体 JPA [Categorie]

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

 

其代码如下:


package spring.data.entities;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFilter;

@Entity
@Table(name = "CATEGORIES")
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {

    // 属性
    @Column(name = "NOM")
    private String nom;

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

    // 构造函数
    public Categorie() {

    }

    public Categorie(String nom) {
        this.nom = nom;
    }

    // 方法
    public void addProduit(Produit produit) {
        // 添加产品
        produits.add(produit);
        // 设置其类别
        produit.setCategorie(this);
    }

    // 获取器和设置器
...
}
  • 第 21-22 行:类别名称;
  • 第25-26行:该类别的商品;
  • 第25行:注释[@OneToMany]是我们在实体[Produit]中遇到的关系[@ManyToOne]的反向关系。 属性 [mappedBy = "categorie"] 指明实体 [Produit] 中被反向关系 [@ManyToOne] 注解的字段。 属性 [cascade = { CascadeType.ALL }] 要求对 @Entity [Categorie] 执行的操作(persist、merge、remove)应级联到第 26 行中的 [produits]。 可以使用常量 [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE] 指定部分级联;
  • 第25行:属性[fetch = FetchType.LAZY]要求,当从表[CATEGORIES]中检索某类别时,其关联产品不会立即被检索。这些产品将在首次调用方法[getProduits]时被检索。 现有的 JPA 实现(Hibernate、Eclipselink、OpenJPA)对该注解的处理方式并不一致。 Hibernate 通过调用 SGBD 方法来检索该类别的商品,从而增强了最初的 [getProduits] 方法(该方法仅返回 produits 字段)。 要实现这一点,最初用于获取类别的 SGBD 连接必须仍处于打开状态。此属性具有约束力。 JPA 实现无法忽略该属性。由于 [produits] 属性可能已初始化也可能未初始化,因此我们在第 17 行引入了 jSON 过滤器,以便指定是否需要该属性;
  • 第26行:类型[Set]是一个接口。类型[HashSet]是实现该接口的类。它实现了一个名为ensemble的元素集合。 集合中不能包含两个相同的对象。此处的对象类型为 [Produit]。因此,在集合中不可能存在两个相同的对象。 由于父类 [AbstractEntity] 的方法 [equals] 已被重写,规定如果两个产品具有相同的主键,则视为相同,因此字段 [produits] 不能包含两个主键相同的产品;
  • 第 38-43 行:方法 [addProduit] 用于将产品添加到类别中;

11.3.6. [Spring Data] 层

[DAO]

[console]

图层

[JPA]

夹克

[JDBC]

连体衣

[Spring Data]

Spring 4

SGBD

  

接口 [CategoriesRepository] 管理对表 [CATEGORIES] 的访问:


package spring.data.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.data.entities.Categorie;

public interface CategoriesRepository extends CrudRepository<Categorie, Long> {

    // 包含产品的分类
    @Query("select c from Categorie c left join fetch c.produits p where c.id=?1")
    public Categorie getCategorieByIdWithProduits(Long id);

    @Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")
    public Categorie getCategorieByNameWithProduits(String nom);

    // 仅按名称显示的无商品分类
    public Categorie findByNom(String nom);
}
  • 第 8 行:接口 [CrudRepository] 已在第 11.2.3 节中使用并解释。需要提醒的是:
    • 该接口的第一个类型是实体 JPA,用于管理对 CRUD(findOne、findAll、 save、delete、deleteAll) 而管理;
    • 第二种类型是实体 JPA 的主键类型,此处为整数 [Long];
  • 第 12 行:第 12 行中的方法由第 11 行中的查询 JPQL(Java 持久化查询语言)实现。该查询请求实体 JPA。在此类查询中:
    • 表被替换为与其关联的 JPA 实体;
    • 列被替换为查询中使用的 JPA 实体的字段;
  • 第 11 行:查询 JPQL 返回了一个类别及其产品。需要提醒的是,在实体 [Categorie] 中,字段 [produits] 具有属性 [fetch = FetchType.LAZY] (延迟加载)。在查询 JPQL 中,我们使用关键字 [fetch] 强制加载产品。 查询中的参数 ?1 在执行时将被替换为第 12 行方法的第一个参数值,即参数 [Long id];
  • 第14-15行:针对通过名称标识的类别,采用类似的方法;
  • 第 18 行:由于类型 [Category] 包含字段 [nom],因此方法 [findByNom] 将由 [Spring Data] 自动实现;

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


package spring.data.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.data.entities.Produit;

public interface ProduitsRepository extends CrudRepository<Produit, Long> {

    // 带分类的商品
    @Query("select p from Produit p left join fetch p.categorie c where p.id=?1")
    public Produit getProduitByIdWithCategorie(Long id);

    @Query("select p from Produit p left join fetch p.categorie c where p.nom=?1")
    public Produit getProduitByNameWithCategorie(String nom);

    // 通过名称指定、不带所属类别的商品
    public Produit findByNom(String nom);
}

说明与接口 [CategoriesRepository] 相同。

这些接口将在项目运行时由 [Spring Data] 生成的类进行实现。 此类类被称为 [proxy]。默认情况下,实现类的方法在事务中执行。由于这些接口继承了 [CrudRepository] 类,因此它们是 Spring 组件。

11.3.7. [DAO] 层

[DAO]

[console]

图层

[JPA]

飞行员夹克

[JDBC]

夹克

[Spring Data]

Spring 4

SGBD

  

[DAO] 层的 [IDao] 接口如下:


package spring.data.dao;

import java.util.List;

import spring.data.entities.Categorie;
import spring.data.entities.Produit;

public interface IDao {

    // 插入产品列表
    public List<Produit> addProduits(List<Produit> produits);

    // 删除所有产品
    public void deleteAllProduits();

    // 更新产品列表
    public List<Produit> updateProduits(List<Produit> produits);

    // 获取所有产品
    public List<Produit> getAllProduits();

    // 插入分类列表
    public List<Categorie> addCategories(List<Categorie> categories);

    // 删除所有分类
    public void deleteAllCategories();

    // 更新分类列表
    public List<Categorie> updateCategories(List<Categorie> categories);

    // 获取所有分类
    public List<Categorie> getAllCategories();

    // 查询特定产品(可选是否包含其所属类别)
    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // 获取特定分类及其下是否包含商品
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);
}

此处采用的规则是:任何修改其输入参数对象的方法,都必须在返回结果中包含这些对象。该规则的理由已在第 4.2 节中解释:它允许一个层及其客户端位于两个独立的 JVM 中,从而实现客户端/服务器模式。

该接口的实现如下:


package spring.data.dao;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.common.collect.Lists;

import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;

@Component
public class Dao implements IDao {

    @Autowired
    private ProduitsRepository produitsRepository;

    @Autowired
    private CategoriesRepository categoriesRepository;

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(101, getMessagesForException(e));
        }
    }

    @Override
    public void deleteAllProduits() {
        try {
            produitsRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(102, getMessagesForException(e));
        }
    }

    @Override
    public List<Produit> updateProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(103, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(104, getMessagesForException(e));
        }
    }

    @Override
    public void deleteAllCategories() {
        try {
            categoriesRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(105, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> updateCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(106, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> getAllCategories() {
        try {
            return Lists.newArrayList(categoriesRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(107, getMessagesForException(e));
        }
    }

    @Override
    public List<Produit> getAllProduits() {
        try {
            return Lists.newArrayList(produitsRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(108, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByIdWithCategorie(Long idProduit) {
        try {
            return produitsRepository.getProduitByIdWithCategorie(idProduit);
        } catch (Exception e) {
            throw new DaoException(109, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByIdWithProduits(Long idCategorie) {
        try {
            return categoriesRepository.getCategorieByIdWithProduits(idCategorie);
        } catch (Exception e) {
            throw new DaoException(110, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        try {
            return categoriesRepository.getCategorieByNameWithProduits(nom);
        } catch (Exception e) {
            throw new DaoException(111, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByNameWithCategorie(String nom) {
        try {
            return produitsRepository.getProduitByNameWithCategorie(nom);
        } catch (Exception e) {
            throw new DaoException(112, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByIdWithoutCategorie(Long idProduit) {
        try {
            return produitsRepository.findOne(idProduit);
        } catch (Exception e) {
            throw new DaoException(113, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
        try {
            return categoriesRepository.findOne(idCategorie);
        } catch (Exception e) {
            throw new DaoException(114, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByNameWithoutCategorie(String nom) {
        try {
            return produitsRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(115, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByNameWithoutProduits(String nom) {
        try {
            return categoriesRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(116, getMessagesForException(e));
        }
    }

}
  • 第 16 行:注解 [@Component] 将类 [Dao] 定义为 Spring 组件;
  • 第19-23行:将引用注入到两个接口[CrudRepository]和[Spring Data]中。此注入将在Spring对象实例化时进行,通常发生在Spring项目开始运行时;
  • 请注意第28行和第46行,接口[produitsRepository]中的方法[save]既用于产品的插入,也用于产品的更新。 [Spring Data] 通过产品的主键来判断是插入还是更新。如果主键值为 [null],则执行插入操作;否则执行更新操作;
  • 第 82 行:使用 Guava 库中的 [Lists.newArrayList] 方法获取产品列表。[produitsRepository.findAll()] 方法返回 [Iterable<Produit>] 类型;
  • 第28行:方法[produitsRepository.save(produits)]返回类型[Iterable<Produit>]。该类中的其他[save]操作也是如此;

在上述 [Dao] 类中,可能发生的异常被封装在以下 [DaoException] 类型中:


package spring.data.dao;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

// “Elections”应用程序的异常类
// 异常未被捕获

public class DaoException extends RuntimeException implements Serializable {

    // 序列号 ID
    private static final long serialVersionUID = 1L;

    // 本地字段
    private int code;
    private List<String> erreurs;

    // 构造函数
    public DaoException() {
        super();
    }

    public DaoException(int code, Throwable e) {
        // 父类
        super(e);
        // 本地
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public DaoException(int code, String message, Throwable e) {
        // 父级
        super(message, e);
        // 本地
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public DaoException(int code, String message) {
        // 父级
        super(message);
        // 本地
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public DaoException(int code, List<String> erreurs) {
        // 父级
        super();
        // 本地
        this.code = code;
        this.erreurs = erreurs;
    }

    // 异常的错误消息列表
    private List<String> getErreursForException(Throwable th) {
        // 获取异常的错误消息列表
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // 仅当消息不为空且不为空字符串时才获取消息
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // 下一个原因
            cause = cause.getCause();
        }
        return erreurs;
    }

    // getter 和 setter
...
}
  • 第 10 行:该类继承自 [RuntimeException] 类,因此属于未受控异常;
  • 第 16 行:一个错误代码;
  • 第 17 行:错误消息列表,这些消息与引发 [DaoException] 的异常堆栈相关;
  • 第59-76行:私有方法[getMessagesForException]用于获取与异常堆栈中异常相关的错误消息列表。实际上,可以通过Exception类的以下构造函数将异常堆叠起来:
    • Exception(String message, Throwable cause):创建一个包含指定消息及待封装异常的异常;
    • Exception(Throwable cause):创建一个包含要封装的异常的异常;

类型 [Throwable] 是类 [Exception] 的父类。如果反复调用上述构造函数,最终的异常将包含多个异常。这被称为异常堆栈。

  • 通过表达式 [e1.getCause()] 可获取异常 e1 的最终原因;
  • 通过表达式 [e1.getCause().getCause()] 可获得异常 e1 的倒数第二个原因;
  • 以此类推,直至得到 [getCause()==null];

11.3.8. Spring 项目配置

  

类 [DaoConfig] 配置了层 [DAO]:


package spring.data.config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.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;

@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
public class DaoConfig {

    // 常量
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroSpringData";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    final static String[] ENTITIES_PACKAGES = { "spring.data.entities" };

    // 数据源[tomcat-jdbc]
    @Bean
    public DataSource dataSource() {
        // 数据源 TomcatJdbc
        DataSource dataSource = new DataSource();
        // 访问配置 JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // 最初建立的连接
        dataSource.setInitialSize(1);
        // 结果
        return dataSource;
    }

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

    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    // 事务管理器
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}

11.2.5 节中已遇到并解释过类似的配置。我们在其中添加了以下 Spring 注解:

  • 第 17 行:注解 [@EnableJpaRepositories] 用于指定包含 [CrudRepository] 和 [Spring Data] 接口的包;
  • 第 18 行:该类是 Spring 配置类。此信息至关重要。若将其移除,项目仍可运行。但在文档后续部分,当构建基于此项目的子项目时,若移除第 18 行的注解,部分子项目将无法运行;
  • 第19行:注解[@ComponentScan]指明了Spring对象所在的包。这些是被[@Component, @Service, @Controller, ...]注解的类。在此处,Spring组件[Dao]将被找到并实例化;
  • 第73-76行:我们定义了一个Bean,该Bean表示用于查找JPA实体的待扫描包数组。 这将允许导入 [DaoConfig] 类的项目重新定义该 Bean,从而更改第 59 行中扫描的包。在文档的后续部分,我们将遇到这一问题;

类 [AppConfig] 配置了整个项目:


package spring.data.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@Configuration
@Import({DaoConfig.class})
public class AppConfig {
    // 过滤器 jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // 映射器 jSON
        ObjectMapper mapper = new ObjectMapper();
        // 过滤器
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // 结果
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // 映射器 jSON
        ObjectMapper mapper = new ObjectMapper();
        // 过滤器
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // 结果
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // 映射器 jSON
        ObjectMapper mapper = new ObjectMapper();
        // 过滤器
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // 结果
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // 映射器 jSON
        ObjectMapper mapper = new ObjectMapper();
        // 过滤器
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // 结果
        return mapper;
    }
}
  • 第 11 行:该类是 Spring 配置类;
  • 第 12 行:该类导入了我们刚才看到的 [DaoConfig] 类所定义的 Bean;
  • [console] 层使用 jSON 映射器,这些映射器在此处定义;
  • 第 14-64 行:定义了五个 jSON 映射器;
  • 第15-18行:过滤器jSON [jsonMapper]没有子过滤器;
  • 第20-30行:过滤器jSON [jsonMapperCategorieWithProduits]用于对[Categorie]对象及其派生对象进行序列化/反序列化;
  • 第 32-42 行:过滤器 jSON [jsonMapperProduitWithCategorie] 用于对 [Produit] 对象及其类别进行序列化/反序列化;
  • 第 43-53 行:过滤器 jSON [jsonMapperCategorieWithoutProduits] 用于对 [Categorie] 对象(不含其产品进行序列化/反序列化;
  • 第 55-64 行:过滤器 jSON [jsonMapperProduitWithoutCategorie] 用于序列化/反序列化不包含其类别的 [Produit] 对象;

需要注意的是,当为实体 T 构建过滤器 jSON 时,不仅需要配置实体 T 的过滤器,还需配置其可能包含的实体 Ti 的过滤器。

11.3.9. [console] 层

[DAO]

[console]

图层

[JPA]

夹克

[JDBC]

连体衣

[Spring Data]

Spring 4

SGBD

  

类 [Main] 如下:


package spring.data.console;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

public class Main {

    public static void main(String[] args) throws JsonProcessingException {
        AnnotationConfigApplicationContext context = null;
        try {
            // Spring 上下文实例化
            context = new AnnotationConfigApplicationContext(AppConfig.class);
            ObjectMapper jsonMapperCategorieWithProduits = context.getBean("jsonMapperCategorieWithProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithCategorie = context.getBean("jsonMapperProduitWithCategorie",
                    ObjectMapper.class);
            ObjectMapper jsonMapperCategorieWithoutProduits = context.getBean("jsonMapperCategorieWithoutProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithoutCategorie = context.getBean("jsonMapperProduitWithoutCategorie",
                    ObjectMapper.class);
            IDao dao = context.getBean(IDao.class);
            // --------------------------------------------------------------------------------------
            // 清空数据库
            log("Vidage de la base de données", 1);
            // 清空表 [CATEGORIES] - 通过级联操作,表 [PRODUITS] 将被清空
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // 填充表
            List<Categorie> categories = new ArrayList<Categorie>();
            for (int i = 0; i < 2; i++) {
                Categorie categorie = new Categorie(String.format("categorie%d", i));
                for (int j = 0; j < 5; j++) {
                    categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                            String.format("desc%d%d", i, j)));
                }
                categories.add(categorie);
            }
            // 添加分类 - 通过级联操作,产品也将被插入
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // 分类列表
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // 产品列表
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // 第1类及其产品
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // 产品 [produit14] 及其所属类别
            Produit p = dao.getProduitByNameWithCategorie("produit14");
            log("Produit [produit14] avec sa catégorie", 2);
            affiche(p, jsonMapperProduitWithCategorie);
            // --------------------------------------------------------------------------------------
            log("Mise à jour du prix des produits de [categorie1]", 1);
            log("Produits de la catégorie [categorie1] avant la mise à jour", 2);
            Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
            Set<Produit> produits = categorie1.getProduits();
            affiche(categorie1, jsonMapperCategorieWithProduits);
            for (Produit produit : produits) {
                produit.setPrix(1.1 * produit.getPrix());
            }
            dao.updateProduits(Lists.newArrayList(produits));
            log("Produits de la catégorie [categorie1] après la mise à jour", 2);
            affiche(dao.getCategorieByNameWithProduits("categorie1"), jsonMapperCategorieWithProduits);
            // --------------------------------------------------------------------------------------
            log("Vidage de la base de données", 1);
            // 清空表 [CATEGORIES] - 随后表 [PRODUITS] 将被清空
            dao.deleteAllCategories();
            // 显示数据库
            log("Liste des categories avant l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits avant l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
            // 执行插入操作
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // 添加类别 - 通过级联,产品也将被插入
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // 验证
            log("Liste des categories après l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits après l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
        } catch (DaoException e) {
            System.out.println(e);
        } finally {
            if (context != null) {
                // 完成
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }

    // 显示类型为 T 的元素
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // 显示 T 类型元素列表
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static 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);
    }
}
  • 第25行:根据配置类[AppConfig]实例化Spring Bean;
  • 第26-33行:获取映射器jSON的引用。使用[ApplicationContext].getBean方法的以下签名:
    • [ApplicationContext].getBean(String id, Class 类名):当存在多个类型为 [classe] 的 Bean 时使用。此时需指定所需 Bean 的标识符。 如果该 Bean 是通过 [@Bean] 注解定义的,其标识符即为被注解方法的名称。如果通过 [@Bean(« identifiant »] 注解定义,其标识符即为注解中指定的值;
  • 第34行:获取[DAO]层的引用;
  • 第37-39行:清空数据库。清空类别表(第39行)。因为我们写了:

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();

当删除一个类别时,与其关联的所有产品也会被删除;

  • 第43-53行:向表中插入两个类别,每个类别包含5个产品。第50行,插入这两个类别将同时触发其关联产品的插入,这同样是因为我们写了 [cascade = { CascadeType.ALL }];
  • 第58行:显示分类。使用映射器 jSON [jsonMapperCategorieWithoutProduits] 来显示不含产品的分类。实际上,方法 [dao.getAllCategories()] 会使分类不包含其产品(延迟加载);
  • 第 61 行:显示不包含所属分类的商品。因为方法 [dao.getAllProduits()] 会返回不包含所属分类的商品(延迟加载);
  • 第 63-65 行:显示名称为 [categorie1] 的分类及其商品(立即加载);
  • 第67-69行:显示一个商品及其所属类别;
  • 第 71-81 行:将类别 [categorie1] 中所有产品的价格上调 10%;
  • 第 91-101 行:添加一个包含两个同名产品的类别。但在表 [PRODUITS] 中,列 [NOM] 存在唯一性约束。因此,第二个产品的插入将被拒绝并抛出异常。 而方法 [dao.addProduits] 在事务中执行。因此,第二次插入失败也应导致第一个产品的插入以及其所属类别 [cat1] 的插入被撤销。这就是我们要验证的内容;
  • 第 119-121 行:一个通用的方法,能够显示任何类型 T 元素的字符串 jSON。序列化 jSON 由作为参数传递的映射器控制;
  • 第 124-128 行:一个类似的方法,这次针对类型为 T 的元素列表;

执行 [Main] 类将产生以下结果(不包括 Spring 日志):


Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Affichage de la base --------------------------------
-- Liste des catégories
{"id":4,"version":0,"nom":"categorie0"}
{"id":5,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":13,"version":0,"nom":"produit00","idCategorie":4,"prix":100.0,"description":"desc00"}
{"id":14,"version":0,"nom":"produit01","idCategorie":4,"prix":101.0,"description":"desc01"}
{"id":15,"version":0,"nom":"produit02","idCategorie":4,"prix":102.0,"description":"desc02"}
{"id":16,"version":0,"nom":"produit03","idCategorie":4,"prix":103.0,"description":"desc03"}
{"id":17,"version":0,"nom":"produit04","idCategorie":4,"prix":104.0,"description":"desc04"}
{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"}
{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"}
{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"}
{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"}
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}
-- Catégorie 1 avec ses produits
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produit [produit14] avec sa catégorie
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14","categorie":{"id":5,"version":0,"nom":"categorie1"}}
Mise à jour du prix des produits de [categorie1] --------------------------------
-- Produits de la catégorie [categorie1] avant la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produits de la catégorie [categorie1] après la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":1,"nom":"produit10","idCategorie":5,"prix":121.0,"description":"desc10"},{"id":19,"version":1,"nom":"produit11","idCategorie":5,"prix":122.1,"description":"desc11"},{"id":20,"version":1,"nom":"produit12","idCategorie":5,"prix":123.2,"description":"desc12"},{"id":21,"version":1,"nom":"produit13","idCategorie":5,"prix":124.3,"description":"desc13"},{"id":22,"version":1,"nom":"produit14","idCategorie":5,"prix":125.4,"description":"desc14"}]}
Vidage de la base de données --------------------------------
-- Liste des categories avant l'ajout
-- Liste des produits avant l'ajout
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites : 
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
-- Liste des categories après l'ajout
-- Liste des produits après l'ajout
Travail terminé
  • 第 4-17 行:插入到表中的分类和产品;
  • 第 18-19 行:一个类别及其所属产品;
  • 第 20-21 行:一个产品及其所属类别;
  • 第22-26行:更新了部分产品的价格。第24行可见,价格确实上涨了10%;
  • 第27-36行:添加了类别[cat1]及两个同名产品。可见表在添加前(第28-29行)与添加后(第35-36行)内容一致,这表明该事务中的所有插入操作均已被撤销;
  • 第31-34行:在插入第二个产品时发生的异常,导致整个事务失败;

11.3.10. 单元测试 JUnit

  

类 [Test01] 如下:


package spring.data.tests;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // 图层 [DAO]
    @Autowired
    private IDao dao;

    // 过滤器 jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;

    @Before
    public void cleanAndFill() {
        // 每次测试前清理数据库
        log("Vidage de la base de données", 1);
        // 清空表 [CATEGORIES] - 由此引发连锁反应,表 [PRODUITS] 将被清空
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // 填充表
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // 添加分类 - 通过级联,产品也将被插入
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // 类别列表
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // 产品列表
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // 进行一些验证
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }

    @Test
    public void getCategorieByNameWithProduits() {
        log("getCategorieByNameWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals(5, categorie1.getProduits().size());
    }

    @Test
    public void getCategorieByNameWithoutProduits() {
        log("getCategorieByNameWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals("categorie1", categorie1.getNom());
    }

    @Test
    public void getProduitByIdWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
        Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
    }

    @Test
    public void getProduitByIdWithoutCategorie() {
        log("getProduitByIdWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
    }
...
    // -------------- 私有方法
    private Produit findProduitByName(String nom, List<Produit> produits) {
        for (Produit produit : produits) {
            if (produit.getNom().equals(nom)) {
                return produit;
            }
        }
        return null;
    }

    private Categorie findCategorieByName(String nom, List<Categorie> categories) {
        for (Categorie categorie : categories) {
            if (categorie.getNom().equals(nom)) {
                return categorie;
            }
        }
        return null;
    }

    // 显示 T 类型的元素
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // 显示 T 类型元素列表
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static 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);
    }

    private static void show(String title, List<String> messages) {
        // 标题
        System.out.println(String.format("%s : ", title));
        // 消息
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}
  • 第 27 行:单元测试由第 11.3.8 节中已介绍的类 [AppConfig] 配置;
  • 第32-33行:向[DAO]层注入一个引用;
  • 第36-50行:注入五个映射器jSON;
  • 第60-71行:清空数据库(第57行)后,向数据库中填充2个类别,每个类别包含5个产品。由于第52行的注释[@Before],该方法在每次测试前都会执行;
  • 第75-93行:显示数据库内容;
  • 第95-101行:查询某类及其所属产品,该类通过名称进行标识;
  • 第103-109行:查询某类别的所有产品(不显示产品),该类别通过名称进行标识;
  • 第111-120行:查询包含所属类别的商品,商品通过编号识别;
  • 第122-130行:查询不包含所属类别的商品,商品通过编号识别;
  • 第133-184行:各测试共用的私有方法;

待完成任务:运行该测试。测试应成功通过。


11.3.11. 日志管理

控制台应用程序或测试 JUnit 的日志由以下文件 [logback.xml] 配置:

  

该文件应命名为 [logback.xml],并位于项目的类路径中。为此,它已被放置在 [src/main/resources] 文件夹中,该文件夹属于类路径的一部分。其内容如下:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- 编码器默认被分配为该类型
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- 日志级别控制 -->
  <root level="info"> <!-- info、debug、warn -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • 第 12 行:[<root level="info">] 标签用于显示 [info] 级别的日志。在 [info] 处,可以替换为:
    • [debug]:这是日志级别中最详细的一种。建议在项目调试阶段使用,因为其中包含关于客户端/服务器交互的非常有价值的日志。这是了解“幕后”发生情况的一种方式;
    • [off]:完全不生成日志;
    • [warn]:这是一个中间日志级别,Spring 会在此级别显示异常,但这些异常并不一定代表错误。如果未获得预期结果,则需要查看这些日志;

待办事项:将第 12 行处的日志级别改为 [debug],然后运行单元测试。观察日志内容的差异。


11.3.12. 生成项目的 Maven 归档

要将项目归档文件安装到本地 Maven 仓库,请按以下步骤操作 [1-3]:

将使用文件[pom.xml]中找到的标识符生成归档文件:


    <groupId>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

Maven本地仓库的位置可在Eclipse配置中找到:

 

随后可验证 Maven 构建产物是否安装正确:

 

现在,另一个本地 Maven 项目可以使用该归档文件。