在上一篇文章中,我們使用了基本的 Java SQL API 來操作 H2 資料庫,雖然這種方法可行,但程式碼冗長且容易出錯
今天,我們來介紹兩個工具:JdbcTemplate
和 JdbcClient
,它們可以大大簡化我們的資料庫操作程式碼
在此之前,讓我們先了解一下 JDBC API 和原生 Java SQL 的關係。
JDBC(Java Database Connectivity
)API 是 Java 標準函式庫中用於資料庫操作的介面集合
它提供了一套統一的方法來連接和操作各種關係型資料庫
而原生 Java SQL 則是直接使用 JDBC API 來執行 SQL 語句的方式
現在,讓我們看看 Spring 框架如何通過 JdbcTemplate 和 JdbcClient 來簡化這些操作。
JdbcTemplate 和 JdbcClient 都是 Spring Framework
提供的工具,用於簡化 JDBC 操作
它們的主要目的是減少樣板程式碼,提高開發效率,並降低錯誤率
JdbcTemplate 是 Spring Framework 中較早引入的 JDBC 抽象層級
它封裝了許多 JDBC 操作,如建立連接、準備和執行語句、處理異常等,讓開發者能夠專注於 SQL 邏輯而不是底層細節
JdbcClient 是 Spring Framework 6.0 中引入的新 API,專為簡化 JDBC 操作而設計
它提供了一個流暢的 API,使得資料庫操作更加直觀和簡潔
如果還沒有加入 JDBC
的依賴的話,需要在 build.gradle
檔案中添加必要的依賴
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
在 application.properties
裡面要先設定 datadsource 相關的設定,Spring Boot 就會自動配置 DataSource 和 JdbcTemplate
spring.datasource.url=jdbc:h2:~/test
spring.datasource.username=sa
spring.datasource.password=
如果沒有在 application.properties 設定 datasource 相關參數的話,預設會把資料庫建立在記憶體裡面
可以在 log
看到名稱為一個 GUID
, mem
代表 memory 的意思
讓我們來看看如何使用 JdbcTemplate 重寫之前的 TodoController
@RestController
@RequestMapping("/api/todos")
public class TodoController {
// 為了方便,在這裡只展示了修改後,所增加的 DB 操作相關程式碼
private final JdbcTemplate jdbcTemplate;
public TodoController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@PostMapping
public ResponseEntity<MyApiResponse<Todo>> createTodo(@RequestBody Todo todo) {
try {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(
"INSERT INTO todos (title, completed) VALUES (?, ?)",
Statement.RETURN_GENERATED_KEYS);
ps.setString(1, todo.getTitle());
ps.setBoolean(2, todo.isCompleted());
return ps;
}, keyHolder);
todo.setId(keyHolder.getKey().longValue());
return ResponseEntity.ok(new MyApiResponse<>(true, todo, null));
} catch (Exception e) {
// return error …
}
}
@GetMapping
public ResponseEntity<MyApiResponse<List<Todo>>> getAllTodos() {
try {
List<Todo> todos = jdbcTemplate.query("SELECT * FROM todos", todoRowMapper());
return ResponseEntity.ok(new MyApiResponse<>(true, todos, null));
} catch (Exception e) {
// return error …
}
}
@GetMapping("/{id}")
public ResponseEntity<MyApiResponse<Todo>> getTodo(@PathVariable Long id) {
try {
Todo todo = jdbcTemplate.query(
"SELECT * FROM todos WHERE id = ?",
todoRowMapper(),
id).get(0);
if (todo == null) {
return createNotFoundError(id);
}
return ResponseEntity.ok(new MyApiResponse<>(true, todo, null));
} catch (Exception e) {
// return error …
}
}
@PutMapping("/{id}")
public ResponseEntity<MyApiResponse<Todo>> updateTodo(@PathVariable Long id, @RequestBody Todo updatedTodo) {
try {
int affectedRows = jdbcTemplate.update(
"UPDATE todos SET title = ?, completed = ? WHERE id = ?",
updatedTodo.getTitle(), updatedTodo.isCompleted(), id);
if (affectedRows > 0) {
updatedTodo.setId(id);
return ResponseEntity.ok(new MyApiResponse<>(true, updatedTodo, null));
} else {
return createNotFoundError(id);
}
} catch (Exception e) {
// return error …
}
}
@DeleteMapping("/{id}")
public ResponseEntity<MyApiResponse<Todo>> deleteTodo(@PathVariable Long id) {
try {
int affectedRows = jdbcTemplate.update("DELETE FROM todos WHERE id = ?", id);
if (affectedRows > 0) {
return ResponseEntity.ok(new MyApiResponse<>(true, null, null));
} else {
return createNotFoundError(id);
}
} catch (Exception e) {
// return error …
}
}
private RowMapper<Todo> todoRowMapper() {
return (rs, rowNum) -> {
Todo todo = new Todo();
todo.setId(rs.getLong("id"));
todo.setTitle(rs.getString("title"));
todo.setCompleted(rs.getBoolean("completed"));
return todo;
};
}
}
之前的 DatabaseInitializer 也可以使用 JdbcTemplate 改寫一下
@Component
public class DatabaseInitializer {
private final JdbcTemplate jdbcTemplate;
public DatabaseInitializer(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@PostConstruct
public void initDatabase() {
String sql = "CREATE TABLE IF NOT EXISTS todos (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY," +
"title VARCHAR(255) NOT NULL," +
"completed BOOLEAN NOT NULL)";
try {
jdbcTemplate.execute(sql);
System.out.println("初始化資料庫成功");
} catch (Exception e) {
System.err.println("初始化資料庫失敗:" + e.getMessage());
}
}
}
現在,讓我們看看如何使用 JdbcClient 來實現相同的功能
在使用 JdbcClient 時,必須確認映射的類別 (在這裡就是 Todo ),是否有預設無參數的建構子
.以及,屬性名稱
與資料庫的名稱有沒有一樣
如果不一樣的話,需要提供一個自定義的 RowMapper
(就跟 JdbcTemplate
的 RowMapper
一樣)
@RestController
@RequestMapping("/api/todos")
public class TodoController {
// 為了方便,在這裡只展示了修改後,所增加的 DB 操作相關程式碼
private final JdbcClient jdbcClient;
public TodoController(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
@PostMapping
public ResponseEntity<MyApiResponse<Todo>> createTodo(@RequestBody Todo todo) {
try {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcClient.sql("INSERT INTO todos (title, completed) VALUES (:title, :completed)")
.param("title", todo.getTitle())
.param("completed", todo.isCompleted())
.update(keyHolder);
Long id = keyHolder.getKey().longValue();
todo.setId(id);
return ResponseEntity.ok(new MyApiResponse<>(true, todo, null));
} catch (Exception e) {
// return error …
}
}
@GetMapping
public ResponseEntity<MyApiResponse<List<Todo>>> getAllTodos() {
logger.info("get all todo");
try {
List<Todo> todos = jdbcClient.sql("SELECT * FROM todos")
.query(Todo.class)
.list();
return ResponseEntity.ok(new MyApiResponse<>(true, todos, null));
} catch (Exception e) {
// return error …
}
}
@GetMapping("/{id}")
public ResponseEntity<MyApiResponse<Todo>> getTodo(@PathVariable Long id) {
try {
Todo todo = jdbcClient.sql("SELECT * FROM todos WHERE id = :id")
.param("id", id)
.query(Todo.class)
.optional()
.orElse(null);
if (todo == null) {
return createNotFoundError(id);
}
return ResponseEntity.ok(new MyApiResponse<>(true, todo, null));
} catch (Exception e) {
// return error …
}
}
@PutMapping("/{id}")
public ResponseEntity<MyApiResponse<Todo>> updateTodo(@PathVariable Long id, @RequestBody Todo updatedTodo) {
try {
int affectedRows = jdbcClient.sql("UPDATE todos SET title = :title, completed = :completed WHERE id = :id")
.param("title", updatedTodo.getTitle())
.param("completed", updatedTodo.isCompleted())
.param("id", id)
.update();
if (affectedRows > 0) {
updatedTodo.setId(id);
return ResponseEntity.ok(new MyApiResponse<>(true, updatedTodo, null));
} else {
return createNotFoundError(id);
}
} catch (Exception e) {
// return error …
}
}
@DeleteMapping("/{id}")
public ResponseEntity<MyApiResponse<Todo>> deleteTodo(@PathVariable Long id) {
try {
int affectedRows = jdbcClient.sql("DELETE FROM todos WHERE id = :id")
.param("id", id)
.update();
if (affectedRows > 0) {
return ResponseEntity.ok(new MyApiResponse<>(true, null, null));
} else {
return createNotFoundError(id);
}
} catch (Exception e) {
// return error …
}
}
}
DatabaseInitializer 再使用 JdbcClient 改寫一下
@Component
public class DatabaseInitializer {
private final JdbcClient jdbcClient;
public DatabaseInitializer(JdbcClient jdbcClient) {
this.jdbcClient = jdbcClient;
}
@PostConstruct
public void initDatabase() {
String sql = "CREATE TABLE IF NOT EXISTS todos (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY," +
"title VARCHAR(255) NOT NULL," +
"completed BOOLEAN NOT NULL)";
try {
jdbcClient.sql(sql).update();
System.out.println("初始化資料庫成功");
} catch (Exception e) {
System.err.println("初始化資料庫失敗:" + e.getMessage());
}
}
}
HikariCP
是一個在 Spring Boot 應用程式中常見的高效能 JDBC 連線池
(Connection Pool)函式庫
它以其速度和穩定性著稱,常用於 Java 應用程式中來管理資料庫連接
HikariCP 指的是 Hikari Connection Pool
hikari 是日文中的「光」的意思
以下是幾個重點
spring.datasource.hikari
命名空間來設定 Hikari 特定的屬性無論選擇 JdbcTemplate 還是 JdbcClient,它們都能為 Java 開發者提供強大的工具
以下是幾個主要觀點
選擇適合自己專案需求的工具,將有助於更高效地開發和維護資料庫相關的應用程式
同步刊登於 Blog 「Spring Boot API 開發:從 0 到 1」Day 18 JdbcTemplate 與 JdbcClient
我的粉絲專頁
圖片來源:AI 產生