07 | 数据访问:如何使用 JdbcTemplate 访问关系型数据库?

06 讲我们详细介绍了 JDBC 规范的相关内容,JDBC 规范是 Java 领域中使用最广泛的数据访问标准,目前市面上主流的数据访问框架都是构建在 JDBC 规范之上。

因为 JDBC 是偏底层的操作规范,所以关于如何使用 JDBC 规范进行关系型数据访问的实现方式有很多(区别在于对 JDBC 规范的封装程度不同),而在 Spring 中,同样提供了 JdbcTemplate 模板工具类实现数据访问,它简化了 JDBC 规范的使用方法,今天我们将围绕这个模板类展开讨论。

数据模型和 Repository 层设计

引入 JdbcTemplate 模板工具类之前,我们回到 SpringCSS 案例,先给出 order-service 中的数据模型为本讲内容的展开做一些铺垫。

我们知道一个订单中往往涉及一个或多个商品,所以在本案例中,我们主要通过一对多的关系来展示数据库设计和实现方面的技巧。而为了使描述更简单,我们把具体的业务字段做了简化。Order 类的定义如下代码所示:

复制代码
  1. public class Order{
  2.     private Long id; //订单Id
  3.     private String orderNumber; //订单编号
  4.     private String deliveryAddress; //物流地址
  5.     private List<Goods> goodsList;  //商品列表
  6.     //省略了 getter/setter
  7. }

其中代表商品的 Goods 类定义如下:

复制代码
  1. public class Goods {
  2.     private Long id; //商品Id
  3.     private String goodsCode; //商品编号
  4.     private String goodsName; //商品名称
  5.     private Double price; //商品价格
  6.     //省略了 getter/setter
  7. }

从以上代码,我们不难看出一个订单可以包含多个商品,因此设计关系型数据库表时,我们首先会构建一个中间表来保存 Order 和 Goods 这层一对多关系。在本课程中,我们使用 MySQL 作为关系型数据库,对应的数据库 Schema 定义如下代码所示:

复制代码
  1. DROP TABLE IF EXISTS `order`;
  2. DROP TABLE IF EXISTS `goods`;
  3. DROP TABLE IF EXISTS `order_goods`;
  4. create table `order` (
  5.     `id` bigint(20) NOT NULL AUTO_INCREMENT,
  6.     `order_number` varchar(50) not null,
  7.     `delivery_address` varchar(100) not null,
  8.   `create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,
  9.     PRIMARY KEY (`id`)
  10. );
  11. create table `goods` (
  12.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  13.   `goods_code` varchar(50) not null,
  14.   `goods_name` varchar(50) not null,
  15.   `goods_price` double not null,
  16.   `create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,
  17.     PRIMARY KEY (`id`)
  18. );
  19. create table `order_goods` (
  20.     `order_id` bigint(20) not null,
  21.     `goods_id` bigint(20) not null,
  22.     foreign key(`order_id`) references `order`(`id`),
  23.     foreign key(`goods_id`) references `goods`(`id`)
  24. );

基于以上数据模型,我们将完成 order-server 中的 Repository 层组件的设计和实现。首先,我们需要设计一个 OrderRepository 接口,用来抽象数据库访问的入口,如下代码所示:

复制代码
  1. public interface OrderRepository {
  2.     Order addOrder(Order order);
  3.     Order getOrderById(Long orderId);
  4.     Order getOrderDetailByOrderNumber(String orderNumber);
  5. }

这个接口非常简单,方法都是自解释的。不过请注意,这里的 OrderRepository 并没有继承任何父接口,完全是一个自定义的、独立的 Repository。

针对上述 OrderRepository 中的接口定义,我们将构建一系列的实现类。

  • OrderRawJdbcRepository:使用原生 JDBC 进行数据库访问

  • OrderJdbcRepository:使用 JdbcTemplate 进行数据库访问

  • OrderJpaRepository:使用 Spring Data JPA 进行数据库访问

