​Spring JdbcTemplate​​使用实例

​JDBC 基础​

Java程序使用​​JDBC​​接口访问关系数据库的时候,需要以下几步:

  • 创建全局​​DataSource​​实例,表示数据库连接池;
  • 在需要读写数据库的方法内部,按如下步骤访问数据库:
  • 从全局​​DataSource​​实例获取​​Connection​​实例;
  • 通过​​Connection​​实例创建​​PreparedStatement​​实例;
  • 执行​​SQL​​语句,如果是查询,则通过​​ResultSet​​读取结果集,如果是修改,则获得​​int​​结果。

正确编写​​JDBC​​代码的关键是使用​​try ... finally​​释放资源,涉及到事务的代码需要正确提交或回滚事务。

在​​Spring​​使用​​JDBC​​,首先我们通过​​IoC​​容器创建并管理一个​​DataSource​​实例,然后,​​Spring​​提供了一个​​JdbcTemplate​​,可以方便地让我们操作​​JDBC​​,因此,通常情况下,我们会实例化一个​​JdbcTemplate​​。顾名思义,这个类主要使用了​​Template​​模式。

编写示例代码或者测试代码时,我们强烈推荐使用​​HSQLDB​​这个数据库,它是一个用Java编写的关系数据库,可以以内存模式或者文件模式运行,本身只有一个​​jar​​包,非常适合演示代码或者测试代码,本文使用MySql

我们以实际工程为例,先创建​​Maven​​工程​​spring-data-jdbc​​,然后引入以下依赖:

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.5.0</version>
</dependency>
</dependencies>

在​​AppConfig​​中,我们需要创建以下几个必须的​​Bean​​:

@Configuration
@ComponentScan
@PropertySource("jdbc.properties")
public class AppConfig {

@Value("${jdbc.url}")
String jdbcUrl;

@Value("${jdbc.username}")
String jdbcUsername;

@Value("${jdbc.password}")
String jdbcPassword;

@Bean
DataSource createDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(jdbcUrl);
config.setUsername(jdbcUsername);
config.setPassword(jdbcPassword);
config.addDataSourceProperty("autoCommit", "true");
config.addDataSourceProperty("connectionTimeout", "5");
config.addDataSourceProperty("idleTimeout", "60");
return new HikariDataSource(config);
}

@Bean
JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}

在上述配置中:

  • 通过​​@PropertySource("jdbc.properties")​​读取数据库配置文件;
  • 通过​​@Value("${jdbc.url}")​​注入配置文件的相关配置;
  • 创建一个​​DataSource​​实例,它的实际类型是​​HikariDataSource​​,创建时需要用到注入的配置;
  • 创建一个​​JdbcTemplate​​实例,它需要注入​​DataSource​​,这是通过方法参数完成注入的。

最后,针对​​HSQLD​​B写一个配置文件​​jdbc.properties​​:

# 数据库文件名为testdb:
jdbc.url=jdbc:hsqldb:file:testdb

# Hsqldb默认的用户名是sa,口令是空字符串:
jdbc.username=sa
jdbc.password=


# mysql配置
# 数据库文件名为testdb:
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/springjdbc?characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false

jdbc.user=root
jdbc.password=1234

可以通过​​HSQLDB​​自带的工具来初始化数据库表,这里我们写一个​​Bean​​,在​​Spring​​容器启动时自动创建一个​​users​​表:

@Component
public class DatabaseInitializer {
@Autowired
JdbcTemplate jdbcTemplate;

@PostConstruct
public void init() {
jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" //
+ "id BIGINT IDENTITY NOT NULL PRIMARY KEY, " //
+ "email VARCHAR(100) NOT NULL, " //
+ "password VARCHAR(100) NOT NULL, " //
+ "name VARCHAR(100) NOT NULL, " //
+ "UNIQUE (email))");
}
}

现在,所有准备工作都已完毕。我们只需要在需要访问数据库的​​Bean​​中,注入​​JdbcTemplate​​即可:

@Component
public class UserService {
@Autowired
JdbcTemplate jdbcTemplate;
...
}



​JdbcTemplate​​ 用法

​Spring​​提供的​​JdbcTemplate​​采用​​Template​​模式,提供了一系列以回调为特点的工具方法,目的是避免繁琐的​​try...catch​​语句。

我们以具体的示例来说明​​JdbcTemplate​​的用法。

首先我们看​​T execute(ConnectionCallback<​​T​​> action)​​方法,它提供了​​Jdbc​​的​​Connection​​供我们使用:

public User getUserById(long id) {
// 注意传入的是ConnectionCallback:
return jdbcTemplate.execute((Connection conn) -> {
// 可以直接使用conn实例,不要释放它,回调结束后JdbcTemplate自动释放:
// 在内部手动创建的PreparedStatement、ResultSet必须用try(...)释放:
try (var ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
ps.setObject(1, id);
try (var rs = ps.executeQuery()) {
if (rs.next()) {
return new User( // new User object:
rs.getLong("id"), // id
rs.getString("email"), // email
rs.getString("password"), // password
rs.getString("name")); // name
}
throw new RuntimeException("user not found by id.");
}
}
});
}

