Spring Boot中如何使用JDBC讀取和寫入資料,JDBC和JPA的對比,JdbcTemplate和SimpleJdbcInsert的用法對比

ZhaoSimonone發表於2020-12-05

JDBC和JPA的對比

  • JDBC(Java Database Connectivity)提供一種介面,它是由各種資料庫廠商提供類和介面組成的資料庫驅動,為多種資料庫提供統一訪問。我們使用資料庫時只需要呼叫JDBC介面就行了。

    JDBC的用途:與資料庫建立連線、傳送 運算元據庫的語句並處理結果。

    JDBC示意圖
    在這裡插入圖片描述

  • JPA(Java Peisitence API)是Java持久層API。它是對java應用程式訪問ORM(物件關係對映)框架的規範。為了我們能用相同的方法使用各種ORM框架。

    JPA用途:簡化現有Java EE和Java SE應用開發工作;整合ORM技術。

    使用JPA只需要建立實體(這和建立一個POJO(Plain Ordinary Java Object)簡單的Java物件一樣簡單),用@entity進行註解。在Spring Data JPA中,定義一個簡單的介面,用於把物件持久化到資料庫的倉庫。

    常見ORM框架:Hibernate。由於MyBatis需要手寫SQL,所以不完全屬於ORM框架,而Hibernate則完全不需要手寫SQL。

    JPA示意圖
    在這裡插入圖片描述
    不同點:

  1. 使用的sql語言不同:

    JDBC使用的是基於關係型資料庫的標準SQL語言;

    JPA通過物件導向而非面向資料庫的查詢語言查詢資料,避免程式的SQL語句緊密耦合。

  2. 操作的物件不同:

    JDBC操作的是資料,將資料通過SQL語句直接傳送到資料庫中執行:

    JPA操作的是持久化物件,由底層持久化物件的資料更新到資料庫中。

  3. 資料狀態不同:

    JDBC操作的資料是“瞬時”的,變數的值無法與資料庫中的值保持一致;

    JPA操作的資料時可持久的,即持久化物件的資料屬性的值是可以跟資料庫中的值保持一致的。

Spring Boot中使用JDBC讀取和寫入資料

Spring對JDBC的支援主要在於JdbcTemplate

JdbcTemplate類中主要有如下方法

  • batchUpdate(...)//批量更新

  • execute(...)//執行SQL語句

  • query(...)//查詢並返回相應值

  • queryForList(...)//查詢並返回一個List

  • queryForObject(...)//查詢並返回一個Object

  • queryForMap(...)//查詢並返回一個Map

  • queryForRowSet(...)//查詢並返回一個RowSet

  • update(...)//執行一條插入或更新語句

使用JdbcTemplate查詢資料庫的例子

JdbcTemplate中的queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args)方法是將查詢得到的結果對映為一個Object。

public Ingredient findById(String id) {
    return jdbc.queryForObject(
        "select id, name, type from Ingredient where id=?",
        this::mapRowToIngredient, id);
}

//mapRowToIngredient方法,ResultSet是查詢返回的結果集
private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
    throws SQLException {
    return new Ingredient(
        rs.getString("id"), 
        rs.getString("name"),
        Ingredient.Type.valueOf(rs.getString("type")));
}

/****************************	等效於	 ************************************/
public Ingredient findById(String id) {
    return jdbc.queryForObject(
        "select id, name, type from Ingredient where id=?",
        new RowMapper<Ingredient>() {
            public Ingredient mapRow(ResultSet rs, int rowNum) 
                throws SQLException {
                return new Ingredient(
                    rs.getString("id"), 
                    rs.getString("name"),
                    Ingredient.Type.valueOf(rs.getString("type")));
            };
        }, id);
}

findById方法中呼叫的queryForObject方法中需要傳入一個RowMapper的例項

@Override
@Nullable
public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException {
    List<T> results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1));
    return DataAccessUtils.nullableSingleResult(results);
}

RowMapper<T>介面中只有一個T mapRow(ResultSet rs, int rowNum) throws SQLException抽象方法。需要一個子類來繼承RowMapper並實現mapRow方法。如果使用Lambda,則只需要傳入相應所需執行的程式碼。

@FunctionalInterface
public interface RowMapper<T> {
    
	@Nullable
	T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
  • ResultSet是查詢資料庫所返回的結果集。

  • mapRow方法是將查詢所得到資料庫的一行對映為一個物件,也就是將ResultSet的第一行對映為一個Object。

對Lambda表示式不熟悉的可以移步我的另一篇博文:
Java中Lambda對比匿名內部類

使用JdbcTemplate的步驟

調整物件

一般來說,為了將物件持久化到資料庫中需要增加idcreatedTime欄位,id一般都設定為自增,由資料庫自動生成。同時需要為每個實體類增加get,set方法。如果使用了Lombok,只需要新增@Data註解,就會在執行時自動為物件增加上get和set方法,從而避免了手寫get和set方法時的繁瑣。

import java.util.Date;
import java.util.List;

import lombok.Data;

@Data
public class Taco {

