说说如何在 Spring Boot 中使用 JdbcTemplate 读写数据

首先在 pom.xml 中引入依赖。

<!--jdbc-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!--h2-->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

示例程序打算使用 h2 缓存数据库,所以这里也一并引用。

1 h2 缓存数据库

h2是一个开源的嵌入式(非嵌入式设备)数据库引擎,基于Java开发,可直接嵌入到应用程序中,与应用程序一起打包发布,不受平台限制。

启动应用后,在浏览器地址栏输入 http://127.0.0.1:8080/h2-console,就可以打开 h2 控制台。

首先选择控制台编码格式为中文,接着输入 JDBC URL,然后点击“测试连接”,如果连接成功,就会提示“测试成功”。

最后点击“连接”按钮,就会打开数据库控制台客户端,连接到 h2 数据库:

2 初始化表结构与数据

在 src/main/resources/ 下,新建 schema.sql 文件编写表结构 SQL。在同一个目录下,新建 data.sql 文件,编写初始化数据 SQL。这样在应用启动时,Spring Boot 就会执行这些脚本。

schema.sql:

create table if not exists Book
( id varchar( 4) not null, name varchar( 25) not null, type varchar( 10) not null );

data.sql:

insert into Book
  (id, name, type)
values
  ('1', '两京十五日', '小说');
insert into Book
  (id, name, type)
values
  ('2', '把自己作为方法', '历史');
insert into Book
  (id, name, type)
values
  ('3', '正常人', '小说');

启动成功后,就会在 h2 数据库控制台客户端中看到新建好的表与数据。

点击左侧的 Book,就会在右侧的 SQL 输入框中自动生成查询该表的 SQL 语句,然后点击 “Run”,执行它。我们就会在右下角看到初始化的表数据。

3 编码

3.1 新建实体类

@Data
@RequiredArgsConstructor
public class Book {

    private final String id;
    private final String name;
    private final String type;
}

这里用了 Lombok 插件。Lombok 是一种 Java 实用工具,可用来帮助我们消除 Java 冗长的样板式代码。

加了 @Data 注解的Java 类,在编译之后会自动为我们加上这些方法:

  1. 所有属性的get和set方法;
  2. toString 方法;
  3. hashCode方法;
  4. equals方法。

@RequiredArgsConstructor 注解会将类中所有带有 @NonNull 注解和以final修饰的未经初始化的字段作为构造函数的入参。

3.2 新建 Repository 类

首先定义一个 Repository 接口,然后新建这个接口的实现类。

接口:

public interface BookRepository {

    Iterable<Book> findAll();

    Book findOne(String id);

    Book save(Book Book);
}

实现类:

@Repository
public class JdbcBookRepository implements BookRepository {

    private JdbcTemplate jdbc;

    @Autowired
    public JdbcBookRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    @Override
    public Iterable<Book> findAll() {
        return null;
    }

    @Override
    public Book findOne(String id) {
        return null;
    }

    @Override
    public Book save(Book Book) {
        return null;
    }
}

@Repository和@Controller、@Service、@Component的作用差不多,目的都是把对象交给spring管理。@Repository一般用在持久层的实现类上。

@Autowired注解可通过byType的形式,来给指定的字段或方法注入所需的外部资源。

autowire 有以下四种模式:

模式 说明
byName 根据属性的名字自动装配
byType 根据属性的类型自动装配
constructor 与 byType 类似,不同之处在于它应用于构造器参数,如果没有找到会抛出异常
autodetect 会在 byType 和 constructor 中智能选择

这里通过 @Autowired 标注的构造器将 JdbcTemplate 注入进来。这个构造器将 JdbcTemplate 赋值给一个实例变量,这个变量会被其他方法用来执行数据库查询或更新等操作。

3.3 查询操作

假设我们需要查询出所有的书籍,那么就可以调用 JdbcTemplate 的 List<T> query(String sql, RowMapper<T> rowMapper) 方法。

@Override
public Iterable<Book> findAll() {
    return jdbc.query("select id, name, type from Book",
            this::mapRowToBook);
}

private Book mapRowToBook(ResultSet rs, int rowNum) throws SQLException {
    return new Book(rs.getString("id"), rs.getString("name"),
            rs.getString("type"));
}

这里利用了 Java 的方法引用,来编写 RowMapper 入参。这样做的好处是:相对于原来的匿名内部类的写法,方法引用的写法更加简洁。

3.4 update()

JdbcTemplate 的 update() 方法可以用来新增或更新数据。

@Override
public Book save(Book book) {
    jdbc.update("insert into Book (id,name,type) values (?,?,?)",
            book.getId(),
            book.getName(),
            book.getType()
    );
    return book;
}

