亦有资源网

C++语言基础到进阶学习资源汇总

精通Spring Boot 3 : 4. Spring Boot SQL 数据访问指南 (1)

在前面的章节中,您学习了如何创建使用内存持久化的数据应用程序。在本章中,您将学习如何使用 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、用户名、密码等)与数据库进行交互。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言