目录
1 手写MyBatis框架(理解原理)
上面我们已经对MyBatis有个大致的了解,那下面我们尝试手写一个MyBatis框架来加深对MyBatis的原理理解
过段时间再学吧
2 在web中应用MyBatis(使用MVC架构模式)
目标:
- 掌握MyBatis在web应用中的使用
- MyBatis三大对象的作用域和生命周期
- ThreadLocal原理及应用
- 巩固mvc架构模式
- 为学习MyBatis的接口代码机制做准备
实现功能:
- 银行账户转账
使用技巧:
- HTML+Servlet+MyBatis
web应用的名称:
- bank
2.1 准备数据库表

添加数据:

2.2 实现步骤
第一步:环境搭建

项目生成后我们修改web.xml文件,它的版本太低了
本次测试我们使用tomcat9,打开tomcat9中的examples\WEB-INF\web.xml,用里面的配置如下:
<?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"
metadata-complete="true">
</web-app>
我们发现生成的项目结构没有java和resources文件,我们直接右击添加即可
接下来编译器版本修改为17,添加依赖:servlet、mysql、junit、mybatis、logback
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itzw</groupId>
<artifactId>mybatis-004-web</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mybatis-004-web Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<!--junit-->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--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>
<!--logback-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!--servlet-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
<build>
<finalName>mybatis-004-web</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
引入相关配置文件放到resources目录下:
我们直接复制之前配置好的,修修改改即可

配置tomcat服务器:选择tomcat 9,这个配置大家应该很熟悉了,这就不展示了
第二步:编写前端页面
<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>
第三步:创建pojo、service、dao、utils、web包
这就是mvc三层架构必须有的包,其中utils下直接把之前写的SqlSessionUtil工具类复制到这

第四步:定义pojo类
package com.itzw.bank.pojo;
public class Account {
private Long id;
private String actno;
private Double balance;
public Account(){}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", 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;
}
}
第五步:后端代码的编写
这是最核心也是最繁琐的一步。因为分层的原因,不可能说一次就把一个文件(一层)编写完整,我们三个层一起编写,也就是说根据业务流程顺序编写相应的代码,不要乱。
首先在前端提交数据,这个数据会交给表示层(Servlet),我们在web目录中写Servlet,获取前端提交的信息,把它交给service目录(业务逻辑层)
层与层直接要使用接口衔接,在service目录中编写接口,编写需要的方法,本次我们只需要它负责转账即可。然后再在service目录中创建一个包用来编写实现这个接口的类,我们就在这里编写业务逻辑。本次我们需要转账也就是需要得到账户的余额。然后还要更新,也就是对余额修改。这就需要持久层连接数据库了。
从业务逻辑层转到持久层(dao目录),同样我们先编写接口,本次我们只需要查到账户和修改账户这两个数据库操作即可。然后实现这个接口,编写对数据库操作的方法,这里终于用到了mybatis知识点,结合Mapper配置文件编写方法。
编写数据库操作的方法之后再转回到业务逻辑层我们继续编写逻辑判断。这里就很好写了,因为存在转账失败的情况,我们最好编写异常用来抛出,比如余额不足异常和转账失败异常等等。
业务逻辑层写完再转到表示层,成功就重定向到成功的页面,失败就重定向到失败的页面。编写这几个html页面即可。到这就编写完成了,下面测试。
注意在web.xml文件中如果这个属性的值为true,则表示只能用这个配置文件配置servlet信息,也就是说不能使用注解进行配置,改为false就可以使用了。

