Skip to content

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

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

我们将使用 Spring 生态系统中的组件 [Spring Data] 来实现作业中的 [DAO] 层。[Spring Data] 依赖于 JPA(Java Persistence API)层,该层允许 [DAO] 层操作对象而非 SQL 语句。最终,[DAO] 层并不知道它正在与数据库交互,它只了解 [Spring Data] 层的接口。

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

11.1. 支持

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

11.2. 示例 1

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>
        <!-- use UTF-8 for everything -->
        <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 行:定义对 H2 数据库管理系统 (DBMS) 的依赖,它允许您创建和管理内存数据库。

让我们来看看这些依赖项提供的类:

数量众多:

  • 其中一部分属于 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 实现将使用所用数据库管理系统(此处为 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 行)。它继承了 [CrudRepository] 接口,这是一个 Spring Data 接口(第 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 实体持久化到数据库中。它使用 DBMS 分配的主键将实体持久化。它还允许更新由主键 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. [控制台] 层

  

[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 {
        // save a couple of customers
        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"));
 
        // fetch all customers
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();
 
        // fetch an individual customer by ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();
 
        // fetch customers by last name
        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] 根据各种属性(特别是项目类路径中的内容)自动创建若干 Bean。由于 Hibernate 库位于类路径中,因此 [entityManagerFactory] Bean 将使用 Hibernate 实现。由于 H2 DBMS 库位于类路径中,因此 [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 行:[entityManagerFactory] Bean 通过 Spring 类 [LocalContainerEntityManagerFactory] 实现;
  • 第 12 行:出现了 [Hibernate]。这是所选的 JPA 实现;
  • 第 19 行:Hibernate 方言是与数据库管理系统 (DBMS) 配合使用的 SQL 变体。此处的 [H2Dialect] 方言表明 Hibernate 将与 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 Database -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>
 
    <properties>
        <!-- use UTF-8 for everything -->
        <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 行: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) {
        // instantiation Spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);
 
        // save a couple of customers
        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"));
 
        ...
 
        // closing context
        context.close();
    }
}
  • 第 9 行:[Application] 类不再包含任何配置注解;
  • 第 3–7 行:请注意,这里不再包含任何 [Spring Boot] 包的导入;
  • 第 12 行:我们实例化 Spring Bean。我们获取 Spring 上下文,其中包含对已创建 Bean 的引用;
  • 第 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 database
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        // an initially open connection
        dataSource.setInitialSize(1);
        // result
        return dataSource;
    }
 
    // the provider 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();
    }
 
    // Transaction manager
    @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 组件是指带有 @Service@Component@Controller 等 Spring 注解的类。此处除了 [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 应用程序]
  • 在 [3] 中:指定要运行的项目(使用“浏览”按钮);
  • 在 [4] 中:指定要运行的类;
  • 在 [5] 中:运行配置的名称——可以是任意名称;
  • 在 [6] 中:导出项目;
  • 在 [7] 中:作为可执行 JAR 归档文件;
  • 在 [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 for further details.
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. 简介

我们将重新审视之前用于介绍 JDBC API 的产品表示例,并构建以下架构:

[dbintrospringjpa] 数据库包含两个表:[PRODUCTS] 和 [CATEGORIES]。[CATEGORIES] 表的结构如下:

 
  • [ID]:主键,采用 AUTO_INCREMENT 模式;
  • [VERSION]: 记录版本号;
  • [NAME]: 类别名称 - 唯一;

[PRODUCTS] 表如下所示:

 
  • [ID]:主键,采用 AUTO_INCREMENT 模式;
  • [VERSION]: 记录版本号;
  • [NAME]: 产品名称 - 唯一;
  • [CATEGORY_ID]:分类 ID——[CATEGORIES.ID] 字段的外键;
  • [PRICE]: 其价格;
  • [DESCRIPTION]: 产品描述;

任务:使用支持材料中的 [dbintrospringdata.sql] SQL 脚本创建 [dbintrospringdata] 数据库:


11.3.2. 创建 Maven 项目

要创建 Spring Data 项目模板,请按照以下步骤操作:

  • 在 [1] 中,创建一个新项目;
  • 在 [2] 中,选择 [Spring Starter Project] 类型;
  • 生成的项目将是一个 Maven 项目。在 [3] 中,指定项目组名称;
  • 在 [4] 中,指定项目构建时将生成的工件(此处为 JAR 文件)的名称;
  • 在 [5] 中:Eclipse 项目名称——可以是任意名称(不必与 [4] 相同);
  • 在 [7] 中:指定您正在创建一个使用 MySQL 数据库管理系统(DBMS)并包含 [JPA] 层的项目。此类项目所需的依赖项随后将被包含在 [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/> <!-- lookup parent from repository -->
    </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 行:集成 Spring 的 JUnit 测试所需的依赖项;

可执行类 [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]:[Product] 和 [Category] 类的父类;
  • [Product.java]:与数据库中 [PRODUCTS] 表中某行对应的类;
  • [Category.java]:与数据库中 [CATEGORIES] 表中某行对应的类;
  • [ProductsRepository]:用于访问 [PRODUCTS] 表的 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 Database -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!-- library 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 Test -->
        <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>
        <!-- log library -->
        <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 库,用于 [Product] 类的 [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 4DBMS

  

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 {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;
 
    // manufacturers
    public AbstractEntity() {
 
    }
 
    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }
 
    // redefine [equals] and [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();
    }
 
    // signature jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
 
    // getters and setters
....
}
 

本类的目的是为 JPA 实体提供一个父类,通过将 [Product] 和 [Category] 实体(与数据库相关联)共有的 [id, version] 属性(第 19、22 行)封装在单一位置来实现。这些属性与表中的 [ID, VERSION] 列相关联(第 18、21 行)。

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

11.3.5.2. JPA 实体 [Product]

[Product] 类是一个与 [PRODUCTS] 表中某行关联的 JPA 实体:

 

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 {
 
    // properties
    @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;
 
    // the category
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;
 
    // manufacturers
    public Produit() {
 
    }
 
    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }
 
    // getters and setters
...
}
  • 第 12 行:[@Entity] 注解使 [Product] 类成为由 [JPA] 层管理的实体;
  • 第 13 行:注解 [@Table(name = "PRODUCTS")] 表示 [Product] 类代表数据库中 [PRODUCTS] 表的一行;
  • 第 14 行:要应用于该实体的 JSON 过滤器的名称。我们将看到,第 13 行中的 [categorie] 属性并非总是可用。因此,必须将其从对象的 JSON 表示中排除。为此,我们需要一个过滤器。因此,我们将通过一个名为 [jsonFilterCategorie] 的过滤器来指定是否包含 [categorie] 属性;
  • 第 18 行:[@Column] 注解将 [nom] 字段与 [PRODUITS] 表中的 [NOM] 列关联起来。当字段名称与关联列名称相同时,可以省略 [@Column] 注解。本例即属于这种情况;
  • 第 31–33 行:产品类别;
  • 第 31 行:[@ManyToOne] 注解表明,第 32 行注解 [@JoinColumn(name = "CATEGORIE_ID")] 所引用的列是 [Product] 实体的 [PRODUCTS] 表到第 33 行实体关联的 [CATEGORIES] 表的外键。此注解必须应用于 JPA 实体。 因此,第 33 行的类必须是 JPA 实体;
  • 第 31 行:注解 [fetch = FetchType.LAZY] 指定从 [PRODUCTS] 表检索产品时,其类别(第 33 行)不会立即被检索(延迟加载)。该类别将在首次调用 [getCategory] 方法时获取。此属性并非强制要求。 所使用的 JPA 实现可以忽略该注解。正因为 [category] 属性可能存在也可能不存在,我们才在第 14 行引入了 JSON 过滤器。现有的 JPA 实现(Hibernate、Eclipselink、OpenJPA)对该注解的处理方式并不一致。 Hibernate 通过向 DBMS 发起查询来检索类别,从而增强了初始的 [getCategory] 方法(该方法原本仅返回 category 字段)。要使此功能正常工作,最初用于检索产品的 DBMS 连接必须保持打开状态;否则将引发异常。

