Web TX - Spring Boot 168 EP 5-4

Web TX – Spring Boot 168 EP 5-4

將把交易從 Service 抽離,獨立為單一類別,因內部調用 @Transactional 將不會有作用,加上交易隱含著鎖的概念,前置檢查、資料庫查詢等,將不寫在交易裡面, EP 5-4 使用 JPA 增加範例 ,並透過 JUnit 5 來驗證產出結果。

前言

Spring Data JPA 是基於 Hibernate 開發的 JPA 框架,簡化了 JPA 的寫法,可以在幾乎不用寫實作程式碼的情況下,實現對資料庫的存取操作,也能設定多個資料庫來源,使用多個資料表查詢,採用原生 SQL 自訂查詢等功能。

Web TX

檔案目錄

./
   +- build.gradle
       +- src
           +- main
               +- java
                   +- org
                       +- ruoxue
                           +- spring_boot_168
                               +- sso
                               |   +- account
                               |       +- service
                               |       |   +- AccountService.java 
                               |       |   +- AccountServiceImpl.java 
                               |       |   +- AccountTX.java 

設定

AccountService.java
Service 如何建立,參考此篇:

實作 Spring Boot Web TX Example

AccountTX.java

實作 TX ,注入 AccountRepository ,加上 @Transactional 實現交易機制。

package org.ruoxue.spring_boot_168.sso.account.service;

import org.ruoxue.spring_boot_168.sso.account.model.Account;
import org.ruoxue.spring_boot_168.sso.account.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

/**
 * 帳號 TX
 */
@Component
public class AccountTX {

	/**
	 * 帳號儲存庫
	 */
	@Autowired
	private AccountRepository accountRepository;

	public AccountTX() {

	}

	/**
	 * TX
	 * 
	 * 新增帳號
	 * 
	 * @param account
	 * @return
	 */
	@Transactional
	public Account insert(Account account) {
		Account result = accountRepository.save(account);
		return result;
	}

	/**
	 * TX
	 * 
	 * 更新帳號
	 * 
	 * @param account
	 * @return
	 */
	@Transactional
	public Account update(Account account) {
		Account result = accountRepository.save(account);
		return result;
	}

	/**
	 * TX
	 * 
	 * 刪除帳號
	 * 
	 * @param account
	 */
	@Transactional
	public void delete(Account account) {
		accountRepository.delete(account);
	}
}

AccountService.java

建立 Service 介面,宣告 insert 、 update 、 delete 方法,此時並沒有任何實作。

package org.ruoxue.spring_boot_168.sso.account.service;

import org.ruoxue.spring_boot_168.sso.account.model.Account;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

/**
 * 帳號服務
 */
public interface AccountService {

	/**
	 * 新增帳號資料
	 * 
	 * @param account
	 * @return
	 */
	Account insertAccount(Account account);

	/**
	 * 取得帳號資料
	 * 
	 * @param cid
	 * @return
	 */
	Account findByCid(String cid);

	/**
	 * 取得所有帳號資料
	 * 
	 * @param pageable
	 * @return
	 */
	Page<Account> findAll(Pageable pageable);

	/**
	 * 修改帳號資料
	 * 
	 * @param account
	 * @return
	 */
	Account updateAccount(Account account);

	/**
	 * 刪除帳號資料
	 * 
	 * @param account
	 * @return
	 */
	int deleteAccount(Account account);

	/**
	 * 新增帳號
	 * 
	 * @param account
	 * @return
	 */
	Account insert(Account account);

	/**
	 * 更新帳號
	 * 
	 * @param account
	 * @return
	 */
	Account update(Account account);

	/**
	 * 刪除帳號
	 * 
	 * @param account
	 */
	void delete(Account account);
}

AccountServiceImpl.java

重構 Service ,交易改為調用外部類別,移除 @Transactional 註解 ,當錯誤發生時,拋出自定義例外。

package org.ruoxue.spring_boot_168.sso.account.service;

