一、在 Web 應用中使用 MyBatis
項目目錄結構
pojo ?
package org.qiu.bank.pojo;/*** 賬戶類,封裝賬戶數據* @author 秋玄* @version 1.0* @package org.qiu.bank.pojo* @date 2022-09-27-20:31* @since 1.0*/
public class Account {private Long id;private String actno;private Double balance;@Overridepublic String toString() {return "Account{" +"id=" + id +", actno='" + actno + '\'' +", balance=" + balance +'}';}public Account(Long id, String actno, Double balance) {this.id = id;this.actno = actno;this.balance = balance;}public Account() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getActno() {return actno;}public void setActno(String actno) {this.actno = actno;}public Double getBalance() {return balance;}public void setBalance(Double balance) {this.balance = balance;}
}
dao
package org.qiu.bank.dao;import org.qiu.bank.pojo.Account;/*** @author 秋玄* @version 1.0* @package org.qiu.bank.dao* @date 2022-09-28-10:11* @since 1.0*/
public interface AccountDao {Account select(String actno);int update(Account account);
}
package org.qiu.bank.dao.impl;import org.apache.ibatis.session.SqlSession;
import org.qiu.bank.dao.AccountDao;
import org.qiu.bank.pojo.Account;
import org.qiu.bank.utils.SqlSessionUtil;/*** @author 秋玄* @version 1.0* @package org.qiu.bank.dao.impl* @date 2022-09-28-10:13* @since 1.0*/
public class AccountDaoImpl implements AccountDao {@Overridepublic Account select(String actno) {SqlSession sqlSession = SqlSessionUtil.openSession();Account account = sqlSession.selectOne("account.selectById", actno);sqlSession.close();return account;}@Overridepublic int update(Account account) {SqlSession sqlSession = SqlSessionUtil.openSession();int count = sqlSession.update("account.updateByActno", account);sqlSession.commit();sqlSession.close();return count;}
}
mybatis 的 SQL 映射文件 ?
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="account"><select id="selectById" resultType="org.qiu.bank.pojo.Account">select * from t_act where actno = #{actno};</select><update id="updateByActno">update t_act set balance = #{balance} where actno = #{actno}</update>
</mapper>
service
package org.qiu.bank.service;import org.qiu.bank.exceptions.MoneyNotEnoughException;
import org.qiu.bank.exceptions.TransferException;/*** @author 秋玄* @version 1.0* @package org.qiu.bank.service* @date 2022-09-28-10:06* @since 1.0*/
public interface AccountService {void transfer(String fromActno,String toActno,Double money) throws MoneyNotEnoughException, TransferException;
}
package org.qiu.bank.service.impl;import org.qiu.bank.dao.AccountDao;
import org.qiu.bank.dao.impl.AccountDaoImpl;
import org.qiu.bank.exceptions.MoneyNotEnoughException;
import org.qiu.bank.exceptions.TransferException;
import org.qiu.bank.pojo.Account;
import org.qiu.bank.service.AccountService;/*** @author 秋玄* @version 1.0* @package org.qiu.bank.service.impl* @date 2022-09-28-10:08* @since 1.0*/
public class AccountServiceImpl implements AccountService {AccountDao accountDao = new AccountDaoImpl();@Overridepublic void transfer(String fromActno, String toActno, Double money) throws MoneyNotEnoughException, TransferException {Account fromAct = accountDao.select(fromActno);if (fromAct.getBalance() < money) {// 余額不足throw new MoneyNotEnoughException("對不起,余額不足");}Account toAct = accountDao.select(toActno);fromAct.setBalance(fromAct.getBalance() - money);toAct.setBalance(toAct.getBalance() + money);int count = accountDao.update(fromAct);count += accountDao.update(toAct);if (count != 2) {throw new TransferException("轉賬異常");}}
}
異常處理類 ?
package org.qiu.bank.exceptions;/*** @author 秋玄* @version 1.0* @package org.qiu.bank.exceptions* @date 2022-09-28-10:22* @since 1.0*/
public class MoneyNotEnoughException extends Exception{public MoneyNotEnoughException(){}public MoneyNotEnoughException(String message) {super(message);}
}
package org.qiu.bank.exceptions;/*** @author 秋玄* @version 1.0* @package org.qiu.bank.exceptions* @date 2022-09-28-10:35* @since 1.0*/
public class TransferException extends Exception{public TransferException() {}public TransferException(String message) {super(message);}
}
controller ?
package org.qiu.bank.web;import org.qiu.bank.exceptions.MoneyNotEnoughException;
import org.qiu.bank.exceptions.TransferException;
import org.qiu.bank.service.AccountService;
import org.qiu.bank.service.impl.AccountServiceImpl;import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @author 秋玄* @version 1.0* @package org.qiu.bank.web* @date 2022-09-28-09:59* @since 1.0*/@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {AccountService accountService = new AccountServiceImpl();@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException {// 獲取表單數據String fromActno = request.getParameter("fromActno");String toActno = request.getParameter("toActno");Double money = Double.parseDouble(request.getParameter("money"));try {// 調用 service 的轉賬方法完成轉賬accountService.transfer(fromActno,toActno,money);// 調用 View 展示結果response.sendRedirect(request.getContextPath() + "/success.html");} catch (MoneyNotEnoughException e) {response.sendRedirect(request.getContextPath() + "/err1.html");} catch (TransferException e) {response.sendRedirect(request.getContextPath() + "/err2.html");}}
}
測試:瀏覽器訪問 http://localhost:8080/bank/ ?
存在的問題:
當用戶進行轉賬時,需要更新兩個賬號的余額信息,若兩次更新操作之間,程序出現了異常,此時對于收款賬號的更新操作不會執行,但是轉賬賬號的余額更新操作已經完成,所以會造成數據丟失問題。
解決思路:
首先考慮的肯定是給更新操作添加事務,使得程序對兩個賬號余額的更新操作同時成功或者同時失敗。在 transfer 方法開始執行時開啟事務,直到兩個更新都成功之后,再提交事務
?
存在的問題:
在給兩次更新操作添加事務后發現,上述的問題并未得到解決。原因是 service 和 dao 里使用的 SqlSession 對象不是同一個。
解決思路:
為了保證 service 和 dao 中使用的 SqlSession 對象是同一個,可以將 SqlSession 對象存放到 ThreadLocal 當中
?
改造工具類 ?
package org.qiu.bank.utils;import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;/*** MyBatis工具類** @author 秋玄* @version 1.0.0* @since 1.0.0*/
public class SqlSessionUtil {private static SqlSessionFactory sqlSessionFactory;private static ThreadLocal<SqlSession> local = new ThreadLocal<>();/*** 類加載時初始化sqlSessionFactory對象*/static {try {SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));} catch (Exception e) {e.printStackTrace();}}/*** 每調用一次openSession()可獲取一個新的會話,該會話支持自動提交。* @return 新的會話對象*/public static SqlSession openSession() {SqlSession sqlSession = local.get();if (sqlSession == null) {sqlSession = sqlSessionFactory.openSession();local.set(sqlSession);}return sqlSessionFactory.openSession();}/*** 關閉 SqlSession 對象* @param sqlSession*/public static void close(SqlSession sqlSession){if (sqlSession != null) {sqlSession.close();// tomcat 支持線程池,所以關閉 SqlSession 需要將其從當前線程中移除local.remove();}}
}
改造 transfer 方法 ?
@Override
public void transfer(String fromActno, String toActno, Double money) throws MoneyNotEnoughException, TransferException {SqlSession sqlSession = SqlSessionUtil.openSession();Account fromAct = accountDao.select(fromActno);if (fromAct.getBalance() < money) {// 余額不足throw new MoneyNotEnoughException("對不起,余額不足");}Account toAct = accountDao.select(toActno);fromAct.setBalance(fromAct.getBalance() - money);toAct.setBalance(toAct.getBalance() + money);int count = accountDao.update(fromAct);// 模擬異常String s = null;s.toString();count += accountDao.update(toAct);if (count != 2) {throw new TransferException("轉賬異常");}sqlSession.commit();SqlSessionUtil.close(sqlSession);
}
改造 DaoImpl ?
public class AccountDaoImpl implements AccountDao {@Overridepublic Account select(String actno) {SqlSession sqlSession = SqlSessionUtil.openSession();Account account = sqlSession.selectOne("account.selectById", actno);return account;}@Overridepublic int update(Account account) {SqlSession sqlSession = SqlSessionUtil.openSession();int count = sqlSession.update("account.updateByActno", account);return count;}
}
?
二、MyBatis 對象作用域
SqlSessionFactoryBuilder
這個類可以被實例化、使用和丟棄,一旦創建了 SqlSessionFactory,就不再需要它了。
因此 SqlSessionFactoryBuilder 實例的最佳作用域是方法作用域(也就是局部方法變量)。
可以重用 SqlSessionFactoryBuilder 來創建多個 SqlSessionFactory 實例,但最好還是不要一直保留著它,以保證所有的 XML 解析資源可以被釋放給更重要的事情。
SqlSessionFactory
SqlSessionFactory 一旦被創建就應該在應用的運行期間一直存在,沒有任何理由丟棄它或重新創建另一個實例。
使用 SqlSessionFactory 的最佳實踐是在應用運行期間不要重復創建多次,多次重建 SqlSessionFactory 被視為一種代碼“壞習慣”。
因此 SqlSessionFactory 的最佳作用域是應用作用域。
有很多方法可以做到,最簡單的就是使用單例模式或者靜態單例模式。
SqlSession
每個線程都應該有它自己的 SqlSession 實例。
SqlSession 的實例不是線程安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。
絕對不能將 SqlSession 實例的引用放在一個類的靜態域,甚至一個類的實例變量也不行。
也絕不能將 SqlSession 實例的引用放在任何類型的托管作用域中,比如 Servlet 框架中的 HttpSession。
如果現在正在使用一種 Web 框架,考慮將 SqlSession 放在一個和 HTTP 請求相似的作用域中。
換句話說,每次收到 HTTP 請求,就可以打開一個 SqlSession,返回一個響應后,就關閉它。
這個關閉操作很重要,為了確保每次都能執行關閉操作,應該把這個關閉操作放到 finally 塊中。
下面的示例就是一個確保 SqlSession 關閉的標準模式:
try (SqlSession session = sqlSessionFactory.openSession()) {// 應用邏輯代碼
}
?
一? 葉? 知? 秋,奧? 妙? 玄? 心