11.3.5.3. JPA 实体 [Category]

[Category] 类是一个与 [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 {
 
    // properties
    @Column(name = "NOM")
    private String nom;
 
    // related products
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();
 
    // manufacturers
    public Categorie() {
 
    }
 
    public Categorie(String nom) {
        this.nom = nom;
    }
 
    // methods
    public void addProduit(Produit produit) {
        // we add the product
        produits.add(produit);
        // set your category
        produit.setCategorie(this);
    }
 
    // getters and setters
...
}
  • 第 21-22 行:类别名称;
  • 第 25-26 行:该类别的商品;
  • 第 25 行:[@OneToMany] 注解是我们在 [Product] 实体中遇到的 [@ManyToOne] 关系的逆向关系。属性 [mappedBy = "category"] 指定了 [Product] 实体中被逆向 [@ManyToOne] 关系注解的字段,该字段位于 中。 属性 [cascade = { CascadeType.ALL }] 指定对 @Entity [Category] 执行的操作(persist、merge、remove)应级联到第 26 行的 [products]。可使用常量 [CascadeType.PERSISTCascadeType.MERGECascadeType.REMOVE] 指定部分级联;
  • 第 25 行:[fetch = FetchType.LAZY] 属性指定,当从 [CATEGORIES] 表中检索类别时,其关联的产品不会立即被检索。这些产品将在首次调用 [getProduits] 方法时被检索。现有的 JPA 实现(Hibernate、Eclipselink、OpenJPA)对该注解的处理方式并不一致。 Hibernate 通过向 DBMS 发起调用以获取该类别的商品,从而增强了初始的 [getProduits] 方法(该方法原本仅返回 products 字段)。要实现这一点,最初用于检索该类别的 DBMS 连接必须保持打开状态。此属性是必填的。 JPA 实现无法忽略该属性。由于 [products] 属性可能已初始化也可能未初始化,我们在第 17 行引入了 JSON 过滤器,以便指定是否需要该属性;
  • 第 26 行:[Set] 类型是一个接口。[HashSet] 类型是实现该接口的类。它实现了一种称为集合(set)的元素集合。 集合中不能包含两个相同的对象。此处,对象的类型为 [Product]。因此,在集合中,我们不能有两个相同的对象。由于父类 [AbstractEntity] 的 [equals] 方法已被重写,规定如果两个产品的主键相同则视为相同,因此 [products] 字段不能包含两个主键相同的产品;
  • 第 38–43 行:[addProduct] 方法允许将产品添加到类别中;

11.3.6. [Spring Data] 层

[DAO] 层[Console] 层[JPA] 层[JDBC] 驱动程序[Spring Data] 层Spring 4DBMS

  

[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> {
 
    // categorie avec ses produits
    @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);
 
    // une catégorie sans ses produits désignée par son nom
    public Categorie findByNom(String nom);
}
  • 第 8 行:第 11.2.3 节中已使用并解释了 [CrudRepository] 接口。回顾一下:
    • 该接口的第一个类型是用于 CRUD 操作(findOne、findAll、save、delete、deleteAll)的 JPA 实体,
    • 第二个类型是该 JPA 实体的主键,此处为整型 [Long];
  • 第 12 行:第 12 行中的方法由第 11 行中的 JPQL(Java 持久化查询语言)查询实现。该查询用于检索 JPA 实体。在此类查询中:
    • 表被替换为与其关联的 JPA 实体;
    • 列被查询中使用的 JPA 实体的字段所替换;
  • 第 11 行:JPQL 查询返回一个类别及其关联的产品。回顾 [Category] 实体中,[products] 字段具有 [fetch = FetchType.LAZY] 属性(延迟加载)。 在 JPQL 查询中,我们使用 [fetch] 关键字强制加载产品。查询中的 ?1 参数将在运行时被第 12 行方法的第一个参数值替换,即 [Long id] 参数;
  • 第 14–15 行:针对通过名称标识的类别,采用类似的方法;
  • 第 18 行:由于 [Category] 类型具有 [name] 字段,[findByName] 方法将由 [Spring Data] 自动实现;

[ProductsRepository] 接口负责管理对 [PRODUCTS] 表的访问:


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> {
 
    // un produit avec sa catégorie
    @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);
 
    // un produit sans sa catégorie désigné par son nom
    public Produit findByNom(String nom);
}