也就是说,上述回调方法允许获取​​Connection​​,然后做任何基于​​Connection​​的操作。

我们再看​​T execute(String sql, PreparedStatementCallback<'T'> action)​​的用法:

public User getUserByName(String name) {
// 需要传入SQL语句,以及PreparedStatementCallback:
return jdbcTemplate.execute("SELECT * FROM users WHERE name = ?", (PreparedStatement ps) -> {
// PreparedStatement实例已经由JdbcTemplate创建,并在回调后自动释放:
ps.setObject(1, name);
try (var rs = ps.executeQuery()) {
if (rs.next()) {
return new User( // new User object:
rs.getLong("id"), // id
rs.getString("email"), // email
rs.getString("password"), // password
rs.getString("name")); // name
}
throw new RuntimeException("user not found by id.");
}
});
}

最后,我们看​​T queryForObject(String sql, Object[] args, RowMapper<'T'> rowMapper)​​方法:

public User getUserByEmail(String email) {
// 传入SQL,参数和RowMapper实例:
return jdbcTemplate.queryForObject("SELECT * FROM users WHERE email = ?", new Object[] { email },
(ResultSet rs, int rowNum) -> {
// 将ResultSet的当前行映射为一个JavaBean:
return new User( // new User object:
rs.getLong("id"), // id
rs.getString("email"), // email
rs.getString("password"), // password
rs.getString("name")); // name
});
}

​RowMapper​​定义:

@FunctionalInterface
public interface RowMapper<T> {
@Nullable
T mapRow(ResultSet var1, int var2) throws SQLException;
}

方法​​getUserByEmail​​中​​(ResultSet rs, int rowNum) -> {}​​语句实现一个RowMapper继承类的对象

在​​queryForObject()​​方法中,传入​​SQL​​以及​​SQL​​参数后,​​JdbcTemplate​​会自动创建​​PreparedStatement​​,自动执行查询并返回​​ResultSet​​,我们提供的​​RowMapper​​需要做的事情就是把​​ResultSet​​的当前行映射成一个​​JavaBean​​并返回。整个过程中,使用​​Connection​​、​​PreparedStatement​​和​​ResultSet​​都不需要我们手动管理。

​RowMapper​​不一定返回​​JavaBean​​,实际上它可以返回任何Java对象。例如,使用​​SELECT COUNT(*)​​查询时,可以返回​​Long​​:

public long getUsers() {
return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM users", null, (ResultSet rs, int rowNum) -> {
// SELECT COUNT(*)查询只有一列,取第一列数据:
return rs.getLong(1);
});
}

如果我们期望返回多行记录,而不是一行,可以用​​query()​​方法:

public List<User> getUsers(int pageIndex) {
int limit = 100;
int offset = limit * (pageIndex - 1);
return jdbcTemplate.query("SELECT * FROM users LIMIT ? OFFSET ?", new Object[] { limit, offset },
new BeanPropertyRowMapper<>(User.class));
}

上述​​query()​​方法传入的参数仍然是​​SQL​​、​​SQL​​参数以及​​RowMapper​​实例。这里我们直接使用Spring提供的​​BeanPropertyRowMapper​​。如果数据库表的结构恰好和​​JavaBean​​的属性名称一致,那么​​BeanPropertyRowMapper​​就可以直接把一行记录按列名转换为​​JavaBean​​。

如果我们执行的不是查询,而是插入、更新和删除操作,那么需要使用​​update()​​方法:

public void updateUser(User user) {
// 传入SQL,SQL参数,返回更新的行数:
if (1 != jdbcTemplate.update("UPDATE user SET name = ? WHERE id=?", user.getName(), user.getId())) {
throw new RuntimeException("User not found by id");
}
}

只有一种​​INSERT​​操作比较特殊,那就是如果某一列是自增列(例如自增主键),通常,我们需要获取插入后的自增值。​​JdbcTemplate​​提供了一个​​KeyHolder​​来简化这一操作:

public User register(String email, String password, String name) {
// 创建一个KeyHolder:
KeyHolder holder = new GeneratedKeyHolder();
if (1 != jdbcTemplate.update(
// 参数1:PreparedStatementCreator
(conn) -> {
// 创建PreparedStatement时,必须指定RETURN_GENERATED_KEYS:
var ps = conn.prepareStatement("INSERT INTO users(email,password,name) VALUES(?,?,?)",
Statement.RETURN_GENERATED_KEYS);
ps.setObject(1, email);
ps.setObject(2, password);
ps.setObject(3, name);
return ps;
},
// 参数2:KeyHolder
holder)
) {
throw new RuntimeException("Insert failed.");
}
// 从KeyHolder中获取返回的自增值:
return new User(holder.getKey().longValue(), email, password, name);
}