上述实现类中的 OrderJpaRepository 我们会放到 10 讲《ORM 集成:如何使用 Spring Data JPA 访问关系型数据库?》中进行展开,而 OrderRawJdbcRepository 最基础,不是本课程的重点,因此 07 讲我们只针对 OrderRepository 中 getOrderById 方法的实现过程重点介绍,也算是对 06 讲的回顾和扩展。

OrderRawJdbcRepository 类中实现方法如下代码所示:

复制代码
  1. @Repository("orderRawJdbcRepository")
  2. public class OrderRawJdbcRepository implements OrderRepository {
  3.     @Autowired
  4.     private DataSource dataSource;
  5.     @Override
  6.     public Order getOrderById(Long orderId) {
  7.         Connection connection = null;
  8.         PreparedStatement statement = null;
  9.         ResultSet resultSet = null;
  10.         try {
  11.             connection = dataSource.getConnection();
  12.             statement = connection.prepareStatement("select id, order_number, delivery_address from `order` where id=?");
  13.             statement.setLong(1, orderId);
  14.             resultSet = statement.executeQuery();
  15.             Order order = null;
  16.             if (resultSet.next()) {
  17.                 order = new Order(resultSet.getLong("id"), resultSet.getString("order_number"),
  18.                         resultSet.getString("delivery_address"));
  19.             }
  20.             return order;
  21.         } catch (SQLException e) {
  22.             System.out.print(e);
  23.         } finally {
  24.             if (resultSet != null) {
  25.                 try {
  26.                     resultSet.close();
  27.                 } catch (SQLException e) {
  28.                 }
  29.             }
  30.             if (statement != null) {
  31.                 try {
  32.                     statement.close();
  33.                 } catch (SQLException e) {
  34.                 }
  35.             }
  36.             if (connection != null) {
  37.                 try {
  38.                     connection.close();
  39.                 } catch (SQLException e) {
  40.                 }
  41.             }
  42.         }
  43.         return null;
  44.     }
  45.     //省略其他 OrderRepository 接口方法实现
  46. }

这里,值得注意的是,我们首先需要在类定义上添加 @Repository 注解,标明这是能够被 Spring 容器自动扫描的 Javabean,再在 @Repository 注解中指定这个 Javabean 的名称为"orderRawJdbcRepository",方便 Service 层中根据该名称注入 OrderRawJdbcRepository 类。

可以看到,上述代码使用了 JDBC 原生 DataSource、Connection、PreparedStatement、ResultSet 等核心编程对象完成针对“order”表的一次查询。代码流程看起来比较简单,其实也比较烦琐,学到这里,我们可以结合上一课时的内容理解上述代码。

请注意,如果我们想运行这些代码,千万别忘了在 Spring Boot 的配置文件中添加对 DataSource 的定义,如下代码所示:

复制代码
  1. spring:
  2.   datasource:
  3.     driver-class-name: com.mysql.cj.jdbc.Driver
  4.     url: jdbc:mysql://127.0.0.1:3306/appointment
  5.     username: root
  6.     password: root

回顾完原生 JDBC 的使用方法,接下来就引出今天的重点,即 JdbcTemplate 模板工具类,我们来看看它如何简化数据访问操作。

使用 JdbcTemplate 操作数据库

要想在应用程序中使用 JdbcTemplate,首先我们需要引入对它的依赖,如下代码所示:

复制代码
  1. <dependency>
  2.     <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-jdbc</artifactId>
  4. </dependency>

JdbcTemplate 提供了一系列的 query、update、execute 重载方法应对数据的 CRUD 操作。

使用 JdbcTemplate 实现查询

基于 SpringCSS 案例,我们先来讨论一下最简单的查询操作,并对 OrderRawJdbcRepository 中的 getOrderById 方法进行重构。为此,我们构建了一个新的 OrderJdbcRepository 类并同样实现了 OrderRepository 接口,如下代码所示:

复制代码
  1. @Repository("orderJdbcRepository")
  2. public class OrderJdbcRepository implements OrderRepository {
  3.     private JdbcTemplate jdbcTemplate;
  4.     @Autowired
  5.     public OrderJdbcRepository(JdbcTemplate jdbcTemplate) {
  6.         this.jdbcTemplate = jdbcTemplate;
  7. }
  8. }

