默认方式获取 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
- 这种方式就是获取默认数据库环境
- 该 SqlSessionFactory 对象对应在 environments 标签的 default 属性中指定的数据库环境
通过数据库环境id获取 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory1 = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "powernodeDB");
- 第二个参数为数据库环境id
- 这种方式就是通过数据库环境id来使用指定的数据库环境
- 通过该方式获取的 SqlSessionFactory 对象对应 id 相应的数据库环境
transactionManager
<transactionManager type="JDBC"/>
- 作用:配置事务管理器,指定mybatis具体使用什么方式去管理事务。
- type属性有两个值:
- 第一个:JDBC: 使用原生的JDBC代码来管理事务。
conn.setAutoCommit(false);
...
conn.commit();
- 第二个:MANAGED:mybatis不再负责事务的管理,将事务管理交给其它的JEE(JavaEE)容器来管理。
- 例如:spring
- 大小写无所谓。不缺分大小写。但是不能写其他值。只能是二选一: jdbc、managed
- 在mybatis中提供了一个事务管理器接口:Transaction
- 该接口下有两个实现类:
- JdbcTransaction
- ManagedTransaction
- 如果type=“JDBC”,那么底层会实例化JdbcTransaction对象。
- 如果type=“MANAGED”,那么底层会实例化ManagedTransaction
dataSource
<dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource>
- dataSource被称为数据源。
- dataSource的作用:为程序提供Connection对象。
- 凡是给程序提供Connection对象的,都叫做数据源。
- 数据源实际上是一套规范。
- JDK中有这套规范:javax.sql.DataSource,这个数据源的规范,这套接口是JDK规定的。
- 我们自己也可以编写数据源组件,只要实现javax.sql.DataSource接口就行,实现接口当中所有的方法。
- 常见的数据源组件有哪些呢【常见的数据库连接池有哪些呢】?
- 阿里巴巴的德鲁伊连接池:druid
- c3p0
- dbcp
- …
- type属性用来指定数据源的类型,就是指定具体使用什么方式来获取Connection对象:
- type属性有三个值:必须是三选一。
- type=“[UNPOOLED|POOLED|JNDI]”
- UNPOOLED:不使用数据库连接池技术。每一次请求过来之后,都是创建新的Connection对象。
- POOLED:使用mybatis自己实现的数据库连接池。
- JNDI:集成其它第三方的数据库连接池。
- JNDI是一套规范,大部分的web容器都实现了JNDI规范,例如:Tomcat、Jetty、WebLogic、WebSphere,这些服务器(容器)都实现了JNDI规范。
- JNDI是:java命名目录接口。Tomcat服务器实现了这个规范。
- 就好比,WEB容器实现了这个规范,为第三方数据库连接池提供了一个接入到web项目的“接口”,第三方数据库连接池可以通过该“接口”接入web项目,web项目中可以直接通过JNDI来使用第三方数据库连接池
- 数据源的type属性指定不同的值,可以配置不同的属性 https://mybatis.net.cn/configuration.html#environments
<dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 以下配置数据库连接池的参数 --> <!--提醒:正常使用连接池的话,池中有很多参数是需要设置的。设置好参数,可以让连接池发挥的更好。事半功倍的效果。--> <!--具体连接池当中的参数如何配置呢?需要反复的根据当前业务情况进行测试。--> <!--poolMaximumActiveConnections:连接池当中最多的正在使用的连接对象的数量上限。最多有多少个连接可以活动。默认值10--> <property name="poolMaximumActiveConnections" value="10"/> <!--每隔2秒打印日志,并且尝试获取连接对象--> <property name="poolTimeToWait" value="2000"/> <!--强行让某个连接空闲,超时时间的设置--> <property name="poolMaximumCheckoutTime" value="10000"/> <!--最多的空闲数量--> <!-- 假设最多的连接数量为10个,最多空闲数量为5个,现在已经空闲5个了,马上第六个也要空闲了 如果第六个空闲下来,连接池为了保证空闲的数量最多5个,会真正关闭多余的空闲连接对象 可以节省系统资源 --> <property name="poolMaximumIdleConnections" value="5"/> </dataSource>
mappers
- 在mappers标签中可以配置多个sql映射文件的路径
<mappers> <!-- mapper:配置某个sql映射文件的路径 --> <!-- resource属性:使用相对于类路径的资源引用方式 --> <!-- url属性:使用完全限定资源定位符(URL)方式 --> <mapper resource="CarMapper.xml"/> </mappers>
代码文档注释版
<?xml version="1.0" encoding="UTF-8" ?> <!-- 文档类型说明中的 configuration,是根标签的名称,一个文档一个根标签 --> <!-- http://mybatis.org/dtd/mybatis-3-config.dtd xml文档的dtd约束,约束文档中可以出现什么标签、标签能有什么子标签、标签中可以有什么属性以及标签出现的顺序 --> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!-- configuration:根标签,表示配置信息。 --> <configuration> <!--java.util.Properties类。是一个Map集合。key和value都是String类型--> <!-- property标签中的name属性为key,value属性为value --> <!--在properties标签中可以配置很多属性--> <!-- 在下面的标签中可以使用property配置的属性值,使用 ${property的name属性} 取出相应的值 --> <!--<properties>--> <!--这是其中的一个属性--> <!--<property name="属性名" value="属性值"/>--> <!--<property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/> <property name="jdbc.url" value="jdbc:mysql://localhost:3306/powernode"/> <property name="jdbc.username" value="root"/> <property name="jdbc.password" value="root"/>--> <!--</properties>--> <!-- properties中的property可以配置到配置文件中 --> <!--resource,一定是从类路径下开始查找资源--> <properties resource="jdbc.properties" /> <!--从绝对路径当中加载资源。绝对路径怎么写?file:///路径--> <!--<properties url="file:///d:/jdbc.properties" />--> <!-- environments:环境(多个),以“s”结尾表示复数,也就是说mybatis的环境可以配置多个数据源。 --> <!--default表示默认使用的环境。--> <!--默认环境什么意思?当你使用mybatis创建SqlSessionFactory对象的时候,没有指定环境的话,默认使用哪个环境。--> <environments default="powernodeDB"> <!--其中的一个环境。连接的数据库是powernode--> <!--一般一个数据库会对应一个SqlSessionFactory对象。--> <!--一个环境environment会对应一个SqlSessionFactory对象--> <!-- 一个数据库对应一个环境 --> <!-- // 这种方式就是获取的默认环境 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml")); // 这种方式就是通过环境id来使用指定的环境,第二个参数为环境id SqlSessionFactory sqlSessionFactory1 = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "powernodeDB"); --> <environment id="powernodeDB"> <!-- transactionManager标签: 1.作用:配置事务管理器。指定mybatis具体使用什么方式去管理事务。 2.type属性有两个值: 第一个:JDBC: 使用原生的JDBC代码来管理事务。 conn.setAutoCommit(false); .... conn.commit(); 第二个:MANAGED:mybatis不再负责事务的管理,将事务管理交给其它的JEE(JavaEE)容器来管理。例如:spring 3. 大小写无所谓。不缺分大小写。但是不能写其他值。只能是二选一: jdbc、managed 4. 在mybatis中提供了一个事务管理器接口:Transaction 该接口下有两个实现类: JdbcTransaction ManagedTransaction 如果type="JDBC",那么底层会实例化JdbcTransaction对象。 如果type="MANAGED",那么底层会实例化ManagedTransaction --> <transactionManager type="JDBC"/> <!-- dataSource配置: 1.dataSource被称为数据源。 2.dataSource作用是什么?为程序提供Connection对象。(但凡是给程序提供Connection对象的,都叫做数据源。) 3.数据源实际上是一套规范。JDK中有这套规范:javax.sql.DataSource(这个数据源的规范,这套接口实际上是JDK规定的。) 4.我们自己也可以编写数据源组件,只要实现javax.sql.DataSource接口就行了。实现接口当中所有的方法。这样就有了自己的数据源。 比如你可以写一个属于自己的数据库连接池(数据库连接池是提供连接对象的,所以数据库连接池就是一个数据源)。 5.常见的数据源组件有哪些呢【常见的数据库连接池有哪些呢】? 阿里巴巴的德鲁伊连接池:druid c3p0 dbcp .... 6. type属性用来指定数据源的类型,就是指定具体使用什么方式来获取Connection对象: type属性有三个值:必须是三选一。 type="[UNPOOLED|POOLED|JNDI]" UNPOOLED:不使用数据库连接池技术。每一次请求过来之后,都是创建新的Connection对象。 POOLED:使用mybatis自己实现的数据库连接池。 JNDI:集成其它第三方的数据库连接池。 JNDI是一套规范。谁实现了这套规范呢?大部分的web容器都实现了JNDI规范: 例如:Tomcat、Jetty、WebLogic、WebSphere,这些服务器(容器)都实现了JNDI规范。 JNDI是:java命名目录接口。Tomcat服务器实现了这个规范。 --> <!-- 数据源的type属性指定不同的值,需要配置不太的属性 https://mybatis.net.cn/configuration.html#environments --> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 以下配置数据库连接池的参数 --> <!--提醒:正常使用连接池的话,池中有很多参数是需要设置的。设置好参数,可以让连接池发挥的更好。事半功倍的效果。--> <!--具体连接池当中的参数如何配置呢?需要反复的根据当前业务情况进行测试。--> <!--poolMaximumActiveConnections:连接池当中最多的正在使用的连接对象的数量上限。最多有多少个连接可以活动。默认值10--> <property name="poolMaximumActiveConnections" value="10"/> <!--每隔2秒打印日志,并且尝试获取连接对象--> <property name="poolTimeToWait" value="2000"/> <!--强行让某个连接空闲,超时时间的设置--> <property name="poolMaximumCheckoutTime" value="10000"/> <!--最多的空闲数量--> <!-- 假设最多的连接数量为10个,最多空闲数量为5个,现在已经空闲5个了,马上第六个也要空闲了 如果第六个空闲下来,连接池为了保证空闲的数量最多5个,会真正关闭多余的空闲连接对象 可以节省系统资源 --> <property name="poolMaximumIdleConnections" value="5"/> </dataSource> </environment> <!--这是mybatis的另一个环境,也就是连接的数据库是另一个数据库mybatis--> <environment id="mybatisDB"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mybatis"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <!-- mappers:在mappers标签中可以配置多个sql映射文件的路径。 --> <mappers> <!-- mapper:配置某个sql映射文件的路径 --> <!-- resource属性:使用相对于类路径的资源引用方式 --> <!-- url属性:使用完全限定资源定位符(URL)方式 --> <mapper resource="CarMapper.xml"/> </mappers> </configuration>
手写MyBatis框架
- 手写 MyBatis 框架[GodBatis]:https://www.yuque.com/u27599042/un32ge/ru8czamo6trse3rl
在 WEB 中应用 MyBatis(MVC架构模式)
- 实现功能:银行账户转账
- 使用技术:HTML + Servlet + MyBatis
- WEB应用的名称:bank
需求描述
数据库表的设计和准备数据
USE dbtest; DROP TABLE IF EXISTS t_act; CREATE TABLE t_act ( id int PRIMARY KEY AUTO_INCREMENT, actno VARCHAR(255), balance DECIMAL(10, 2) ); INSERT INTO t_act(actno, balance) VALUES ('act001', 50000); INSERT INTO t_act(actno, balance) VALUES ('act002', 0);
环境搭建
- IDEA中使用Maven原型创建WEB应用
- 使用Maven原型创建的web应用默认没有java和resources目录,包括两种解决方案
- 第一种:自己手动加上。
- 第二种:找到原型jar包所在的位置,修改maven-archetype-webapp-1.4.jar中的配置文件
- web.xml文件的版本较低,可以从tomcat的样例文件中复制,然后修改
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> </web-app>
- 删除index.jsp文件,因为我们这个项目不使用JSP,使用html。
- 确定pom.xml文件中的打包方式是war包。
<packaging>war</packaging>
- 引入相关依赖
<dependencies> <!-- MyBatis依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <!-- MySQL驱动依赖 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <!-- servlet依赖 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <!-- logback依赖 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.5</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies>
- 编译器版本修改为17
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties>
- IDEA配置Tomcat,并部署应用到tomcat,应用名 bank
- 准备相关配置文件,放到resources目录下(相当于放到类的根路径下)
- mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties resource="jdbc.properties"/> <environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="AccountMapper.xml"/> </mappers> </configuration>
- AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="account"> </mapper>
- logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="false"> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> </appender> <!-- 按照每天生成日志文件 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日志文件输出的文件名--> <FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern> <!--日志文件保留天数--> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern> </encoder> <!--日志文件最大的大小--> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>100MB</MaxFileSize> </triggeringPolicy> </appender> <!--mybatis log configure--> <logger name="com.apache.ibatis" level="TRACE"/> <logger name="java.sql.Connection" level="DEBUG"/> <logger name="java.sql.Statement" level="DEBUG"/> <logger name="java.sql.PreparedStatement" level="DEBUG"/> <!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR --> <root level="DEBUG"> <appender-ref ref="STDOUT"/> <appender-ref ref="FILE"/> </root> </configuration>
- jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/dbtest jdbc.username=root jdbc.password=root
前端页面 index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>银行账户转账</title> </head> <body> <!--/bank是应用的根,部署web应用到tomcat的时候一定要注意这个名字--> <form action="/bank/transfer" method="post"> 转出账户:<input type="text" name="fromActno"/><br> 转入账户:<input type="text" name="toActno"/><br> 转账金额:<input type="text" name="money"/><br> <input type="submit" value="转账"/> </form> </body> </html>
创建软件包(MVC架构模式)
工具类 SqlSessionUtil
package cw.study.mybatis.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; import java.io.IOException; /** * ClassName: SqlSessionUtil * Package: cw.study.mybatis.utils * Description: * * @Author tcw * @Create 2023-05-28 14:32 * @Version 1.0 */ public class SqlSessionUtil { private SqlSessionUtil(){} private static SqlSessionFactory sqlSessionFactory; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")); } catch (IOException e) { throw new RuntimeException(e); } } /** * 获取会话对象。 * @return 会话对象 */ public static SqlSession openSession(){ return sqlSessionFactory.openSession(); } }
定义实体类 Account
public class Account { private Long id; private String actno; private Double balance; @Override public String toString() { return "Account{" + "id=" + id + ", actno='" + actno + '\'' + ", balance=" + balance + '}'; } public Account() { } public Account(Long id, String actno, Double balance) { this.id = id; this.actno = actno; this.balance = balance; } 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; } }
后端代码实现
@WebServlet({"/transfer"}) public class AccountServlet extends HttpServlet { // 为了让这个对象在其他方法中也可以用。声明为实例变量。 private AccountService accountService = new AccountServiceImpl(); @Override protected 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 { accountService.transfer(fromActno, toActno, money); // 到这转账成功 // 调用视图完成结果展示 response.sendRedirect(request.getContextPath() + "/success.html"); } catch (MoneyNotEnoughException e) { response.sendRedirect(request.getContextPath() + "/error1.html"); } catch (TransferException e) { response.sendRedirect(request.getContextPath() + "/error2.html"); } catch (Exception e) { response.sendRedirect(request.getContextPath() + "/error2.html"); } } }
/** * 注意:业务类当中的业务方法的名字在起名的时候,最好见名知意,能够体现出具体的业务是做什么的。 * 账户业务类 */ public interface AccountService { /** * 转账业务 * @param fromActno 转出账户 * @param toActno 转入账户 * @param money 转账金额 */ void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException; }
public class AccountServiceImpl implements AccountService { private AccountDao accountDao = new AccountDaoImpl(); @Override public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException { // 1. 判断转出账户的余额是否充足(select) Account fromAct = accountDao.selectByActno(fromActno); // 2. 如果转出账户余额不足,提示用户 if (fromAct.getBalance() < money) { // 转出账户余额不足 throw new MoneyNotEnoughException("余额不足"); } // 3. 如果转出账户余额充足,更新转出账户余额(update) // 先更新java内存中对象的余额再更新数据库 Account toAct = accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); int count = accountDao.updateByAccount(fromAct); // 4. 更新转入账户余额(update) count += accountDao.updateByAccount(toAct); if (count != 2) { throw new TransferException("转账失败"); } } }
/** * 账户的DAO对象。负责t_act表中数据的CRUD. * 强调一下:DAO对象中的任何一个方法和业务不挂钩。没有任何业务逻辑在里面。 * DAO中的方法就是做CRUD的。所以方法名大部分是:insertXXX deleteXXX updateXXX selectXXX */ public interface AccountDao { /** * 根据账号查询账户信息。 * @param actno 账号 * @return 账户信息 */ Account selectByActno(String actno); /** * 更新账户信息 * @param act 被更新的账户对象 * @return 1表示更新成功,其他值表示失败。 */ int updateByAccount(Account act); }
public class AccountDaoImpl implements AccountDao { @Override public Account selectByActno(String actno) { SqlSession sqlSession = SqlSessionUtil.openSession(); Account account = (Account) sqlSession.selectOne("account.selectByActno", actno); sqlSession.close(); return account; } @Override public int updateByAccount(Account act) { SqlSession sqlSession = SqlSessionUtil.openSession(); int count = sqlSession.update("account.updateByAccount", act); sqlSession.commit(); sqlSession.close(); return count; } }
public class MoneyNotEnoughException extends Exception{ public MoneyNotEnoughException() { } public MoneyNotEnoughException(String message) { super(message); } }
public class TransferException extends Exception{ public TransferException() { } public TransferException(String message) { super(message); } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>转账报告</title> </head> <body> <h1>转账成功!</h1> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>转账报告</title> </head> <body> <h1>余额不足!!!</h1> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>转账报告</title> </head> <body> <h1>转账失败,未知原因!!!</h1> </body> </html>
使用 ThreadLocal 进行事务控制
- 在之前的转账业务中,更新了两个账户,我们需要保证它们的同时成功或同时失败,这个时候就需要使用事务机制,在transfer方法开始执行时开启事务,直到两个更新都成功之后,再提交事务
- 如果在转账业务中,更新两个账户的操作在两个事务之中,如果中间出现异常,可能会导致钱丢失,只更新了一个账户的数据,另一个账户的数据未进行更新。
- 前面代码的实现,可能会存在上述的问题,**主要是因为service和dao中使用的SqlSession对象不是同一个,**不能将更新两个账户的操作控制在一个事务之内
- 为了解决可能存在的事务问题,为了保证service和dao中使用的SqlSession对象是同一个,将更新两个账户的操作控制在一个事务之内,我们可以将SqlSession对象存放到ThreadLocal当中,保证一个线程都使用同一个SqlSession对象,把更新两个账户的操作控制在一个事务之内
- tomcat服务器是支持多线程的,对应不同的来自客户端的请求,tomcat会使用线程池中的一个线程来处理该请求,将SqlSession对象存放到ThreadLocal当中,为当前线程绑定一个属于该线程的SqlSession对象,可以实现只要是同一个线程中的操作,使用的就是同一个SqlSession对象
- 修改SqlSessionUtil工具类:
public class SqlSessionUtil { private SqlSessionUtil(){} private static SqlSessionFactory sqlSessionFactory; static { try { sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml")); } catch (IOException e) { throw new RuntimeException(e); } } // 全局的,服务器级别的,一个服务器当中定义一个即可。 // 为什么把SqlSession对象放到ThreadLocal当中呢? // 为了保证一个线程对应一个SqlSession。可以把更新两个账户的操作控制在一个事务内 private static ThreadLocal<SqlSession> local = new ThreadLocal<>(); /** * 获取会话对象。 * @return 会话对象 */ public static SqlSession openSession(){ SqlSession sqlSession = local.get(); if (sqlSession == null) { sqlSession = sqlSessionFactory.openSession(); // 将sqlSession对象绑定到当前线程上。 local.set(sqlSession); } return sqlSession; } /** * 关闭SqlSession对象(从当前线程中移除SqlSession对象。) * @param sqlSession */ public static void close(SqlSession sqlSession) { if (sqlSession != null) { sqlSession.close(); // 注意移除SqlSession对象和当前线程的绑定关系。 // 因为Tomcat服务器支持线程池。也就是说:用过的线程对象t1,可能下一次还会使用这个t1线程。 local.remove(); } } }
- 修改service中的方法:
public class AccountServiceImpl implements AccountService { private AccountDao accountDao = new AccountDaoImpl(); @Override public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException { // 添加事务控制代码 SqlSession sqlSession = SqlSessionUtil.openSession(); // 1. 判断转出账户的余额是否充足(select) Account fromAct = accountDao.selectByActno(fromActno); // 2. 如果转出账户余额不足,提示用户 if (fromAct.getBalance() < money) { // 转出账户余额不足 throw new MoneyNotEnoughException("余额不足"); } // 3. 如果转出账户余额充足,更新转出账户余额(update) // 先更新java内存中对象的余额再更新数据库 Account toAct = accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); int count = accountDao.updateByAccount(fromAct); String s = null; s.toUpperCase(); // 4. 更新转入账户余额(update) count += accountDao.updateByAccount(toAct); if (count != 2) { throw new TransferException("转账失败"); } // 提交事务 sqlSession.commit(); // 关闭事务 SqlSessionUtil.close(sqlSession); } }
- 修改dao中的方法:AccountDaoImpl中所有方法中的提交commit和关闭close代码全部删除。
public class AccountDaoImpl implements AccountDao { @Override public Account selectByActno(String actno) { SqlSession sqlSession = SqlSessionUtil.openSession(); Account account = (Account) sqlSession.selectOne("account.selectByActno", actno); // sqlSession.close(); return account; } @Override public int updateByAccount(Account act) { SqlSession sqlSession = SqlSessionUtil.openSession(); int count = sqlSession.update("account.updateByAccount", act); // sqlSession.commit(); // sqlSession.close(); return count; } }