06 講我們?cè)敿?xì)介紹了 JDBC 規(guī)范的相關(guān)內(nèi)容,JDBC 規(guī)范是 Java 領(lǐng)域中使用最廣泛的數(shù)據(jù)訪問標(biāo)準(zhǔn),目前市面上主流的數(shù)據(jù)訪問框架都是構(gòu)建在 JDBC 規(guī)范之上。
因?yàn)?JDBC 是偏底層的操作規(guī)范,所以關(guān)于如何使用 JDBC 規(guī)范進(jìn)行關(guān)系型數(shù)據(jù)訪問的實(shí)現(xiàn)方式有很多(區(qū)別在于對(duì) JDBC 規(guī)范的封裝程度不同),而在 Spring 中,同樣提供了 JdbcTemplate 模板工具類實(shí)現(xiàn)數(shù)據(jù)訪問,它簡(jiǎn)化了 JDBC 規(guī)范的使用方法,今天我們將圍繞這個(gè)模板類展開討論。
引入 JdbcTemplate 模板工具類之前,我們回到 SpringCSS 案例,先給出 order-service 中的數(shù)據(jù)模型為本講內(nèi)容的展開做一些鋪墊。
我們知道一個(gè)訂單中往往涉及一個(gè)或多個(gè)商品,所以在本案例中,我們主要通過一對(duì)多的關(guān)系來展示數(shù)據(jù)庫(kù)設(shè)計(jì)和實(shí)現(xiàn)方面的技巧。而為了使描述更簡(jiǎn)單,我們把具體的業(yè)務(wù)字段做了簡(jiǎn)化。Order 類的定義如下代碼所示:
- public class Order{
- private Long id; //訂單Id
- private String orderNumber; //訂單編號(hào)
- private String deliveryAddress; //物流地址
- private List<Goods> goodsList; //商品列表
- //省略了 getter/setter
- }
其中代表商品的 Goods 類定義如下:
- public class Goods {
- private Long id; //商品Id
- private String goodsCode; //商品編號(hào)
- private String goodsName; //商品名稱
- private Double price; //商品價(jià)格
- //省略了 getter/setter
- }
從以上代碼,我們不難看出一個(gè)訂單可以包含多個(gè)商品,因此設(shè)計(jì)關(guān)系型數(shù)據(jù)庫(kù)表時(shí),我們首先會(huì)構(gòu)建一個(gè)中間表來保存 Order 和 Goods 這層一對(duì)多關(guān)系。在本課程中,我們使用 MySQL 作為關(guān)系型數(shù)據(jù)庫(kù),對(duì)應(yīng)的數(shù)據(jù)庫(kù) Schema 定義如下代碼所示:
- DROP TABLE IF EXISTS `order`;
- DROP TABLE IF EXISTS `goods`;
- DROP TABLE IF EXISTS `order_goods`;
- create table `order` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `order_number` varchar(50) not null,
- `delivery_address` varchar(100) not null,
- `create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (`id`)
- );
- create table `goods` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `goods_code` varchar(50) not null,
- `goods_name` varchar(50) not null,
- `goods_price` double not null,
- `create_time` timestamp not null DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (`id`)
- );
- create table `order_goods` (
- `order_id` bigint(20) not null,
- `goods_id` bigint(20) not null,
- foreign key(`order_id`) references `order`(`id`),
- foreign key(`goods_id`) references `goods`(`id`)
- );
基于以上數(shù)據(jù)模型,我們將完成 order-server 中的 Repository 層組件的設(shè)計(jì)和實(shí)現(xiàn)。首先,我們需要設(shè)計(jì)一個(gè) OrderRepository 接口,用來抽象數(shù)據(jù)庫(kù)訪問的入口,如下代碼所示:
- public interface OrderRepository {
- Order addOrder(Order order);
- Order getOrderById(Long orderId);
- Order getOrderDetailByOrderNumber(String orderNumber);
- }
這個(gè)接口非常簡(jiǎn)單,方法都是自解釋的。不過請(qǐng)注意,這里的 OrderRepository 并沒有繼承任何父接口,完全是一個(gè)自定義的、獨(dú)立的 Repository。
針對(duì)上述 OrderRepository 中的接口定義,我們將構(gòu)建一系列的實(shí)現(xiàn)類。
OrderRawJdbcRepository:使用原生 JDBC 進(jìn)行數(shù)據(jù)庫(kù)訪問
OrderJdbcRepository:使用 JdbcTemplate 進(jìn)行數(shù)據(jù)庫(kù)訪問
OrderJpaRepository:使用 Spring Data JPA 進(jìn)行數(shù)據(jù)庫(kù)訪問
上述實(shí)現(xiàn)類中的 OrderJpaRepository 我們會(huì)放到 10 講《ORM 集成:如何使用 Spring Data JPA 訪問關(guān)系型數(shù)據(jù)庫(kù)?》中進(jìn)行展開,而 OrderRawJdbcRepository 最基礎(chǔ),不是本課程的重點(diǎn),因此 07 講我們只針對(duì) OrderRepository 中 getOrderById 方法的實(shí)現(xiàn)過程重點(diǎn)介紹,也算是對(duì) 06 講的回顧和擴(kuò)展。
OrderRawJdbcRepository 類中實(shí)現(xiàn)方法如下代碼所示:
- @Repository("orderRawJdbcRepository")
- public class OrderRawJdbcRepository implements OrderRepository {
- @Autowired
- private DataSource dataSource;
- @Override
- public Order getOrderById(Long orderId) {
- Connection connection = null;
- PreparedStatement statement = null;
- ResultSet resultSet = null;
- try {
- connection = dataSource.getConnection();
- statement = connection.prepareStatement("select id, order_number, delivery_address from `order` where id=?");
- statement.setLong(1, orderId);
- resultSet = statement.executeQuery();
- Order order = null;
- if (resultSet.next()) {
- order = new Order(resultSet.getLong("id"), resultSet.getString("order_number"),
- resultSet.getString("delivery_address"));
- }
- return order;
- } catch (SQLException e) {
- System.out.print(e);
- } finally {
- if (resultSet != null) {
- try {
- resultSet.close();
- } catch (SQLException e) {
- }
- }
- if (statement != null) {
- try {
- statement.close();
- } catch (SQLException e) {
- }
- }
- if (connection != null) {
- try {
- connection.close();
- } catch (SQLException e) {
- }
- }
- }
- return null;
- }
- //省略其他 OrderRepository 接口方法實(shí)現(xiàn)
- }
這里,值得注意的是,我們首先需要在類定義上添加 @Repository 注解,標(biāo)明這是能夠被 Spring 容器自動(dòng)掃描的 Javabean,再在 @Repository 注解中指定這個(gè) Javabean 的名稱為"orderRawJdbcRepository",方便 Service 層中根據(jù)該名稱注入 OrderRawJdbcRepository 類。
可以看到,上述代碼使用了 JDBC 原生 DataSource、Connection、PreparedStatement、ResultSet 等核心編程對(duì)象完成針對(duì)“order”表的一次查詢。代碼流程看起來比較簡(jiǎn)單,其實(shí)也比較煩瑣,學(xué)到這里,我們可以結(jié)合上一課時(shí)的內(nèi)容理解上述代碼。
請(qǐng)注意,如果我們想運(yùn)行這些代碼,千萬別忘了在 Spring Boot 的配置文件中添加對(duì) DataSource 的定義,如下代碼所示:
- spring:
- datasource:
- driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://127.0.0.1:3306/appointment
- username: root
- password: root
回顧完原生 JDBC 的使用方法,接下來就引出今天的重點(diǎn),即 JdbcTemplate 模板工具類,我們來看看它如何簡(jiǎn)化數(shù)據(jù)訪問操作。
要想在應(yīng)用程序中使用 JdbcTemplate,首先我們需要引入對(duì)它的依賴,如下代碼所示:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-jdbc</artifactId>
- </dependency>
JdbcTemplate 提供了一系列的 query、update、execute 重載方法應(yīng)對(duì)數(shù)據(jù)的 CRUD 操作。
基于 SpringCSS 案例,我們先來討論一下最簡(jiǎn)單的查詢操作,并對(duì) OrderRawJdbcRepository 中的 getOrderById 方法進(jìn)行重構(gòu)。為此,我們構(gòu)建了一個(gè)新的 OrderJdbcRepository 類并同樣實(shí)現(xiàn)了 OrderRepository 接口,如下代碼所示:
- @Repository("orderJdbcRepository")
- public class OrderJdbcRepository implements OrderRepository {
- private JdbcTemplate jdbcTemplate;
- @Autowired
- public OrderJdbcRepository(JdbcTemplate jdbcTemplate) {
- this.jdbcTemplate = jdbcTemplate;
- }
- }
可以看到,這里通過構(gòu)造函數(shù)注入了 JdbcTemplate 模板類。
而 OrderJdbcRepository 的 getOrderById 方法實(shí)現(xiàn)過程如下代碼所示:
- @Override
- public Order getOrderById(Long orderId) {
- Order order = jdbcTemplate.queryForObject("select id, order_number, delivery_address from `order` where id=?",
- this::mapRowToOrder, orderId);
- return order;
- }
顯然,這里使用了 JdbcTemplate 的 queryForObject 方法執(zhí)行查詢操作,該方法傳入目標(biāo) SQL、參數(shù)以及一個(gè) RowMapper 對(duì)象。其中 RowMapper 定義如下:
- public interface RowMapper<T> {
- T mapRow(ResultSet rs, int rowNum) throws SQLException;
- }
從 mapRow 方法定義中,我們不難看出 RowMapper 的作用就是處理來自 ResultSet 中的每一行數(shù)據(jù),并將來自數(shù)據(jù)庫(kù)中的數(shù)據(jù)映射成領(lǐng)域?qū)ο?。例如,使?getOrderById 中用到的 mapRowToOrder 方法完成對(duì) Order 對(duì)象的映射,如下代碼所示:
- private Order mapRowToOrder(ResultSet rs, int rowNum) throws SQLException {
- return new Order(rs.getLong("id"), rs.getString("order_number"), rs.getString("delivery_address"));
- }
講到這里,你可能注意到 getOrderById 方法實(shí)際上只是獲取了 Order 對(duì)象中的訂單部分信息,并不包含商品數(shù)據(jù)。
接下來,我們?cè)賮碓O(shè)計(jì)一個(gè) getOrderDetailByOrderNumber 方法,根據(jù)訂單編號(hào)獲取訂單以及訂單中所包含的所有商品信息,如下代碼所示:
- @Override
- public Order getOrderDetailByOrderNumber(String orderNumber) {
- //獲取 Order 基礎(chǔ)信息
- Order order = jdbcTemplate.queryForObject(
- "select id, order_number, delivery_address from `order` where order_number=?", this::mapRowToOrder,
- orderNumber);
- if (order == null)
- return order;
- //獲取 Order 與 Goods 之間的關(guān)聯(lián)關(guān)系,找到給 Order 中的所有 GoodsId
- Long orderId = order.getId();
- List<Long> goodsIds = jdbcTemplate.query("select order_id, goods_id from order_goods where order_id=?",
- new ResultSetExtractor<List<Long>>() {
- public List<Long> extractData(ResultSet rs) throws SQLException, DataAccessException {
- List<Long> list = new ArrayList<Long>();
- while (rs.next()) {
- list.add(rs.getLong("goods_id"));
- }
- return list;
- }
- }, orderId);
- //根據(jù) GoodsId 分別獲取 Goods 信息并填充到 Order 對(duì)象中
- for (Long goodsId : goodsIds) {
- Goods goods = getGoodsById(goodsId);
- order.addGoods(goods);
- }
- return order;
- }
上述代碼有點(diǎn)復(fù)雜,我們分成幾個(gè)部分來講解。
首先,我們獲取 Order 基礎(chǔ)信息,并通過 Order 中的 Id 編號(hào)從中間表中獲取所有 Goods 的 Id 列表,通過遍歷這個(gè) Id 列表再分別獲取 Goods 信息,最后將 Goods 信息填充到 Order 中,從而構(gòu)建一個(gè)完整的 Order 對(duì)象。
這里通過 Id 獲取 Goods 數(shù)據(jù)的實(shí)現(xiàn)方法也與 getOrderById 方法的實(shí)現(xiàn)過程一樣,如下代碼所示:
- private Goods getGoodsById(Long goodsId) {
- return jdbcTemplate.queryForObject("select id, goods_code, goods_name, price from goods where id=?",
- this::mapRowToGoods, goodsId);
- }
- private Goods mapRowToGoods(ResultSet rs, int rowNum) throws SQLException {
- return new Goods(rs.getLong("id"), rs.getString("goods_code"), rs.getString("goods_name"),
- rs.getDouble("price"));
- }
在 JdbcTemplate 中,我們可以通過 update 方法實(shí)現(xiàn)數(shù)據(jù)的插入和更新。針對(duì) Order 和 Goods 中的關(guān)聯(lián)關(guān)系,插入一個(gè) Order 對(duì)象需要同時(shí)完成兩張表的更新,即 order 表和 order_goods 表,因此插入 Order 的實(shí)現(xiàn)過程也分成兩個(gè)階段,如下代碼所示的 addOrderWithJdbcTemplate 方法展示了這一過程:
- private Order addOrderDetailWithJdbcTemplate(Order order) {
- //插入 Order 基礎(chǔ)信息
- Long orderId = saveOrderWithJdbcTemplate(order);
- order.setId(orderId);
- //插入 Order 與 Goods 的對(duì)應(yīng)關(guān)系
- List<Goods> goodsList = order.getGoods();
- for (Goods goods : goodsList) {
- saveGoodsToOrderWithJdbcTemplate(goods, orderId);
- }
- return order;
- }
可以看到,這里同樣先是插入 Order 的基礎(chǔ)信息,然后再遍歷 Order 中的 Goods 列表并逐條進(jìn)行插入。其中的 saveOrderWithJdbcTemplate 方法如下代碼所示:
- private Long saveOrderWithJdbcTemplate(Order order) {
- PreparedStatementCreator psc = new PreparedStatementCreator() {
- @Override
- public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
- PreparedStatement ps = con.prepareStatement(
- "insert into `order` (order_number, delivery_address) values (?, ?)",
- Statement.RETURN_GENERATED_KEYS);
- ps.setString(1, order.getOrderNumber());
- ps.setString(2, order.getDeliveryAddress());
- return ps;
- }
- };
- KeyHolder keyHolder = new GeneratedKeyHolder();
- jdbcTemplate.update(psc, keyHolder);
- return keyHolder.getKey().longValue();
- }
上述 saveOrderWithJdbcTemplate 的方法比想象中要復(fù)雜,主要原因在于我們需要在插入 order 表的同時(shí)返回?cái)?shù)據(jù)庫(kù)中所生成的自增主鍵,因此,這里使用了 PreparedStatementCreator 工具類封裝 PreparedStatement 對(duì)象的構(gòu)建過程,并在 PreparedStatement 的創(chuàng)建過程中設(shè)置了 Statement.RETURN_GENERATED_KEYS 用于返回自增主鍵。然后我們構(gòu)建了一個(gè) GeneratedKeyHolder 對(duì)象用于保存所返回的自增主鍵。這是使用 JdbcTemplate 實(shí)現(xiàn)帶有自增主鍵數(shù)據(jù)插入的一種標(biāo)準(zhǔn)做法,你可以參考這一做法并應(yīng)用到日常開發(fā)過程中。
至于用于插入 Order 與 Goods 關(guān)聯(lián)關(guān)系的 saveGoodsToOrderWithJdbcTemplate 方法就比較簡(jiǎn)單了,直接調(diào)用 JdbcTemplate 的 update 方法插入數(shù)據(jù)即可,如下代碼所示:
- private void saveGoodsToOrderWithJdbcTemplate(Goods goods, long orderId) {
- jdbcTemplate.update("insert into order_goods (order_id, goods_id) " + "values (?, ?)", orderId, goods.getId());
- }
接下來,我們需要實(shí)現(xiàn)插入 Order 的整個(gè)流程,先實(shí)現(xiàn) Service 類和 Controller 類,如下代碼所示:
- @Service
- public class OrderService {
- @Autowired
- @Qualifier("orderJdbcRepository")
- private OrderRepository orderRepository;
- public Order addOrder(Order order) {
- return orderRepository.addOrder(order);
- }
- }
- @RestController
- @RequestMapping(value="orders")
- public class OrderController {
- @RequestMapping(value = "", method = RequestMethod.POST)
- public Order addOrder(@RequestBody Order order) {
- Order result = orderService.addOrder(order);
- return result;
- }
- }
這兩個(gè)類都是直接對(duì) orderJdbcRepository 中的方法進(jìn)行封裝調(diào)用,操作非常簡(jiǎn)單。然后,我們打開 Postman,并在請(qǐng)求消息體中輸入如下內(nèi)容:
- {
- "orderNumber" : "Order10002",
- "deliveryAddress" : "test_address2",
- "goods": [
- {
- "id": 1,
- "goodsCode": "GoodsCode1",
- "goodsName": "GoodsName1",
- "price": 100.0
- }
- ]
- }
通過 Postman 向http://localhost:8081/orders端點(diǎn)發(fā)起 Post 請(qǐng)求后,我們發(fā)現(xiàn) order 表和 order_goods 表中的數(shù)據(jù)都已經(jīng)正常插入。
雖然通過 JdbcTemplate 的 update 方法可以完成數(shù)據(jù)的正確插入,我們不禁發(fā)現(xiàn)這個(gè)實(shí)現(xiàn)過程還是比較復(fù)雜,尤其是涉及自增主鍵的處理時(shí),代碼顯得有點(diǎn)臃腫。那么有沒有更加簡(jiǎn)單的實(shí)現(xiàn)方法呢?
答案是肯定的,Spring Boot 針對(duì)數(shù)據(jù)插入場(chǎng)景專門提供了一個(gè) SimpleJdbcInsert 工具類,SimpleJdbcInsert 本質(zhì)上是在 JdbcTemplate 的基礎(chǔ)上添加了一層封裝,提供了一組 execute、executeAndReturnKey 以及 executeBatch 重載方法來簡(jiǎn)化數(shù)據(jù)插入操作。
通常,我們可以在 Repository 實(shí)現(xiàn)類的構(gòu)造函數(shù)中對(duì) SimpleJdbcInsert 進(jìn)行初始化,如下代碼所示:
- private JdbcTemplate jdbcTemplate;
- private SimpleJdbcInsert orderInserter;
- private SimpleJdbcInsert orderGoodsInserter;
- public OrderJdbcRepository(JdbcTemplate jdbcTemplate) {
- this.jdbcTemplate = jdbcTemplate;
- this.orderInserter = new SimpleJdbcInsert(jdbcTemplate).withTableName("`order`").usingGeneratedKeyColumns("id");
- this.orderGoodsInserter = new SimpleJdbcInsert(jdbcTemplate).withTableName("order_goods");
- }
可以看到,這里首先注入了一個(gè) JdbcTemplate 對(duì)象,然后我們基于 JdbcTemplate 并針對(duì) order 表和 order_goods 表分別初始化了兩個(gè) SimpleJdbcInsert 對(duì)象 orderInserter 和 orderGoodsInserter。其中 orderInserter 中還使用了 usingGeneratedKeyColumns 方法設(shè)置自增主鍵列。
基于 SimpleJdbcInsert,完成 Order 對(duì)象的插入就非常簡(jiǎn)單了,實(shí)現(xiàn)方式如下所示:
- private Long saveOrderWithSimpleJdbcInsert(Order order) {
- Map<String, Object> values = new HashMap<String, Object>();
- values.put("order_number", order.getOrderNumber());
- values.put("delivery_address", order.getDeliveryAddress());
- Long orderId = orderInserter.executeAndReturnKey(values).longValue();
- return orderId;
- }
我們通過構(gòu)建一個(gè) Map 對(duì)象,然后把需要添加的字段設(shè)置成一個(gè)個(gè)鍵值對(duì)。通過SimpleJdbcInsert 的 executeAndReturnKey 方法在插入數(shù)據(jù)的同時(shí)直接返回自增主鍵。同樣,完成 order_goods 表的操作只需要幾行代碼就可以了,如下代碼所示:
- private void saveGoodsToOrderWithSimpleJdbcInsert(Goods goods, long orderId) {
- Map<String, Object> values = new HashMap<>();
- values.put("order_id", orderId);
- values.put("goods_id", goods.getId());
- orderGoodsInserter.execute(values);
- }
這里用到了 SimpleJdbcInsert 提供的 execute 方法,我們可以把這些方法組合起來對(duì) addOrderDetailWithJdbcTemplate 方法進(jìn)行重構(gòu),從而得到如下所示的 addOrderDetailWithSimpleJdbcInsert 方法:
- private Order addOrderDetailWithSimpleJdbcInsert(Order order) {
- //插入 Order 基礎(chǔ)信息
- Long orderId = saveOrderWithSimpleJdbcInsert(order);
- order.setId(orderId);
- //插入 Order 與 Goods 的對(duì)應(yīng)關(guān)系
- List<Goods> goodsList = order.getGoods();
- for (Goods goods : goodsList) {
- saveGoodsToOrderWithSimpleJdbcInsert(goods, orderId);
- }
- return order;
- }
詳細(xì)的代碼清單可以參考課程的案例代碼,你也可以基于 Postman 對(duì)重構(gòu)后的代碼進(jìn)行嘗試。
JdbcTemplate 模板工具類是一個(gè)基于 JDBC 規(guī)范實(shí)現(xiàn)數(shù)據(jù)訪問的強(qiáng)大工具,是一個(gè)優(yōu)秀的工具類。它對(duì)常見的 CRUD 操作做了封裝并提供了一大批簡(jiǎn)化的 API。今天我們分別針對(duì)查詢和插入這兩大類數(shù)據(jù)操作給出了基于 JdbcTemplate 的實(shí)現(xiàn)方案,特別是針對(duì)插入場(chǎng)景,我們還引入了基于 JdbcTemplate 所構(gòu)建的 SimpleJdbcInsert 簡(jiǎn)化這一操作。
這里給你留一道思考題:在使用 JdbcTemplate 時(shí),如果想要返回?cái)?shù)據(jù)庫(kù)的自增主鍵值有哪些實(shí)現(xiàn)方法?
在 Spring 中存在一組以 -Template 結(jié)尾的模板工具類,這些類都是模板方法這一設(shè)計(jì)模式的典型應(yīng)用,同時(shí)還充分利用了回調(diào)機(jī)制完成解耦和擴(kuò)展。在 08 講中,我們將對(duì) JdbcTemplate 的具體實(shí)現(xiàn)機(jī)制進(jìn)行詳細(xì)剖析。
聯(lián)系客服