簡介 Spring Data JDBC


不少開發者都知道,Spring Data 提供了持久層的支援,目的是希望與 Spring 其他元件易於整合,亦提供一致的存取模型。

談到 Spring Data 時,經常搭配的是 Spring Data JPA,當然,如果你對於 JPA 有一定的認識(可參考我 EJB3 文件中的 JPA 介紹),而且是以物件導向模型來看待持久層,可以考慮使用 Spring Data JPA 來進一步簡化 JPA 的使用。

然而,如果不是以物件導向模型在在設計的話,沒必要使用到 JPA 或 Spring Data JPA,以免面對複雜的物件關係對應、生命週期等問題,對於不同的資料庫,Spring Data 本身有相對應的方案。

你也許會想問了,Spring Data 有沒有 JDBC 的方案呢?就撰寫本文的這個時間點來說,答案是 YES,有個 Spring Data JDBC 的方案,General Available 版本為 1.0.3!就功能完備性來說,應該是還有待加強,然而,就使用的模型來說,確實是有助於簡化程式碼的設計與撰寫,日後功能若能持續加強,或許是可期待的方案之一,在這邊就順便談談好了!

使用 Spring Boot 的話,要使用 Spring Data JDBC 是很簡單的,只要加入 data-jdbc 的 starter 就好了,在這邊使用 H2 資料庫做示範,就一併加入好了:

implementation('org.springframework.boot:spring-boot-starter-data-jdbc')
runtimeOnly('com.h2database:h2')

接著可以設計一個與資料庫對應的物件:

package cc.openhome;

import org.springframework.data.annotation.Id;

public class Message {
    @Id
    private Integer id;
    private String username;
    private Long millis;
    private String blabla;

    public Message(String username, Long millis, String blabla) {
        this.username = username;
        this.millis = millis;
        this.blabla = blabla;
    }

    public Integer getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public Long getMillis() {
        return millis;
    }

    public String getBlabla() {
        return blabla;
    }
}

如果你使用過 JPA,應該是很熟悉這個動作,Spring Data JDBC 需要有 id 才能運作,不過上頭的 @Id 並不是 JPA 中的 @Id,而是 org.springframework.data.annotation.Id

接著建立一個繼承 CrudRepository 的介面:

package cc.openhome;

import org.springframework.data.repository.CrudRepository;

public interface MessageRepository extends CrudRepository<Message, Integer> {
}

顧名思義,CrudRepository 這個介面,定義了 savefindByIddelete 等方法,繼承的時候,只要指定對應表格的類型與 id 類型,剩下的就交給 Spring Data JDBC 來處理了(如果曾經使用過 Spring Data JPA,會覺得設定方式是大同小異,畢竟是基於 Spring Data 的設計模型)。

Spring Boot 預設會掃描同一套件下的 Repository,如果不是同一套件,記得在 JavaConfig 上加上 @EnableJdbcRepositories,並設定 basePackages 屬性。例如:

@EnableJdbcRepositories(
    basePackages= {
        "cc.openhome.model"
    }
)

預設的表格與上頭定義的 Message 是對應的,因此建立表格的 SQL 可以是:

CREATE TABLE message (
    id INT NOT NULL AUTO_INCREMENT,
    username VARCHAR(15) NOT NULL,
    millis BIGINT NOT NULL,
    blabla VARCHAR(512) NOT NULL,
    PRIMARY KEY (id)
);

你可以將上頭的 SQL 儲存為 schema.sql,並放在類別路徑之中,Spring Boot 的話,會試著找 schema.sql 這個名稱,如果有的話會使用記憶體作為資料庫,並執行其中的 SQL;如果你要儲存為別的名稱,就自行設置一個 Bean。

@Bean(destroyMethod="shutdown")
public DataSource dataSource(){
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:schema_message.sql")
            .build();
}

來寫個簡單的測試看看:

package cc.openhome;

import static org.junit.Assert.assertNotNull;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DataJdbcApplicationTests {
    @Autowired
    MessageRepository messageRepository;

    @Test
    public void messageRepository() {
        Message message = messageRepository.save(
            new Message(
                "caterpillar",
                1518666769369L, 
                "JavaScript 名稱空間管理 https://openhome.cc/Gossip/ECMAScript/NameSpace.html"
            )
        );

        assertNotNull(message.getId());
    }
}

如果你想要的方法,並不存在於 CrudRepository 呢?這可以使用 @Query@Modifying 來定義,例如設計一個 MessageDAO

package cc.openhome;

import java.util.List;

import org.springframework.data.jdbc.repository.query.Modifying;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

public interface MessageDAO extends CrudRepository<Message, Integer> {
    @Query("SELECT * FROM message m WHERE m.username = :username")
    List<Message> messagesBy(@Param("username") String username);

    @Modifying
    @Query("DELETE FROM message WHERE username = :username AND millis = :millis")   
    void deleteMessageBy(@Param("username") String username, @Param("millis") String millis);

    @Query("SELECT * FROM message ORDER BY millis DESC LIMIT :n")
    List<Message> newestMessages(@Param("n") int n);
}

@Query 中可以撰寫 SQL,相關的參數可以使用 @Param 來標註對應,如果是個更新操作,額外標註 @Modifying 就可以了。

可以在上頭的測試案例中加上 MessageDAO 的測試:

package cc.openhome;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DataJdbcApplicationTests {
    @Autowired
    MessageRepository messageRepository;

    @Autowired
    MessageDAO messageDAO;

    @Test
    public void messageRepository() {
        Message message = messageRepository.save(
            new Message(
                "caterpillar",
                1518666769369L, 
                "JavaScript 名稱空間管理 https://openhome.cc/Gossip/ECMAScript/NameSpace.html"
            )
        );

        assertNotNull(message.getId());
    }

    @Test
    public void messageDAO() {
        messageDAO.save(
            new Message(
                "caterpillar",
                1518666769369L, 
                "JavaScript 名稱空間管理 https://openhome.cc/Gossip/ECMAScript/NameSpace.html"
            )
        );

        assertEquals(messageDAO.messagesBy("caterpillar").size(), 1);

        assertEquals(messageDAO.newestMessages(1).size(), 1);

        messageDAO.deleteMessageBy("caterpillar", String.valueOf(1518666769369L));
        assertEquals(messageDAO.messagesBy("caterpillar").size(), 0);
    }
}

如果 Spring Data JDBC 引起你的興趣的話,可以看看官方網站上的資源,像是〈Spring Data JDBC - Reference Documentation〉。

你可以在 DataJDBC 找到以上的範例專案。