說說如何在 Spring Boot 中使用 JdbcTemplate 讀寫資料

deniro_li發表於2020-09-26

首先在 pom.xml 中引入依賴。

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

<!--h2-->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

示例程式打算使用 h2 快取資料庫,所以這裡也一併引用。

1 h2 快取資料庫

h2是一個開源的嵌入式(非嵌入式裝置)資料庫引擎,基於Java開發,可直接嵌入到應用程式中,與應用程式一起打包釋出,不受平臺限制。

啟動應用後,在瀏覽器位址列輸入 http://127.0.0.1:8080/h2-console,就可以開啟 h2 控制檯。

首先選擇控制檯編碼格式為中文,接著輸入 JDBC URL,然後點選“測試連線”,如果連線成功,就會提示“測試成功”。

最後點選“連線”按鈕,就會開啟資料庫控制檯客戶端,連線到 h2 資料庫:

2 初始化表結構與資料

在 src/main/resources/ 下,新建 schema.sql 檔案編寫表結構 SQL。在同一個目錄下,新建 data.sql 檔案,編寫初始化資料 SQL。這樣在應用啟動時,Spring Boot 就會執行這些指令碼。

schema.sql:

create table if not exists Book
( id varchar( 4) not null, name varchar( 25) not null, type varchar( 10) not null );

data.sql:

insert into Book
  (id, name, type)
values
  ('1', '兩京十五日', '小說');
insert into Book
  (id, name, type)
values
  ('2', '把自己作為方法', '歷史');
insert into Book
  (id, name, type)
values
  ('3', '正常人', '小說');

啟動成功後,就會在 h2 資料庫控制檯客戶端中看到新建好的表與資料。

點選左側的 Book,就會在右側的 SQL 輸入框中自動生成查詢該表的 SQL 語句,然後點選 “Run”,執行它。我們就會在右下角看到初始化的表資料。

3 編碼

3.1 新建實體類

@Data
@RequiredArgsConstructor
public class Book {

    private final String id;
    private final String name;
    private final String type;
}

這裡用了 Lombok 外掛。Lombok 是一種 Java 實用工具,可用來幫助我們消除 Java 冗長的樣板式程式碼。

加了 @Data 註解的Java 類,在編譯之後會自動為我們加上這些方法:

  1. 所有屬性的get和set方法;
  2. toString 方法;
  3. hashCode方法;
  4. equals方法。

@RequiredArgsConstructor 註解會將類中所有帶有 @NonNull 註解和以final修飾的未經初始化的欄位作為建構函式的入參。

3.2 新建 Repository 類

首先定義一個 Repository 介面,然後新建這個介面的實現類。

介面:

public interface BookRepository {

    Iterable<Book> findAll();

    Book findOne(String id);

    Book save(Book Book);
}

實現類:

@Repository
public class JdbcBookRepository implements BookRepository {

    private JdbcTemplate jdbc;

    @Autowired
    public JdbcBookRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    @Override
    public Iterable<Book> findAll() {
        return null;
    }

    @Override
    public Book findOne(String id) {
        return null;
    }

    @Override
    public Book save(Book Book) {
        return null;
    }
}

@Repository和@Controller、@Service、@Component的作用差不多,目的都是把物件交給spring管理。@Repository一般用在持久層的實現類上。

@Autowired註解可通過byType的形式,來給指定的欄位或方法注入所需的外部資源。

autowire 有以下四種模式:

模式說明
byName根據屬性的名字自動裝配
byType根據屬性的型別自動裝配
constructor與 byType 類似,不同之處在於它應用於構造器引數,如果沒有找到會丟擲異常
autodetect會在 byType 和 constructor 中智慧選擇

這裡通過 @Autowired 標註的構造器將 JdbcTemplate 注入進來。這個構造器將 JdbcTemplate 賦值給一個例項變數,這個變數會被其他方法用來執行資料庫查詢或更新等操作。

3.3 查詢操作

假設我們需要查詢出所有的書籍,那麼就可以呼叫 JdbcTemplate 的 List<T> query(String sql, RowMapper<T> rowMapper) 方法。

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

private Book mapRowToBook(ResultSet rs, int rowNum) throws SQLException {
    return new Book(rs.getString("id"), rs.getString("name"),
            rs.getString("type"));
}

這裡利用了 Java 的方法引用,來編寫 RowMapper 入參。這樣做的好處是:相對於原來的匿名內部類的寫法,方法引用的寫法更加簡潔。

3.4 update()

JdbcTemplate 的 update() 方法可以用來新增或更新資料。

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

update() 方法定義如下:

int update(String sql, @Nullable Object... args)

它接受一個包含佔位符的 SQL 語句以及多個入參。返回實際影響到的記錄數。

建立 Spring 單元測試類來驗證剛剛新建的 save() 方法:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class JdbcBookRepositoryTest {

    @Autowired
    private JdbcBookRepository jdbcBookRepository;

    @Test
    public void save() {

        Book book = new Book("4", "比利時的哀愁", "小說");
        jdbcBookRepository.save(book);
        Book find=jdbcBookRepository.findOne("4");
        assertEquals("比利時的哀愁",find.getName());
    }
}

3.5 SimpleJdbcInsert 包裝器類

SimpleJdbcInsert 一般用於多表插入場景。SimpleJdbcInsert 有兩個方法執行資料插入操作: execute() 和 executeAndReturnKey()。 它們都接受 Map<String ,Object> 作為引數,其中的 key 對應資料表中的列名,而 value 對應要插入到列中的實際值。

我們舉一個圖書示例。一本圖書可以包含多個標籤;而一個標籤也可以隸屬於多本圖書。它們之間是多對多的關係,因此建立圖書與標籤的對映表來專門存放這些關係。具體如下圖所示:

首先在 schema.sql 中,加入這些表結構建立語句:

create table if not exists Book
( id identity, name varchar( 25) not null, type varchar( 10) not null );


create table if not exists Tag
( id identity, name varchar( 25) not null);


create table if not exists Book_Tags ( book bigint not null, tag bigint not null );

alter table Book_Tags add foreign key (book) references Book( id);
alter table Book_Tags add foreign key (tag) references Tag( id);

接著,建立這些實體類:

@Data
@RequiredArgsConstructor
public class Tag {

    private final Long id;
    private final String name;
}

@Data
@RequiredArgsConstructor
public class Book {

    private final String id;
    private final String name;
    private final String type;

    private List<Tag> tags = new ArrayList<>();
}

然後在 Repository 實現類的建構函式中,初始化每張表的 SimpleJdbcInsert 例項:

@Repository
public class JdbcBookRepository implements BookRepository {

    private static final Logger log = LogManager.getFormatterLogger();

    private final SimpleJdbcInsert bookInserter;
    private final SimpleJdbcInsert tagInserter;
    private final SimpleJdbcInsert bookTagsInserter;
    private final ObjectMapper objectMapper;
    private JdbcTemplate jdbc;

    @Autowired
    public JdbcBookRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
        this.bookInserter = new SimpleJdbcInsert(jdbc)
                .withTableName("Book")
                .usingGeneratedKeyColumns("id");

        this.tagInserter = new SimpleJdbcInsert(jdbc)
                .withTableName("Tag")
                .usingGeneratedKeyColumns("id");

        this.bookTagsInserter = new SimpleJdbcInsert(jdbc)
                .withTableName("Book_Tags");

        this.objectMapper = new ObjectMapper();
    }
    ...
}

SimpleJdbcInsert 的 withTableName() 方法用於指定表名;而 usingGeneratedKeyColumns() 方法用於指定主鍵。

具體儲存操作程式碼為:

public Book saveIncludeTags(Book book) {
    //儲存圖書
    Map<String, Object> values = objectMapper.convertValue(book, Map.class);
    long bookId = bookInserter.executeAndReturnKey(values).longValue();

    //儲存標籤
    List<Long> tagIds = new ArrayList<>();
    for (Tag tag : book.getTags()) {
        values = objectMapper.convertValue(tag, Map.class);
        long tagId = tagInserter.executeAndReturnKey(values).longValue();
        tagIds.add(tagId);
    }

    //關聯圖書與標籤
    for (Long tagId : tagIds) {
        values.clear();
        values.put("book", bookId);
        values.put("tag", tagId);
        log.info("values -> %s", values);
        bookTagsInserter.execute(values);
    }

    return book;
}
  • SimpleJdbcInsert 的 executeAndReturnKey() 與 execute() 方法都支援 Map<String, ?> 形式的入參。它們之間的區別是 executeAndReturnKey() 會返回 Number 形式的主鍵值。
  • 可以利用 Jackson 的 ObjectMapper.convertValue(Object fromValue, Class<T> toValueType) 方法把一個 POJO 轉換為相應的 Map 物件值。
  • Number 型別可以根據場景對其進行轉換。

這段程式碼首先先儲存圖書,得到圖書主鍵;然後儲存標籤,得到標籤主鍵;最後把前面得到的圖書主鍵與標籤主鍵儲存到它們之間的關係表中。

相關文章