MyBatis框架从零入门老杜版笔记(上)
一、MyBatis概述
1.1 框架 framework
1.2 MyBatis和JDBC的关系
MyBatis是增强版的jdbc,把一些功能通过反射封装好了
1.3 JDBC不足
- sql语句写死,如果拓展功能,可能整个sql语句作废
- set操作繁琐,如果sql语句有一百个?可能要写一百个set
1.4 了解MyBatis
- MyBatis是对jdbc的封装
- MyBatis之前叫做ibatis,后来才改名
- MyBatis是持久层框架
1.5 了解ORM
- O(Object):JVM中的java对象
- R(Relational):关系型数据库
- M(Mapping):映射
- 什么是ORM?:JavaBean与数据表记录的互相映射
二、MyBatis入门程序
2.1 建数据库表
添加两条字段
2.2 加载mybatis的五个步骤
- 配置Maven环境,打包方式设置为jar,加载MyBatis,MySQL
<packaging>jar</packaging><dependencies><!-- MyBatis --><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.11</version></dependency><!-- MySQL --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency></dependencies>
- 新建,编辑mybatis-config.xml文件(放入resources文件夹)
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEconfigurationPUBLIC"-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><environmentsdefault="development"><environmentid="development"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="com.mysql.cj.jdbc.Driver"/><propertyname="url"value="jdbc:mysql://localhost:3306/powernode"/><propertyname="username"value="xxxx"/><propertyname="password"value="xxxx"/></dataSource></environment></environments><mappers><mapperresource="CarMapper.xml"/></mappers></configuration>
- 新建,配置xxxMapper.xml文件
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="org.mybatis.example.BlogMapper"><insertid="">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values (null,1003,"面包车",13.00,"2020-10-13","飞行汽车")
</insert></mapper>
- 在xxxMapper中编写sql代码(在3中已完成)
- 把xxxMapper.txt文件路径放入mybatis-config.txt中(在2中已完成)
<mappers><mapperresource="CarMapper.xml"/></mappers>
2.3 MyBatis中的事务
在mybatis-config.xml中有一行为
<transactionManagertype="JDBC"/>
type类型可以写成两种,一种是JDBC另一种是MANAGED(不区分大小写)
- JDBC:交给JDBC处理事务(默认false,表示开启事务,需要手动提交)
- MANAGED:有用到spring框架时设置为此,表交给框架处理事务,如果没有用到框架设置为此类型,则没有人处理事务,会自动提交
注意事项:
- SqlSessionFactory.openSession()默认开启事务
2.4 编写MyBatis代码
SqlSession sqlSession =null;try{SqlSessionFactoryBuilder sqlSessionFactoryBuilder =newSqlSessionFactoryBuilder();SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis-config.xml"));
sqlSession = sqlSessionFactory.openSession();int count = sqlSession.insert("insertCar");System.out.println("新增了记录"+count);//提交事务
sqlSession.commit();}catch(Exception e){if(sqlSession !=null){
sqlSession.rollback();}
e.printStackTrace();}finally{if(sqlSession !=null){
sqlSession.close();}}
2.5 在MyBatis中引入JUnit
在maven中添加junit依赖
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency>
新建test包,以及创建CarMapperTest类
在新建的类中编写MyBatis代码
publicclassCarMapperTest{@TestpublicvoidinsertCar(){SqlSession sqlSession =null;try{SqlSessionFactoryBuilder sqlSessionFactoryBuilder =newSqlSessionFactoryBuilder();SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsReader("mybatis-config.xml"));
sqlSession = sqlSessionFactory.openSession();int count = sqlSession.insert("insertCar");System.out.println("新增了记录"+count);//提交事务
sqlSession.commit();}catch(Exception e){if(sqlSession !=null){
sqlSession.rollback();}
e.printStackTrace();}finally{if(sqlSession !=null){
sqlSession.close();}}}}
运行,测试一下,绿色就是没问题
junit小知识点:
- 断言测试方法:Assert.assertEquals(期望值,实际值);
2.6 MyBatis集成日志组件
- SLF4J(沙拉疯):logback就是继承沙拉疯实现的
- LOG4J:
- LOG4J2:
- STDOUT_LOGGING:MyBatis内部实现的日志组件,
logback、log4j、log4j2是同一个作者编写的
若使用STDOUT_LOGGING,需要在mybatis-config.xml里添加配置文件
注意:settings标签必须添加在configuration下面
<configuration><settings><settingname="logImpl"value="STDOUT_LOGGING"/></settings></configuration>
这边不使用STDOUT_LOGGING日志组件,我们使用最常用的logback组件
配置步骤:
- 在maven中添加logback的依赖
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.11</version><scope>test</scope></dependency>
- 在mybatis-config.xml中删除日志设置(除了STDOUT_LOGGING自带日志组件外,其他组件不需要写日志设置)
<!-- 删除这些 --><settings><settingname="logImpl"value="STDOUT_LOGGING"/></settings>
- 写配置文件(名字必须为logback.xml或logback-test.xml) 必须放在resources目录下
<?xml version="1.0" encoding="UTF-8"?><!-- 配置文件修改时重新加载,默认true --><configurationdebug="false"><!-- 控制台输出 --><appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender"><encoderclass="ch.qos.logback.classic.encoder.PatternLayoutEncoder"charset="UTF-8"><!-- 输出日志记录格式 --><pattern>[%thread] %-5level %logger{50} - %msg%n</pattern></encoder></appender><!-- mybatis log configure--><loggername="com.apache.ibatis"level="TRACE"/><loggername="java.sql.Connection"level="DEBUG"/><loggername="java.sql.Statement"level="DEBUG"/><loggername="java.sql.PreparedStatement"level="DEBUG"/><!-- 日志输出级别,LOGBACK日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR--><rootlevel="DEBUG"><appender-refref="STDOUT"/><appender-refref="FILE"/></root></configuration>
- 运行测试
2.7 MyBatis工具类的编写
在编写代码中,每次都通过SqlSessionFactoryBuilder三步骤获取session太麻烦,我们编写一个工具类get会比较方便点
- 新建类utils.SqlSessionUtil
- 编写代码
publicclassSqlSessionUtilTest{/**
* 测试工具类
*/@TestpublicvoidopenSessionTest(){SqlSession session =SqlSessionUtil.openSession();int count = session.insert("insertCar");System.out.println(count);
session.commit();
session.close();}}
- 运行测试(数据库正常添加,完成)
三、使用MyBatis完成CRUD
3.1 实现Insert新增
1.1 MyBatis中完成Map集合传参
第一个入门程序已完成,现在我们来做MyBatis的CRUD操作
入门程序有个问题:实战的时候新增数据表行数据不可能是固定值
回顾:
<mappernamespace="org.mybatis.example.BlogMapper"><insertid="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values (null,1003,"面包车",13.00,"2020-10-13","飞行汽车")
</insert></mapper>
- 继续完善这个程序,新建一个模块叫CRUD,如图,把模块1的一些文件拷贝过来,maven依赖引用
- 修改CarMapper.xml文件,加上占位符 在MyBatis中占位符为#{},对应JDBC的?
<insertid="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values (null,#{},#{},#{},#{},#{})
</insert>
- 编写测试代码
publicclassCarMapperTest{@TestpublicvoidinsertCar(){SqlSession session =SqlSessionUtil.openSession();Map<String,Object> map =newHashMap<>();
map.put("carNum","1004");
map.put("brand","比亚迪汉");
map.put("guidePrice",160.00);
map.put("produceTime","2022-06-08");
map.put("carType","新能源汽车");int count= session.insert("insertCar", map);System.out.println("新增的条目:"+count);
session.commit();
session.close();}}
- 修改mapper文件中的占位符#{}(大括号中间填入map.get(key)里面的key)
<mappernamespace="org.mybatis.example.BlogMapper"><insertid="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values (null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert></mapper>
- 测试,完成
1.2 MyBatis中通过POJO类完成传参
- 新建POJO包、类(pojo.Car)
publicclassCar{privateLong id;privateString carNum;privateString brand;privateDouble guidePrice;privateString produceTime;privateString carType;//此处忽略构造方法、getting setting方法.....}
- 编写测试代码
@TestpublicvoidinsertCar(){SqlSession session =SqlSessionUtil.openSession();Car car =newCar(null,"1005","比亚迪秦",30.0,"2020-10-20","新能源");int count= session.insert("insertCar", car);System.out.println("新增的条目:"+count);
session.commit();
session.close();}
- 编辑mapper文件
<mappernamespace="org.mybatis.example.BlogMapper"><insertid="insertCar">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values (null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert></mapper>
- 运行测试方法,总结: mapper文件中填写的#{carType}),MyBatis会通过反射机制查找getCarType()方法得到值
3.3 实现Delete删除
- 新增Mapper删除
<deleteid="deleteById">
delete from t_car where id = #{id}
</delete>
- 编写测试方法
@TestpublicvoiddeleteById(){SqlSession session =SqlSessionUtil.openSession();
session.delete("deleteById",17);
session.commit();
session.close();}
- 注意事项 当参数只有一个时,参数名可以随意,但是尽量做到见名知意
3.4 实现update修改
- 编写mapper
<updateid="updateById">
update t_car
set car_num=#{carNum},brand=#{brand},guide_price=#{guidePrice},produce_time=#{produceTime},car_type=#{carType}
where id=#{id}
</update>
- 编写测试类
@TestpublicvoidtestUpdateById(){SqlSession session =SqlSessionUtil.openSession();Car car =newCar(3L,"5123","哈哈车",1.5,"2011-01-04","新技术");int count = session.update("updateById", car);System.out.println(count);
session.commit();
session.close();}
3.5 实现select查询
1.1 selectOne查询一个
- 编写Mapper文件
<selectid="selectOneById"resultType="com.powernode.mybatis.pojo.Car">
select car_num as carNum,
brand,guide_price as guidePrice,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from t_car
where id = #{id}
</select>
- 编写测试方法
@TestpublicvoidselectOneById(){SqlSession session =SqlSessionUtil.openSession();Car car = session.selectOne("selectOneById",16L);System.out.println(car);
session.close();}
注意事项:
- Mapper配置文件里要增加:resultType=“com.powernode.mybatis.pojo.Car”,指定查询出来是什么类,MyBatis会自动转换成该类
- guide_price as guidePrice,取别名是为了让pojo类属性和查询结果对应上,符合ORM
1.2 selectList查询所有,返回一个集合
- 编写Mapper配置文件
<selectid="selectAll"resultType="com.powernode.mybatis.pojo.Car">
select car_num as carNum,
brand,guide_price as guidePrice,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from t_car
</select>
- 编写测试代码
@TestpublicvoidselectAll(){SqlSession session =SqlSessionUtil.openSession();List<Car> cars = session.selectList("selectAll");
cars.forEach( car ->System.out.println(car));
session.close();}
3.6 Mapper映射文件的namespace
在mapper.xml文件中有一个namespasce,这个属性是用来指定命名空间的,用来防止id重复
在java程序中的写法:
List<Car> cars = session.selectList("namespace.selectAll");
这样写才是严谨、完整的
四、MyBatis核心配置文件详解
4.1 environment标签
一个environment对应一个SqlSessionFactory
一个SqlSessionFactory对应一个数据库
- 多环境下,配置文件这样写(两个environment)
<environmentsdefault="development"><!-- 一个environment对应一个数据库 --><environmentid="development"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="com.mysql.cj.jdbc.Driver"/><propertyname="url"value="jdbc:mysql://localhost:3306/powernode"/><propertyname="username"value="root"/><propertyname="password"value="root"/></dataSource></environment><environmentid="development2"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="com.mysql.cj.jdbc.Driver"/><propertyname="url"value="jdbc:mysql://localhost:3306/powernode2"/><propertyname="username"value="root"/><propertyname="password"value="root"/></dataSource></environment></environments>
- 写测试方法
publicclass testConfiguration {@TestpublicvoidtestEnvironment()throwsIOException{SqlSessionFactoryBuilder sessionFactoryBuilder =newSqlSessionFactoryBuilder();SqlSessionFactory sessionFactory = sessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"),"development2");SqlSession session = sessionFactory.openSession();int count = session.insert("insertCar");System.out.println(count);
session.commit();
session.close();}}
4.2 transactionManager标签
在mybatis-config.xml配置文件中,有transactionManager子标签,表示设置MyBatis的事务管理器
<transactionManagertype="JDBC"/>
MyBatis中有两个事务管理器
- JDBC:交给原生JDBC管理setAutoComit(false);需要手动comit()提交
- MANAGED:交给JEE处理,一般由JEE框架处理,如Spring
MyBatis底层有一个Transaction接口,实现两个事务
- 如果type=“JDBC”,那么底层会实例化JdbcTransaciton对象
- 如果type=“MANAGED”,那么底层会实例化ManagedTransaciton
4.3 dataSource标签
<dataSourcetype="POOLED">
.......
</dataSource>
问:这个标签有啥用
答:dataSource表示数据源,用来获取Connection对象
它的type属性可以填写三个值:
- UNPOOLED:不使用数据库连接池,每次获取Connection都要创建一个新对象
- POOLED:使用MyBatis自带的连接池
- JNDI:连接第三方的数据源(如果自己手写框架也可以用这个)
3.1 type属性详解
不同类型的数据源下有不同的属性,比如
<dataSourcetype="POOLED">
....
<properties/>
....
</dataSource>
和
<dataSourcetype="JNDI">
....
<properties/>
....
</dataSource>
的
<properties/>
数量、内容是不一样的,具体需要看官方文档
JDNI是:java命名目录接口,Tomcat服务器实现了这个规范
假如需要用第三方数据源,Tomcat中实现了这个数据源,则在properties中填写对应的数据,要和tomat对应上
3.2 pooled和unpooled的区别
- unpooled表示不使用连接池,每次请求过来都会创建一个Connection
- pooled表示使用MyBatis自带的连接池:请求过来会先从连接池获取Connection对象
问:使用连接池有什么好处?
答:
3. 迟内的Connection数量是固定的,比如池子大小是5,如果5个连接都被占用,第6个要获取连接就先等待,数量固定
4. 假如有人一直F5刷新,没有用连接池的话,就会一直创建Connection对象,如果实例化对象过多,可能会导致服务器宕机,数量固定
5. 有新请求,第一反应去池中查找,可以增加效率
3.3 配置具体的数据库连接池参数
<!--连接池最大连接数,默认:10--><propertyname="poolMaximumActiveConnections "value="10"/><!--可以同时存在的最大空闲连接数,空闲太多则真正关闭一些Connection--><propertyname="poolMaximumIdleConnections "value="5"/><!--超时强制关闭时间,默认20000--><propertyname="poolMaximumCheckoutTime "value="20000"/><!--如果连接花费时间很长,连接池会隔断时间尝试重新连接并打印日志--><propertyname="poolTimeToWait "value="2000"/>
4.4 properties标签
1. 第一种写法(写在配置文件内)
在configuration标签下有一个properties子标签,是用来设置变量的
<propertyname="key"value="value"/>
这样写相当于往map集合里放入一个key,可以通过key拿到value,在mybatis-config.xml的其他地方,可以写成${key}获取到value
2. 第二种写法(外部文件-相对路径)
<propertiesresource="jdbc.properties"/>
然后在外部新建一个jdbc.properties,如(等号左边是key右边是value)
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/powernode
username=root
password=root
3. 第三种写法(外部文件-绝对路径)
不推荐这种写法
<propertiesurl="file:///D://jdbc.properties"/>
五、在Web中应用MyBatis(使用MVC架构模式)
5.1 环境搭建
建立如下数据库:
5.2 idea环境搭建
1. idea新建一个maven项目,Create from archetype勾选上,选择如图所示webapp
2. maven依赖配置好
<dependencies><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency><dependency><groupId>servletapi</groupId><artifactId>servletapi</artifactId><version>2.4-20040521</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.11</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.11</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope></dependency></dependencies>
3. 修改web.xml配置文件
<?xml version="1.0" encoding="UTF-8"?><web-appxmlns="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_3_1.xsd"version="3.1"metadata-complete="true"></web-app>
4. 编写mybatis-config.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEconfigurationPUBLIC"-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><propertiesresource="jdbc.properties"/><environmentsdefault="development"><!-- 一个environment对应一个数据库 --><environmentid="development"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="${driver}"/><propertyname="url"value="${url}"/><propertyname="username"value="${username}"/><propertyname="password"value="${password}"/></dataSource></environment><environmentid="development2"><transactionManagertype="JDBC"/><dataSourcetype="POOLED"><propertyname="driver"value="com.mysql.cj.jdbc.Driver"/><propertyname="url"value="jdbc:mysql://localhost:3306/powernode2"/><propertyname="username"value="root"/><propertyname="password"value="root"/></dataSource></environment></environments><mappers><mapperresource="ActMapper.xml"/></mappers></configuration>
5. 在resources目录下新建、编辑jdbc.properties文件
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/powernode
username=root
password=root
6. 在resources目录下新建、编写Mapper文件
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="account"><insertid="insertAct">
insert into t_act(id,name,balance)
values (null,#{name},#{balance})
</insert></mapper>
7. 在resources目录下新建、编写logback.xml配置文件
<?xml version="1.0" encoding="UTF-8"?><!-- 配置文件修改时重新加载,默认true --><configurationdebug="false"><!-- 控制台输出 --><appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender"><encoderclass="ch.qos.logback.classic.encoder.PatternLayoutEncoder"charset="UTF-8"><!-- 输出日志记录格式 --><pattern>{%thread} %-5level %logger{50} - %msg%n</pattern></encoder></appender><!-- mybatis log configure--><loggername="com.apache.ibatis"level="TRACE"/><loggername="java.sql.Connection"level="DEBUG"/><loggername="java.sql.Statement"level="DEBUG"/><loggername="java.sql.PreparedStatement"level="DEBUG"/><!-- 日志输出级别,LOGBACK日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR--><rootlevel="DEBUG"><appender-refref="STDOUT"/><appender-refref="FILE"/></root></configuration>
8. 新建pojo类,放到com.powernode.bank.pojo包中
publicclassAccount{privateLong id;privateString name;privateDouble balance;......//此处省略构造方法、getting setting toString方法,请自行创建
9. 新建dao, servce, utils, web,并在utils中新建SqlSessionUtil类
packagecom.powpernode.bank.utils;importorg.apache.ibatis.io.Resources;importorg.apache.ibatis.session.SqlSession;importorg.apache.ibatis.session.SqlSessionFactory;importorg.apache.ibatis.session.SqlSessionFactoryBuilder;importjava.io.IOException;/**
* @author huochai
* @date 2022/10/15 9:22
*/publicclassSqlSessionUtil{/**
* 一个sessionFactory对应一个environment(数据库),所以不要每次运行都new一个新的
*//**
* 构造方法设置为私有的,防止被实例化
*/privateSqlSessionUtil(){}privatestaticSqlSessionFactory sessionFactory;static{try{
sessionFactory =newSqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));}catch(IOException e){
e.printStackTrace();}}publicstaticSqlSessionopenSession(){SqlSession session = sessionFactory.openSession();return session;}}
10. 在webapp中新建index.html
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>银行转账</title></head><body><formaction="/bank/transfer"method="post">
转出账户:<inputtype="text"name="fromActno"><br>
转入账户:<inputtype="text"name="toActno"><br>
转账金额<inputtype="text"name="money"><br><inputtype="submit"value="转账"></form></body></html>
5.3 后端代码实现
根据前端请求发送的路线完善后端代码
1. 新建、完善AccountServlet(接口与实现类)
packagecom.powpernode.bank.web;importcom.powpernode.bank.exception.MoneyNotEnoughException;importcom.powpernode.bank.exception.TransferNotSuccessException;importcom.powpernode.bank.service.AccountService;importcom.powpernode.bank.service.impl.AccountServiceImpl;importjavax.jws.WebService;importjavax.servlet.ServletException;importjavax.servlet.annotation.WebServlet;importjavax.servlet.http.HttpServlet;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;/**
* @author huochai
* @date 2022/10/16 15:15
*/@WebServlet("/transfer")publicclassAccountServletextendsHttpServlet{/**
* 调用业务类处理业务
*/privateAccountService accountService =newAccountServiceImpl();@OverrideprotectedvoiddoPost(HttpServletRequest request,HttpServletResponse response)throwsServletException,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){
e.printStackTrace();
response.sendRedirect(request.getContextPath()+"/error1.html");}catch(TransferNotSuccessException e){
e.printStackTrace();
response.sendRedirect(request.getContextPath()+"/error2.html");}}}
2. 完善业务层AccountService接口、实现类
packagecom.powpernode.bank.service;importcom.powpernode.bank.exception.MoneyNotEnoughException;importcom.powpernode.bank.exception.TransferNotSuccessException;/**
* @author huochai
* @date 2022/10/16 15:17
*/publicinterfaceAccountService{/**
* 转账业务方法
* @param fromAct 转出账户
* @param toAct 转入账户
* @param money 转账金额
* @throws MoneyNotEnoughException 转出账户余额不足异常
* @throws TransferNotSuccessException 转账失败异常
*/voidtransfer(String fromAct,String toAct,Double money)throwsMoneyNotEnoughException,TransferNotSuccessException;}
packagecom.powpernode.bank.service.impl;importcom.powpernode.bank.dao.AccountDao;importcom.powpernode.bank.dao.impl.AccountDaoImpl;importcom.powpernode.bank.exception.MoneyNotEnoughException;importcom.powpernode.bank.exception.TransferNotSuccessException;importcom.powpernode.bank.pojo.Account;importcom.powpernode.bank.service.AccountService;importjavax.jws.WebService;/**
* @author huochai
* @date 2022/10/16 15:19
* 只负责处理业务逻辑,不涉及数据库修改
*/publicclassAccountServiceImplimplementsAccountService{privateAccountDao accountDao =newAccountDaoImpl();@Overridepublicvoidtransfer(String fromAct,String toAct,Double money)throwsMoneyNotEnoughException,TransferNotSuccessException{//判断余额是否充足Account fromAccount = accountDao.selectById(Long.parseLong(fromAct));if(fromAccount.getBalance()<money){thrownewMoneyNotEnoughException("对不起,余额不足");}//将fromAct减少money,toAct增加moneyAccount toAccount = accountDao.selectById(Long.parseLong(toAct));
fromAccount.setBalance(fromAccount.getBalance()-money);
toAccount.setBalance(toAccount.getBalance()+money);//更新两个账户int count = accountDao.updateAccount(fromAccount);
count += accountDao.updateAccount(toAccount);if(count<2){thrownewTransferNotSuccessException("转账失败,未知错误");}}}
3. 完善DAO层,接口、实现类
packagecom.powpernode.bank.dao;importcom.powpernode.bank.pojo.Account;/**
* @author huochai
* @date 2022/10/16 15:20
*/publicinterfaceAccountDao{/**
* 根据ID查询账户
* @param id id
* @return 返回账户
*/AccountselectById(Long id);/**
* 更新用户
* @param account 需要更新的用户
* @return 1表示更新完成
*/intupdateAccount(Account account);/**
* 插入用户
* @param account 需要插入的用户
* @return 1表示插入完成
*/intinsertAccount(Account account);}
packagecom.powpernode.bank.dao.impl;importcom.powpernode.bank.dao.AccountDao;importcom.powpernode.bank.pojo.Account;importcom.powpernode.bank.utils.SqlSessionUtil;importorg.apache.ibatis.session.SqlSession;/**
* @author huochai
* @date 2022/10/16 15:21
*/publicclassAccountDaoImplimplementsAccountDao{@OverridepublicAccountselectById(Long id){SqlSession session =SqlSessionUtil.openSession();Account account = session.selectOne("account.selectById", id);
session.close();return account;}@OverridepublicintupdateAccount(Account account){SqlSession session =SqlSessionUtil.openSession();int count = session.update("account.updateById", account);
session.commit();return count;}@OverridepublicintinsertAccount(Account account){SqlSession session =SqlSessionUtil.openSession();int count = session.insert("account.insertAct", account);
session.commit();return count;}}
4. 完善两个异常类
/**
* @author huochai
* @date 2022/10/16 15:47
*/publicclassMoneyNotEnoughExceptionextendsException{publicMoneyNotEnoughException(){};publicMoneyNotEnoughException(String msg){super(msg);}}
packagecom.powpernode.bank.exception;/**
* @author huochai
* @date 2022/10/16 15:51
*/publicclassTransferNotSuccessExceptionextendsException{publicTransferNotSuccessException(){};publicTransferNotSuccessException(String msg){super(msg);}}
5. 当前项目结构
6. 完善事务处理
目前为止项目里存在一个问题,没有事务处理机制
如果在更新完账户1之后异常,就会出现少钱的现象
6.1 加入线程池
/**
* 完善后的SqlSessionUtil工具类
*/packagecom.powpernode.bank.utils;importorg.apache.ibatis.io.Resources;importorg.apache.ibatis.session.SqlSession;importorg.apache.ibatis.session.SqlSessionFactory;importorg.apache.ibatis.session.SqlSessionFactoryBuilder;importjava.io.IOException;/**
* @author huochai
* @date 2022/10/15 9:22
*/publicclassSqlSessionUtil{publicstaticThreadLocal<SqlSession> local =newThreadLocal();privateSqlSessionUtil(){}privatestaticSqlSessionFactory sessionFactory;static{try{
sessionFactory =newSqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));}catch(IOException e){
e.printStackTrace();}}publicstaticSqlSessionopenSession(){SqlSession session = local.get();if(session==null){
session = sessionFactory.openSession();
local.set(session);}return session;}publicstaticvoidclose(SqlSession session){if(session!=null){
session.close();
local.remove();}}}
6.2 在业务层控制事务处理
publicclassAccountServiceImplimplementsAccountService{privateAccountDao accountDao =newAccountDaoImpl();@Overridepublicvoidtransfer(String fromAct,String toAct,Double money)throwsMoneyNotEnoughException,TransferNotSuccessException{SqlSession session =SqlSessionUtil.openSession();//判断余额是否充足Account fromAccount = accountDao.selectById(Long.parseLong(fromAct));if(fromAccount.getBalance()<money){thrownewMoneyNotEnoughException("对不起,余额不足");}//将fromAct减少money,toAct增加moneyAccount toAccount = accountDao.selectById(Long.parseLong(toAct));
fromAccount.setBalance(fromAccount.getBalance()-money);
toAccount.setBalance(toAccount.getBalance()+money);//更新两个账户int count = accountDao.updateAccount(fromAccount);
count += accountDao.updateAccount(toAccount);if(count<2){thrownewTransferNotSuccessException("转账失败,未知错误");}
session.commit();SqlSessionUtil.close(session);}}
5.4 MyBatis作用域(Scope)和生命周期
名称生命周期SqlSessionFactoryBuilder只是用来buildFactory的,利用完即可丢弃SqlSessionFactory一个数据库对应一个Factory,最好不要丢弃SqlSession一个线程对应一个SqlSession
观察项目还有什么问题,发现DAO层代码很少并且很固定
publicclassAccountDaoImplimplementsAccountDao{//可以发现第一行都是获取Session,第二行执行,第三行return@OverridepublicAccountselectById(Long id){SqlSession session =SqlSessionUtil.openSession();Account account = session.selectOne("account.selectById", id);return account;}@OverridepublicintupdateAccount(Account account){SqlSession session =SqlSessionUtil.openSession();int count = session.update("account.updateById", account);return count;}@OverridepublicintinsertAccount(Account account){SqlSession session =SqlSessionUtil.openSession();int count = session.insert("account.insertAct", account);return count;}}
有没有什么框架可以代替这种简单重复的工作呢?
答:可以利用javassist框架
通过使用Javassist对字节码操作为JBoss实现动态“AOP”框架
5.5 使用javassist动态生成类
publicclassJavassistTest{@TestpublicvoidgenerateClass()throwsException{//获取类池ClassPool classPool =ClassPool.getDefault();// 制造类CtClass ctClass = classPool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");// 制造方法String method ="public void insert(){System.out.println(166);}";CtMethod make =CtMethod.make(method, ctClass);// 把方法加到类中
ctClass.addMethod(make);//在内存中生成类
ctClass.toClass();Class<?> aClass =Class.forName("com.powernode.bank.dao.impl.AccountDaoImpl");Object o = aClass.newInstance();Method insert = aClass.getDeclaredMethod("insert");
insert.invoke(o);}}
5.6 使用javassist生成类并实现接口
@TestpublicvoidgenerateInterface()throwsException{//获取类池ClassPool pool =ClassPool.getDefault();//制造类CtClass ctClass = pool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");//制造接口CtClass ctClass1 = pool.makeInterface("com.powernode.javassist.test.AccountDao");//添加接口
ctClass.addInterface(ctClass1);CtMethod make =CtMethod.make("public void delete(){System.out.println(\"Hello delete\");}", ctClass);
ctClass.addMethod(make);//装载类Class<?> aClass = ctClass.toClass();AccountDao o =(AccountDao) aClass.newInstance();
o.delete();}
5.7 使用javassist动态实现接口中所有方法
@TestpublicvoidgenerateInterfaceAll()throwsException{//获取类池ClassPool pool =ClassPool.getDefault();//制造类CtClass ctClass = pool.makeClass("com.powernode.bank.dao.impl.AccountDaoImpl");//制造接口CtClass ctInterface = pool.makeInterface("com.powernode.javassist.test.AccountDao");//类实现接口
ctClass.addInterface(ctInterface);Method[] declaredMethods =AccountDao.class.getDeclaredMethods();//制造方法//方法内容Arrays.stream(declaredMethods).forEach(method ->{try{StringBuffer methodValue =newStringBuffer();
methodValue.append("public ");
methodValue.append(method.getReturnType().getName()+" ");
methodValue.append(method.getName()+"(");Class<?>[] parameterTypes = method.getParameterTypes();for(int i =0; i < parameterTypes.length; i++){
methodValue.append(parameterTypes[i].getName()+" ");
methodValue.append("args"+i);if(i!= parameterTypes.length-1){
methodValue.append(",");}}
methodValue.append(")");
methodValue.append("{System.out.println(\"hello all\");");if("void".equals(method.getReturnType().getName())){}elseif("int".equals(method.getReturnType().getName())){
methodValue.append("return 1;");}elseif("java.lang.String".equals(method.getReturnType().getName())){
methodValue.append("return \"666\";");}
methodValue.append("}");CtMethod make =CtMethod.make(String.valueOf(methodValue), ctClass);
ctClass.addMethod(make);}catch(CannotCompileException e){
e.printStackTrace();}});Class<?> toClass = ctClass.toClass();AccountDao accountDao =(AccountDao) toClass.newInstance();
accountDao.delete();
accountDao.insert("666",50.00);
accountDao.selectByActno("555");}
5.8 编写工具类GenerateDaoProxy(自动实现DAO层接口)
注意:若想利用GenerateDaoProxy,
则Mapper.xml文件的namespace必须为DAO层接口的全类名,
sqlId必须为DAO层接口中的方法
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.powpernode.bank.dao.AccountDao"><insertid="insertAccount">
insert into t_act(id,name,balance)
values (null,#{name},#{balance})
</insert><selectid="selectById"resultType="com.powpernode.bank.pojo.Account">
select * from t_act where id=#{id}
</select><updateid="updateAccount">
update t_act set name=#{name},balance=#{balance} where id=#{id}
</update></mapper>
packagecom.powpernode.bank.utils;importorg.apache.ibatis.javassist.CannotCompileException;importorg.apache.ibatis.javassist.ClassPool;importorg.apache.ibatis.javassist.CtClass;importorg.apache.ibatis.javassist.CtMethod;importorg.apache.ibatis.mapping.SqlCommandType;importorg.apache.ibatis.session.SqlSession;importjava.lang.reflect.Method;importjava.util.Arrays;/**
* @author huochai
* @date 2022/10/16 23:30
*/publicclassGenerateDaoProxy{/**
* 这个工具类是框架的开发者提供的
* 开发者可以给使用者规定传进哪些参数
*
* 传进接口,返回实现所有方法的类
* @param daoInterface 接口
* @return Impl类
*/publicstaticObjectgenerate(SqlSession session,Class daoInterface){//类池ClassPool pool =ClassPool.getDefault();//制造类CtClass ctClass = pool.makeClass(daoInterface.getName()+"Proxy");//制造接口CtClass ctInterface = pool.makeInterface(daoInterface.getName());
ctClass.addInterface(ctInterface);Method[] declaredMethods = daoInterface.getDeclaredMethods();Arrays.stream(declaredMethods).forEach(method ->{try{StringBuffer methodCode =newStringBuffer();//添加修饰符
methodCode.append("public ");//添加返回值
methodCode.append(method.getReturnType().getName()+" ");
methodCode.append(method.getName());
methodCode.append("(");Class<?>[] parameterTypes = method.getParameterTypes();for(int i =0; i < parameterTypes.length; i++){
methodCode.append(parameterTypes[i].getName()+" ");
methodCode.append("arg"+i);if(i!= parameterTypes.length-1){
methodCode.append(",");}}
methodCode.append("){");/**
* 括号中间需要写对应的session.insert或session.select方法
*/String sqlId = daoInterface.getName()+"."+method.getName();SqlCommandType sqlCommandType = session.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
methodCode.append("org.apache.ibatis.session.SqlSession session = com.powpernode.bank.utils.SqlSessionUtil.openSession();");if(sqlCommandType ==SqlCommandType.INSERT){}if(sqlCommandType ==SqlCommandType.DELETE){}if(sqlCommandType ==SqlCommandType.UPDATE){
methodCode.append("return session.update(\""+sqlId+"\", arg0);");}if(sqlCommandType ==SqlCommandType.SELECT){String resultType = method.getReturnType().getName();
methodCode.append("return ("+resultType+")session.selectOne(\""+sqlId+"\", arg0);");}
methodCode.append("}");System.out.println(methodCode.toString());CtMethod ctMethod =CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);}catch(CannotCompileException e){
e.printStackTrace();}});Object obj =null;try{Class<?> toClass = ctClass.toClass();
obj = toClass.newInstance();}catch(Exception e){
e.printStackTrace();}return obj;}}
最后在业务层引用Dao的时候改一下即可:
publicclassAccountServiceImplimplementsAccountService{privateAccountDao accountDao =(AccountDao)GenerateDaoProxy.generate(SqlSessionUtil.openSession(),AccountDao.class);.....
写完以后得知一个好消息,MyBatis已经实现了映射机制,不用自己手写代码了(要求和上面一样,对namespace以及sqlId有格式要求)
//自己写的privateAccountDao accountDao =(AccountDao)GenerateDaoProxy.generate(SqlSessionUtil.openSession(),AccountDao.class);//MyBatis自带privateAccountDao accountDao =SqlSessionUtil.openSession().getMapper(AccountDao.class);
5.9 面向接口的方式进行CRUD
新建maven模块mybatis-005-crud2
添加maven依赖
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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"><parent><artifactId>MyBatis</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><packaging>jar</packaging><dependencies><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.11</version></dependency><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.4.6</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.11</version><scope>test</scope></dependency></dependencies><groupId>com.powernode</groupId><artifactId>mybatis-005-crud2</artifactId><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties></project>
添加mapper类
/**
* @author huochai
* @date 2022/10/17 9:38
* 在MyBatis中,一般不叫XXXDao,一般叫xxxMapper
*/publicinterfaceCarMapper{/**
* 插入数据
* @param car
* @return 返回1表示正常
*/intinsert(Car car);/**
* 根据ID删除
* @param id
* @return 返回1表示正常
*/intdeleteById(Long id);/**
* 更新数据
* @param car
* @return 返回1表示正常
*/intupdate(Car car);/**
* 根据ID查询
* @param id
* @return 返回1表示正常
*/CarselectById(Long id);/**
* 查询所有的数据
* @return 返回给List集合
*/List<Car>selectAll();}
编写CarMapper.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mappernamespace="com.powernode.mybatis.mapper.CarMapper"><insertid="insert">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null, #{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert><deleteid="delete">
delete from t_car where id=#{id}
</delete><updateid="update">
update t_car
set car_num=#{carNum},brand=#{brand},guide_price=#{guidePrice},produce_time=#{produceTime},car_type=#{carType}
where id=#{id}
</update><selectid="selectById"resultType="com.powernode.mybatis.pojo.Car">
select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from t_car where id=#{id}
</select><selectid="selectAll"resultType="com.powernode.mybatis.pojo.Car">
select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from t_car
</select></mapper>
新建工具类,SqlSessionUtil
publicclassSqlSessionUtil{publicstaticThreadLocal<SqlSession> local =newThreadLocal();privateSqlSessionUtil(){}privatestaticSqlSessionFactory sessionFactory;static{try{
sessionFactory =newSqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));}catch(IOException e){
e.printStackTrace();}}publicstaticSqlSessionopenSession(){SqlSession session = local.get();if(session==null){
session = sessionFactory.openSession();
local.set(session);}return session;}publicstaticvoidclose(SqlSession session){if(session!=null){
session.close();
local.remove();}}}
编写测试用例
publicclassTestMapper{@Testpublicvoidinsert(){SqlSession session =SqlSessionUtil.openSession();CarMapper mapper = session.getMapper(CarMapper.class);Car car =newCar(null,"5556","兰博基尼六",1952.1,"2060-02-06","跑车");
mapper.insert(car);
session.commit();}@Testpublicvoiddelete(){SqlSession session =SqlSessionUtil.openSession();CarMapper mapper = session.getMapper(CarMapper.class);
mapper.deleteById(21L);
session.commit();}@Testpublicvoidupdate(){SqlSession session =SqlSessionUtil.openSession();CarMapper mapper = session.getMapper(CarMapper.class);Car car =newCar(18L,"5556","兰博基尼六",1952.1,"2060-02-06","跑车");
mapper.update(car);
session.commit();}@TestpublicvoidselectById(){SqlSession session =SqlSessionUtil.openSession();CarMapper mapper = session.getMapper(CarMapper.class);Car car = mapper.selectById(18L);System.out.println(car);}@TestpublicvoidselectAll(){SqlSession session =SqlSessionUtil.openSession();CarMapper mapper = session.getMapper(CarMapper.class);List<Car> cars = mapper.selectAll();
cars.forEach(car ->System.out.println(car));}}
六、MyBatis小技巧
6.1 #{}和${}的区别
- **#{property}**:底层使用PreparedStatement。特点:先进行SQL语句的编译,然后给SQL语句的占位符?传值。可以避免SQL注入的风险
- **${property}**:底层使用Statement。特点:先进行SQL语句的拼接,然后再对SQL语句进行编译。存在SQL注入的风险
6.2 什么时候使用${}
- 如果需要SQL语句的关键字放到SQL语句中,只能使用${},因为#{}是以值的形式放到SQL语句当中的。 例:
<mappernamespace="com.powernode.mybatis.mapper.CarMapper"><selectid="selectByType"resultType="com.powernode.mybatis.pojo.Car">
select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from t_car
order by produce_time ${ascOrDesc}
</select></mapper>
publicclassCarMapperTest{@TestpublicvoidselectAllSortedTest(){SqlSession session =SqlSessionUtil.openSession();List<Car> cars = session.getMapper(CarMapper.class).selectByType("desc");
cars.forEach( car ->System.out.println(car));
session.close();}}
- 拼接表名的时候,比如需要记录日志信息,如果每天都往同一个日志表中存储数据,慢慢的表信息就会越来越多,可以采用加日期分类的方法,直接查询天表可以增加效率,如t_log_20221017这样分类(然后拼接表名就可以采用${}的方法)
- 批量删除,可以写,删除用户123、456、8/7
deletefrom t_car where id in(123,456,789)//用美元括号deletefrom t_car where id in(${})
6.3 查找包含某个关键词str的方法
- concat()函数
<select id="selectLikeSome" resultType="com.powernode.mybatis.pojo.Car">select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from t_car
where car_type like Concat('%',#{str},'%')</select>
- 用双引号把通配符引出去,让#{}在外面好被jdbc检测到(常用)
<select id="selectLikeSome" resultType="com.powernode.mybatis.pojo.Car">select id,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carType
from t_car
where car_type like"%"#{str}"%"</select>
6.4 MyBatis中起别名
namespace不能起别名
所有别名不区分大小写
<typeAliases><!--给type起别名,可以直接用alias名称读取到--><typeAliastype="com.powernode.mybatis.pojo.Car"alias="car"/><!--省略alias,默认就是类简名,比如car--><typeAliastype="com.powernode.mybatis.pojo.Car"/><!--包下所有类自动起别名,不区分大小写--><packagename="com.powernode.mybatis.pojo"/></typeAliases>
6.5 MyBatis小技巧之Mapper的配置
<mappers><!--1. 根路径下查找CarMapper.xml文件--><mapperresource="CarMapper.xml"/><!--2. 绝对路径查找--><mapperurl="file://c:/CarMapper.xml"/><!--3. 查找映射接口同级目录下的Mapper.xml文件--><!-- com/powernode/mybatis/mapper 建包需要这样建--><mapperclass="com.powernode.mybatis.mapper.CarMapper"/><!--最常用:路径下自动查找接口对应名字xml文件--><packagename="com.powernode.mybatis.mapper"/></mappers>
6.6 插入数据时获取自动生成的主键
插入一条数据的时候,自动返回主键到制定属性中
如
useGeneratedKeys=“true” 表示使用自动生成的主键值
keyProperty=“id” 制定属性值赋值给对象的哪个属性
<insertid="insertCar"useGeneratedKeys="true"keyProperty="id">
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values (null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
@TestpublicvoidinsertCar(){SqlSession session =SqlSessionUtil.openSession();Car car =newCar(null,"9851","比亚迪ao",30.0,"2020-10-20","新能源");
session.getMapper(CarMapper.class).insertCar(car);System.out.println(car);
session.close();}
结果:Car{id=22, carNum=‘9851’, brand=‘比亚迪ao’, guidePrice=30.0, produceTime=‘2020-10-20’, carType=‘新能源’}
把自动递增的主键返回给了id
版权归原作者 火柴哟 所有, 如有侵权,请联系我们删除。