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

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

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

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

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

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

    1. @Configuration
    2. @ComponentScan
    3. @PropertySource("jdbc.properties")
    4. public class AppConfig {
    5. @Value("${jdbc.url}")
    6. String jdbcUrl;
    7. @Value("${jdbc.username}")
    8. String jdbcUsername;
    9. @Value("${jdbc.password}")
    10. String jdbcPassword;
    11. @Bean
    12. DataSource createDataSource() {
    13. HikariConfig config = new HikariConfig();
    14. config.setJdbcUrl(jdbcUrl);
    15. config.setUsername(jdbcUsername);
    16. config.setPassword(jdbcPassword);
    17. config.addDataSourceProperty("autoCommit", "true");
    18. config.addDataSourceProperty("connectionTimeout", "5");
    19. config.addDataSourceProperty("idleTimeout", "60");
    20. return new HikariDataSource(config);
    21. }
    22. @Bean
    23. JdbcTemplate createJdbcTemplate(@Autowired DataSource dataSource) {
    24. return new JdbcTemplate(dataSource);
    25. }
    26. }

    在上述配置中:

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

    最后,针对HSQLDB写一个配置文件jdbc.properties

    1. # 数据库文件名为testdb:
    2. jdbc.url=jdbc:hsqldb:file:testdb
    3. # Hsqldb默认的用户名是sa,口令是空字符串:
    4. jdbc.username=sa

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

    1. @Component
    2. public class DatabaseInitializer {
    3. @Autowired
    4. JdbcTemplate jdbcTemplate;
    5. @PostConstruct
    6. public void init() {
    7. jdbcTemplate.update("CREATE TABLE IF NOT EXISTS users (" //
    8. + "id BIGINT IDENTITY NOT NULL PRIMARY KEY, " //
    9. + "email VARCHAR(100) NOT NULL, " //
    10. + "password VARCHAR(100) NOT NULL, " //
    11. + "UNIQUE (email))");
    12. }
    13. }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    1. public User register(String email, String password, String name) {
    2. // 创建一个KeyHolder:
    3. KeyHolder holder = new GeneratedKeyHolder();
    4. if (1 != jdbcTemplate.update(
    5. // 参数1:PreparedStatementCreator
    6. (conn) -> {
    7. // 创建PreparedStatement时,必须指定RETURN_GENERATED_KEYS:
    8. var ps = conn.prepareStatement("INSERT INTO users(email,password,name) VALUES(?,?,?)",
    9. Statement.RETURN_GENERATED_KEYS);
    10. ps.setObject(1, email);
    11. ps.setObject(2, password);
    12. ps.setObject(3, name);
    13. return ps;
    14. },
    15. // 参数2:KeyHolder
    16. holder)
    17. ) {
    18. throw new RuntimeException("Insert failed.");
    19. }
    20. // 从KeyHolder中获取返回的自增值:
    21. return new User(holder.getKey().longValue(), email, password, name);
    22. }

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

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

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

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

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

    从下载练习:使用JdbcTemplate (推荐使用快速下载)

    Spring提供了JdbcTemplate来简化JDBC操作;

    使用JdbcTemplate时,根据需要优先选择高级方法;

    任何JDBC操作都可以使用保底的execute(ConnectionCallback)方法。