本文章包括了:
如何用mybatis写一个简易的web项目,
手写MyBatis底层的GenerateDaoProxy来帮我们动态生成类,
MyBatis配置文件的完整进阶写法,
MyBatis高级映射及延迟加载,
MyBatis和Spring框架结合... ...
MyBatis搭建简易web项目
(1)首先我们先进行数据库表的搭建,如下图
在其中我们存放两条数据,为实现账户之间的转账功能:
(2)按照web项目经典的MVC架构模式创建对应的包,项目结构如下图:
dao层存放和数据库做相关CRUD操作的代码,exception层存放各类异常代码,pojo层存放与数据库表对应的实体类对象,service层存放除了和数据库表之间做CRUD操作之外的其他业务代码,web层存放servlet代码和前端连接,utils层存放各工具类,resources层存放各种配置文件。
(3)在maven中我们引入相关依赖,配置相关配置文件
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ryy</groupId>
<artifactId>MyBatis002-web</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>MyBatis002-web Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<!--logback日志依赖-->
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.14</version>
<scope>test</scope>
</dependency>
<!--servlet依赖-->
<!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>MyBatis002-web</finalName>
</build>
</project>
logback配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义⽇志⽂件的存储地址-->
<property name="LOG_HOME" value="/home"/>
<!-- 控制台输出 -->
<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 %logge
r{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 %logge
r{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>
mybatis核心配置文件:
细节:配置<properties resource="jdbc.properties"/>之后就能将连接数据库的相关数据单独存放到jdbc.properties配置文件中了,而不用都写在mybatis核心配置文件中。
<?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>
(4)然后根据前端表单提交过来的数据先进行servlet层的编写,根据前端代码我们可以看到form表单向后端发送的数据有fromActno,toActno,money三个变量。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行账户转账</title>
</head>
<body>
<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>
我们在web层进行接收并且在web层调用service层的transfer方法完成转账:
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
//为了让这个对象在其他方法中也可以使用,声明为实例变量
private AccountService accountService = new AccountServiceImpl();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取表单数据
String fromActno = req.getParameter("fromActno");
String toActno = req.getParameter("toActno");
double money = Double.parseDouble(req.getParameter("money"));
//调用service的转账方法完成转账
try {
accountService.transfer(fromActno, toActno, money);
//程序走到这里表示转账一定成功了
//调用视图层view展示结果
resp.sendRedirect(req.getContextPath() + "/success.html");
} catch (MoneyNotEnoughException e) {
resp.sendRedirect(req.getContextPath() + "/error1.html");
} catch (TransferException e) {
resp.sendRedirect(req.getContextPath() + "/error2.html");
}
}
}
(5)业务层的编写
在业务层我们需要的是除了和数据库做CRUD相关操作的其他业务代码,转账需求我们需要的是:* 判断转出账户的余额是否充足(select),
* 如果转出账户余额不足,提示用户,
* 如果转出账户余额充足,更新转出账户余额(update),
* 最后更新转入账户余额(update)。
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
//(AccountDao) GenerateDaoProxy.generate(SqlSessionUtil.openSession(), AccountDao.class);
@Override
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
//添加事务控制代码
SqlSession sqlSession = SqlSessionUtil.openSession();
//1,判断转出账户的余额是否充足(select)
Account fromAct = accountDao.selectByActno(fromActno);
if(fromAct.getBalance() < money){
//2,如果转出账户余额不足,提示用户
throw new MoneyNotEnoughException("对不起,余额不足... ...");
}
//3,如果转出账户余额充足,更新转出账户余额(update)
//先更新内存中java对象account的余额,再更新数据库中的数据
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.updateByActno(fromAct);
//4,更新转入账户余额(update)
count += accountDao.updateByActno(toAct);
if (count != 2) {
throw new TransferException("转账异常,原因未知");
}
//提交事务
sqlSession.commit();
//关闭事务
SqlSessionUtil.close(sqlSession);
}
}
这里先更新内存中的account余额原因是:对象状态同步。
在方法内部的操作(如日志记录、异常处理、业务逻辑等)可能会依赖于内存中的对象状态。通过先更新内存对象,可以确保后续代码操作时,拿到的都是最新状态的对象,避免数据不一致的问题。
(6)然后就是来到dao层编写和数据库相关代码,先写接口代码:
import com.ryy.bank.pojo.Account;
/**
* 账户的dao对象,负责t_act表中数据的CRUD
* DAO中的方法就是做CRUD的,所以大部分方法名是:insertXxx,updateXxx,selectXxx
*/
public interface AccountDao {
/**
* 根据账号查询账户信息
* @param actno 账号
* @return 账户信息
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act 被更新的账户对象
* @return 1表示更新成功,其他值表示更新失败
*/
int updateByActno(Account act);
}
Impl实现类:
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account act = (Account)sqlSession.selectOne("selectByActno", actno);
return act;
}
@Override
public int update(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("update", act);
return count;
}
}
Threadlocal
在上面的代码中我们可以看到在dao层中的两个方法中我并没有添加事务提交和关闭相关代码,而是把事务提交和关闭代码放在了service层中。这是因为在编写类似转账一类的业务需求中,我们一定会使用到事务,而如果直接提交事务就会遇到:service层和dao层的SqlSession对象不是同一个,然后就会导致就算程序发生了异常,数据库中的数据依旧会修改。这时我们需要使用到Threadlocal技术来保证一个SqlSession对应一个线程。
注意:事务需要在程序开始时就开启,在程序结束时就提交,因此在service层开始,在转账方法开始时就创建SqlSession对象,在方法最后对SqlSession对象进行提交。
工具类代码如下:
public class SqlSessionUtil {
private SqlSessionUtil(){}
private static SqlSessionFactory sqlSessionFactory;
//mybatis核心配置文件只需要解析一次即可,因此sqlSessionFactory不得被多次创建,放在静态代码块中,类加载时只执行一次
static{
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//全局,服务器级别的,一个服务器当中定义一个即可
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
/**
* 获取会话对象
*/
public static SqlSession openSession(){
SqlSession sqlSession = local.get();
//第一次传入由于是空,因此会创建一个sqlSession对象,后面再调用该方法都认为是同一个线程
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();
//因为tomcat服务器支持线程池,也就是说:用过的线程对象t1,可能下一次还会使用这个t1线程
local.remove();
}
}
}
重点部分:第一次传入由于是空,因此会创建一个sqlSession对象,后面再调用该方法都认为是同一个线程。
//全局,服务器级别的,一个服务器当中定义一个即可
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
/**
* 获取会话对象
*/
public static SqlSession openSession(){
SqlSession sqlSession = local.get();
//第一次传入由于是空,因此会创建一个sqlSession对象,后面再调用该方法都认为是同一个线程
if(sqlSession == null){
sqlSession = sqlSessionFactory.openSession();
//重要:将sqlSession对象绑定到当前线程上
local.set(sqlSession);
}
return sqlSession;
}
(7)最后就是Mapper映射文件的编写,由于转账只需要修改数据库和查询数据库操作,因此只需要<select>和<update>标签,代码如下:
<?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.ryy.bank.dao.AccountDao">
<select id="selectByActno" resultType="com.ryy.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>
总结:相比于传统JavaWeb编写,利用mybatis的优势在于我们不用将sql语句编写在dao层中了,将sql语句写在配置文件中,dao层中只需要做给sql语句赋值的操作,两步分离更易于代码的维护。同时由于mybatis底层帮我们封装好了各种给sql语句赋值的方法了(诸如selectOne,update等),因此无需我们再创建BaseDao写给sql语句赋值的代码了。
** 但是我们还是会觉得这里的dao层的Impl实现类有些多余,里面的代码还是耦合度高,我们可不可以通过一些手段只通过接口就能够动态地帮我们生成dao层的实现类呢?
MyBatis底层动态创建类
使用javassist动态生成类:
mybatis底层为我们提供了能够动态生成dao实现类的api,这里我们来研究一下它的底层是如何做的。
这里我们需要知道一个技术叫javassist,这项技术就是能够帮我们动态生成类的核心,这项技术里面有重要的5步:
(1)获取类池,这个类池就是用来给我生成Class的
ClassPool pool = ClassPool.getDefault();
(2)制造类(这里需要告诉javassist类名是什么,以刚刚web项目中的AccountDaoImpl举例)
CtClass ctClass = pool.makeClass("com.ryy.bank.dao.Impl.AccountDaoImpl");
(3)制造接口(需要指定接口)
CtClass ctInterface = pool.makeInterface("com.ryy.bank.dao.AccountDao");
(4)实现接口(添加接口到类中)
ctClass.addInterface(ctInterface);
(5)制造方法
String methodCode = "public void insert(){sout(123);}";
CtMethod ctMethod = CtMethod.make(methodCode, ctClass); //这里的两个参数分别是方法的代码 和 将方法要放入的目标类(即制造的类)
(6)将方法添加到类中
CtClass.addMethod(ctMethod);
(7)实现接口中的方法
Class<?> clazz = ctClass.toClass();
AccountDao accountDao = (AccountDao) clazz.newInstance(); //强制转换为接口类型
(8)然后接口就能够直接用实现类下的实现方法了
accountDao.delete();
正式编写GenerateDaoProxy工具类:
首先我们还是先将javassist的框架先搭好:(即上面提到的8步)
/**
* 工具类:可以动态生成DAO的实现类(或者说可以动态生成dao的代理类)
*/
public class GenerateDaoProxy {
/**
* 生成dao接口实现类,并且将实现类的对象创建出来并返回
*
* @param daoInterface dao接口
* @return dao接口实现类的实例化对象
*/
public static Object generate(SqlSession sqlSession, Class daoInterface) {
//类池
ClassPool pool = ClassPool.getDefault();
//制造类
CtClass ctClass = pool.makeClass("com.ryy.bank.dao.Impl.AccountDaoImpl");
//制造接口
CtClass ctInterface = pool.makeInterface("com.ryy.bank.dao.AccountDao");
//实现接口
ctClass.addInterface(ctInterface);
//实现接口中的所有方法(先通过反射获取接口中的所有方法)
Method[] methods = AccountDao.class.getDeclaredMethods();
//创建对象
Object obj = null;
try {
Class<?> clazz = ctClass.toClass();
obj = clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
return obj;
}
}
然后我们就需要编写核心的代码,即方法的拼接,我们既然需要动态生成类,那就一定要让其动态生成方法,而要动态的获取方法就需要使用到反射技术来获取方法的所有内容,再通过字符串的拼接来构成make方法的第一个参数,我们这里先对除了方法的实现之外的代码进行字符串拼接:
相关注释已在代码上添加,我们在拼接的时候可以用一个示例对照着写,如:
public Account selectByActno(String arg0, int arg1, int arg2){代码}
Arrays.stream(methods).forEach(method -> {
//method是抽象方法,将这个抽象方法进行实现
try {
//然后开始拼接字符串,传给make方法的第一个参数
StringBuilder methodCode = new StringBuilder();
//Account selectByActno(String actno);
//public Account selectByActno(String arg0, int arg1, int arg2){代码}
methodCode.append("public "); //追加修饰符列表
methodCode.append(method.getReturnType().getName()); //追加返回值类型(Account, int, void... ...)
methodCode.append(" ");
methodCode.append(method.getName()); //追加方法名
methodCode.append("(");
//以下开始拼接参数
Class<?>[] parameterTypes = method.getParameterTypes(); //获取参数类型
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
methodCode.append(parameterType.getName()); //参数的类型
methodCode.append(" ");
methodCode.append("arg" + i); //追加的参数名,用数组下标来防止参数名重复
if (i != parameterTypes.length - 1) {
methodCode.append(","); //如果不是最后一个参数就要加逗号
}
}
methodCode.append(")");
//编写大括号中的代码
methodCode.append("{");
methodCode.append("}");
CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (CannotCompileException e) {
throw new RuntimeException(e);
}
});
接下来就是难点,如何去编写大括号中的代码。我们还是先把原先dao层实现类的代码搬过来对照一下:
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account act = (Account)sqlSession.selectOne("selectByActno", actno);
return act;
}
(1)我们可以看到第一句代码是:SqlSession sqlSession = SqlSessionUtil.openSession(); 直接进行字符串的拼接:
//这里需要注意使用全限定类名才能被javassist识别出来
methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = com.ryy.bank.utils.SqlSessionUtil.openSession();");
(2)SqlSession应该执行哪些CRUD操作?
注意这里在工具类中的方法需要再传入一个参数SqlSession才能知道是执行insert还是update还是... ...
SqlCommandType是一个枚举类,存放了SqlSession中执行sql语句方法类型。
SqlCommandType sqlCommandType=sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
getMappedStatement中需要一个sqlId,只要获取了sqlId就能获取对应的sql标签名。sqlId是框架使用者提供的,框架开发者无法获取到这个id,mybatis框架开发者就出台了规定:凡是使用GenerateDaoProxy机制的,sqlId都不能随便写,必须按照如下要求去写:
namespace必须是dao接口的全限定类名
id必须是dao接口中的方法名
因此在执行以上那段代码之前还应该添加如下代码通过反射拿到接口类名和方法名:
String sqlId = daoInterface.getName() + "." + method.getName();
(3)然后进行动态判断,这里只做update和select语句的演示,然后拼接原先AccountDaoImpl中的方法代码,一共两个参数,一个sqlId,一个是接口的参数。
if (sqlCommandType == SqlCommandType.UPDATE) {
methodCode.append("return sqlSession.update(\"" + sqlId + "\", arg0);");
}
if (sqlCommandType == SqlCommandType.SELECT) {
String returnType = method.getReturnType().getName();
methodCode.append("return (" + returnType + ")sqlSession.selectOne(\"" + sqlId + "\", arg0);");
}
** 在这里我在观看视频的时候也遇到了一个疑问,就是这里接口的参数如果不止有一个该怎么办,我也是通过代码自己尝试着用遍历参数的方法实现了一下多参数情况:(举例)
public interface UserDao {
// 假设这是一个查询用户信息的方法,需要两个参数:用户名和用户年龄
User selectUserByNameAndAge(String username, int age);
}
所要添加和修改的代码:
// 根据SqlCommandType生成不同的代码
methodStr.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();");
if ("SELECT".equals(sqlCommandTypeName)) {
methodStr.append("Object obj = sqlSession.selectOne(\"" + sqlId + "\"");
} else if ("UPDATE".equals(sqlCommandTypeName)) {
methodStr.append("int count = sqlSession.update(\"" + sqlId + "\"");
}
// 添加所有参数
if (parameterTypes.length > 0) {
methodStr.append(", ");
for (int i = 0; i < parameterTypes.length; i++) {
methodStr.append("arg").append(i);
if (i != parameterTypes.length - 1) {
methodStr.append(", ");
}
}
}
methodStr.append(");");
// 返回类型处理
if ("SELECT".equals(sqlCommandTypeName)) {
methodStr.append("return (" + returnTypeName + ")obj;");
} else if ("UPDATE".equals(sqlCommandTypeName)) {
methodStr.append("return count;");
}
methodStr.append("}");
核心思路:和之前在遍历方法需要的参数时的方式一样,这里只需要检测如果参数大于1就在后面追加arg1,arg2...,然后将这些arg0,arg1等追加到字符串的后面,就能解决多参数的情况了。
总结:mybatis中实际采用了代理机制,在内存中生成dao接口的代理类,然后创建代理类的实例。
GenerateDaoProxy实际使用
在上面我们了解了GenerateDaoProxy底层运行逻辑之后,我们一定会问这在实际应用中有什么用?怎么用?
以刚才的web项目为例,我们在服务层关联dao层的时候就不需要再new AccountDaoImpl();了,只需要调用工具类中的generate方法就能够帮我们动态生成dao接口的Impl实现类,代码如下:
private AccountDao accountDao = (AccountDao) GenerateDaoProxy.generate(SqlSessionUtil.openSession(), AccountDao.class);
而在mybatis底层是无需我们去写这么大段的工具类的,底层已经为我们封装好了一个api能够直接动态生成Impl实现类,写法如下:
private AccountDao accountDao = SqlSessionUtil.openSession().getMapper(AccountDao.class); //这里的参数是接口对象
然后我们就可以使用accountDao去调用接口中的各个方法就能实现对数据库表的操作了。
MyBatis配置文件的完整写法
(1)<typeAliases>起别名标签
<typeAliases>中有两个重要的属性:type是指定给哪个类型起别名,alias是指定别名
例如:type="com.ryy.mybatis.pojo.Car" alias="aaa" 这样在<select>标签中的resultType属性值就能直接写为aaa,不区分大小写。
(2)<package>标签
在<mappers>标签下写<package name="com.ryy.mybatis.mapper"/>,为什么要这样写呢?
这样写的前提条件是Mapper的映射配置文件所在目录和其对应的接口所在的目录相同,即这边的name属性指定的是接口所在目录和Mapper映射文件所在目录。这样的指定能使mybatis框架自动去com/ryy/mybatis/mapper目录下查找CarMapper.xml映射文件。
注意:Mapper映射文件所在目录的写法不能用com.xxx.,而是要用com/xxx/...。
(3)@param注解
如果我们创建的接口中有多个参数的情况,例如:List<Student> selectBynameAndSex(String name, Character sex); 然后mybatis底层会给我们生成一个Map集合,以下为存储方式:
map.put("arg0", name); map.put("arg1", sex);
map.put("param1", name) map.put("param2", sex);
然后对于sql语句就是:(arg0和arg1就是底层map的key)
select * from t_student where name=#{arg0} and sex=#{arg1}
那我们用这种默认的arg0,arg1写sql语句未来一定会搞混,那我们可以使用@param注解来指定我们的map集合的key:
List<Student> selectBynameAndSex(@param("name")String name, @param("sex")Character sex);
这样指定之后我们就不需要再用arg0,arg1了,而是用name和sex。底层原理如下图:
创建一个数组用来存放目标方法的参数。
创建第一个map集合用来存放创建的变量名(name,sex) ,key就为数字。
创建第二个map集合用来存放刚才数组中的参数,而如何关联到参数就是通过数组的下标获取,而数组的下标又可以通过第一个集合的key来获取,这样就构成了arg[0],arg[1],即取到了“张三”“男”。而第二个集合的key又是通过第一个集合的value值获取,这样就实现了自己定义的参数名与参数值对应的需求。
(4)resultMap结果映射
查询结果的列名和java对象的属性名对应不上怎么办?我们在之前使用的是用as给列名起别名的方式(查询多个字段),现在我们可以使用resultMap进行结果映射。
** 专门定义一个结果映射,在这个结果映射中指定数据库表的字段名和java类属性名对应关系。
<resultMap id="carResultMap" type="Car">
type属性:用来指定POJO类的类名
id属性:指定resultMap的唯一标识,这个id将来要在<select>标签中使用
<result property="" column=""/>
property后面填写POJO类的属性名
column后面填写数据库表的字段名
<id property="id" column="id"/>
数据库表中有主键id,这里再配一个id标签提高mybatis执行效率
然后来到<select>标签中就不是resultType了,而是resultMap:
<select id="" resultMap="carResultMap">
MyBatis高级映射及延迟加载
案例:数据存储在两张表内,数据和数据之间还存在关系,该如何映射到Java对象?(这里我们以多对一的情景举例)
例如:有一个班级表和学生表,学生表有外键,多个学生对应一个班级。
** 主表和副表的原则:谁在前谁是主表,例如多对一原则,多在前,多就是主表。按例子来看主表是t_student,那JVM中的主对象也是Student对象。
解决方案:我们可以在学生类中再创建一个班级类属性:private Clazz clazz;这样就能通过学生对象对应到相应的班级对象(关联关系)。
高级映射
(1)一条sql语句,级联属性映射
接口中:
Student selectById(Integer id); //根据id获取学生信息及学生关联的班级信息
Mapper映射文件中:
<resultMap id="studentResultMap" type="Student">
<result property="sname" column="sname"/>
<result property="clazz.cid" column="cid"/> //这里需要关联到clazz表的cid
</resultMap>
SQL语句(联合查询):
<select id="selectById" resultMap="studentResultMap">
select s.sid, s.sname, c.cid, c.cname //对应上面的column属性
from
t_stu s left join t_clazz c on s.cid=c.cid //left join表示左外连接,s是t_stu的别名,最后s.cid=c.cid为表的连接条件
where
s.sid=#{sid}
</select>
(2)一条sql语句,association
SQL语句和接口中的方法都不变,只有<resultmap>要改动,在其中加上:
<association property="clazz" javaType="Clazz">
property:提供要映射的POJO类的属性名
javaType:用来指定要映射的java类型
(3)两条SQL语句,分步查询
第一步:现根据学生的sid查询学生信息,包含班级编号(即外键)
sql语句:select sid,sname,cid from t_stu where sid=#{sid}
第二步:配置<resultMap>,依旧要在其中配置<association>标签
<association property="clazz" select="这里需要指定另外第二个SQL语句的ID(包括路径名)" column="cid"(外键名)/>
第三步:写第二条SQL语句(ClazzMapper)
在CalzzMapper接口中写根据有cid查询班级信息的方法
select cid,cname from t_clazz where cid=#{cid}
延迟加载
以上第三种方法写了两条sql语句,而为了提高程序性能,延迟加载的核心原理就是用的时候再执行查询语句,不用的时候不查询。
一般我们会设置为全局开启延迟加载,在核心配置文件中配置:
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
如果开启了全局延迟加载,但有一个分布查询不需要延迟加载可以配置:fetchType="eager"。
一对多和多对一的思路差不多,只不过是在Clazz类中关联Student类。然后要用用集合或数组来容纳多个学生对象:List<Student> stus;
MyBatis和Spring框架结合
我们在这里依旧以之前的web项目的银行转账为例子展开,项目结构如下:
需要引入的依赖(注意一定要按照对应的版本进行依赖引入,不然就会出现兼容性报错)
<?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.ryy</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
在这里需要改动的是mybatis核心配置文件,数据源的配置,起别名,Mapper映射文件等工作就全部交给Spring容器进行管理了:
<?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>
<!--帮助我们打印mybatis的日志信息,sql语句等-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>
Spring.xml配置文件的编写,分为以下步骤:
组件扫描
引入外部的属性文件
配置数据源
SqlSessionFactoryBean配置:注⼊mybatis核⼼配置⽂件路径,指定别名包,注⼊数据源。
Mapper扫描配置器:指定扫描的包。
事务管理器DataSourceTransactionManager:注⼊数据源。
启⽤事务注解:注⼊事务管理器。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--组件扫描-->
<context:component-scan base-package="com.ryy.bank"/>
<!--引入外部的配置文件-->
<context:property-placeholder location="jdbc.properties"/>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--配置SqlSessionFactoryBean-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据源-->
<property name="dataSource" ref="dataSource"/>
<!--指定mybatis核心配置文件-->
<property name="configLocation" value="mybatis-config.xml"/>
<!--指定别名-->
<property name="typeAliasesPackage" value="com.ryy.bank.pojo"/>
</bean>
<!--Mapper扫描配置器,主要扫描Mapper接口,生成代理类-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--指定包名来扫描接口-->
<property name="basePackage" value="com.ryy.bank.mapper"/>
</bean>
<!--事务管理器-->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--启用事务注解-->
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
最后我们编写测试程序:
import com.ryy.bank.service.AccountService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SMTest {
@Test
public void testSM(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("Spring.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
try{
accountService.transfer("act001", "act002", 10000);
} catch (Exception e){
e.printStackTrace();
}
}
}