  private Long id;
  private Date createdAt;
  ...
}

匯入依賴

首先需要Jdbc的依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

然後需要配置資料庫

如下給出了H2資料庫和MySQL的配置示例

配置H2資料庫

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- 引入spring-boot-devtools的目的是在執行時可以訪問H2資料庫 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>

如果需要初始化資料庫,特別是H2資料庫。

可以在resourse資料夾下新建兩個sql檔案:schema.sqldata.sql

  • scheaml.sql中的SQL用於初始化資料庫,比如建立表。

  • data.sql中的SQL語句用來插入資料

預設的訪問地址如下:

http://localhost:8080/h2-console/

預設的JDBC的連線為:jdbc:h2:mem:testdb,預設的使用者名稱為sa,密碼為空
Spring Boot控制檯列印的日誌顯示了連線H2資料庫的JDBC URL。
在這裡插入圖片描述
H2資料庫訪問頁面
在這裡插入圖片描述

配置MySQL

mysql-connector得根據資料庫的版本來選擇,8.0的資料庫就得選擇版本為8或者以上的mysql-connector

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</dependency>

然後在application.properties中配置連線相關的資訊

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123456

定義JDBC Repository

比如有一個Ingredient的物件對應著資料庫中Ingredient的表,其屬性分別對應著資料庫中(id, name, type)這幾個欄位。我們需要定義如下方法:

  1. 從資料中查詢所有的Ingredient的資訊,並將其儲存到一個Ingredient的集合中
  2. 根據id查詢單個Ingredient
  3. 儲存Ingredient物件到資料庫中

首先需要定義IngredientRepository的interface

package tacos.data;
import tacos.Ingredient;

public interface IngredientRepository {

  Iterable<Ingredient> findAll();
  
  Ingredient findById(String id);
  
  Ingredient save(Ingredient ingredient);
  
}

然後需要使用JdbcTemplate來具體實現這個介面

package tacos.data;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import tacos.Ingredient;

@Repository//為JdbcIngredientRepository定義了@Repository以後,Spring掃描到這個類時,就會將其初始化為Spring上下文中的一個Bean
public class JdbcIngredientRepository implements IngredientRepository {

  private JdbcTemplate jdbc;
  
  //只要我們在pom.xml中匯入JDBC的依賴,Spring Boot就會為我們自動配置一個JdbcTemplate的Bean,
  //我們只需要將這個Bean注入到我們的程式碼中
  @Autowired
  public JdbcIngredientRepository(JdbcTemplate jdbc) {
    this.jdbc = jdbc;
  }

  @Override
  public Iterable<Ingredient> findAll() {
    return jdbc.query("select id, name, type from Ingredient",
        this::mapRowToIngredient);
  }

  @Override
  public Ingredient findById(String id) {
    return jdbc.queryForObject(
        "select id, name, type from Ingredient where id=?",
        this::mapRowToIngredient, id);
  }

  @Override
  public Ingredient save(Ingredient ingredient) {
    jdbc.update(
        "insert into Ingredient (id, name, type) values (?, ?, ?)",
        ingredient.getId(), 
        ingredient.getName(),
        ingredient.getType().toString());
    return ingredient;
  }

  private Ingredient mapRowToIngredient(ResultSet rs, int rowNum)
      throws SQLException {
    return new Ingredient(
        rs.getString("id"), 
        rs.getString("name"),
        Ingredient.Type.valueOf(rs.getString("type")));
  }
}

在Controller中注入和使用repository

package tacos.web;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;

import tacos.Ingredient;
import tacos.Ingredient.Type;
import tacos.data.IngredientRepository;

@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
  
  private final IngredientRepository ingredientRepo;

  //IngredientRepository使用了@Repository註解,IngredientRepository的Bean就會被註冊到Spring的上下文中,因此這裡只需注入即可
  @Autowired
  public DesignTacoController(IngredientRepository ingredientRepo) {
    this.ingredientRepo = ingredientRepo;
  }

  @GetMapping
  public String showDesignForm(Model model) {
    List<Ingredient> ingredients = new ArrayList<>();
    ingredientRepo.findAll().forEach(i -> ingredients.add(i));
    
    Type[] types = Ingredient.Type.values();
    for (Type type : types) {
      model.addAttribute(type.toString().toLowerCase(), 
          filterByType(ingredients, type));      
    }
    return "design";
  }

  private List<Ingredient> filterByType(List<Ingredient> ingredients, Type type) {
    return ingredients
              .stream()
              .filter(x -> x.getType().equals(type))
              .collect(Collectors.toList());
  }
}