可以看到,这里通过构造函数注入了 JdbcTemplate 模板类。

而 OrderJdbcRepository 的 getOrderById 方法实现过程如下代码所示:

复制代码
  1. @Override
  2. public Order getOrderById(Long orderId) {
  3.         Order order = jdbcTemplate.queryForObject("select id, order_number, delivery_address from `order` where id=?",
  4.                 this::mapRowToOrder, orderId);
  5.         return order;
  6. }

显然,这里使用了 JdbcTemplate 的 queryForObject 方法执行查询操作,该方法传入目标 SQL、参数以及一个 RowMapper 对象。其中 RowMapper 定义如下:

复制代码
  1. public interface RowMapper<T> {
  2.     T mapRow(ResultSet rs, int rowNum) throws SQLException;
  3. }

从 mapRow 方法定义中,我们不难看出 RowMapper 的作用就是处理来自 ResultSet 中的每一行数据,并将来自数据库中的数据映射成领域对象。例如,使用 getOrderById 中用到的 mapRowToOrder 方法完成对 Order 对象的映射,如下代码所示:

复制代码
  1. private Order mapRowToOrder(ResultSet rs, int rowNum) throws SQLException {
  2.         return new Order(rs.getLong("id"), rs.getString("order_number"), rs.getString("delivery_address"));
  3. }

讲到这里,你可能注意到 getOrderById 方法实际上只是获取了 Order 对象中的订单部分信息,并不包含商品数据。

接下来,我们再来设计一个 getOrderDetailByOrderNumber 方法,根据订单编号获取订单以及订单中所包含的所有商品信息,如下代码所示:

复制代码
  1. @Override
  2. public Order getOrderDetailByOrderNumber(String orderNumber) {
  3.         //获取 Order 基础信息
  4.         Order order = jdbcTemplate.queryForObject(
  5.                 "select id, order_number, delivery_address from `order` where order_number=?", this::mapRowToOrder,
  6.                 orderNumber);
  7.         if (order == null)
  8.             return order;
  9.         //获取 Order 与 Goods 之间的关联关系,找到给 Order 中的所有 GoodsId
  10.         Long orderId = order.getId();
  11.         List<Long> goodsIds = jdbcTemplate.query("select order_id, goods_id from order_goods where order_id=?",
  12.                 new ResultSetExtractor<List<Long>>() {
  13.                     public List<Long> extractData(ResultSet rs) throws SQLException, DataAccessException {
  14.                         List<Long> list = new ArrayList<Long>();
  15.                         while (rs.next()) {
  16.                             list.add(rs.getLong("goods_id"));
  17.                         }
  18.                         return list;
  19.                     }
  20.                 }, orderId);
  21.         //根据 GoodsId 分别获取 Goods 信息并填充到 Order 对象中
  22.         for (Long goodsId : goodsIds) {
  23.             Goods goods = getGoodsById(goodsId);
  24.             order.addGoods(goods);
  25.         }
  26.         return order;
  27. }

上述代码有点复杂,我们分成几个部分来讲解。

首先,我们获取 Order 基础信息,并通过 Order 中的 Id 编号从中间表中获取所有 Goods 的 Id 列表,通过遍历这个 Id 列表再分别获取 Goods 信息,最后将 Goods 信息填充到 Order 中,从而构建一个完整的 Order 对象。

这里通过 Id 获取 Goods 数据的实现方法也与 getOrderById 方法的实现过程一样,如下代码所示:

复制代码
  1. private Goods getGoodsById(Long goodsId) {
  2.         return jdbcTemplate.queryForObject("select id, goods_code, goods_name, price from goods where id=?",
  3.                 this::mapRowToGoods, goodsId);
  4. }
  5. private Goods mapRowToGoods(ResultSet rs, int rowNum) throws SQLException {
  6.         return new Goods(rs.getLong("id"), rs.getString("goods_code"), rs.getString("goods_name"),
  7.                 rs.getDouble("price"));
  8. }