代码如下:
持久层:
接口:
package com.itzw.bank.dao;
import com.itzw.bank.pojo.Account;
/**
* 按理说我们应该把增删改查的方法都写了,但是本次不需要就先不写了
*/
public interface AccountDao {
/**
* 根据账号查询账户信息
* @param actno 账号
* @return 账户信息
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act 账户
* @return 1表示更新成功,其它表示失败
*/
int updateByActno(Account act);
}
接口的实现:
package com.itzw.bank.dao.impl;
import com.itzw.bank.dao.AccountDao;
import com.itzw.bank.pojo.Account;
import com.itzw.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
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 updateByActno(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.updateByActnno", act);
sqlSession.commit();
sqlSession.close();
return count;
}
}
业务逻辑层:
接口:
package com.itzw.bank.service;
import com.itzw.bank.exceptions.MoneyNotEnoughException;
import com.itzw.bank.exceptions.TransferException;
public interface AccountService {
/**
* 账户转账业务
* @param fromActno 转出账户
* @param toActno 转入账户
* @param money 转账金额
*/
void transfer(String fromActno,String toActno,double money) throws MoneyNotEnoughException, TransferException;
}
接口的实现:
package com.itzw.bank.service.impl;
import com.itzw.bank.dao.AccountDao;
import com.itzw.bank.dao.impl.AccountDaoImpl;
import com.itzw.bank.exceptions.MoneyNotEnoughException;
import com.itzw.bank.exceptions.TransferException;
import com.itzw.bank.pojo.Account;
import com.itzw.bank.service.AccountService;
public class AccountServiceImpl implements AccountService {
@Override
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
//1.账户余额是否充足
AccountDao accountDao = new AccountDaoImpl();
Account fromact = accountDao.selectByActno(fromActno);
Double balance = fromact.getBalance();
//2.不充足提示用户
if (balance < money){
//余额不足,抛异常
throw new MoneyNotEnoughException("您的余额不足,感觉充钱吧!");
}
//余额充足
//3.若余额充足完成更新两个账户的余额
//更新formAct账户
fromact.setBalance(fromact.getBalance() - money);
int count = accountDao.updateByActno(fromact);
//更新toAct账户
Account toAct = accountDao.selectByActno(toActno);
toAct.setBalance(toAct.getBalance() + money);
count += accountDao.updateByActno(toAct);
if (count != 2){
//转账失败,抛异常
throw new TransferException("转账失败,未知错误");
}
}
}
表示层:
package com.itzw.bank.web;
import com.itzw.bank.exceptions.MoneyNotEnoughException;
import com.itzw.bank.exceptions.TransferException;
import com.itzw.bank.service.AccountService;
import com.itzw.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;
@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"));
//使用service中的转账方法完成转账
//层和层之间要用接口衔接,为了降低层与层之间的耦合度
try {
accountService.transfer(fromActno,toActno,money);
//转账成功
//调用view完成展示
response.sendRedirect(request.getContextPath()+"/success.html");
} catch (MoneyNotEnoughException e) {
//转账失败
response.sendRedirect(request.getContextPath()+"/error1.html");
} catch (TransferException e) {
//转账失败
response.sendRedirect(request.getContextPath()+"/error2.html");
}
}
}
AccountMapper配置文件:
<?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">
<select id="selectByActno" resultType="com.itzw.bank.pojo.Account">
select * from t_act where actno = #{actno}
</select>
<update id="updateByActnno">
update t_act set balance = #{balance} where actno = #{actno}
</update>
</mapper>
2.3 事务处理
以上代码还有漏洞,就是加入程序出现异常,可能转账出问题,比如白白丢失钱,我们要保证转出用户余额的改变和转进账户余额的改名处于一个事务中,要不一起成功要不一起失败
很显然我们在持久层(dao)下的提交和关闭代码要删掉,不能在那执行这些代码
修改SqlSessionUtil:
package com.itzw.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;
import java.io.IOException;
public class SqlSessionUtil {
private SqlSessionUtil() {}
/**
* 类加载时获取sqlSessionFactory对象
*/
private static SqlSessionFactory sqlSessionFactory ;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
//全局的,一个服务器定义一个即可
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
/**
* 每调用一次openSession获得一个新的会话
* @return
*/
public static SqlSession openSession(){
SqlSession sqlSession = local.get();
if (sqlSession == null){
sqlSession = sqlSessionFactory.openSession();
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭SqlSession对象(从当前线程移除SqlSession对象)
* @param sqlSession
*/
public static void close(SqlSession sqlSession){
if (sqlSession != null){
sqlSession.close();
local.remove();
}
}
}
注意SqlSession对象最后要移除,因为Tomcat服务器支持线程池,也就是说里面的线程是可以重复使用的,也就是说,以后有可能会被别人用到这个线程。
2.4 生命周期
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦床架了SqlSessionFactory就不在需要它了,因此它实例的最佳作用域是方法作用域(也就是局部方法变量)。我们可以重用SqlSessionFactoryBuilder来创建多个SqlSessionFactory实例,但最好不要一直保留它。以保证节省资源。
SqlSessionFactory
SqlSessionFactory一旦被创建就应该在应用的运行期间一直存在。使用SqlSessionFactory的最佳实践是在应用期间不需要重复创建多次,就比如在我们写SqlSession工具类的时候把创建SqlSessionFactory对象放在static代码块中。因此SqlSessionFactory的最佳作用域为应用作用域。
SqlSession
每个线程都有自己的SqlSession实例。SqlSession的实例不是线程安全的,因此是不能被共享的,所以它的最佳作用域是请求或者方法作用域,绝对不能将SqlSession实例的引用放在一个类的静态域或者一个类的实例变量也不行。
2.5 当前程序存在的问题
dao实现类中的代码是很固定的,基本上每个方法就一行代码不一样,就是那些固定的增删改查,这个类的方法中没有任何逻辑,既然这,这个类我们能不能动态生成,以后不写这个类了。
3 使用javassit生成类
这部分有点繁琐,以后再看。。。
4 MyBatis中接口代理机制
之所以不对上部分细讲,是因为MyBatis中已经实现了这个功能,我们直接调用即可
调用方法:AccountDao accountDao = sqlSession.getMapper(AccountDao.class)
需要注意的是:在使用MyBatis这种代理机制的前提是:SQLMapper.xml文件中namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名
为什么要这样规定呢?因为我们使用的是MyBatis提供的代理机制,它是不知道我们写的SQL语句的id是什么的。那干脆直接规定好这个名字,那我就能准确知道这个id和namespace了。
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="com.itzw.bank.dao.AccountDao">
<select id="selectByActno" resultType="com.itzw.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>
然后只要将之前的新建dao类对象改为如下即可:
//使用代理机制
private AccountDao accountDao = SqlSessionUtil.openSession().getMapper(AccountDao.class);
因为我们面向接口,使用MyBatis的代理机制,就不需要我们自己写dao类了,只需要写dao的接口就可以。那么我们可以直接把dao包命名为mapper,这只是一个规范,看您自己。
5 MyBatis小技巧
5.1 #{}和${}
#{}和${}的区别:
- #{}:底层使用PreparedStatement。特点:先进行SQL语句的编译,然后给SQL语句的占位符问号?传值。可以避免SQL注入的风险
- ${}:底层使用Statement。特点:先进行SQL语句的拼接,然后再对SQL语句进行编译。存在SQL注入的风险。
- 大部分我们都是用#{}。但如果我们需要将SQL语句的关键字放入SQL语句中就不能使用#{},只能使用${},因为${}是以值得形式存放在SQL语句当中的,是直接拼在SQL语句上然后再执行SQL语句的。
下面看看${}的用处的案例:
(1)查询信息根据某项信息排序
接口方法:
List<Car> selectAll(String ascOrDesc);
SQL语句:
<select id="selectAll" resultType="com.itzw.mybatis.pojo.Car">
select id,
car_num as carName,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
order by car_num ${ascOrDesc}
</select>
测试方法:
@Test
public void testSelectAll(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAll("asc");
cars.forEach(car -> System.out.println(car));
}
如果SQL语句中使用#{}是不可以的
(2)拼接表名
和上个类似,sql语句大致为:select * from tablename${}
(3)批量删除
int deleteByIds(String ids);
<delete id="deleteByIds">
delete from t_car where id in(${ids})
</delete>
@Test
public void testDeleteBuIds(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
int count = mapper.deleteByIds("23,26");
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
(4)模糊查询
List<Car> selectByBrandLike(String brand);
<select id="selectByBrandLike" resultType="com.itzw.mybatis.pojo.Car">
select id,
car_num as carName,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from
t_car
where brand like '%${brand}%'
</select>
@Test
public void testSelectByBrandLike(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByBrandLike("亚迪");
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
模糊查询也可以使用#{},这样使用:"%"#{brand}"%"
5.2 别名机制
因为在写SQL语句中的查询语句时要写resultType(返回类型),但是如果太多的话写起来非常不方便,而且都是重复的内容,我们可以简写吗。
使用typeAliases标签,其中type是指定给哪个类起别名,alias是别名
<typeAliases>
<typeAlias type="com.itzw.mybatis.pojo.Car" alias="aaa"></typeAlias>
</typeAliases>
我们可以不自己起别名,它会自动生成别名为类名的简类名,就是类名不区分大小写
但是即使这样我们还是要写很多类和别名的对应,可不可以更简单呢
<typeAliases>
<!--<typeAlias type="com.itzw.mybatis.pojo.Car" alias="aaa"></typeAlias>-->
<package name="com.itzw.mybatis.pojo"/>
</typeAliases>
使用package配置,只需要写包名,它会自动识别里面的所有类然后生成对应简别名
5.3 mappers
我们在mybatis核心配置文件引入Mapper.xml文件是使用mapper中的resources属性,或者url属性,我们习惯使用resources属性这样更灵活。但是mapper还有个class属性,我们来看看这个如何使用。
class属性的值为mapper接口名。比如我的接口名为com.itzw.mybatis.mapper.CarMapper,写上这个接口名,mybatis框架会自动去com/itzw/mybatis/mapper目录下找CarMapper.xml这个文件。也就是说如果采用这种方式我们必须保证CarMapper.xml文件和CarMapper接口必须在同一个目录下并且名字一致。那我们就需要在resources目录下新建一个这样的包。还要注意在resources目录下建包不能直接用点“.”来建,要是用斜杠“/”,比如本次在resources新建包要这样建:com/itzw/mybatis/mapper,resources目录就是根目录,和java目录是同级的。

这样每一个接口都要写一个class属性指定接口也挺麻烦,我们可以使用package指定包名。它会自动将包下的所有接口与mapper.xml文件匹配
<mappers>
<!--<mapper resource="CarMapper.xml"/>-->
<!--<mapper class="com.itzw.mybatis.mapper.CarMapper"></mapper>-->
<package name="com.itzw.mybatis.mapper"/>
</mappers>
5.4 idea配置模板
因为mybatis核心配置文件和SQL映射文件没有模板,只能我们自己复制或者自己写,我们可以在idea中创建一个这样的模板,每次创建文件后直接生成模板,我们只要写关键信息即可。

5.5 插入数据时获取自动生成的主键
比如有个场景,B表的外键是A表的主键,当我们插入A表数据时需要把主键插入到B表的外键中,但是因为主键是自动生成的,我们不知道主键,这时就可以使用获取自动生成主键方法
int insertUseGeneratedKeys(Car car);
配置文件:
<insert id="insertUseGeneratedKeys" useGeneratedKeys="true" keyProperty="id">
insert into t_car values
(null,#{carName},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
其中useGeneratedKeys=“true”表示自动生成主键值;keyProperty=“id”表示把主键值赋给哪个属性
测试:
@Test
public void testInsertUseGeneratedKeys(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(null,"898","凯迪拉克",21.0,"2032-10-2","新能源");
mapper.insertUseGeneratedKeys(car);
System.out.println(car);
sqlSession.commit();
sqlSession.close();
}
在我们提交数据前确实能拿到主键值
注意:前提是主键是自动生成的
624

被折叠的 条评论
为什么被折叠?