​JdbcTemplate​​还有许多重载方法,这里我们不一一介绍。需要强调的是,​​JdbcTemplate​​只是对​​JDBC​​操作的一个简单封装,它的目的是尽量减少手动编写​​try(resource) {...}​​的代码,对于查询,主要通过​​RowMapper​​实现了​​JDBC​​结果集到Java对象的转换。

我们总结一下​​JdbcTemplate​​的用法,那就是:

  • 针对简单查询,优选​​query()​​和​​queryForObject()​​,因为只需提供​​SQL​​语句、参数和​​RowMapper​​;
  • 针对更新操作,优选​​update()​​,因为只需提供​​SQL​​语句和参数;
  • 任何复杂的操作,最终也可以通过​​execute(ConnectionCallback)​​实现,因为拿到​​Connection​​就可以做任何​​JDBC​​操作。

实际上我们使用最多的仍然是各种查询。如果在设计表结构的时候,能够和​​JavaBean​​的属性一一对应,那么直接使用​​BeanPropertyRowMapper​​就很方便。如果表结构和​​JavaBean​​不一致怎么办?那就需要稍微改写一下查询,使结果集的结构和​​JavaBean​​保持一致。

例如,表的列名是​​office_address​​,而​​JavaBean​​属性是​​workAddress​​,就需要指定别名,改写查询如下:

SELECT id, email, office_address AS workAddress, name FROM users WHERE email = ?

​RowMapper 的基本使用​

使用过原生​​JDBC​​的朋友应该知道,从数据库查询出来的记录全都被保存在​​ResultSet​​结果集中,我们需要将结果集中的数据一条条地获取并设置到具体的实体类上,如此,该实体类才能在接下来的程序中使用。然而问题是,每次都要这么操作实在是太麻烦了,​​Spring​​就不应该提供什么功能来替我们做这些事情吗?答案当然是有的,那就是本文的主角——​​RowMapper​​。

​Spring JDBC​​中目前有两个主要的​​RowMapper​​实现,使用它们应该能解决大部分的场景了。

​SingleColumnRowMapper​

通过名字我们就能大概了解,在查询返回单列数据的时候,就该使用这个​​RowMapper​​,下面我们来看看具体的代码:

@Override
public String getStudentNameById(String id) {
String sql = "select name from test_student where id = ?";
return this.jdbcTemplate.queryForObject(sql, new Object[]{id},
new SingleColumnRowMapper<>(String.class));
}

@Override
public List<String> getStudentNamesByGrade(Integer grade) {
String sql = "select name from test_student where grade = ?";
return this.jdbcTemplate.query(sql, new Object[]{grade},
new SingleColumnRowMapper<>(String.class));
}

测试:

@Test
public void getStudentNameById(){
String name = studentService.getStudentNameById("3");
assertEquals("李清照", name);
}

@Test
public void getStudentNamesByGrade(){
List<String> names = studentService.getStudentNamesByGrade(1);
assertTrue(2 == names.size());
}

​BeanPropertyRowMapper​

当查询数据库返回的是多列数据,且你需要将这些多列数据映射到某个具体的实体类上,那么就该使用这个​​RowMapper​​,下面是具体的使用代码:

@Override
public Student getStudentByName2(String name) {
String sql = "select name, gender from test_student where name = ?";
return this.jdbcTemplate.queryForObject(sql, new Object[]{name},
new BeanPropertyRowMapper<>(Student.class));
}

@Override
public List<Student> getStudentsByName2(String name) {
String sql = "select name, gender from test_student where name = ?";
return this.jdbcTemplate.query(sql, new Object[]{name},
new BeanPropertyRowMapper<>(Student.class));

这种使用方式有一个前提,那就是数据库​​SQL​​查出来的数据其列名与实体类中的属性名是一致的,当然个数和顺序可以不一致。比如数据库​​SQL​​查出来的姓名列叫​​name​​,那么对应的实体类中的姓名也必须叫​​name​​,而不能叫​​studentName​​或者其它。

定义自己的​​RowMapper​

当然,如果你​​SQL​​查询出来的数据列名就是和实体类的属性名不一样,或者想按照自己的规则来装配实体类,那么就可以定义并使用自己的​​Row Mapper​​。

public class StudentRowMapper implements RowMapper<Student> {

@Override
public Student mapRow(ResultSet rs, int i) throws SQLException {
Student student = new Student();
student.setName(rs.getString("name"));
student.setGender(rs.getString("gender"));
student.setEmail(rs.getString("email"));
return student;
}
}

@Override
public Student getStudentByName3(String name) {
String sql = "select name, gender, email from test_student where name = ?";
return this.jdbcTemplate.queryForObject(sql, new Object[]{name}, new StudentRowMapper());
}

@Override
public List<Student> getStudentsByName3(String name) {
String sql = "select name, gender, email from test_student where name = ?";
return this.jdbcTemplate.query(sql, new Object[]{name}, new StudentRowMapper());
}

小结

  • Spring提供了JdbcTemplate来简化JDBC操作;
  • 使用JdbcTemplate时,根据需要优先选择高级方法;
  • 任何JDBC操作都可以使用保底的execute(ConnectionCallback)方法。