使用 JdbcTemplate 实现插入

在 JdbcTemplate 中,我们可以通过 update 方法实现数据的插入和更新。针对 Order 和 Goods 中的关联关系,插入一个 Order 对象需要同时完成两张表的更新,即 order 表和 order_goods 表,因此插入 Order 的实现过程也分成两个阶段,如下代码所示的 addOrderWithJdbcTemplate 方法展示了这一过程:

复制代码
  1. private Order addOrderDetailWithJdbcTemplate(Order order) {
  2.         //插入 Order 基础信息
  3.         Long orderId = saveOrderWithJdbcTemplate(order);
  4.         order.setId(orderId);
  5.         //插入 Order 与 Goods 的对应关系
  6.         List<Goods> goodsList = order.getGoods();
  7.         for (Goods goods : goodsList) {
  8.             saveGoodsToOrderWithJdbcTemplate(goods, orderId);
  9.         }
  10.         return order;
  11. }

可以看到,这里同样先是插入 Order 的基础信息,然后再遍历 Order 中的 Goods 列表并逐条进行插入。其中的 saveOrderWithJdbcTemplate 方法如下代码所示:

复制代码
  1. private Long saveOrderWithJdbcTemplate(Order order) {
  2.         PreparedStatementCreator psc = new PreparedStatementCreator() {
  3.             @Override
  4.             public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
  5.                 PreparedStatement ps = con.prepareStatement(
  6.                         "insert into `order` (order_number, delivery_address) values (?, ?)",
  7.                         Statement.RETURN_GENERATED_KEYS);
  8.                 ps.setString(1, order.getOrderNumber());
  9.                 ps.setString(2, order.getDeliveryAddress());
  10.                 return ps;
  11.             }
  12.         };
  13.         KeyHolder keyHolder = new GeneratedKeyHolder();
  14.         jdbcTemplate.update(psc, keyHolder);
  15.         return keyHolder.getKey().longValue();
  16. }

上述 saveOrderWithJdbcTemplate 的方法比想象中要复杂,主要原因在于我们需要在插入 order 表的同时返回数据库中所生成的自增主键,因此,这里使用了 PreparedStatementCreator 工具类封装 PreparedStatement 对象的构建过程,并在 PreparedStatement 的创建过程中设置了 Statement.RETURN_GENERATED_KEYS 用于返回自增主键。然后我们构建了一个 GeneratedKeyHolder 对象用于保存所返回的自增主键。这是使用 JdbcTemplate 实现带有自增主键数据插入的一种标准做法,你可以参考这一做法并应用到日常开发过程中。

至于用于插入 Order 与 Goods 关联关系的 saveGoodsToOrderWithJdbcTemplate 方法就比较简单了,直接调用 JdbcTemplate 的 update 方法插入数据即可,如下代码所示:

复制代码
  1. private void saveGoodsToOrderWithJdbcTemplate(Goods goods, long orderId) {
  2.         jdbcTemplate.update("insert into order_goods (order_id, goods_id) " + "values (?, ?)", orderId, goods.getId());
  3. }

接下来,我们需要实现插入 Order 的整个流程,先实现 Service 类和 Controller 类,如下代码所示:

复制代码
  1. @Service
  2. public class OrderService {
  3.     @Autowired
  4.     @Qualifier("orderJdbcRepository")
  5.     private OrderRepository orderRepository;
  6.     public Order addOrder(Order order) {
  7.         return orderRepository.addOrder(order);
  8.     }
  9. }
  10. @RestController
  11. @RequestMapping(value="orders")
  12. public class OrderController {
  13.     @RequestMapping(value = "", method = RequestMethod.POST)
  14.     public Order addOrder(@RequestBody Order order) {
  15.         Order result = orderService.addOrder(order);
  16.     return result;
  17.     }
  18. }

这两个类都是直接对 orderJdbcRepository 中的方法进行封装调用,操作非常简单。然后,我们打开 Postman,并在请求消息体中输入如下内容:

