1. 引言
在之前的教程中,我們介紹了 JDBI 的基本知識,一個開源庫,用於關係數據庫訪問,它消除了大量與直接 JDBC 使用相關的樣板代碼。
現在,我們將學習如何在 Spring Boot 應用程序中使用 JDBI。 我們還將涵蓋該庫的一些方面,使其在某些情況下成為 Spring Data JPA 的良好替代方案。
2. 項目設置
首先,讓我們將適當的 JDBI 依賴項添加到我們的項目中。現在我們將使用 JDBI 的 Spring 集成插件,它包含了所有必需的核心依賴項。 我們還將引入 SqlObject 插件,該插件為基礎 JDBI 添加了一些額外的功能,我們將在此插件中使用這些功能:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-spring5</artifactId>
<version>3.38.0</version>
</dependency>
<dependency>
<groupId>org.jdbi</groupId>
<artifactId>jdbi3-sqlobject</artifactId>
<version>3.38.0</version>
</dependency>最新的這些組件可以從 Maven Central 找到:
我們還需要一個合適的 JDBC 驅動程序來訪問我們的數據庫。 在本文中,我們將使用 H2,因此必須將其驅動程序添加到我們的依賴項列表中:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>runtime</scope>
</dependency>3. JDBI 實例化與配置
我們之前文章已經提到,Jdbi 實例是訪問 JDBI API 的入口點。 考慮到我們身處 Spring 生態中,將該類的實例作為 Bean 暴露是一個不錯的選擇。
我們將利用 Spring Boot 的自動配置功能,初始化 DataSource 並將其傳遞到一個帶有 @Bean 註解的方法中,該方法將創建我們的全局 Jdbi 實例。
我們還將傳遞任何發現的插件和 RowMapper 實例到該方法中,以便在應用程序啓動時進行註冊。
@Configuration
public class JdbiConfiguration {
@Bean
public Jdbi jdbi(DataSource ds, List<JdbiPlugin> jdbiPlugins, List<RowMapper<?>> rowMappers) {
TransactionAwareDataSourceProxy proxy = new TransactionAwareDataSourceProxy(ds);
Jdbi jdbi = Jdbi.create(proxy);
// Register all available plugins
log.info("[I27] Installing plugins... ({} found)", jdbiPlugins.size());
jdbiPlugins.forEach(jdbi::installPlugin);
// Register all available rowMappers
log.info("[I31] Installing rowMappers... ({} found)", rowMappers.size());
rowMappers.forEach(jdbi::registerRowMapper);
return jdbi;
}
}在這裏,我們使用了可用的 DataSource 並將其包裹在 TransactionAwareDataSourceProxy 中。 我們需要這個包裝器以便將 Spring 託管的事務與 JDBI 集成,正如稍後會看到的
註冊插件和 RowMapper 實例非常簡單。我們只需要為每個可用的 JdbiPlugin 和 RowMapper 調用 installPlugin 和 installRowMapper。 之後,我們就可以使用完全配置好的 Jdbi 實例在我們的應用程序中使用。
4. 示例領域
我們的示例使用一個非常簡單的領域模型,僅包含兩個類:CarMaker 和 CarModel。由於 JDBI 不需要對領域類進行任何註解,因此我們可以使用簡單的 POJO:
public class CarMaker {
private Long id;
private String name;
private List<CarModel> models;
// getters and setters ...
}
public class CarModel {
private Long id;
private String name;
private Integer yearDate;
private String sku;
private Long makerId;
// getters and setters ...
}
5. 創建 DAO
現在,讓我們為我們的領域類創建數據訪問對象 (DAO)。 JDBI 的 SqlObject 插件提供了一種簡單的方法來實現這些類,這與 Spring Data 處理類似的主題方式相似。
我們只需要定義一個接口並添加一些註解,JDBI 將自動處理所有低級別的任務,例如處理 JDBC 連接、創建和銷燬語句以及 ResultSet:
@UseClasspathSqlLocator
public interface CarMakerDao {
@SqlUpdate
@GetGeneratedKeys
Long insert(@BindBean CarMaker carMaker);
@SqlBatch("insert")
@GetGeneratedKeys
List<Long> bulkInsert(@BindBean List<CarMaker> carMakers);
@SqlQuery
CarMaker findById(Long id);
}
@UseClasspathSqlLocator
public interface CarModelDao {
@SqlUpdate
@GetGeneratedKeys
Long insert(@BindBean CarModel carModel);
@SqlBatch("insert")
@GetGeneratedKeys
List<Long> bulkInsert(@BindBean List<CarModel> models);
@SqlQuery
CarModel findByMakerIdAndSku(@Bind("makerId") Long makerId, @Bind("sku") String sku );
}這些接口被廣泛註釋,我們現在快速地查看一下它們。
5.1. @UseClasspathSqlLocator
@UseClasspathSqlLocator 標註告訴 JDBI 實際與每個方法關聯的 SQL 語句位於外部資源文件中。 默認情況下,JDBI 將使用接口的完全限定名和方法來查找資源。 例如,給定接口的 FQN 為 a.b.c.Foo 且具有 findById() 方法,JDBI 將查找名為 a/b/c/Foo/findById.sql 的資源。
此默認行為可以通過為 @SqlXXX 註解傳遞資源名稱來覆蓋任何給定方法的行為。
5.2. @SqlUpdate/@SqlBatch/@SqlQuery
我們使用 @SqlUpdate、@SqlBatch 和 @SqlQuery 註解來標記數據訪問方法,這些方法將使用提供的參數執行。這些註解可以接受可選的字符串值,該字符串值將是執行的字面 SQL 語句——包括任何命名參數——或者當與 @UseClasspathSqlLocator 一起使用時,它將包含該語句的資源名稱。
當註解為 @SqlBatch 的方法具有類似集合的參數,並且可以為可用項中的每個項執行相同的 SQL 語句,通過批量語句。
5.3. @GetGeneratedKeys
作為名稱所示,<strong class="annotation">@GetGeneratedKeys</strong> 註解允許我們在成功執行後檢索生成的鍵。它主要用於 <em class="statement">insert</em> 語句,其中數據庫會自動生成新的標識符,並且我們需要在代碼中檢索它們。
5.4. @BindBean/@Bind
我們使用 @BindBean 和 @Bind 註解,將 SQL 語句中的命名參數與方法參數綁定。 @BindBean 使用標準的 Bean 約定從 POJO 中提取屬性,包括嵌套屬性。 @Bind 使用參數名稱或提供的值將其值映射到命名參數。
6. 使用 DAO
為了在我們的應用程序中使用這些 DAO,我們需要使用 JDBI 中提供的工廠方法進行實例化。
在 Spring 上下文中,最簡單的方法是為每個 DAO 創建一個 Bean,使用 onDemand 方法:
@Bean
public CarMakerDao carMakerDao(Jdbi jdbi) {
return jdbi.onDemand(CarMakerDao.class);
}
@Bean
public CarModelDao carModelDao(Jdbi jdbi) {
return jdbi.onDemand(CarModelDao.class);
}
onDemand 創建的實例是線程安全的,並在方法調用期間僅使用數據庫連接。由於我們使用 TransactionAwareDataSourceProxy,這意味着我們可以無縫地與 Spring 管理的事務一起使用。
雖然這種方法簡單,但在處理多於幾個表的情況下,它並不是理想的。為了避免編寫這種樣板代碼,可以創建一個自定義 。但描述如何實現這樣一個組件超出了本教程的範圍。
7. 事務性服務
讓我們使用 DAO 類在一個簡單的服務類中創建幾個 CarModel 實例,給定一個填充了模型 CarMaker。 首先,我們將檢查給定的 CarMaker 是否之前已保存,如果需要則將其保存到數據庫中。 然後,我們將逐個插入每個 CarModel。
如果在任何時候發生唯一鍵衝突(或其他錯誤)時,整個操作必須失敗,並且應執行完整的回滾。
JDBI 提供了一個 @Transaction 註解,但我們在這裏不能使用它,因為它不瞭解可能參與同一業務事務的其他資源。 相反,我們將使用服務方法中的 Spring 的 @Transactional 註解:
@Service
public class CarMakerService {
private CarMakerDao carMakerDao;
private CarModelDao carModelDao;
public CarMakerService(CarMakerDao carMakerDao,CarModelDao carModelDao) {
this.carMakerDao = carMakerDao;
this.carModelDao = carModelDao;
}
@Transactional
public int bulkInsert(CarMaker carMaker) {
Long carMakerId;
if (carMaker.getId() == null ) {
carMakerId = carMakerDao.insert(carMaker);
carMaker.setId(carMakerId);
}
carMaker.getModels().forEach(m -> {
m.setMakerId(carMaker.getId());
carModelDao.insert(m);
});
return carMaker.getModels().size();
}
}
這個操作的實現本身相當簡單:我們採用的標準約定是,在 id 字段中值為 null 的情況下,表示該實體尚未持久化到數據庫。如果這種情況發生,我們使用構造函數中注入的 CarMakerDao 實例來在數據庫中插入一條新記錄並獲取生成的 id。
一旦我們獲取了 CarMaker 的 id,我們就遍歷模型,為每個模型設置 makerId 字段,然後再將其保存到數據庫中。
所有這些數據庫操作將使用相同的底層連接,並且將作為同一事務的一部分。這裏的關鍵在於我們如何使用 TransactionAwareDataSourceProxy 將 JDBI 與 Spring 綁定,以及創建 onDemand DAOs。當 JDBI 請求一個新的 Connection 時,它將獲取與當前事務關聯的現有連接,從而將它的生命週期集成到其他可能已註冊的資源中。
8. 結論
在本文中,我們展示瞭如何快速地將 JDBI 集成到 Spring Boot 應用程序中。這在某些原因下我們無法使用 Spring Data JPA,但仍然希望利用所有其他功能(如事務管理、集成等)時,是一種強大的組合。