在前面的章节中,您学习了如何创建使用内存持久化的数据应用程序。在本章中,您将学习如何使用 Spring Boot 创建将数据存储在 SQL 数据库中的应用程序。Spring Boot 依赖于 Spring Framework 的数据访问功能,通过 JdbcTemplate 类提供对 SQL 数据库的访问。这个类简化了连接数据库引擎、会话管理、事务管理等繁琐的代码。
Spring Boot 还可以充分利用 Spring Data 项目的强大功能,该项目提供了额外的功能,比如可以使用 Repository 接口而无需担心具体实现,因为 Spring Data 会在后台处理这些细节。
在本章中,我们将继续进行两个项目的工作:用户应用和我的复古应用。
Spring Boot 在 SQL 数据库中的特性
在您创建一个 Spring Boot 项目并添加数据驱动依赖(例如,org.postgresql:postgresql)后,当应用程序启动时,Spring Boot 的自动配置会尝试配置所有内容以创建 DataSource 实现。如果您是数据开发的新手,通常需要一些关于数据库的信息:URL(格式为 jdbc:://:/[/|?),数据库的用户名和密码,有时还需要数据库引擎驱动程序。除非您覆盖这些默认值,否则所有这些配置将由 Spring Boot 自动完成。
另一个重要的特点是,Spring Boot 使用连接池,这使得应用程序在具备持久性时能够更好地管理、更高效地并发以及提升性能。默认情况下,如果存在 HikariCP 连接池,则会被选中;通常在使用 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 作为依赖时会出现这种情况。您可以选择更改默认的连接池。
如果您将 Spring Boot 应用程序部署到 Tomcat、IBM WebSphere、Jetty、GlassFish、WildFly 或 JBoss 等应用服务器上,您可以在 DataSource 中使用 JNDI 连接属性(spring.datasource.jndi-name)。
本章将介绍我们主要应用程序的许多新功能,让我们开始探索 Spring 框架的数据访问。
Spring 框架的数据访问
Spring 框架的核心部分包括数据访问功能,提供以下特点:
- 事务管理:Spring 提供了完整的事务管理抽象,具备跨不同 API(如 JTA、JDBC、Hibernate 和 JPA)的一致编程模型,支持声明式事务管理或基于注解的事务管理(使用 @Transactional),可以配置事务隔离级别,并与 Spring 数据访问抽象实现了良好的集成。
- 数据访问对象(DAO)支持:DAO 支持提供了一种与不同 API(如 JDBC、Hibernate 和 JPA)进行一致交互的方式,使得在它们之间切换变得简单且易于维护。Spring 还提供了一种在不同技术间一致的异常翻译器,因此您无需担心特定 API 的错误。为了充分利用 DAO 支持,必须使用@Repository 注解。
- 使用 Spring JDBC 进行数据访问时,您只需关注指定 SQL 语句、声明参数及其值,以及提供连接参数。您可以使用 JdbcTemplate 类来处理与数据库交互的样板代码,并在出现错误时提供更清晰的错误解释。Spring JDBC 还提供了批处理操作的执行、使用 SimpleJDBC 类以及将 JDBC 操作建模为 Java 对象等功能。此外,它还支持嵌入式使用,并提供初始化 DataSource 的方法。 使用 Spring JDBC 的简单方法是提供 DataSource(需要数据库引擎的 URL、用户名、密码和驱动程序),然后可以使用 JdbcTemplate(该类需要 DataSource)。另一个重要的 Spring JDBC 特性是支持嵌入式数据库(如 H2、HSQL 和 Derby 等数据库引擎),您可以通过提供包含模式定义和数据的 SQL 文件来初始化数据库。
- 数据访问与 R2DBC:Spring 支持在使用 SQL 进行非阻塞场景的数据库中实现反应式模式。Spring R2DBC 引入了 DatabaseClient 类,这是基本 R2DBC 处理和错误处理的核心,此外还有其他实用类;同时也提供了 ConnectionFactory 的实现,以支持 R2DBC 连接及其他实用类。
- 对象关系映射(ORM):Spring 的 ORM 支持与 Java 持久化 API(JPA)和 Hibernate 的集成,以及数据访问对象(DAO)实现和事务策略。这项技术最受欢迎的功能之一是逆向工程,因为它可以根据类自动创建表之间的关系,而无需任何 XML 映射。
- 对象-XML 映射器:Spring 支持将 XML 文档与对象之间进行转换。
正如您所见,Spring 框架的数据访问是多种数据技术的核心,这些技术使开发者能够创建符合企业标准的数据应用程序,并遵循一致的 Spring 编程模型。当然,Spring Boot 充分利用这些优势,帮助开发者应用常见的默认实践,以避免错误并促进开发。
基于 Spring Boot 的 JDBC 使用
正如之前提到的,Spring JDBC 需要您提供连接参数、指定 SQL 语句、声明参数及其值,并在获取结果集时进行一些操作等设置。而使用 Spring Boot,您可以享受这些功能,无需任何配置。
通过使用 spring-boot-starter-jdbc 启动器依赖,Spring 的 JdbcTemplate 和 NamedParameterJdbcTemplate 会自动配置,这使得我们可以在类中通过构造函数直接使用 Spring JDBC。此外,还有一些 spring.jdbc.template.* 属性可以根据需要进行修改。
用户应用:基于 Spring Boot 的 JDBC 使用指南
我们的用户应用程序目前使用内存持久化,现在是时候切换到 JDBC 了。我建议您从 Spring Initializr(https://start.spring.io)创建一个空项目,并从那里开始。在下载并解压项目后,您可以将其导入到您喜欢的 IDE 中。如果您觉得修改现有代码也很顺手,那也是可以的。图 4-1 展示了我们将在本节中开发的结构和代码。
正如你所看到的,我们将重新使用控制器编程。
首先打开 build.gradle 文件,并将其内容替换为列表 4-1 中的内容。
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.apress'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// Web
implementation 'org.webjars:bootstrap:5.2.3'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
test {
testLogging {
events "passed", "skipped", "failed"
showExceptions true
exceptionFormat "full"
showCauses true
showStackTraces true
showStandardStreams = false
}
}
列表 4-1 的 build.gradle 文件
对于 build.gradle 文件的重要更改(与之前的版本相比),我们添加了三个额外的依赖项:spring-boot-starter-jdbc 依赖项,以及仅在运行时使用的 h2 和 postgresql 驱动程序。请注意,我们使用了两个驱动程序。如果您想知道 Spring Boot 将配置哪个数据库驱动程序,h2 还是 postgresql,答案将在本章后面给出。
接下来,按照清单 4-2 中的代码创建 SimpleRepository 接口。
package com.apress.users;
import java.util.Optional;
public interface SimpleRepository<D,ID>{
Optional<D> findById(ID id);
Iterable<D> findAll();
D save(D d);
void deleteById(ID id);
}
列表 4-2 源代码:src/main/java/com/apress/users/SimpleRepository.java
请注意,SimpleRepository 接口与之前的版本是一样的。
Gravatar - https://gravatar.com: 用户身份识别
按照清单 4-3 的示例创建 UserGravatar 类。
package com.apress.users;
}
private static String hex(byte[] array) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < array.length; ++i) {
sb.append(Integer.toHexString((array[i]
& 0xFF) | 0x100).substring(1, 3));
}
return sb.toString();
}
private static String md5Hex(String message) {
try {
MessageDigest md =
MessageDigest.getInstance("MD5");
return hex(md.digest(message.getBytes("CP1252")));
} catch (NoSuchAlgorithmException e) {
} catch (UnsupportedEncodingException e) {
}
return "23bb62a7d0ca63c9a804908e57bf6bd4";
}
}
示例 4-3 源代码:src/main/java/com/apress/users/UserGravatar.java
UserGravatar 类是一个简单的实用工具类,可以根据用户的电子邮件地址为我们的网页应用程序添加 Gravatar 图标。
模型:枚举类型和记录类型
按照清单 4-4 的示例创建 UserRole 枚举。这个枚举非常简单,和之前的版本是一样的。
package com.apress.users;
public enum UserRole {
USER, ADMIN, INFO
}
文件路径:src/main/java/com/apress/users/User.java(列表 4-4)
接下来,我们将使用用户的新 Java 记录类型。请打开用户记录,并将其内容替换为清单 4-5 中所示的内容。
package com.apress.users;
import lombok.Builder;
import lombok.Singular;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Builder
public record User(Integer id, String email, String name, String password, boolean active, String gravatarUrl, @Singular("role") List<UserRole> userRole) {
public User {
Objects.requireNonNull(email);
Objects.requireNonNull(name);
Objects.requireNonNull(password);
Pattern pattern = Pattern.compile("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!])(?=\\S+$).{8,}#34;);
Matcher matcher = pattern.matcher(password);
if (!matcher.matches())
throw new IllegalArgumentException("Password must be at least 8 characters long and contain at least one number, one uppercase, one lowercase and one special character");
pattern = Pattern.compile("^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+#34;);
matcher = pattern.matcher(email);
if (!matcher.matches())
throw new IllegalArgumentException("Email must be a valid email address");
if (gravatarUrl == null) {
gravatarUrl = UserGravatar.getGravatarUrlFromEmail(email);
}
if (userRole == null) {
userRole = new ArrayList<>(){{ add(UserRole.INFO); }};
}
}
public User withId(Integer id){
return new User(id, this.email(), this.name(), this.password(), this.active(), this.gravatarUrl(), this.userRole());
}
}
文件 4-5 源代码:src/main/java/com/apress/users/User.java
列表 4-5 展示了基于 Java 14 特性的用户记录。记录是一种不可变的数据类型,您只需提供字段的名称和类型。在之前的版本中,我们使用 Lombok,它在使用@Data 注解时会自动生成 setter、getter 以及 toString()、equals()和 hashCode()方法。而在这里,使用记录同样可以达到相同的效果,只是获取值的方式略有不同。请记住,由于记录是不可变的,一旦创建,您无法修改该对象。让我们来分析一下这个记录:
- 这是声明记录的方式,提供字段名称及其类型。请注意,我们还可以使用注解,例如 Lombok 中的@Singular。
- 对于这种新的记录类型,您可以使用规范构造函数、紧凑构造函数或自定义构造函数。在这种情况下,我们使用了紧凑构造函数,这样我们就可以省略所有参数并应用特定的逻辑。
- 我们使用这个方法调用是因为它可以确保对象正确构建,从而实现可控的行为;而且这个方法调用也更容易进行调试。
- 公共用户 withId(Integer id){}。使用记录类型时,您可以调用返回对象副本的方法,例如在这种情况下,我们根据其 Integer id 类型创建了一个副本。
请注意,我们仍在使用 Lombok 注解。我认为这些注解仍然很有用。你可能觉得这段代码是多余的,但请记住,我们正在处理不可变性,而这正是我们需要做的。选择对你和你的应用程序更方便的方式完全取决于你。
JdbcTemplate 和行映射器
按照清单 4-6 的示例创建 UserRowMapper 类。
package com.apress.users;
import org.springframework.jdbc.core.RowMapper;
import java.sql.Array;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
Array array = rs.getArray("user_role");
String[] rolesArray = Arrays.copyOf((Object[])array.getArray(), ((Object[])array.getArray()).length, String[].class);
List<UserRole> roles = Arrays.stream(rolesArray).map(UserRole::valueOf).collect(Collectors.toList());
User newUser = User.builder()
.id(rs.getInt("id"))
.name(rs.getString("name"))
.email(rs.getString("email"))
.password(rs.getString("password"))
.userRole(roles)
.build();
return newUser;
}
}
文件路径:src/main/java/com/apress/users/UserRowMapper.java(列表 4-6)
UserRowMapper 类实现了 RowMapper 函数式接口(包含 mapRow(ResultSet,int) 方法),并与 JdbcTemplate 类(稍后介绍)一起使用,以逐行映射 java.sql.ResultSet 的结果。正如你所看到的,我们正在构建 User 对象并将其返回。
接下来,创建 UserRepository 类。请查看第 4-7 号列表。
package com.apress.users;
import lombok.AllArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Types;
import java.util.Optional;
@AllArgsConstructor
@Repository
public class UserRepository implements SimpleRepository<User, Integer> {
private JdbcTemplate jdbcTemplate;
@Override
public Optional<User> findById(Integer id) {
String sql = "SELECT * FROM users WHERE id = ?";
Object[] params = new Object[] { id };
User user = jdbcTemplate.queryForObject(sql, params, new int[] { Types.INTEGER }, new UserRowMapper());
return Optional.ofNullable(user);
}
@Override
public Iterable<User> findAll() {
String sql = "SELECT * FROM users";
return this.jdbcTemplate.query(sql, new UserRowMapper());
}
@Override
public User save(User user) {
String sql = "INSERT INTO users (name, email, password, gravatar_url,user_role,active) VALUES (?, ?, ?, ?, ?, ?)";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
String[] array = user.userRole().stream().map(Enum::name).toArray(String[]::new);
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, user.name());
ps.setString(2, user.email());
ps.setString(3, user.password());
ps.setString(4, user.gravatarUrl());
ps.setArray(5, connection.createArrayOf("varchar", array));
ps.setBoolean(6, user.active());
return ps;
}, keyHolder);
User userCreated = user.withId((Integer)keyHolder.getKeys().get("id"));
return userCreated;
}
@Override
public void deleteById(Long id) {
String sql = "DELETE FROM users WHERE id = ?";
jdbcTemplate.update(sql, id);
}
}
用户仓库的代码示例 4-7(路径:src/main/java/apress/com/users/UserRepository.java)
UserRepository 类实现了 SimpleRepository 接口。在这个类中,我们使用 JdbcTemplate 类。这个类将由 Spring Boot 的自动配置进行自动注入。请记住,这个类将通过 JDBC 调用与数据库进行繁重的交互。使用 JdbcTemplate,我们可以通过多种方式与数据库进行交互:
- 这是几个重载方法之一,它执行 SQL 查询,并通过 RowMapper 将每一行映射到结果对象,这里使用的是我们的 UserRowMapper。
- 这是几个重载方法之一,它根据给定的 SQL 语句,从要绑定到查询的参数列表中创建一个预编译语句,并通过 RowMapper 将单个结果行映射到结果对象。
- 更新:这是几种重载方法之一,它通过预编译语句执行单个 SQL 更新操作(您可以使用 INSERT、UPDATE 或 DELETE 语句),并绑定相应的参数。
值得一提的是,在 save(User user)方法中,我们使用了 record withId 方法调用,因此我们从 keyHolder 中获取 Id(这就是我们如何获得自增值)。keyHolder 将会填充不同的键,而我们需要的键将会被生成。
如果你查看 JdbcTemplate 的文档(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html),你会发现它实现了许多方法,比如 queryForMap、queryForList、queryForRowSet、queryForStream、execute、batchUpdate 等,还有更多其他方法。
请记住,JdbcTemplate 已经能够根据 DataSource 的信息(如 URL、用户名、密码等)与数据库进行交互。