复制代码
  1. {
  2.     "orderNumber" : "Order10002",
  3.     "deliveryAddress" : "test_address2",
  4.     "goods": [
  5.         {
  6.             "id": 1,
  7.             "goodsCode": "GoodsCode1",
  8.             "goodsName": "GoodsName1",
  9.             "price": 100.0
  10.         }
  11.     ]
  12. }

通过 Postman 向http://localhost:8081/orders端点发起 Post 请求后,我们发现 order 表和 order_goods 表中的数据都已经正常插入。

使用 SimpleJdbcInsert 简化数据插入过程

虽然通过 JdbcTemplate 的 update 方法可以完成数据的正确插入,我们不禁发现这个实现过程还是比较复杂,尤其是涉及自增主键的处理时,代码显得有点臃肿。那么有没有更加简单的实现方法呢?

答案是肯定的,Spring Boot 针对数据插入场景专门提供了一个 SimpleJdbcInsert 工具类,SimpleJdbcInsert 本质上是在 JdbcTemplate 的基础上添加了一层封装,提供了一组 execute、executeAndReturnKey 以及 executeBatch 重载方法来简化数据插入操作。

通常,我们可以在 Repository 实现类的构造函数中对 SimpleJdbcInsert 进行初始化,如下代码所示:

复制代码
  1. private JdbcTemplate jdbcTemplate;
  2. private SimpleJdbcInsert orderInserter;
  3. private SimpleJdbcInsert orderGoodsInserter;
  4. public OrderJdbcRepository(JdbcTemplate jdbcTemplate) {
  5.         this.jdbcTemplate = jdbcTemplate;
  6.         this.orderInserter = new SimpleJdbcInsert(jdbcTemplate).withTableName("`order`").usingGeneratedKeyColumns("id");
  7.         this.orderGoodsInserter = new SimpleJdbcInsert(jdbcTemplate).withTableName("order_goods");
  8. }

可以看到,这里首先注入了一个 JdbcTemplate 对象,然后我们基于 JdbcTemplate 并针对 order 表和 order_goods 表分别初始化了两个 SimpleJdbcInsert 对象 orderInserter 和 orderGoodsInserter。其中 orderInserter 中还使用了 usingGeneratedKeyColumns 方法设置自增主键列。

基于 SimpleJdbcInsert,完成 Order 对象的插入就非常简单了,实现方式如下所示:

复制代码
  1. private Long saveOrderWithSimpleJdbcInsert(Order order) {
  2.         Map<String, Object> values = new HashMap<String, Object>();
  3.         values.put("order_number", order.getOrderNumber());
  4.         values.put("delivery_address", order.getDeliveryAddress());
  5.         Long orderId = orderInserter.executeAndReturnKey(values).longValue();
  6.         return orderId;
  7. }

我们通过构建一个 Map 对象,然后把需要添加的字段设置成一个个键值对。通过SimpleJdbcInsert 的 executeAndReturnKey 方法在插入数据的同时直接返回自增主键。同样,完成 order_goods 表的操作只需要几行代码就可以了,如下代码所示:

复制代码
  1. private void saveGoodsToOrderWithSimpleJdbcInsert(Goods goods, long orderId) {
  2.         Map<String, Object> values = new HashMap<>();
  3.         values.put("order_id", orderId);
  4.         values.put("goods_id", goods.getId());
  5.         orderGoodsInserter.execute(values);
  6. }

这里用到了 SimpleJdbcInsert 提供的 execute 方法,我们可以把这些方法组合起来对 addOrderDetailWithJdbcTemplate 方法进行重构,从而得到如下所示的 addOrderDetailWithSimpleJdbcInsert 方法:

复制代码
  1. private Order addOrderDetailWithSimpleJdbcInsert(Order order) {
  2.         //插入 Order 基础信息
  3.         Long orderId = saveOrderWithSimpleJdbcInsert(order);
  4.         order.setId(orderId);
  5.         //插入 Order 与 Goods 的对应关系
  6.         List<Goods> goodsList = order.getGoods();
  7.         for (Goods goods : goodsList) {
  8.             saveGoodsToOrderWithSimpleJdbcInsert(goods, orderId);
  9.         }
  10.         return order;
  11. }