import org.ruoxue.spring_boot_168.sso.account.ex.AccountException;
import org.ruoxue.spring_boot_168.sso.account.model.Account;
import org.ruoxue.spring_boot_168.sso.account.repository.AccountRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class AccountServiceImpl implements AccountService {

	/**
	 * 帳號儲存庫
	 */
	@Autowired
	private AccountRepository accountRepository;

	/**
	 * 帳號 TX
	 */
	@Autowired
	private AccountTX tx;

	public AccountServiceImpl() {
	}

	/**
	 * 新增帳號資料
	 * 
	 * @param account
	 * @return
	 */
	@Override
	public Account insertAccount(Account account) {
		Account result = null;
		try {
			if (account == null) {
				throw new AccountException("account 必須有值。");
			}
			result = insert(account);
		} catch (AccountException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new AccountException(ex);
		}
		return result;
	}

	/**
	 * 取得帳號資料
	 * 
	 * @param cid
	 * @return
	 */
	@Override
	public Account findByCid(String cid) {
		Account result = null;
		try {
			if (cid == null) {
				throw new AccountException("cid 必須有值。");
			}
			result = accountRepository.findByCid(cid);
		} catch (AccountException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new AccountException(ex);
		}
		return result;
	}

	/**
	 * 取得所有帳號資料
	 * 
	 * @param pageable
	 * @return
	 */
	@Override
	public Page<Account> findAll(Pageable pageable) {
		Page<Account> result = null;
		try {
			if (pageable == null) {
				throw new AccountException("pageable 必須有值。");
			}
			result = accountRepository.findAll(pageable);
		} catch (AccountException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new AccountException(ex);
		}
		return result;
	}

	/**
	 * 修改帳號資料
	 * 
	 * @param account
	 * @return
	 */
	@Override
	public Account updateAccount(Account account) {
		Account result = null;
		try {
			if (account == null) {
				throw new AccountException("account 必須有值。");
			}
			result = update(account);
		} catch (AccountException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new AccountException(ex);
		}
		return result;
	}

	/**
	 * 刪除帳號資料
	 * 
	 * @param account
	 * @return
	 */
	@Override
	public int deleteAccount(Account account) {
		int result = 0;
		try {
			if (account == null) {
				throw new AccountException("account 必須有值。");
			}
			delete(account);
			result = 1;
		} catch (AccountException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new AccountException(ex);
		}
		return result;
	}

	/**
	 * 新增帳號
	 * 
	 * @param account
	 * @return
	 */
	@Override
	public Account insert(Account account) {
		Account result = null;
		try {
			result = tx.insert(account);
		} catch (AccountException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new AccountException(ex);
		}
		return result;
	}

	/**
	 * 更新帳號
	 * 
	 * @param account
	 * @return
	 */
	@Override
	public Account update(Account account) {
		Account result = null;
		try {
			result = tx.update(account);
		} catch (AccountException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new AccountException(ex);
		}
		return result;
	}

	/**
	 * 刪除帳號
	 * 
	 * @param account
	 */
	@Override
	public void delete(Account account) {
		try {
			tx.delete(account);
		} catch (AccountException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new AccountException(ex);
		}
	}
}

測試 JUnit 5

AccountTXTest.java

新增單元測試,驗證是否符合預期 。

package org.ruoxue.spring_boot_168.sso.account.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.sso.account.model.Account;
import org.ruoxue.spring_boot_168.sso.account.repository.AccountRepository;
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 AccountTXTest {

	@Autowired
	private AccountRepository repository;

	@Autowired
	private AccountTX tx;

	@Test
	public void service() {
		System.out.println(tx);
		assertNotNull(tx);
	}

	@Test
	public void insert() {
		String value = "ruoxue";
		Account found = repository.findByCid(value);
		if (found != null) {
			return;
		}

		Account account = new Account();
		account.setCid(value);
		account.setName("player");
		account.setPassword("");
		account.setSalt("");
		Account result = tx.insert(account);
		System.out.println(result);
		assertNotNull(result);
		assertEquals(value, result.getCid());
	}

	@Test
	public void update() {
		String value = "ruoxue";
		String name = "test_player";
		Account account = repository.findByCid(value);
		if (account != null) {
			account.setName(name);
			Account updated = tx.update(account);
			System.out.println(account);
			assertEquals(name, updated.getName());
		}
	}

	@Test
	public void delete() {
		String value = "ruoxue";
		Account account = repository.findByCid(value);
		if (account != null) {
			tx.delete(account);
			System.out.println(account);
		}
	}
}

web tx insert

測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。

Hibernate: select account0_.id as id1_1_, account0_.cid as cid2_1_, account0_.name as name3_1_, account0_.password as password4_1_, account0_.salt as salt5_1_ from sso_account account0_ where account0_.cid=?
Hibernate: insert into sso_account (cid, name, password, salt, id) values (?, ?, ?, ?, ?)
{"cid":"ruoxue","name":"player"}

web tx update

測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。

Hibernate: select account0_.id as id1_1_, account0_.cid as cid2_1_, account0_.name as name3_1_, account0_.password as password4_1_, account0_.salt as salt5_1_ from sso_account account0_ where account0_.cid=?
Hibernate: select account0_.id as id1_1_0_, account0_.cid as cid2_1_0_, account0_.name as name3_1_0_, account0_.password as password4_1_0_, account0_.salt as salt5_1_0_ from sso_account account0_ where account0_.id=?
Hibernate: update sso_account set cid=?, name=?, password=?, salt=? where id=?
{"cid":"ruoxue","name":"test_player"}

web tx delete

測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。

Hibernate: select account0_.id as id1_1_, account0_.cid as cid2_1_, account0_.name as name3_1_, account0_.password as password4_1_, account0_.salt as salt5_1_ from sso_account account0_ where account0_.cid=?
Hibernate: select account0_.id as id1_1_0_, account0_.cid as cid2_1_0_, account0_.name as name3_1_0_, account0_.password as password4_1_0_, account0_.salt as salt5_1_0_ from sso_account account0_ where account0_.id=?
Hibernate: delete from sso_account where id=?
{"cid":"ruoxue","name":"test_player"}

心得分享

Spring Web JPA

獨立 TX 為外部類別,是為了讓交易鎖定的範圍成為最小, 一個交易的範圍內,若包含查詢資料庫,與新增或修改或刪除的語法時,效能將會明顯地降低,因此為了避免太多查詢語法在一個交易範圍內,將交易獨立成外部調用。

發佈留言