[Java]Mybatis学习笔记(动力节点老杜)(三)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
日志服务 SLS,月写入数据量 50GB 1个月
简介: [Java]Mybatis学习笔记(动力节点老杜)(三)
默认方式获取 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来使用第三方数据库连接池
<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框架

在 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;
    }
}


相关文章
|
1月前
|
Java 数据库连接 Maven
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和MyBatis Generator,使用逆向工程来自动生成Java代码,包括实体类、Mapper文件和Example文件,以提高开发效率。
106 2
mybatis使用一:springboot整合mybatis、mybatis generator,使用逆向工程生成java代码。
|
23天前
|
搜索推荐 Java 数据库连接
Java|在 IDEA 里自动生成 MyBatis 模板代码
基于 MyBatis 开发的项目,新增数据库表以后,总是需要编写对应的 Entity、Mapper 和 Service 等等 Class 的代码,这些都是重复的工作,我们可以想一些办法来自动生成这些代码。
30 6
|
2月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
2月前
|
存储 安全 Java
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(进阶篇)
本文是Java基础的进阶篇,对异常、集合、泛型、Java8新特性、I/O流等知识进行深入浅出的介绍,并附有对应的代码示例,重要的地方带有对性能、底层原理、源码的剖析。适合Java初学者。
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(进阶篇)
|
1月前
|
Java 数据安全/隐私保护
java学习笔记(基础习题)
java学习笔记(基础习题)
33 0
|
1月前
|
Java 程序员 开发工具
java学习笔记
java学习笔记
35 0
|
2月前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
2月前
|
存储 安全 Java
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(高级篇)
本文是“Java学习路线”中Java基础知识的高级篇,主要对多线程和反射进行了深入浅出的介绍,在多线程部分,详细介绍了线程的概念、生命周期、多线程的线程安全、线程通信、线程同步,并对synchronized和Lock锁;反射部分对反射的特性、功能、优缺点、适用场景等进行了介绍。
Java修仙之路,十万字吐血整理全网最完整Java学习笔记(高级篇)
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
54 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
1月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
295 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个