详细的代码清单可以参考课程的案例代码,你也可以基于 Postman 对重构后的代码进行尝试。

小结与预告

JdbcTemplate 模板工具类是一个基于 JDBC 规范实现数据访问的强大工具,是一个优秀的工具类。它对常见的 CRUD 操作做了封装并提供了一大批简化的 API。今天我们分别针对查询和插入这两大类数据操作给出了基于 JdbcTemplate 的实现方案,特别是针对插入场景,我们还引入了基于 JdbcTemplate 所构建的 SimpleJdbcInsert 简化这一操作。

这里给你留一道思考题:在使用 JdbcTemplate 时,如果想要返回数据库的自增主键值有哪些实现方法?

在 Spring 中存在一组以 -Template 结尾的模板工具类,这些类都是模板方法这一设计模式的典型应用,同时还充分利用了回调机制完成解耦和扩展。在 08 讲中,我们将对 JdbcTemplate 的具体实现机制进行详细剖析。

(0)

相关推荐

  • JDBC使用+数据库连接池+springJDBC

    一.JDBC基本概念和下载 JDBC(Java DataBase Connectivity),通俗来讲就是用java语言操作数据库 本质 sun公司定义的一套操作所有关系型数据库的规则,即接口. 各个 ...

  • 06 | 基础规范:如何理解 JDBC 关系型数据库访问规范?

    从今天开始,我们将进入 Spring Boot 另一个核心技术体系的讨论,即数据访问技术体系.无论是互联网应用还是传统软件,对于任何一个系统而言,数据的存储和访问都是不可缺少的. 数据访问层的构建可能 ...

  • DRF之访问权限控制和访问频率控制(节流)

    权限控制 前言 用户验证用户权限,根据不同访问权限控制对不同内容的访问. 建议了解视图.token验证的内容. 使用流程 自定义访问权限类,继承BasePermission,重写has_permiss ...

  • Linux系统学习 十八、VSFTP服务—虚拟用户访问—配置虚拟用户访问

    配置虚拟用户访问 首先至少要关闭userlist 改完配置文件是要重启服务来使它生效 其实在刚装好vsftp的时候的配置文件不用修改的情况下配置虚拟用户访问控制是最好的 local_root选项不影响 ...

  • (2条消息) 关系型数据库的范式

    解决问题no解决代码问题 2020-08-06 10:25:33   版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https:/ ...

  • 基于关系型数据库的App Inventor网络应用

    一直想介绍这个题目,但顾虑重重.一方面担心自己为了面面俱到,迷失在对细节的纠缠中,另一方面,也担心读者的知识背景不尽相同,阅读过程中会止步于某个陌生的环节,半途而废. 我们的目标是用App Inven ...

  • 基于关系型数据库的App Inventor网络应用(2)

    第二节 后端技术 1. 网络结构 如图2所示,你一定见到过这类图,它被称作网络拓扑图,描述了网络应用中的几个重要角色以及它们之间的关系.以图中的云(internet)为分界线,云的一端是种类及型号繁多 ...

  • 基于关系型数据库的App Inventor网络应用(3)

    第三节 初识Node-RED 开发环境简介 如图8所示,整个浏览器窗口被划分为四个部分: (1) 顶部黑色通栏,左侧显示Node-RED的LOGO,右侧显著位置为部署按钮,部署按钮的右侧(三条横线)为 ...

  • 基于关系型数据库的App Inventor网络应用(4)

    第四节 消息对象 在上一节的图10中,在设置debug节点的输出(Output)属性时,我们看到了这样的内容: msg.payload 随后我们将其中的payload改为topic.这项操作中的msg ...

  • 基于关系型数据库的App Inventor网络应用(5)

    第五节 安装SQLite数据库模块 SQLite是一款轻型的数据库软件,在Node-RED中使用SQLite数据库,需要安装一个模块.方法如下: (1) 点击系统菜单,选择"编辑调面板&qu ...