说明与 [CategoriesRepository] 接口中的说明相同。

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

11.3.7. [DAO] 层

[DAO] 层[Console] 层[JPA] 层[JDBC] 驱动程序[Spring Data] 层Spring 4DBMS

  

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


package spring.data.dao;
 
import java.util.List;
 
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
 
public interface IDao {

    // insert product list
    public List<Produit> addProduits(List<Produit> produits);
 
    // removal of all products
    public void deleteAllProduits();
 
    // product list update
    public List<Produit> updateProduits(List<Produit> produits);
 
    // all products obtained
    public List<Produit> getAllProduits();
 
    // inserting a list of categories
    public List<Categorie> addCategories(List<Categorie> categories);
 
    // delete all categories
    public void deleteAllCategories();
 
    // updating a list of categories
    public List<Categorie> updateCategories(List<Categorie> categories);
 
    // obtaining all categories
    public List<Categorie> getAllCategories();
 
    // a specific product with or without its category
    public Produit getProduitByIdWithoutCategorie(Long idProduit);
 
    public Produit getProduitByIdWithCategorie(Long idProduit);
 
    public Produit getProduitByNameWithCategorie(String nom);
 
    public Produit getProduitByNameWithoutCategorie(String nom);
 
    // a particular category with or without its products
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
 
