Spring Boot Jdbctemplate 指南-元一软件

未分类3个月前发布 元一软件
20 0

Spring Boot 对 JDBC 的支持

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

Java(编程语言)

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

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

首先,访问 https://start.springboot.io/,选择 JDBC API、PostgreSQL Driver 和 Testcontainers starter,创建 Spring Boot 应用程序。

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

初始化数据源

Spring Boot 提供了一种方便的数据库初始化机制。我们可以在 src/main/resources 下创建 schema.sql 和 data.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.sql 和 data.sql 初始化数据库的简便方法,但我们更倾向于使用合适的数据库迁移工具,如 Liquibase 或 Flyway。

使用 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() 方法

当我们查询数据库时,它会返回一个 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 异常。因此,最好使用分页功能,只获取部分记录并进行处理。

Java(编程语言)

实现 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() 方法中,我们将向 bookmarks 表插入一条记录,并返回自增的主键值。

public Long create(Bookmark bookmark) {

    KeyHolder keyHolder = new GeneratedKeyHolder();



    jdbcTemplate.update(connection -> {

        String sql = "insert into bookmarks(title, url, created_at) values(?,?,?)";

        PreparedStatement ps = connection.prepareStatement(sql, new String[] { "id" });

        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() 方法:如果给定 id 的 bookmark 记录存在,我们就需要更新 title 和 url 列。否则,将抛出异常。

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() 方法:如果指定 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

在测试中,我们应该使用相同类型的 数据库来测试 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<Bookmark> bookmarks = bookmarkRepository.findAll();

        assertThat(bookmarks).isNotEmpty();

        assertThat(bookmarks).hasSize(15);

    }



    @Test

    void shouldCreateBookmark() {

        Bookmark bookmark = new Bookmark(null, "My Title", "https://sivalabs.in", Instant.now());

        Long id = bookmarkRepository.save(bookmark);

        assertThat(id).isNotNull();

    }



    @Test

    void shouldGetBookmarkById() {

        Bookmark bookmark = new Bookmark(null, "My Title", "https://sivalabs.in", Instant.now());

        Long id = bookmarkRepository.save(bookmark);



        Optional<Bookmark> bookmarkOptional = bookmarkRepository.findById(id);

        assertThat(bookmarkOptional).isPresent();

    }



    @Test

    void shouldEmptyWhenBookmarkNotFound() {

        Optional<Bookmark> bookmarkOptional = bookmarkRepository.findById(9999L);

        assertThat(bookmarkOptional).isEmpty();

    }



    @Test

    void shouldUpdateBookmark() {

        Bookmark bookmark = new Bookmark(null, "My Title", "https://sivalabs.in", Instant.now());

        Long id = bookmarkRepository.save(bookmark);



        Bookmark updatedBookmark = new Bookmark(id, "My Updated Title", "https://www.sivalabs.in", bookmark.createdAt());

        bookmarkRepository.update(updatedBookmark);



        updatedBookmark = bookmarkRepository.findById(id).orElseThrow();

        assertThat(updatedBookmark.id()).isEqualTo(id);

        assertThat(updatedBookmark.title()).isEqualTo("My Updated Title");

        assertThat(updatedBookmark.url()).isEqualTo("https://www.sivalabs.in");

    }

    

    @Test

    void shouldDeleteBookmark() {

        Bookmark bookmark = new Bookmark(null, "My Title", "https://sivalabs.in", 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/

© 版权声明