不少開發者都知道,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
這個介面,定義了 save
、findById
、delete
等方法,繼承的時候,只要指定對應表格的類型與 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 找到以上的範例專案。