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 持久化查询语言)查询来实现它:
因此,类型 T 必须包含一个名为 [something] 的字段。这样,方法
将通过类似以下代码实现:
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 中的方法。控制台输出结果如下:
- 第 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>java -jar gs-accessing-data-jpa-02.jar
控制台显示的结果如下:
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 项目可以使用该归档文件。





































