阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

使用JDBC

19次阅读
没有评论

共计 6352 个字符,预计需要花费 16 分钟才能阅读完成。

我们在前面介绍 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);
    }
}

在上述配置中:

  1. 通过 @PropertySource("jdbc.properties") 读取数据库配置文件;
  2. 通过 @Value("${jdbc.url}") 注入配置文件的相关配置;
  3. 创建一个 DataSource 实例,它的实际类型是HikariDataSource,创建时需要用到注入的配置;
  4. 创建一个 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 并返回。整个过程中,使用 ConnectionPreparedStatementResultSet都不需要我们手动管理。

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) 方法。

正文完
星哥说事-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-08-05发表,共计6352字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中