    public Categorie getCategorieByIdWithProduits(Long idCategorie);
 
    public Categorie getCategorieByNameWithProduits(String nom);
 
    public Categorie getCategorieByNameWithoutProduits(String nom);
}

在此,我们遵循了一条规则:任何修改作为输入参数传递的对象的方法,都必须在返回值中包含这些对象。第4.2节中解释了制定此规则的原因:它允许一个层及其客户端驻留在两个独立的JVM中,从而以客户端/服务器配置进行操作。

该接口的 [Dao] 实现如下:


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 行:从 [Spring Data] 向两个 [CrudRepository] 接口注入引用。这种注入发生在 Spring 对象实例化期间,通常在 Spring 项目执行开始时;
  • 请注意第 28 行和第 46 行中,[productsRepository] 接口的 [save] 方法同时用于插入和更新产品。[Spring Data] 会根据产品的主键来判断是执行插入还是更新。如果主键为 [null],则执行插入;否则,则执行更新;
  • 第 82 行:我们使用 Guava 库中的 [Lists.newArrayList] 方法获取产品列表。[productsRepository.findAll()] 方法返回 [Iterable<Product>] 类型;
  • 第 28 行:[productsRepository.save(products)] 方法返回一个 [Iterable<Product>]。该类中的其他 [save] 操作也是如此;

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


package spring.data.dao;
 
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
 
// exception class for the Elections application
// the exception is uncontrolled
 
public class DaoException extends RuntimeException implements Serializable {
 
    // serial ID
    private static final long serialVersionUID = 1L;
 
    // local fields
    private int code;
    private List<String> erreurs;
 
    // manufacturers
    public DaoException() {
        super();
    }
 
    public DaoException(int code, Throwable e) {
        // parent
        super(e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }
 
    public DaoException(int code, String message, Throwable e) {
        // parent
        super(message, e);
        // local
        this.code = code;
        this.erreurs = getErreursForException(e);
    }
 
    public DaoException(int code, String message) {
        // parent
        super(message);
        // local
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }
 
    public DaoException(int code, List<String> erreurs) {
        // parent
        super();
        // local
        this.code = code;
        this.erreurs = erreurs;
    }
 
    // list of exception error messages
    private List<String> getErreursForException(Throwable th) {
        // retrieve the list of exception error messages
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // the message is retrieved only if it is !=null and not blank
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // next cause
            cause = cause.getCause();
        }
        return erreurs;
    }
 
    // getters and setters
...
}
  • 第 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 {
 
    // constants
    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" };
 
    // the [tomcat-jdbc] data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // an initially open connection
        dataSource.setInitialSize(1);
        // result
        return dataSource;
    }
 
    // the provider 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();
    }

    // Transaction manager
    @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] 注解来指示 [Spring Data] 的 [CrudRepository] 接口所在的包;
  • 第 18 行:该类是 Spring 配置类。这一信息至关重要。如果将其移除,项目仍可运行。但在本文后续内容中,当我们构建依赖于此项目的其他项目时,若移除了第 18 行的注解,其中部分项目将无法正常工作;
  • 第 19 行:[@ComponentScan] 注解指定了 Spring 对象所在的包。这些是带有 [@Component、@Service、@Controller 等] 注解的类。在此处,Spring [Dao] 组件将被查找并实例化;
  • 第 73–76 行:我们定义了一个 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 {
    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
}
  • 第 11 行:该类是一个 Spring 配置类;
  • 第 12 行:该类导入了我们刚才看到的 [DaoConfig] 类所定义的 Bean;
  • [console] 层使用此处定义的 JSON 映射器;
  • 第 14–64 行:定义了五个 JSON 映射器;
  • 第 15–18 行:JSON 映射器 [jsonMapper] 没有过滤器;
  • 第 20–30 行:JSON 过滤器 [jsonMapperCategoryWithProducts] 允许将 [Category] 对象与其关联的产品一起序列化/反序列化;
  • 第 32–42 行:JSON 过滤器 [jsonMapperProductWithCategory] 允许序列化/反序列化一个 [Product] 对象及其所属类别;
  • 第 43–53 行:JSON 过滤器 [jsonMapperCategorieWithoutProduits] 允许序列化/反序列化一个不包含其产品的 [Categorie] 对象;
  • 第 55–64 行:JSON 过滤器 [jsonMapperProductWithoutCategory] 允许对 [Product] 对象进行序列化和反序列化,同时省略其类别;

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