update() 方法定义如下:

int update(String sql, @Nullable Object... args)

它接受一个包含占位符的 SQL 语句以及多个入参。返回实际影响到的记录数。

建立 Spring 单元测试类来验证刚刚新建的 save() 方法:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class JdbcBookRepositoryTest {

    @Autowired
    private JdbcBookRepository jdbcBookRepository;

    @Test
    public void save() {

        Book book = new Book("4", "比利时的哀愁", "小说");
        jdbcBookRepository.save(book);
        Book find=jdbcBookRepository.findOne("4");
        assertEquals("比利时的哀愁",find.getName());
    }
}

3.5 SimpleJdbcInsert 包装器类

SimpleJdbcInsert 一般用于多表插入场景。SimpleJdbcInsert 有两个方法执行数据插入操作: execute() 和 executeAndReturnKey()。 它们都接受 Map<String ,Object> 作为参数,其中的 key 对应数据表中的列名,而 value 对应要插入到列中的实际值。

我们举一个图书示例。一本图书可以包含多个标签;而一个标签也可以隶属于多本图书。它们之间是多对多的关系,因此建立图书与标签的映射表来专门存放这些关系。具体如下图所示:

首先在 schema.sql 中,加入这些表结构创建语句:

create table if not exists Book
( id identity, name varchar( 25) not null, type varchar( 10) not null );


create table if not exists Tag
( id identity, name varchar( 25) not null);


create table if not exists Book_Tags ( book bigint not null, tag bigint not null );

alter table Book_Tags add foreign key (book) references Book( id);
alter table Book_Tags add foreign key (tag) references Tag( id);

接着,建立这些实体类:

@Data
@RequiredArgsConstructor
public class Tag {

    private final Long id;
    private final String name;
}

@Data
@RequiredArgsConstructor
public class Book {

    private final String id;
    private final String name;
    private final String type;

    private List<Tag> tags = new ArrayList<>();
}

然后在 Repository 实现类的构造函数中,初始化每张表的 SimpleJdbcInsert 实例:

@Repository
public class JdbcBookRepository implements BookRepository {

    private static final Logger log = LogManager.getFormatterLogger();

    private final SimpleJdbcInsert bookInserter;
    private final SimpleJdbcInsert tagInserter;
    private final SimpleJdbcInsert bookTagsInserter;
    private final ObjectMapper objectMapper;
    private JdbcTemplate jdbc;

    @Autowired
    public JdbcBookRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
        this.bookInserter = new SimpleJdbcInsert(jdbc)
                .withTableName("Book")
                .usingGeneratedKeyColumns("id");

        this.tagInserter = new SimpleJdbcInsert(jdbc)
                .withTableName("Tag")
                .usingGeneratedKeyColumns("id");

        this.bookTagsInserter = new SimpleJdbcInsert(jdbc)
                .withTableName("Book_Tags");

        this.objectMapper = new ObjectMapper();
    }
    ...
}

SimpleJdbcInsert 的 withTableName() 方法用于指定表名;而 usingGeneratedKeyColumns() 方法用于指定主键。

具体保存操作代码为:

public Book saveIncludeTags(Book book) {
    //保存图书
    Map<String, Object> values = objectMapper.convertValue(book, Map.class);
    long bookId = bookInserter.executeAndReturnKey(values).longValue();

    //保存标签
    List<Long> tagIds = new ArrayList<>();
    for (Tag tag : book.getTags()) {
        values = objectMapper.convertValue(tag, Map.class);
        long tagId = tagInserter.executeAndReturnKey(values).longValue();
        tagIds.add(tagId);
    }

    //关联图书与标签
    for (Long tagId : tagIds) {
        values.clear();
        values.put("book", bookId);
        values.put("tag", tagId);
        log.info("values -> %s", values);
        bookTagsInserter.execute(values);
    }

    return book;
}
  • SimpleJdbcInsert 的 executeAndReturnKey() 与 execute() 方法都支持 Map<String, ?> 形式的入参。它们之间的区别是 executeAndReturnKey() 会返回 Number 形式的主键值。
  • 可以利用 Jackson 的 ObjectMapper.convertValue(Object fromValue, Class<T> toValueType) 方法把一个 POJO 转换为相应的 Map 对象值。
  • Number 类型可以根据场景对其进行转换。

这段代码首先先保存图书,得到图书主键;然后保存标签,得到标签主键;最后把前面得到的图书主键与标签主键保存到它们之间的关系表中。

猜你喜欢

转载自blog.csdn.net/deniro_li/article/details/108807674