Spring常用数据库来持久化数据。
12.1 什么是数据源
数据源是一个管理与数据库管理系统 (DBMS) 连接的组件。它使用 JDBC 驱动程序获取其管理的连接。数据源旨在通过允许其逻辑重用与 DBMS 的连接,并仅在需要时请求新连接来提高应用的性能。数据源还会确保在释放连接时关闭它们。
如果没有一个对象承担数据源的责任,应用就需要为每次数据操作请求一个新的连接。由于每次操作都需要通过网络通信来建立新的连接,这会显著降低应用速度并导致性能问题。数据源可以确保应用仅在真正需要时才请求新的连接,从而提升应用的性能。
在 Java 应用中,该语言连接关系数据库的功能被称为 Java 数据库连接 (JDBC)。JDBC 提供了一种连接到 DBMS 以使用数据库的方法。JDK仅提供应用使用关系数据库所需对象的抽象, 并不提供针对特定技术(例如 MySQL、Postgres 或 Oracle)的具体实现。您需要添加一个名为 JDBC 驱动程序的运行时依赖项,由具体的数据库技术供应商提供JDBC 驱动程序。JDBC 驱动程序既不是来自 JDK,也不是来自 Spring 等框架。
// url指定了数据库类型和具体的数据库
// "jdbc:mysql://localhost:3306/mydatabase"
// "jdbc:postgresql://localhost:5432/mydatabase"
// "jdbc:oracle:thin:@//localhost:1521/orclpdb"
Connection con = DriverManager.getConnection(url, username, password);
上述的方法每次都获取一个新的连接,更好的方式是使用数据源来管理连接,从而提升性能,并避免认证,创建和关闭连接等额外操作。
对于 Java 应用,目前最常用的是开源的 HikariCP(Hikari 连接池)数据源。Spring Boot 的约定配置也将 HikariCP 视为默认数据源实现。
12.2 使用 JdbcTemplate 处理持久化数据
JdbcTemplate 是 Spring 提供的使用关系数据库的最简单的工具,
但它对于小型应用来说是一个绝佳的选择,因为它不会强制你使用任何其他特定的持久层框架。当你不希望你的应用有任何其他依赖项时,JdbcTemplate 是实现持久层的最佳 Spring 选择。它也是学习如何实现 Spring 应用持久层的绝佳入门方法。
相关知识,JDBC 部分推荐 Jeanne Boyarsky 和 Scott Selikoff 合著的《OCP Oracle Certified Professional Java SE 11 Developer Complete Study Guide》(Sybex,2020 年)的第 21 章。SQL推荐 Alan Beaulieu 合著的《Learning SQL》第三版(O’Reilly Media,2020 年)。
参考示例sq-ch12-ex1
。我们将开发一个后端服务,它会暴露两个端点。客户端调用一个端点在购买表中添加一条新记录,并调用另一个端点获取购买表中的所有记录。
首先用Spring Initializr初始化项目:
我们需要的是Spring核心的JDBC,即JDBC API。而非Spring Session for JDBC和Spring Data JDBC。
数据库使用的是H2,嵌入式内存SQL数据库,Java写的。但对于 H2,您无需单独添加它,因为它已包含在您在 pom.xml 文件中添加的 H2 数据库依赖项中。
schema.sql放置在目录resources下,会自动创建表。
CREATE TABLE IF NOT EXISTS purchase (
id INT AUTO_INCREMENT PRIMARY KEY,
product varchar(50) NOT NULL,
price double NOT NULL
);
model类为Purchase,注意金额属性是用BigDecimal,而非double,因为其会丢失精度。
public class Purchase {
private int id;
private String product;
private BigDecimal price;
...
}
存储库(repository)是负责处理数据库的类。本例为PurchaseRepository。其中包含JdbcTemplate以与数据库交互。当 Spring Boot 检测到你在 pom.xml 中添加了 H2 依赖项时,它会自动配置一个数据源和一个 JdbcTemplate 实例。在本例中,我们将直接使用它们。
@Repository
public class PurchaseRepository {
private final JdbcTemplate jdbc;
public PurchaseRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
public void storePurchase(Purchase purchase) {
String sql = "INSERT INTO purchase(product, price) VALUES (?, ?)";
jdbc.update(sql, purchase.getProduct(), purchase.getPrice());
}
public List<Purchase> findAllPurchases() {
String sql = "SELECT * FROM purchase";
RowMapper<Purchase> purchaseRowMapper = (r, i) -> {
Purchase rowObject = new Purchase();
rowObject.setId(r.getInt("id"));
rowObject.setProduct(r.getString("product"));
rowObject.setPrice(r.getBigDecimal("price"));
return rowObject;
};
return jdbc.query(sql, purchaseRowMapper);
}
}
注意,原代码中的INSERT语句有误,我已经改了(详见String sql)。原语句如下:
"INSERT INTO purchase VALUES (NULL, ?, ?)";
RowMapper这段 Java 代码定义了一个实现 RowMapper<Purchase>
接口的 lambda 表达式,该接口通常用于 Spring 的 JdbcTemplate 中,将 SQL 结果集中的一行映射到 Java 对象 - 在本例中为 Purchase 对象。整体返回的是Purchase对象的列表。
最后看一下控制类PurchaseController,映射了两个REST端点:
@RestController
@RequestMapping("/purchase")
public class PurchaseController {
private final PurchaseRepository purchaseRepository;
public PurchaseController(PurchaseRepository purchaseRepository) {
this.purchaseRepository = purchaseRepository;
}
@PostMapping
public void storePurchase(@RequestBody Purchase purchase) {
purchaseRepository.storePurchase(purchase);
}
@GetMapping
public List<Purchase> findPurchases() {
return purchaseRepository.findAllPurchases();
}
}
运行如下:
$ curl 'http://localhost:8080/purchase'
[]
$ curl -XPOST 'http://localhost:8080/purchase' -H 'Content-Type: application/json' -d '{
"product" : "Spring Security in Action",
"price" : 25.2
}'
$ curl 'http://localhost:8080/purchase'
[{"id":1,"product":"Spring Security in Action","price":25.2}]
12.3 自定义数据源配置
本例中使用自定义数据源PostgreSQL,而非H2数据库。书中使用的是MySQL,但由于我有一个现成的PG,因此就直接用了。
12.3.1 在应用程序属性文件中定义数据源
去掉H2的依赖,增加了PostgreSQL的依赖:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
schema.sql需改为符合PostgreSQL语法:
CREATE TABLE IF NOT EXISTS purchase (
id SERIAL PRIMARY KEY,
product varchar(50) NOT NULL,
price double PRECISION NOT NULL
);
application.properties需改为如下:
spring.application.name=sq-ch12-ex1
spring.datasource.url=jdbc:postgresql://localhost:5432/sampledb
spring.datasource.username=hr
spring.datasource.password=Welcome1
spring.sql.init.mode=always
spring.sql.init.mode=always
始终在启动时运行 schema.sql 和 data.sql,这是Spring Boot 2.5版本后的新语法。原代码中的spring.datasource.initialization-mode=always
是错的。
也就改了以上两个文件,运行结果同上例。
12.3.2 使用自定义 DataSource bean
有时,您不能依赖 Spring Boot 来创建 DataSource Bean。在这种情况下,您需要自己定义 Bean。场景如下:
- 您需要根据只能在运行时获取的条件使用特定的 DataSource 实现。
- 您的应用连接到多个数据库,因此您必须创建多个数据源,并使用限定符区分它们。
- 您必须在应用仅在运行时才具有的某些条件下配置 DataSource 对象的特定参数。例如,根据启动应用的环境,您希望在连接池中拥有更多或更少的连接,以优化性能。
- 您的应用使用 Spring 框架,而不是 Spring Boot。
参见示例sqch12-ex3
。先在配置类中定义一个DataSource bean:
@Configuration
public class ProjectConfig {
@Value("${custom.datasource.url}")
private String datasourceUrl;
@Value("${custom.datasource.username}")
private String datasourceUsername;
@Value("${custom.datasource.password}")
private String datasourcePassword;
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl(datasourceUrl);
dataSource.setUsername(datasourceUsername);
dataSource.setPassword(datasourcePassword);
dataSource.setConnectionTimeout(1000);
return dataSource;
}
}
注意@Value
中的属性,都是以custom开头的,这并不是Spring的属性,因此application.properties修改如下:
custom.datasource.url=jdbc:postgresql://localhost:5432/sampledb
custom.datasource.username=hr
custom.datasource.password=Welcome1
spring.sql.init.mode=always
但这里有个问题,purchase表必须预先建好,因为spring.sql.init.mode=always
失效了。这倒是正常的行为,如果一定希望表自动建立,可以在配置类ProjectConfig中添加以下代码:
DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.addScript(new ClassPathResource("schema.sql"));
initializer.setDatabasePopulator(populator);
总结
- 对于 Java 应用程序,Java 开发工具包 (JDK) 提供了应用程序连接到关系数据库所需对象的抽象。应用程序始终需要添加一个运行时依赖项来提供这些抽象的实现。我们将此依赖项称为 JDBC 驱动程序。
- 数据源是管理与数据库服务器连接的对象。如果没有数据源,应用会过于频繁地请求连接,从而影响其性能。
- 默认情况下,Spring Boot 会配置一个名为 HikariCP 的数据源实现,它使用连接池来优化应用使用数据库连接的方式。也可使用其他数据源如Tomcat JDBC。
- JdbcTemplate 是一个 Spring 工具,它简化了使用 JDBC 访问关系数据库的代码。JdbcTemplate 对象依赖于数据源来连接到数据库服务器。
- 要发送修改表中数据的查询,可以使用 JdbcTemplate 对象的 update() 方法。要发送 SELECT 查询来检索数据,可以使用 JdbcTemplate 的 query() 方法之一。您通常需要使用此类操作来更改或检索持久化数据。
- 要自定义 Spring Boot 应用使用的数据源,您需要配置一个 java.sql.DataSource 类型的自定义 bean。如果您在 Spring 上下文中声明了此类型的 bean,Spring Boot 将使用它,而不是配置默认 bean。如果您需要自定义 JdbcTemplate 对象,也可以使用相同的方法。我们通常使用 Spring Boot 提供的默认 bean,但特定情况下有时需要自定义配置或实现来进行各种优化。
- 如果您希望应用连接到多个数据库,可以创建多个数据源对象,每个对象都关联各自的 JdbcTemplate 对象。在这种情况下,您需要使用 @Qualifier 注解来区分应用上下文中相同类型的对象(正如您在第 4 章和第 5 章中学习到的)。