11.3.9. [控制台] 层

Layer[DAO]Layer[console]Layer[JPA]Driver[JDBC]Layer[Spring Data]Spring 4DBMS

  

[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 {
            // instantiation Spring context
            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);
            // --------------------------------------------------------------------------------------
            // empty the database
            log("Vidage de la base de données", 1);
            // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            // fill the tables
            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);
            }
            // add the category - the products will be cascaded in as well
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // list of categories
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            // product list
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // category 1 with its products
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            // the product [product14] with its category
            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);
            // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
            dao.deleteAllCategories();
            // base display
            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);
            // we insert
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // add the category - the products will be cascaded in as well
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            // check
            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) {
                // finish
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }
 
    // display of a T-type element
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }
 
    // display a list of elements of type 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) {
        // poster message
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }
}
  • 第 25 行:从 [AppConfig] 配置类实例化 Spring Bean;
  • 第 26–33 行:获取 JSON 映射器的引用。我们使用 [ApplicationContext].getBean 方法的以下签名:
    • [ApplicationContext].getBean(String id, Class class):当存在多个 [class] 类型的 Bean 时使用该方法。 在此情况下,我们指定所需 Bean 的标识符。若 Bean 通过 [@Bean] 注解定义,其标识符即为被注解方法的名称;若通过 [@Bean("identifier")] 注解定义,其标识符即为注解中指定的值;
  • 第 34 行:从 [DAO] 层获取引用;
  • 第 37–39 行:清空数据库。我们清空了 categories 表(第 39 行)。因为我们写道:

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

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

  • 第 43–53 行:向表中插入 2 个类别,每个类别包含 5 个产品。在第 50 行,插入这两个类别时会同时插入其产品,这再次是因为我们写了 [cascade = { CascadeType.ALL }];
  • 第 58 行:我们显示分类。使用 JSON 映射器 [jsonMapperCategorieWithoutProduits] 来显示不包含产品的分类。实际上,方法 [dao.getAllCategories()] 返回的正是不包含产品的分类(延迟加载);
  • 第 61 行:我们显示不包含所属类别的商品。这是因为方法 [dao.getAllProduits()] 返回的是不包含所属类别的商品(延迟加载);
  • 第 63–65 行:显示名为 [categorie1] 的分类及其产品(立即加载);
  • 第 67–69 行:显示一个商品及其所属类别;
  • 第 71–81 行:将 [categorie1] 类别中所有产品的价格上调 10%;
  • 第 91–101 行:添加一个包含两个同名产品的分类。然而,在 [PRODUCTS] 表中,[NAME] 列存在唯一约束。因此,第二个产品的插入将被拒绝并抛出异常。不过,[dao.addProducts] 方法是在事务内运行的。 因此,第二次插入失败必然导致第一次产品的插入以及其所属类别 [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 {
 
    // layer [DAO]
    @Autowired
    private IDao dao;
 
    // filters 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() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // fill the tables
        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);
        }
        // add the category - the products will be cascaded in as well
        categories = dao.addCategories(categories);
    }
 
    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // a few checks
        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 methods
    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;
    }
 
    // display of a T-type element
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }
 
    // display a list of elements of type 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) {
        // poster message
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }
 
    private static void show(String title, List<String> messages) {
        // title
        System.out.println(String.format("%s : ", title));
        // messages
        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 行:根据 ID 检索产品及其所属类别;
  • 第 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"> 
    <!-- encoders are  by default assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
 
  <!-- log level control -->
  <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 项目可以使用此归档文件了。