插入資料

到此JdbcTemplate的基本使用就到此結束,下面將介紹SimpleJdbcInsert。對比JdbcTemplate,它的功能更強大些,插入資料也更加方便

插入一行資料可以直接使用JdbcTemplate中的update方法。

但是考慮到多表關聯的情況,使用JdbcTemplate就有些麻煩:

如下所示:有三張表,一個使用者可以建立多個訂單,所以user_order表中一個user可以對應多條order。

我們在建立訂單的時候,除了需要將該訂單插入order表中,還需要將order_id插入到user_order表中。通常來說id欄位都是自增的,我們只需要往order表中插入order_name以及createTime就會自動為該記錄生成一個order_id。插入成功以後,我們需要取出該記錄的order_id,與user_id一起插入到user_order表中。
在這裡插入圖片描述
對比兩段程式碼來看看在實現方法上兩者的差別

程式碼中相應的表的欄位以及相互關係如下:
在這裡插入圖片描述

Taco對應Java物件有如下屬性:
在這裡插入圖片描述
如下程式碼的作用都是分別將order中的資訊分別插入到Taco表和Taco_Ingredients表

使用JdbcTemplate進行插入

package tacos.data;

import java.sql.Timestamp;
import java.sql.Types;
import java.util.Arrays;
import java.util.Date;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Repository;

import tacos.Ingredient;
import tacos.Taco;

@Repository
public class JdbcTacoRepository implements TacoRepository {

  private JdbcTemplate jdbc;

  public JdbcTacoRepository(JdbcTemplate jdbc) {
    this.jdbc = jdbc;
  }

  @Override
  public Taco save(Taco taco) {
    long tacoId = saveTacoInfo(taco);
    taco.setId(tacoId);
    for (Ingredient ingredient : taco.getIngredients()) {
      saveIngredientToTaco(ingredient, tacoId);
    }

    return taco;
  }

  private long saveTacoInfo(Taco taco) {
    taco.setCreatedAt(new Date());
    PreparedStatementCreator psc =
        new PreparedStatementCreatorFactory(
            "insert into Taco (name, createdAt) values (?, ?)",
            Types.VARCHAR, Types.TIMESTAMP
        ).newPreparedStatementCreator(
            Arrays.asList(
                taco.getName(),
                new Timestamp(taco.getCreatedAt().getTime())));

    KeyHolder keyHolder = new GeneratedKeyHolder();
    jdbc.update(psc, keyHolder);

    return keyHolder.getKey().longValue();
  }

  private void saveIngredientToTaco(
          Ingredient ingredient, long tacoId) {
    jdbc.update(
        "insert into Taco_Ingredients (taco, ingredient) " +
        "values (?, ?)",
        tacoId, ingredient.getId());
  }
}

使用SimpleJdbcInsert進行插入

package tacos.data;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import org.springframework.stereotype.Repository;
import tacos.Ingredient;
import tacos.Taco;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Repository
public class JdbcTacoRepository implements TacoRepository {

  private SimpleJdbcInsert tacoInserter;
  private SimpleJdbcInsert tacoIngredientsInserter;

  @Autowired
  public JdbcTacoRepository(JdbcTemplate jdbc) {
    this.tacoInserter = new SimpleJdbcInsert(jdbc)
            .withTableName("Taco")
            .usingGeneratedKeyColumns("id");
    this.tacoIngredientsInserter = new SimpleJdbcInsert(jdbc)
            .withTableName("Taco_Ingredients");
  }

  @Override
  public Taco save(Taco taco) {
    long tacoId = saveTacoInfo(taco);
    taco.setId(tacoId);
    for (Ingredient ingredient : taco.getIngredients()) {
      saveIngredientToTaco(ingredient, tacoId);
    }
    return taco;
  }

  private void saveIngredientToTaco(Ingredient ingredient, long tacoId) {
    Map<String, Object> values = new HashMap<>();
    values.put("taco", tacoId);
    values.put("ingredient", ingredient.getId());
    tacoIngredientsInserter.execute(values);
  }

  private long saveTacoInfo(Taco taco) {
    taco.setCreatedAt(new Date());
    Map<String, Object> values = new HashMap<>();
    values.put("createdAt", taco.getCreatedAt());
    values.put("name", taco.getName());
    long tacoId = tacoInserter
            .executeAndReturnKey(values)
            .longValue();
    return tacoId;
  }
}

對比SimpleJdbcInsert和JdbcTemplate的使用

左邊是使用JdbcTemplate。為了得到插入資料以後生成的tacoId。需要使用PreparedStatementCreatorkeyHolder。相比之下,SimpleJdbcInsert的程式碼則要簡潔很多。
在這裡插入圖片描述

參考

相關文章