51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Boot Jdbctemplate 指南

Spring Boot 对 JDBC 的支持 {#spring-boot-对-jdbc-的支持}

Spring 的 JdbcTemplateDataSource 的基础上提供了执行数据库操作的高级抽象。配合 Spring 的声明式事务,能以很简单的方式管理数据库事务,而无需编写模板式的代码。

Spring Boot 通过自动配置机制简化了 DataSourceTransactionManager 等的配置。

让我们看看如何使用 JdbcTemplate 对 PostgreSQL 数据库执行 CRUD 操作。

首先,访问 https://start.springboot.io/,选择 JDBC APIPostgreSQL DriverTestcontainers starter,创建 Spring Boot 应用程序。

假设,我们正在开发一个管理书签(bookmarks)的简单应用。因此,我们将创建包含 idtitleurlcreated_at 列的 bookmarks 表。

初始化数据源 {#初始化数据源}

Spring Boot 提供了一种方便的数据库初始化机制。我们可以在 src/main/resources 下创建 schema.sqldata.sql 文件,这些文件将在启动应用程序时自动执行。不过,只有在使用 HSQL、H2 等内存数据库时,才会默认启用自动脚本执行功能,否则就会禁用。

我们可以在 src/main/resources/application.properties 文件中添加以下属性来启用脚本初始化。

spring.sql.init.mode=always

现在,让我们创建 src/main/resources/schema.sql 文件,如下所示:

create table if not exists bookmarks
(
    id         bigserial not null,
    title      varchar   not null,
    url        varchar   not null,
    created_at timestamp,
    primary key (id)
);

要插入一些示例数据,请创建 src/main/resources/data.sql 文件,如下所示:

truncate table bookmarks;
ALTER SEQUENCE bookmarks_id_seq RESTART WITH 1;

insert into bookmarks(title, url, created_at) values ('How (not) to ask for Technical Help?','https://sivalabs.in/how-to-not-to-ask-for-technical-help', CURRENT_TIMESTAMP), ('Announcing My SpringBoot Tips Video Series on YouTube','https://sivalabs.in/announcing-my-springboot-tips-video-series', CURRENT_TIMESTAMP), ('Kubernetes - Exposing Services to outside of Cluster using Ingress','https://sivalabs.in/kubernetes-ingress', CURRENT_TIMESTAMP), ('Kubernetes - Blue/Green Deployments','https://sivalabs.in/kubernetes-blue-green-deployments', CURRENT_TIMESTAMP), ('Kubernetes - Releasing a new version of the application using Deployment Rolling Updates','https://sivalabs.in/kubernetes-deployment-rolling-updates', CURRENT_TIMESTAMP), ('Getting Started with Kubernetes','https://sivalabs.in/getting-started-with-kubernetes', CURRENT_TIMESTAMP), ('Get Super Productive with Intellij File Templates','https://sivalabs.in/get-super-productive-with-intellij-file-templates', CURRENT_TIMESTAMP), ('Few Things I learned in the HardWay in 15 years of my career','https://sivalabs.in/few-things-i-learned-the-hardway-in-15-years-of-my-career', CURRENT_TIMESTAMP), ('All the resources you ever need as a Java & Spring application developer','https://sivalabs.in/all-the-resources-you-ever-need-as-a-java-spring-application-developer', CURRENT_TIMESTAMP), ('GoLang from a Java developer perspective','https://sivalabs.in/golang-from-a-java-developer-perspective', CURRENT_TIMESTAMP), ('Imposing Code Structure Guidelines using ArchUnit','https://sivalabs.in/impose-architecture-guidelines-using-archunit', CURRENT_TIMESTAMP), ('SpringBoot Integration Testing using TestContainers Starter','https://sivalabs.in/spring-boot-integration-testing-using-testcontainers-starter', CURRENT_TIMESTAMP), ('Creating Yeoman based SpringBoot Generator','https://sivalabs.in/creating-yeoman-based-springboot-generator', CURRENT_TIMESTAMP), ('Testing REST APIs using Postman and Newman','https://sivalabs.in/testing-rest-apis-with-postman-newman', CURRENT_TIMESTAMP), ('Testing SpringBoot Applications','https://sivalabs.in/spring-boot-testing', CURRENT_TIMESTAMP) ;

使用数据库迁移工具

虽然 Spring Boot 提供了使用 schema.sqldata.sql 初始化数据库的简便方法,但我们更倾向于使用合适的数据库迁移工具,如 LiquibaseFlyway

使用 JdbcTemplate 实现 CRUD 操作 {#使用-jdbctemplate-实现-crud-操作}

先创建 Bookmark 类。

import java.time.Instant;

public record Bookmark( Long id, String title, String url, Instant createdAt) {}

我们使用 Java record 来创建 Bookmark 对象。

现在,让我们创建 BookmarkRepository 类,并且注入 JdbcTemplate。如下所示:

@Repository
public class BookmarkRepository {
    private final JdbcTemplate jdbcTemplate;
public BookmarkRepository(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
}

}

让我们从实现 findAll() 方法开始,该方法用于从 bookmarks 表中获取所有记录。

实现 findAll() 方法 {#实现-findall-方法}

当我们查询数据库时,它会返回一个 ResultSet。我们可以提供一个 RowMapper 实现,将 ResultSet 数据封装到我们的 Bookmark 对象中,如下所示:

@Repository
public class BookmarkRepository {
    private final JdbcTemplate jdbcTemplate;
public BookmarkRepository(JdbcTemplate jdbcTemplate) {
    this.jdbcTemplate = jdbcTemplate;
}

public List<Bookmark> findAll() { String sql = "select id, title, url, created_at from bookmarks"; return jdbcTemplate.query(sql, INSTANCE); }

static class BookmarkRowMapper implements RowMapper<Bookmark> { public static final BookmarkRowMapper INSTANCE = new BookmarkRowMapper();

private BookmarkRowMapper(){}

@Override public Bookmark mapRow(ResultSet rs, int rowNum) throws SQLException { return new Bookmark( rs.getLong("id"), rs.getString("title"), rs.getString("url"), rs.getTimestamp("created_at").toInstant() ); }

}

}

我们创建了实现 RowMapper 接口的 BookmarkRowMapper 实现类(单例设计)。然后,我们使用 findAll() 方法从 bookmarks 表中获取所有行,并使用 BookmarkRowMapper 将其封装为 Bookmark 对象。

注意 :如果记录数量很多,一次性从表中获取所有记录可能会导致 OutOfMemoryException 异常。因此,最好使用分页功能,只获取部分记录并进行处理。

实现 findById() 方法 {#实现-findbyid-方法}

让我们实现 findById(Long id) 方法,按 id 检索 bookmark,如下所示:

public Optional<Bookmark> findById(Long id) {
    String sql = "select id, title, url, created_at from bookmarks where id = ?";
    try {
        Bookmark bookmark = jdbcTemplate.queryForObject(sql, INSTANCE, id);
        return Optional.of(bookmark);
    } catch (EmptyResultDataAccessException e) {
        return Optional.empty();
    }
}

findById() 方法返回的是 Optional<Bookmark>,因为给定 id 的 bookmark 可能存在,也可能不存在。我们在此使用的是 jdbcTemplate.queryForObject(...) 方法,如果没有找到记录,该方法会抛出 EmptyResultDataAccessException 异常。因此,我们将处理该异常并返回 Optional.empty()

实现 create() 方法 {#实现-create-方法}

create() 方法中,我们将向 bookmarks 表插入一条记录,并返回自增的主键值。

public Long create(Bookmark bookmark) {
    KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -&gt; {
    String sql = &quot;insert into bookmarks(title, url, created_at) values(?,?,?)&quot;;
    PreparedStatement ps = connection.prepareStatement(sql, new String[] { &quot;id&quot; });
    ps.setString(1, bookmark.title());
    ps.setString(2, bookmark.url());
    ps.setTimestamp(3, Timestamp.from(bookmark.createdAt()));
    return ps;
}, keyHolder);

return (long) keyHolder.getKey();

}

请注意,在较新版本的 PostgreSQL 中,我们使用 connection.prepareStatement(sql, new String[] { "id" }) 指定自动生成的 key。在以前的版本中,我们可以使用 connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) 来指定。

实现 update() 方法 {#实现-update-方法}

让我们以这样一种逻辑来实现 update() 方法:如果给定 id 的 bookmark 记录存在,我们就需要更新 titleurl 列。否则,将抛出异常。

public void update(Bookmark bookmark) {
    String sql = "update bookmarks set title = ?, url = ? where id = ?";
    int count = jdbcTemplate.update(sql, bookmark.title(), bookmark.url(), bookmark.id());
    if (count == 0) {
        throw new RuntimeException("Bookmark not found");
    }
}

jdbcTemplate.update(...) 方法会返回受影响的行数。如果 count 为 0,则表示指定 id 的记录不存在,因此我们会抛出异常。

实现 delete() 方法 {#实现-delete-方法}

让我们以这样的逻辑来实现 delete() 方法:如果指定 id 的 bookmark 记录存在,则删除该记录,否则抛出异常。

public void delete(Long id) {
    String sql = "delete from bookmarks where id = ?";
    int count = jdbcTemplate.update(sql, id);
    if (count == 0) {
        throw new RuntimeException("Bookmark not found");
    }
}

使用 Testcontainers 测试 Repository {#使用-testcontainers-测试-repository}

在测试中,我们应该使用相同类型的数据库来测试 repository。Testcontainers 可以帮助我们使用真实的依赖来测试应用程序,而不是使用模拟(mock)或 H2 等内存数据库。

编写 BookmarkRepositoryTest,使用 Testcontainers 测试 repository。

如下:

package com.sivalabs.bookmarks.domain;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.jdbc.core.JdbcTemplate;

import java.time.Instant; import java.util.List; import java.util.Optional;

@JdbcTest(properties = { "spring.test.database.replace=none", "spring.datasource.url=jdbc:tc:postgresql:15.2-alpine:///db" }) class BookmarkRepositoryTest {

@Autowired
JdbcTemplate jdbcTemplate;

BookmarkRepository bookmarkRepository;

@BeforeEach void setUp() { bookmarkRepository = new BookmarkRepository(jdbcTemplate); }

@Test void shouldFindAllBookmarks() { List&lt;Bookmark&gt; bookmarks = bookmarkRepository.findAll(); assertThat(bookmarks).isNotEmpty(); assertThat(bookmarks).hasSize(15); }

@Test void shouldCreateBookmark() { Bookmark bookmark = new Bookmark(null, &quot;My Title&quot;, &quot;https://sivalabs.in&quot;, Instant.now()); Long id = bookmarkRepository.save(bookmark); assertThat(id).isNotNull(); }

@Test void shouldGetBookmarkById() { Bookmark bookmark = new Bookmark(null, &quot;My Title&quot;, &quot;https://sivalabs.in&quot;, Instant.now()); Long id = bookmarkRepository.save(bookmark);

Optional&amp;lt;Bookmark&amp;gt; bookmarkOptional = bookmarkRepository.findById(id);
assertThat(bookmarkOptional).isPresent();

}

@Test void shouldEmptyWhenBookmarkNotFound() { Optional&lt;Bookmark&gt; bookmarkOptional = bookmarkRepository.findById(9999L); assertThat(bookmarkOptional).isEmpty(); }

@Test void shouldUpdateBookmark() { Bookmark bookmark = new Bookmark(null, &quot;My Title&quot;, &quot;https://sivalabs.in&quot;, Instant.now()); Long id = bookmarkRepository.save(bookmark);

Bookmark updatedBookmark = new Bookmark(id, &amp;quot;My Updated Title&amp;quot;, &amp;quot;https://www.sivalabs.in&amp;quot;, bookmark.createdAt());
bookmarkRepository.update(updatedBookmark);

updatedBookmark = bookmarkRepository.findById(id).orElseThrow(); assertThat(updatedBookmark.id()).isEqualTo(id); assertThat(updatedBookmark.title()).isEqualTo(&amp;quot;My Updated Title&amp;quot;); assertThat(updatedBookmark.url()).isEqualTo(&amp;quot;https://www.sivalabs.in&amp;quot;);

}

@Test void shouldDeleteBookmark() { Bookmark bookmark = new Bookmark(null, &quot;My Title&quot;, &quot;https://sivalabs.in&quot;, Instant.now()); Long id = bookmarkRepository.save(bookmark);

bookmarkRepository.delete(id);

}

}

我们使用 Spring Boot Test Slice 注解 @JdbcTest 来只测试 Repository,而不是加载整个应用程序。然后,我们使用 Testcontainers 特殊的 JDBC URL 支持,使用 postgres:15.2-alpine 镜像启动 PostgreSQL 数据库。然后,我们编写了各种测试方法来测试我们的 CRUD 操作。

总结 {#总结}

Spring 的 JdbcTemplate 提供了高级抽象,无需编写模板代码即可执行数据库操作。至此,我们已经学会了如何使用 JdbcTemplate 实现 CRUD 操作,以及如何使用 Testcontainers 进行测试。


参考:https://www.sivalabs.in/spring-boot-jdbctemplate-tutorial/

赞(4)
未经允许不得转载:工具盒子 » Spring Boot Jdbctemplate 指南