建立第三方 API 的 Service 後,將把交易從 Service 抽離,獨立為單一類別,因內部調用 @Transactional 將不會有作用,加上交易隱含著鎖的概念,前置檢查、資料庫查詢等,將不寫在交易裡面, EP 22-7 使用 JPA 增加範例 ,並透過 JUnit 5 來驗證產出結果。
Table of Contents
Toggle前言
Spring Data JPA 是基於 Hibernate 開發的 JPA 框架,簡化了 JPA 的寫法,可以在幾乎不用寫實作程式碼的情況下,實現對資料庫的存取操作,也能設定多個資料庫來源,使用多個資料表查詢,採用原生 SQL 自訂查詢等功能。
Java HttpClient TX
檔案目錄
./
+- build.gradle
+- src
+- main
+- java
+- org
+- ruoxue
+- spring_boot_168
+- game
| +- ggg
| +- ex
| | +- UserException.java
| +- service
| | +- UserServiceImpl.java
| | +- UserTX.java
實作
UserTX.java
實作 TX ,注入 UserRepository ,加上 @Transactional 實現交易機制。
package org.ruoxue.spring_boot_168.game.ggg.service;
import org.ruoxue.spring_boot_168.game.ggg.model.User;
import org.ruoxue.spring_boot_168.game.ggg.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 使用者 TX
*/
@Component
public class UserTX {
/**
* 使用者儲存庫
*/
@Autowired
private UserRepository userRepository;
public UserTX() {
}
/**
* TX
*
* 新增使用者
*
* @param user
* @return
*/
@Transactional
public User insert(User user) {
User result = userRepository.save(user);
return result;
}
/**
* TX
*
* 更新使用者
*
* @param user
* @return
*/
@Transactional
public User update(User user) {
User result = userRepository.save(user);
return result;
}
/**
* TX
*
* 刪除使用者
*
* @param user
*/
@Transactional
public void delete(User user) {
userRepository.delete(user);
}
}
UserException.java
新增檔案,自定義例外,當發生錯誤時,拋出此例外,讓外層調用的服務,處理例外,如:記錄 Log。
package org.ruoxue.spring_boot_168.game.ggg.ex;
public class UserException extends RuntimeException {
private static final long serialVersionUID = 257390917818772971L;
public UserException() {
super();
}
public UserException(String message) {
super(message);
}
public UserException(Throwable cause) {
super(cause);
}
public UserException(String message, Throwable cause) {
super(message, cause);
}
}
UserServiceImpl.java
重構 Service ,交易改為調用外部類別,移除 @Transactional 註解 ,當錯誤發生時,拋出自定義例外。
package org.ruoxue.spring_boot_168.game.ggg.service;
import org.ruoxue.spring_boot_168.game.ggg.ex.UserException;
import org.ruoxue.spring_boot_168.game.ggg.model.User;
import org.ruoxue.spring_boot_168.game.ggg.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 使用者服務實作
*/
@Service
public class UserServiceImpl implements UserService {
/**
* 使用者儲存庫
*/
@Autowired
private UserRepository userRepository;
/**
* 使用者 TX
*/
@Autowired
private UserTX tx;
public UserServiceImpl() {
}
/**
* 新增使用者
*
* @param user
* @return
*/
@Override
public User insert(User user) {
User result = null;
try {
result = tx.insert(user);
} catch (UserException ex) {
throw ex;
} catch (Exception ex) {
throw new UserException(ex);
}
return result;
}
/**
* 依帳號取得使用者
*
* @param cid
* @return
*/
@Override
public User findByCid(String cid) {
User result = null;
try {
result = userRepository.findByCid(cid);
} catch (UserException ex) {
throw ex;
} catch (Exception ex) {
throw new UserException(ex);
}
return result;
}
/**
* 更新使用者
*
* @param user
* @return
*/
@Override
public User update(User user) {
User result = null;
try {
result = tx.update(user);
} catch (UserException ex) {
throw ex;
} catch (Exception ex) {
throw new UserException(ex);
}
return result;
}
/**
* 刪除使用者
*
* @param user
*/
@Override
public void delete(User user) {
try {
tx.delete(user);
} catch (UserException ex) {
throw ex;
} catch (Exception ex) {
throw new UserException(ex);
}
}
}
測試 JUnit 5
UserTXTest.java
新增單元測試,驗證是否符合預期 。
package org.ruoxue.spring_boot_168.game.ggg.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.ruoxue.spring_boot_168.Application;
import org.ruoxue.spring_boot_168.game.ggg.model.User;
import org.ruoxue.spring_boot_168.game.ggg.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class)
public class UserTXTest {
@Autowired
private UserRepository repository;
@Autowired
private UserTX tx;
@Test
public void service() {
System.out.println(tx);
assertNotNull(tx);
}
@Test
public void insert() {
String value = "ruoxue";
User found = repository.findByCid(value);
if (found != null) {
return;
}
User user = new User();
user.setCid(value);
user.setName("player");
user.setPassword("");
user.setSalt("");
User result = tx.insert(user);
System.out.println(result);
assertNotNull(result);
assertEquals(value, result.getCid());
}
@Test
public void update() {
String value = "ruoxue";
String name = "test_player";
User user = repository.findByCid(value);
if (user != null) {
user.setName(name);
User updated = tx.update(user);
System.out.println(user);
assertEquals(name, updated.getName());
}
}
@Test
public void delete() {
String value = "ruoxue";
User user = repository.findByCid(value);
if (user != null) {
tx.delete(user);
System.out.println(user);
}
}
}
insert
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
Hibernate: select user0_.id as id1_0_, user0_.cid as cid2_0_, user0_.name as name3_0_, user0_.password as password4_0_, user0_.salt as salt5_0_ from game_ggg_user user0_ where user0_.cid=?
Hibernate: insert into game_ggg_user (cid, name, password, salt, id) values (?, ?, ?, ?, ?)
{"cid":"ruoxue","name":"player"}
update
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
Hibernate: select user0_.id as id1_0_, user0_.cid as cid2_0_, user0_.name as name3_0_, user0_.password as password4_0_, user0_.salt as salt5_0_ from game_ggg_user user0_ where user0_.cid=?
Hibernate: select user0_.id as id1_0_0_, user0_.cid as cid2_0_0_, user0_.name as name3_0_0_, user0_.password as password4_0_0_, user0_.salt as salt5_0_0_ from game_ggg_user user0_ where user0_.id=?
Hibernate: update game_ggg_user set cid=?, name=?, password=?, salt=? where id=?
{"cid":"ruoxue","name":"test_player"}
delete
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
Hibernate: select user0_.id as id1_0_, user0_.cid as cid2_0_, user0_.name as name3_0_, user0_.password as password4_0_, user0_.salt as salt5_0_ from game_ggg_user user0_ where user0_.cid=?
Hibernate: select user0_.id as id1_0_0_, user0_.cid as cid2_0_0_, user0_.name as name3_0_0_, user0_.password as password4_0_0_, user0_.salt as salt5_0_0_ from game_ggg_user user0_ where user0_.id=?
Hibernate: delete from game_ggg_user where id=?
{"cid":"ruoxue","name":"test_player"}
心得分享
獨立 TX 為外部類別,是為了讓交易鎖定的範圍成為最小, 一個交易的範圍內,若包含查詢資料庫,與新增或修改或刪除的語法時,效能將會明顯地降低,因此為了避免太多查詢語法在一個交易範圍內,將交易獨立成外部調用。