使用JDBC
我们在前面介绍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包,非常适合演示代码或者测试代码。
我们以实际工程为例,先创建Maven工程spring-data-jdbc
,然后引入以下依赖:
- org.springframework:spring-context:6.0.0
- org.springframework:spring-jdbc:6.0.0
- jakarta.annotation:jakarta.annotation-api:2.1.1
- com.zaxxer:HikariCP:5.0.1
- org.hsqldb:hsqldb:2.7.1
在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
,这是通过方法参数完成注入的。
最后,针对HSQLDB写一个配置文件jdbc.properties
:
# 数据库文件名为testdb:
jdbc.url=jdbc:hsqldb:file:testdb
# Hsqldb默认的用户名是sa,口令是空字符串:
jdbc.username=sa
jdbc.password=
可以通过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, RowMapper<T> rowMapper, Object... args)
方法:
public User getUserByEmail(String email) {
// 传入SQL,参数和RowMapper实例:
return jdbcTemplate.queryForObject("SELECT * FROM users WHERE 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
},
email);
}
在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", (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 BeanPropertyRowMapper<>(User.class),
limit, offset);
}
上述query()
方法传入的参数仍然是SQL、SQL参数以及RowMapper
实例。这里我们直接使用Spring提供的BeanPropertyRowMapper
。如果数据库表的结构恰好和JavaBean的属性名称一致,那么BeanPropertyRowMapper
就可以直接把一行记录按列名转换为JavaBean。
如果我们执行的不是查询,而是插入、更新和删除操作,那么需要使用update()
方法:
public void updateUser(User user) {
// 传入SQL,SQL参数,返回更新的行数:
if (1 != jdbcTemplate.update("UPDATE users 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 = ?
练习
使用JdbcTemplate。
小结
Spring提供了JdbcTemplate
来简化JDBC操作;
使用JdbcTemplate
时,根据需要优先选择高级方法;
任何JDBC操作都可以使用保底的execute(ConnectionCallback)
方法。