MyBatis自学笔记
首先感谢动力节点和杜老师的教学分享!Respect!
学习来源:B站
https://www.bilibili.com/video/BV1JP4y1Z73S?p=1&vd_source=07c8a1a7d89af39fe20c3a6894f5ff6a
资料来源:百度网盘
链接:https://pan.baidu.com/s/1pB6AONoK-2yS7OjHQ6YW3w?pwd=o8w7
提取码:o8w7
文章目录
- MyBatis自学笔记
- 六、在WEB中应用MyBatis(使用MVC架构模式)
- 七、使用javassist生成类
- 八、MyBatis中接口代理机制及使用
- 九、MyBatis小技巧
- 十、MyBatis参数处理
- 十一、MyBatis查询语句专题
- 十二、动态SQL
- 十三、MyBatis的高级映射及延迟加载
- 十四、MyBatis的缓存
- 十五、MyBatis的逆向工程
- 十六、MyBatis使用PageHelper
- 十七、MyBatis的注解式开发
- 五、手写MyBatis框架(掌握原理)
- 5.1 dom4j解析XML文件
- 5.2 GodBatis
- 第一步:IDEA中创建模块
- 第二步:资源工具类,方便获取指向配置文件的输入流
- 第三步:定义SqlSessionFactoryBuilder类
- 第四步:分析SqlSessionFactory类中有哪些属性
- 第五步:定义GodJDBCTransaction
- 第六步:事务管理器中需要数据源,定义GodUNPOOLEDDataSource
- 第七步:定义GodMappedStatement
- 第八步:完善SqlSessionFactory类
- 第九步:完善SqlSessionFactoryBuilder中的build方法
- 第十步:在SqlSessionFactory中添加openSession方法
- 第十一步:编写SqlSession类中commit rollback close方法
- 第十二步:编写SqlSession类中的insert方法
- 第十三步:编写SqlSession类中的selectOne方法
- 5.3 GodBatis使用Maven打包
- 5.4 使用GodBatis
六、在WEB中应用MyBatis(使用MVC架构模式)
目标:
- 掌握mybatis在web应用中怎么用
- mybatis三大对象的作用域和生命周期
- ThreadLocal原理及使用
- 巩固MVC架构模式
- 为学习MyBatis的接口代理机制做准备
实现功能:
- 银行账户转账
使用技术:
- HTML + Servlet + MyBatis
WEB应用的名称:
- bank
6.1 需求描述
6.2 数据库表的设计和准备数据
6.3 实现步骤
第一步:环境搭建
- IDEA中创建Maven WEB应用(mybatis-004-web)
- IDEA配置Tomcat,这⾥Tomcat使用10+版本。并部署应用到tomcat。
-
默认创建的maven web应用没有java和resources目录,包括两种解决方案
- 第一种:自己手动加上。
- 第二种:修改maven-archetype-webapp-1.4.jar中的配置文件
-
web.xml文件的版本较低,可以从tomcat10的样例文件中复制,然后修改
- 注:(本人采用的是java 1.8.261, tomcat 8.5.83)
<?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_3_1.xsd"
version="3.1"
metadata-complete="false">
<!--
metadata-complete属性默认为true,
如果需要利用注解开发需要手动设置为false
设置为false之后,既可以用原来的servlet方法,也可以使用注解式方法
-->
<!-- <servlet>-->
<!-- <servlet-name>AccountServlet</servlet-name>-->
<!-- <servlet-class>com.powernode.bank.web.AccountServlet</servlet-class>-->
<!-- </servlet>-->
<!-- <servlet-mapping>-->
<!-- <servlet-name>AccountServlet</servlet-name>-->
<!-- <url-pattern>/bank/transfer</url-pattern>-->
<!-- </servlet-mapping>-->
</web-app>
- 删除index.jsp文件,因为我们这个项目不使用JSP。只使用html。
- 确定pom.xml文件中的打包方式是war包。
- 引入相关依赖
- 编译器版本修改为17(本人使用的是1.8)
- 引入的依赖包括:mybatis,mysql驱动,junit,logback,servlet。
<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.powernode</groupId>
<artifactId>mybatis-004-web</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>mybatis-004-web Maven Webapp</name>
<url>http://maven.apache.org</url>
<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>
<!--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>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>mybatis-004-web</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
-
引入相关配置文件,放到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="powernode"> <environment id="powernode"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/powernode"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="AccountMapper.xml"/> </mappers> </configuration>
-
AccountMapper.xml
-
logback.xml
-
jdbc.properties
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>
- MoneyNotEnoughException.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行转账报告</title>
</head>
<body>
<h1>转账失败,余额不足!</h1>
</body>
</html>
- TransferException.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行转账报告</title>
</head>
<body>
<h1>转账失败,未知错误!</h1>
</body>
</html>
- TransferSuccess.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行转账报告</title>
</head>
<body>
<h1>转账成功!</h1>
</body>
</html>
第三步:创建pojo包、service包、dao包、web包、utils包
- com.powernode.bank.pojo
- com.powernode.bank.service
- com.powernode.bank.service.impl
- com.powernode.bank.dao
- com.powernode.bank.dao.impl
- com.powernode.bank.web.controller
- com.powernode.bank.exception
- com.powernode.bank.utils:将之前编写的SqlSessionUtil工具类拷贝到该包下。
第四步:定义pojo类:Account
package com.powernode.bank.pojo;
/**
* 银行账户类
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
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;
}
}
第五步:编写AccountDao接口,以及AccountDaoImpl实现类
分析dao中⾄少要提供几个方法,才能完成转账:
- 转账前需要查询余额是否充足:selectByActno
- 转账时要更新账户:update
package com.powernode.bank.dao;
import com.powernode.bank.pojo.Account;
/**
* 账户数据访问对象
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public interface AccountDao {
/**
* 根据账号获取账户信息
* @param actno 账号
* @return Account 账户信息
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act 账户信息
* @return 1表示更新成功,其他表示失败
*/
int updateByActno(Account act);
}
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountDaoImpl implements AccountDao {
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account act = (Account) sqlSession.selectOne("account.selectByActno", actno);
sqlSession.close();
return act;
}
public int updateByActno(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.updateByActno", act);
sqlSession.commit();
sqlSession.close();
return count;
}
}
第六步:AccountDaoImpl中编写了mybatis代码,需要编写SQL映射文件了
<?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.powernode.bank.dao.AccountDao">
<select id="selectByActno" resultType="com.powernode.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>
第七步:编写AccountService接口以及AccountServiceImpl
-
自定义异常类
- MoneyNotEnoughException
package com.powernode.bank.exception; /** * 余额不足异常 */ public class MoneyNotEnoughException extends Exception{ public void MoneyNotEnoughException(){} public MoneyNotEnoughException(String msg) { super(msg); } }
- TransferException
package com.powernode.bank.exception; /** * 转账失败异常 */ public class TransferException extends Exception{ public TransferException() { } public TransferException(String message) { super(message); } }
-
AccountService接口以及AccountServiceImpl
package com.powernode.bank.service;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.exception.TransferException;
/**
* 注意:业务类当中的业务方法的名字在起名的时候,最好见名知意,能够体现出具体的业务是做什么的。
* 账户业务类
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public interface AccountService {
/**
* 账户转账业务
* @param fromActno 转出账号
* @param toActno 转入账号
* @param money 转账金额
*/
void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException;
}
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.exception.TransferException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.GenerateDaoProxy;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountServiceImpl implements AccountService {
// 声明dao层的实现类
private AccountDao accountDaoImpl = new AccountDaoImpl();
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
// 1. 判断转出账户的余额是否充足(select)
Account fromAct = accountDaoImpl.selectByActno(fromActno);
// 2. 如果转出账户余额不足,提示用户
if (fromAct.getBalance() < money) {
throw new MoneyNotEnoughException("余额不足,转账失败!");
}
// 3. 如果转出账户余额充足,更新转出账户余额(update)
fromAct.setBalance(fromAct.getBalance() - money);
int count = accountDaoImpl.updateByActno(fromact);
// 4. 更新转入账户余额(update)
Account toAct = accountDaoImpl.selectByActno(toActno);
toAct.setBalance(toAct.getBalance() + money);
count += accountDaoImpl.updateByActno(toAct);
if(2 != count){
throw new TransferException("未知错误,转账失败!");
}
}
}
第八步:编写AccountController
package com.powernode.bank.web;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.exception.TransferException;
import com.powernode.bank.service.AccountService;
import com.powernode.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 {
// 获取form表单信息
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() + "/TransferSuccess.html");
} catch (MoneyNotEnoughException e) {
// 余额不足页面
response.sendRedirect(request.getContextPath() + "/MoneyNotEnoughException.html");
} catch (TransferException e) {
// 转账异常页面
response.sendRedirect(request.getContextPath() + "/TransferException.html");
} catch (Exception e){
// 转账异常页面
response.sendRedirect(request.getContextPath() + "/TransferException.html");
}
}
}
- 启动服务器,打开浏览器,输入地址:http://localhost:8080/bank,测试:
6.4 MyBatis对象作用域以及事务问题
MyBatis核心对象的作用域
-
SqlSessionFactoryBuilder
-
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
-
你可以重用SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有 的 XML 解析资源可以被释放给更重要的事情。
-
-
SqlSessionFactory
- SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。
- 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。
- 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
-
SqlSession
- 每个线程都应该有它自己的 SqlSession 实例。
- SqlSession 的实例不是线程安全的,因此是不能被共享的,所以 它的最佳的作用域是请求或方法作用域。
- 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚⾄一个类 的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。
- 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域 中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操 作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。
- 下面的示例就是一个确 保 SqlSession 关闭的标准模式:
try(SqlSession sqlSession = sqlSessionFactory.openSession()){ // 应用逻辑代码 }
事务问题
- 在之前的转账业务中,更新了两个账户,我们需要保证它们的同时成功或同时失败,这个时候就需要使用事务机 制,在transfer方法开始执行时开启事务,直到两个更新都成功之后,再提交事务,我们尝试将transfer方法进行如下修改(在两次更新操作中添加异常):
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.exception.TransferException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.GenerateDaoProxy;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountServiceImpl implements AccountService {
// 声明dao层的实现类
private AccountDao accountDaoImpl = new AccountDaoImpl();
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
// 1. 判断转出账户的余额是否充足(select)
Account fromAct = accountDaoImpl.selectByActno(fromActno);
// 2. 如果转出账户余额不足,提示用户
if (fromAct.getBalance() < money) {
throw new MoneyNotEnoughException("余额不足,转账失败!");
}
// 3. 如果转出账户余额充足,更新转出账户余额(update)
fromAct.setBalance(fromAct.getBalance() - money);
int count = accountDaoImpl.updateByActno(fromact);
// 模拟异常
String exception = null;
exception.toString();
// 4. 更新转入账户余额(update)
Account toAct = accountDaoImpl.selectByActno(toActno);
toAct.setBalance(toAct.getBalance() + money);
count += accountDaoImpl.updateByActno(toAct);
if(2 != count){
throw new TransferException("未知错误,转账失败!");
}
}
}
- 运行前数据库数据:
- 运行结果:
事务出问题了,转账失败了,钱仍然是少了1万。这是什么原因呢?主要是因为service 和dao中使用的SqlSession对象不是同一个。
- 怎么办?为了保证service和dao中使用的SqlSession对象是同一个,可以将SqlSession对象存放到
ThreadLocal当中。修改SqlSessionUtil工具类:
package com.powernode.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;
/**
* MyBatis工具类
* @author Shanglinsong
* @version 1.0
* @sine 1.0
*/
public class SqlSessionUtil {
// 工具类的构造方法一般都是私有化的。
// 工具类中所有的方法都是静态的,直接采用类名调用即可,不需要new对象。
// 为了防止new对象,构造方法私有化
private SqlSessionUtil() {
}
private static SqlSessionFactory sqlSessionFactory;
// 类加载时执行
// SqlSessionUtil工具类在进行第一次加载的时候,解析mybatis-config.xml文件,创建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<SqlSession>();
/**
* 获取SqlSession对象
* @return SqlSession 会话对象
*/
public static SqlSession openSession(){
SqlSession sqlSession = local.get();
if (sqlSession == null) {
sqlSession = sqlSessionFactory.openSession();
// 将sqlSession对象绑定到当前线程上。
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭会话,释放资源,同时与当前线程解绑
* @param sqlSession 当前线程使用的会话
*/
public static void close(SqlSession sqlSession){
if (sqlSession != null) {
sqlSession.close();
// 注意移除SqlSession对象与当前线程的绑定关系。
// 因为Tomcat服务器支持线程池。也就是说:用过的线程对象t1,可能下一次还会使用这个t1线程
local.remove();
}
}
}
- 修改dao中的方法:AccountDaoImpl中所有方法中的提交commit和关闭close代码全部删除:
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountDaoImpl implements AccountDao {
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
// Account act = (Account) sqlSession.selectOne("account.selectByActno", actno);
// sqlSession.close();
// return act;
return (Account) sqlSession.selectOne("account.selectByActno", actno);
}
public int updateByActno(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
// int count = sqlSession.update("account.updateByActno", act);
// sqlSession.commit();
// sqlSession.close();
// return count;
return sqlSession.update("account.updateByActno", act);
}
}
- 修改service中的方法:
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
//import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.exception.TransferException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.GenerateDaoProxy;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountServiceImpl implements AccountService {
// 声明dao层的实现类
private AccountDao accountDaoImpl = new AccountDaoImpl();
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
// 开启会话
SqlSession sqlSession = SqlSessionUtil.openSession();
// 1. 判断转出账户的余额是否充足(select)
Account fromAct = accountDaoImpl.selectByActno(fromActno);
// 2. 如果转出账户余额不足,提示用户
if (fromAct.getBalance() < money) {
throw new MoneyNotEnoughException("余额不足,转账失败!");
}
// 3. 如果转出账户余额充足,更新转出账户余额(update)
fromAct.setBalance(fromAct.getBalance() - money);
int count = accountDaoImpl.updateByActno(fromact);
// 4. 更新转入账户余额(update)
Account toAct = accountDaoImpl.selectByActno(toActno);
toAct.setBalance(toAct.getBalance() + money);
count += accountDaoImpl.updateByActno(toAct);
if(2 != count){
throw new TransferException("未知错误,转账失败!");
}
// 提交事务
sqlSession.commit();
// 关闭资源
SqlSessionUtil.close(sqlSession);
}
}
- 运行前数据库中的数据:
- 运行结果:
查看数据库数据没有变化:
去除异常查看功能是否可以实现:
- 余额不足的情况:
6.5 分析当前程序存在的问题
- 来看一下DaoImpl的代码
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountDaoImpl implements AccountDao {
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
return (Account) sqlSession.selectOne("account.selectByActno", actno);
}
public int updateByActno(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
return sqlSession.update("account.updateByActno", act);
}
}
-
我们不难发现,这个dao实现类中的方法代码很固定,基本上就是一行代码,通过SqlSession对象调用 insert、delete、update、select等方法,这个类中的方法没有任何业务逻辑,既然是这样,这个类我们能不能动态的生成,以后可以不写这个类吗?
答案:可以。
七、使用javassist生成类
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学 系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用 Javassist对字节码操作为JBoss实现动态"AOP"框架
7.1 Javassist的使用
- 我们要使用javassist,首先要引入它的依赖
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.21.0-GA</version>
</dependency>
- 样例代码:
package com.powernode.javassist.test;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
public class JavassistTest {
public static void main(String[] args) throws Exception{
// 获取类池
ClassPool pool = ClassPool.getDefault();
// 创建类
CtClass ctClass = pool.makeClass("com.powernode.javassist.Test");
// 创建方法
// 1.返回值类型 2.方法名 3.形式参数列表 4.所属类
CtMethod ctMethod = new CtMethod(CtClass.voidType, "execute", new CtClass[]{}, ctClass);
// 设置方法的修饰符列表
ctMethod.setModifiers(Modifier.PUBLIC);
// 设置方法体
ctMethod.setBody("{System.out.println(\"Hello world!\");}");
// 给类添加方法
ctClass.addMethod(ctMethod);
// 调用方法
Class aClass = ctClass.toClass();
Object o = aClass.newInstance();
Method execute = aClass.getDeclaredMethod("execute");
execute.invoke(o);
}
}
-
运行要注意:加两个参数,要不然会有异常。(本人jdk1.8没配置未出现异常)
- –add-opens java.base/java.lang=ALL-UNNAMED
- –add-opens java.base/sun.net.util=ALL-UNNAMED
- 执行结果:
-
课堂测试:
- dao层内容:
package com.powernode.dao; public interface AccountDao { void delete(); int insert(String actno); int update(String actno, Double balance); String selectByActno(String actno); }
- 测试一:testGenerateFirstClass
@Test public void testGenerateFirstClass() throws Exception{ // 获取类池,这个类池就是用来给我生成class的 ClassPool pool = ClassPool.getDefault(); // 制造类(需要告诉javassist,类名是啥) CtClass ctClass = pool.makeClass("com.powernode.dao.impl.AccountDaoImpl"); // 制造方法 String methodCode = "public void insert(){System.out.println(123);}"; CtMethod ctMethod = CtMethod.make(methodCode, ctClass); // 将方法添加到类中 ctClass.addMethod(ctMethod); // 在内存中生成class ctClass.toClass(); // 类加载到JVM当中,返回AccountDaoImpl类的字节码 Class<?> clazz = Class.forName("com.powernode.dao.impl.AccountDaoImpl"); // 创建对象 Object obj = clazz.newInstance(); // 获取AccountDaoImpl中的insert方法 Method insertMethod = clazz.getDeclaredMethod("insert"); // 调用方法insert insertMethod.invoke(obj); }
运行结果:
- 测试二:testGenerateImpl
@Test
public void testGenerateImpl() throws Exception{
// 获取类池
ClassPool pool = ClassPool.getDefault();
// 制造类
CtClass ctClass = pool.makeClass("com.powernode.dao.impl.AccountDaoImpl");
// 制造接口
CtClass ctInterface = pool.makeInterface("com.powernode.dao.AccountDao");
// 添加接口到类中
ctClass.addInterface(ctInterface); // AccountDaoImpl implements AccountDao
// 实现接口中的方法
// 制造方法
CtMethod ctMethod = CtMethod.make("public void delete(){System.out.println(\"hello delete!\");}", ctClass);
// 将方法添加到类中
ctClass.addMethod(ctMethod);
// 在内存中生成类,同时将生成的类加载到JVM当中。
Class<?> clazz = ctClass.toClass();
AccountDao accountDao = (AccountDao)clazz.newInstance();
accountDao.delete();
}
执行结果:
- 测试三:testGenerateAccountDaoImpl
@Test
public void testGenerateAccountDaoImpl() throws Exception{
// 获取类池
ClassPool pool = ClassPool.getDefault();
// 制造类
CtClass ctClass = pool.makeClass("com.powernode.impl.AccountDaoImpl");
// 制造接口
CtClass ctInterface = pool.makeInterface("com.powernode.dao.AccountDao");
// 实现接口
ctClass.addInterface(ctInterface);
// 实现接口中所有的方法
// 获取接口中所有的方法
Method[] methods = AccountDao.class.getDeclaredMethods();
Arrays.stream(methods).forEach(method -> {
// method是接口中的抽象方法
// 把method抽象方法给实现了。
try {
// public void delete(){}
// public int update(String actno, Double balance){}
StringBuilder methodCode = new StringBuilder();
methodCode.append("public "); // 追加修饰符列表
methodCode.append(method.getReturnType().getName()); // 追加返回值类型
methodCode.append(" ");
methodCode.append(method.getName()); //追加方法名
methodCode.append("(");
// 拼接参数 String actno, Double balance
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("){System.out.println(11111); ");
// 动态的添加return语句
String returnTypeSimpleName = method.getReturnType().getSimpleName();
if ("void".equals(returnTypeSimpleName)) {
}else if("int".equals(returnTypeSimpleName)){
methodCode.append("return 1;");
}else if("String".equals(returnTypeSimpleName)){
methodCode.append("return \"hello\";");
}
methodCode.append("}");
System.out.println(methodCode);
CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
// 在内存中生成class,并且加载到JVM当中
Class<?> clazz = ctClass.toClass();
// 创建对象
AccountDao accountDao = (AccountDao) clazz.newInstance();
// 调用方法
accountDao.insert("aaaaa");
accountDao.delete();
accountDao.update("aaaa", 1000.0);
accountDao.selectByActno("aaaa");
}
执行结果:
7.2 使用Javassist生成Proxy类(module以第六章为基础)
- GenerateDaoProxy工具类
package com.powernode.bank.utils;
import com.powernode.bank.dao.AccountDao;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* 工具类:可以动态生成DAO实现类。(或者说可以动态生成DAO的代理类)
* 注意!!!!非常重要!!!
* 凡是使用GenerateDaoProxy的,SQLMapper.xml映射文件中的namespace必须是dao接口的全名,id必须是dao接口中的方法名。
*
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public class GenerateDaoProxy {
/**
* 生成dao接口实现类,并且将累的对象创建出来并返回
*
* @param sqlSession
* @param daoInterface dao接口
* @return dao接口实现类的实例化对象
*/
public static Object generate(SqlSession sqlSession, Class daoInterface) {
// 类池
ClassPool pool = ClassPool.getDefault();
// 制造类(com.powernode.bank.dao.AccountDao --> com.powernode.bank.dao.AccountDaoProxy)
CtClass ctClass = pool.makeClass(daoInterface.getName() + "Proxy");
// 制造接口
CtClass ctInterface = pool.makeInterface(daoInterface.getName());
// 实现接口
ctClass.addInterface(ctInterface);
// 实现接口的所有
Method[] methods = daoInterface.getDeclaredMethods();
Arrays.stream(methods).forEach(method -> {
// method是这个接口中的抽象方法
// 将method这个抽象方法进行实现
try {
// Account selectByActno(String actno);
// public Account selectByActno(String actno){代码};
StringBuilder methodCode = new StringBuilder();
methodCode.append("public ");
methodCode.append(method.getReturnType().getName());
methodCode.append(" ");
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(" ");
methodCode.append("arg" + i);
if (i < parameterTypes.length-1) {
methodCode.append(", ");
}
}
methodCode.append("){\n");
// 需要方法体当中的代码
methodCode.append("org.apache.ibatis.session.SqlSession sqlSession = com.powernode.bank.utils.SqlSessionUtil.openSession();\n");
// 需要知道是什么类型的sql语句
// sql语句的id是框架使用者提供的,具有多变性,对于框架的开发人员来说,是不知道具体内容的。
// 既然框架开发者不知道sqlId,怎么办呢?mybatis框架的开发者于是就出台了一个规定:凡是使用GenerateDaoProxy机制的,sqlId都不能随便写。
// namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名。
String sqlId = daoInterface.getName() + "." + method.getName();
// System.out.println(sqlId);
SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement(sqlId).getSqlCommandType();
switch (sqlCommandType){
case INSERT: break;
case DELETE: break;
case UPDATE: {
methodCode.append("return sqlSession.update(\""+sqlId+"\", arg0);");
}break;
case SELECT: {
methodCode.append("return ("+method.getReturnType().getName()+")sqlSession.selectOne(\""+sqlId+"\", arg0);");
}break;
}
methodCode.append("\n}");
System.out.println(methodCode);
CtMethod ctMethod = CtMethod.make(methodCode.toString(), ctClass);
ctClass.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
// 创建对象
Object obj = null;
try {
Class<?> clazz = ctClass.toClass();
obj = clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
public static void main(String[] args) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Object obj = GenerateDaoProxy.generate(sqlSession, AccountDao.class);
System.out.println(obj.toString());
}
}
- 修改AccountMapper.xml文件:namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名:
<?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.powernode.bank.dao.AccountDao">
<select id="selectByActno" resultType="com.powernode.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>
-
修改service类
中获取dao对象的代码: -
注:本人在采用该方法时遇见以下问题,在寻找许多解决方案后仍然无果,不知道是不是版本问题导致的,但由于不是核心问题(因为采用mybatis提供的getMapper()方法可以正常使用),因此打算暂时搁置下来,等将来再常识解决。
- 启动过程没有任何问题
- 当点击转账按钮时出现以下报错:
网页端:
服务器端:
正常情况下继续:
- 启动服务器:启动过程中显示,tomcat服务器自动添加了以下的两个运行参数。所以不需要再单独配置。
- 测试前数据:
- 测试结果:
八、MyBatis中接口代理机制及使用
以上所讲内容mybatis内部已经实现了。直接调用以下代码即可获取dao接口的代理类:
// 在mybatis当中,mybatis提供了相关的机制。也可以动态为我们生成dao接口的实现类。(代理类:dao接口的代理)
// mybatis当中实际上采用了代理模式。在内存中生成dao接口的代理类,然后创建代理类的实例。
// 使用mybatis的这种代理机制的前提:SqlMapper.xml文件中namespace必须是dao接口的全限定名称,id必须是dao接口中的方法名。
// 怎么用?代码怎么写?AccountDao accountDao = sqlSession.getMapper(AccountDao.class);
private AccountDao accountDao = SqlSessionUtil.openSession().getMapper(AccountDao.class);
- 测试前数据:
- 测试结果:
利用MyBatis中接口代理机制完成crud
-
建包:
- com.powernode.mybatis.utils
- com.powernode.mybatis.pojo
- com.powernode.mybatis.mapper
-
引入类:
- com.powernode.mybatis.utils.SqlSessionUtil.java
- com.powernode.mybatis.pojo.Car.java
- com.powernode.mybatis.mapper.CarMapper.java
package com.powernode.mybatis.mapper; // 包名也有叫做:com.powernode.mybatis.dao // mapper包就是dao包。 import com.powernode.mybatis.pojo.Car; import java.util.List; /** * CarMapper类,提供crud方法接口 * 一般使用mybatis的话,一般不叫做XXXDao了。一般都是XXXMapper。 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface CarMapper { /** * 新增汽车信息 * @param car * @return */ int insert(Car car); /** * 根据id删除汽车信息 * @param id * @return */ int deleteById(Long id); /** * 根据id更新汽车信息 * @param car * @return */ int updateById(Car car); /** * 根据id查询汽车信息 * @param id * @return */ Car selectById(Long id); /** * 查询所有汽车信息 * @return */ List<Car> selectAll(); }
-
引入配置文件:
- mybatis-config.xml
- logback-test.xml
- jdbc.properties
- CarMapper.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.powernode.mybatis.mapper.CarMapper"> <insert id="insert"> insert into t_car values(null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType}) </insert> <delete id="deleteById"> delete from t_car where id=#{id} </delete> <update id="updateById"> update t_car set car_num=#{carNum}, brand=#{brand}, guide_price=#{guidePrice}, produce_time=#{produceTime}, car_type=#{carType} where id=#{id} </update> <select id="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> <select id="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>
-
测试类:
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class CarCRUDTest {
@Test
public void testSelectAll(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAll();
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
@Test
public void testSelectById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = mapper.selectById(15L);
System.out.println(car);
sqlSession.close();
}
@Test
public void testUpdateById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(15L, "7777", "布加迪威龙", 1000.0, "2015-01-19", "燃油车");
int count = mapper.updateById(car);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
@Test
public void testDeleteById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
int count = mapper.deleteById(14L);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
@Test
public void testInsert(){
SqlSession sqlSession = SqlSessionUtil.openSession();
// 面向接口获取接口的代理对象
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(null, "6666", "奔驰600", 60.0, "2011-12-03", "燃油车");
int count = mapper.insert(car);
System.out.println("插入了几条数据:" + count);
sqlSession.commit();
sqlSession.close();
}
}
九、MyBatis小技巧
9.1 #{}和${}
-
#{}:先编译sql语句,再给占位符传值,底层是PreparedStatement实现。可以防止sql注入,比较常用。
-
${}:先进行sql语句拼接,然后再编译sql语句,底层是Statement实现。存在sql注入现象。只有在需要 进行sql语句关键字拼接的情况下才会用到。
-
需求:根据car_type查询汽车
-
模块名:mybatis-005-antic
使用#{}
-
依赖:
- pom.xml
- mybatis
- mysql-connector-java
- logback-classic
- junit
- jdbc.properites
- logback-test.xml
- com.powernode.mybatis.utils.SqlSessionUtil.java
- com.powernode.mybatis.pojo.Car.java
- com.powernode.mybatis.mapper.CarMapper.java接口
package com.powernode.mybatis.mapper; import com.powernode.mybatis.pojo.Car; import java.util.List; /** * Car的sql映射对象 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface CarMapper { /** * 根据car_num获取Car * @param CarType * @return */ List<Car> selectByCarType(String CarType); }
- CarMapper.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.powernode.mybatis.mapper.CarMapper"> <select id="selectByCarType" 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=#{carType} </select> </mapper>
- 测试程序
package com.powernode.mybatis.test; import com.powernode.mybatis.mapper.CarMapper; import com.powernode.mybatis.pojo.Car; import com.powernode.mybatis.utils.SqlSessionUtil; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import java.util.List; public class CarMapperTest { @Test public void testSelectByCarType(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectByCarType("燃油车"); cars.forEach(car -> System.out.println(car)); sqlSession.close(); } }
- pom.xml
-
执行结果:
- 通过执行可以清楚的看到,sql语句中是带有 ? 的,这个 ? 就是大家在JDBC中所学的占位符,专门用来 接收值的。
- 把“燃油车”以String类型的值,传递给 ?
- 这就是 #{},它会先进行sql语句的预编译,然后再给占位符传值
使用${}
-
同样的需求,我们使用${}来完成
-
CarMapper.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.powernode.mybatis.mapper.CarMapper"> <select id="selectByCarType" 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=${carType} </select> </mapper>
- 再次运行测试程序:
- 出现异常了,这是为什么呢?看看生成的sql语句:
-
${} 是先进行sql语句的拼接,然后再编译,出现语法错误是正常的,因为 燃油车 是一个字符串,在sql语句中应该添加单引号
-
修改CarMapper.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.powernode.mybatis.mapper.CarMapper"> <select id="selectByCarType" 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=#{carType}--> <!--car_type=${carType}--> car_type='${carType}' </select> </mapper>
- 再次运行测试程序:
-
通过以上测试,可以看出,对于以上这种需求来说,还是建议使用 #{} 的方式。
-
原则:能用 #{} 就不用 ${}
什么情况下必须使用${}
-
当需要进行sql语句关键字拼接的时候。必须使用${}
-
需求:通过向sql语句中注入asc或desc关键字,来完成数据的升序或降序排列。
-
先使用#{}尝试:
CarMappe接口:
/** * 查询所有的Car * @param ascOrDesc asc或者desc * @return */ List<Car> selectAllOrderByCarNum(String ascOrDesc);
CarMapper.xml
<select id="selectAllOrderByCarNum" 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 car_num #{key} </select>
测试程序:
@Test public void testSelectAllOrderByCarNum(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectAllOrderByCarNum("desc"); cars.forEach(car -> System.out.println(car)); sqlSession.close(); }
- 运行结果:
报错的原因是sql语句不合法,因为采用这种方式传值,最终sql语句会是这样:
select
id, car_num as carNum, brand,
guide_price as guidePrice, produce_time as produceTime, car_type as carType
from t_car
order by car_num 'desc'
desc是一个关键字,不能带单引号的,所以在进行sql语句关键字拼接的时候,必须使用${}
-
使用${} 改造
CarMapper.xml
<select id="selectAllOrderByCarNum" 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 car_num ${key} </select>
再次测试结果:
拼接表名
-
业务背景:实际开发中,有的表数据量非常庞大,可能会采用分表方式进行存储,比如每天生成一张表,表的名字与日期挂钩,例如:2022年8月1日生成的表:t_user20220108。2000年1月1日生成的表:t_user20000101。此时前端在进行查询的时候会提交一个具体的日期,比如前端提交的日期为: 2000年1月1日,那么后端就会根据这个日期动态拼接表名为:t_user20000101。有了这个表名之后,将 表名拼接到sql语句当中,返回查询结果。那么大家思考一下,拼接表名到sql语句当中应该使用#{} 还是 ${} 呢?
-
使用#{}会是这样:select * from ‘t_car’
-
使用${}会是这样:select * from t_car
CarMapper.xml
<select id="selectAllByTableName" 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 ${tableName} </select>
CarMapper接口
/** * 根据表名查询所有的Car * @param tableName * @return */ List<Car> selectAllByTableName(String tableName);
测试类:
@Test public void testSelectAllByTableName(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectAllByTableName("t_car"); cars.forEach(car -> System.out.println(car)); sqlSession.close(); }
执行结果:
-
批量删除
-
业务背景:一次删除多条记录。
-
对应的sql语句:
- delete from t_user where id = 1 or id = 2 or id = 3;
- delete from t_user where id in(1, 2, 3);
-
假设现在使用in的方式处理,前端传过来的字符串:1, 2, 3
-
如果使用mybatis处理,应该使用#{} 还是 ${}
-
使用#{} :delete from t_user where id in(‘1,2,3’)
- 执行错误:1292 - Truncated incorrect DOUBLE value: ‘1,2,3’
-
使用${} :delete from t_user where id in(1, 2, 3)
CarMapper接口
/** * 根据id批量删除 * @param ids * @return */ int deleteBatch(String ids);
CarMapper.xml
<delete id="deleteBatch"> delete from t_car where id in (${ids}) </delete>
测试类:
@Test public void testDeleteBatch(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); int count = mapper.deleteBatch("11, 19, 36"); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
执行结果:
模糊查询
-
需求:查询奔驰系列的汽车。【只要品牌brand中含有奔驰两个字的都查询出来。】
-
使用${}
CarMapper接口
/** * 根据品牌进行模糊查询 * @param likeBrand * @return */ List<Car> selectLikeBrand(String likeBrand);
CarMapper.xml
<select id="selectLikeBrand" 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 brand like '%${brand}%' </select>
测试类:
@Test public void testSelectLikeBrand(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectLikeBrand("奔驰"); cars.forEach(car -> System.out.println(car)); sqlSession.close(); }
执行结果:
-
使用#{}
-
第一种:concat函数
CarMapper.xml
<select id="selectLikeBrand" 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 <!--brand like '%${brand}%'--> brand like concat('%', #{brand}, '%') </select>
执行结果:
-
第二种:双引号方式
CarMapper.xml
<select id="selectLikeBrand" 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 <!--brand like '%${brand}%'--> <!--brand like concat('%', #{brand}, '%')--> brand like "%"#{brand}"%" </select>
执行结果:
-
9.2 typeAliases 别名
- CarMapper.xml的配置信息中,resultType属性用来指定查询结果集的封装类型,这个名字太长,可以起别名吗?
- 可以
- 在mybatis-config.xml文件中使用typeAliases标签来起别名,包括两种方式:
第一种方式:typeAlias
<typeAliases>
<typeAlias type="com.powernode.mybatis.pojo.Car" alias="Car"/>
</typeAliases>
- 首先要注意typeAliases标签的放置位置,如果不清楚的话,可以看看错误提示信息。
- typeAliases标签中的typeAlias可以写多个。
- typeAlias:
- type属性:指定给哪个类起别名
- alias属性:别名。
- alias属性不是必须的,如果缺省的话,type属性指定的类型名的简类名作为别名。
- alias是大小写不敏感的。也就是说假设alias=“Car”,再用的时候,可以CAR,也可以car, 也可以Car,都行。
第二种方式:package
-
如果一个包下的类太多,每个类都要起别名,会导致typeAlias标签配置较多,所以mybatis用提供 package的配置方式,只需要指定包名,该包下的所有类都自动起别名,别名就是简类名。并且别名不区分大小写。
-
package也可以配置多个的。
<typeAliases> <!--<typeAlias type="com.powernode.mybatis.pojo.Car" alias="Car"/>--> <package name="com.powernode.mybatis.pojo"/> </typeAliases>
9.3 mappers
- SQL映射文件的配置方式包括四种:
- resource:从类路径中加载
- url:从指定的全限定资源路径中加载
- class:使用映射器接口实现类的完全限定类名
- package:将包内的映射器接口实现全部注册为映射器
resource
- 这种方式是从类路径中加载配置文件,所以这种方式要求SQL映射文件必须放在resources目录下或其子目录下。
<mappers>
<mapper resource="CarMapper.xml"/>
</mappers>
url
这种方式显然使用了绝对路径的方式,这种配置对SQL映射文件存放的位置没有要求,随意。
<mappers>
<mapper url="file:///E:\ReLearn_MyBatis_Workspace\mybatis-006-antic\src\main\resources\CarMapper.xml"/>
</mappers>
class
-
如果使用这种方式必须满足以下条件:
- SQL映射文件和mapper接口放在同一个目录下。
- SQL映射文件的名字也必须和mapper接口名一致。
-
思考:mapper标签的作用是指定SqlMapper.xml文件的路径,指定接口名有什么用呢?
<mapper class="com.powernode.mybatis.mapper.CarMapper"/>
- 如果你class指定是:com.powernode.mybatis.mapper.CarMapper,那么mybatis框架会自动去com/powernode/mybatis/mapper目录下查找CarMapper.xml文件。
- 注意:如果你采用这种方式,那么你必须保证CarMapper.xml文件和CarMapper接口必须在同一个目录下,并且名字一致。
-
在resources目录下新建:com/powernode/mybatis/mapper
- 这⾥千万要注意:不能这样新建 com.powernode.mybatis.dao
-
将CarMapper.xml文件移动到mapper目录下
-
修改mybatis-config.xml文件
<mappers>
<mapper class="com.powernode.mybatis.mapper.CarMapper"/>
</mappers>
package
- 如果class较多,可以使用这种package的方式,但前提条件和class方式一样。
<mappers>
<package name="com.powernode.mybatis.mapper"/>
</mappers>
9.4 idea配置文件模板
mybatis-config.xml和SqlMapper.xml文件可以在IDEA中提前创建好模板,以后通过模板创建配置文 件。
9.5 插入数据时获取自动生成的主键
-
前提是:主键是自动生成的。
-
业务背景:一个用户有多个角色。
- 插入一条新的记录之后,自动生成了主键,而这个主键需要在其他表中使用时。
- 插入一个用户数据的同时需要给该用户分配角色:需要将生成的用户的id插入到角色表的user_id字段上。
-
第一种方式:可以先插入用户数据,再写一条查询语句获取id,然后再插入user_id字段。【比较麻烦】
-
第二种方式:mybatis提供了一种方式更加便捷:
CarMapper接口
/** * 获取自动生成的主键 * @param car */ int insertGetGeneratedKeys(Car car);
CarMapper.xml
<insert id="insertGetGeneratedKeys" useGeneratedKeys="true" keyProperty="id"> insert into t_car values (null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType}) </insert>
测试类:
@Test public void testInsertGetGeneratedKeys(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(null, "8888", "劳斯莱斯幻影", 920.0, "2018-09-10", "燃油车"); int count = mapper.insertGetGeneratedKeys(car); System.out.println("插入了几条数据:" + count); sqlSession.commit(); System.out.println("自动生成的CarId为:" + car.getId()); sqlSession.close(); }
执行结果:
课堂笔记
mybatis小技巧
1. #{}和${}的区别
#{}的执行结果:
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - ==> Preparing: 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 = ?
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - ==> Parameters: 新能源(String)
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - <== Total: 2
${}的执行结果:
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - ==> Preparing: 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 = 新能源
[main] DEBUG c.p.mybatis.mapper.CarMapper.selectByCarType - ==> Parameters:
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column '新能源' in 'where clause'
### The error may exist in CarMapper.xml
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: 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 = 新能源
### Cause: java.sql.SQLSyntaxErrorException: Unknown column '新能源' in 'where clause'
#{}和${}的区别:
#{}: 底层使用PreparedStatement。特点:先进行SQL语句的编译,然后给SQL语句的占位符问号?传值。可以避免SQL注入的风险。
${}:底层使用Statement。特点:先进行SQL语句的拼接,然后再对SQL语句进行编译。存在SQL注入的风险。
优先使用#{},这是原则。避免SQL注入的风险。
#{}的执行结果:
Preparing: 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 ?
Parameters: asc(String)
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 'asc'
${}的执行结果:
Preparing:
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 asc
Parameters:
如果需要SQL语句的关键字放到SQL语句中,只能使用${},因为#{}是以值的形式放到SQL语句当中的。
2. 向SQL语句当中拼接表名,就需要使用${}
现实业务当中,可能会存在分表存储数据的情况。因为一张表存的话,数据量太大。查询效率比较低。
可以将这些数据有规律的分表存储,这样在查询的时候效率就比较高。因为扫描的数据量变少了。
日志表:专门存储日志信息的。如果t_log只有一张表,这张表中每一天都会产生很多log,慢慢的,这个表中数据会很多。
怎么解决问题?
可以每天生成一个新表。每张表以当天日期作为名称,例如:
t_log_20220901
t_log_20220902
....
你想知道某一天的日志信息怎么办?
假设今天是20220901,那么直接查:t_log_20220901的表即可。
3.批量删除:一次删除多条记录。
批量删除的SQL语句有两种写法:
第一种or:delete from t_car where id=1 or id=2 or id=3;
第二种int:delete from t_car where id in(1,2,3);
应该采用${}的方式:
delete from t_car where id in(${ids});
4.模糊查询:like
需求:根据汽车品牌进行模糊查询
select * from t_car where brand like '%奔驰%';
select * from t_car where brand like '%比亚迪%';
第一种方案:
'%${brand}%'
第二种方案:concat函数,这个是mysql数据库当中的一个函数,专门进行字符串拼接
concat('%',#{brand},'%')
第三种方案:比较鸡肋了。可以不算。
concat('%','${brand}','%')
第四种方案:
"%"#{brand}"%"
5. 关于MyBatis中别名机制:
<typeAliases>
<!--别名自己指定的-->
<typeAlias type="com.powernode.mybatis.pojo.Car" alias="aaa"/>
<typeAlias type="com.powernode.mybatis.pojo.Log" alias="bbb"/>
<!--采用默认的别名机制-->
<typeAlias type="com.powernode.mybatis.pojo.Car"/>
<typeAlias type="com.powernode.mybatis.pojo.Log"/>
<!--包下所有的类自动起别名。使用简名作为别名。-->
<package name="com.powernode.mybatis.pojo"/>
</typeAliases>
所有别名不区分大小写。
namespace不能使用别名机制。
6. mybatis-config.xml文件中的mappers标签。
<mapper resource="CarMapper.xml"/> 要求类的根路径下必须有:CarMapper.xml
<mapper url="file:///d:/CarMapper.xml"/> 要求在d:/下有CarMapper.xml文件
<mapper class="全限定接口名,带有包名"/>
mapper标签的属性可以有三个:
resource:这种方式是从类的根路径下开始查找资源。采用这种方式的话,你的配置文件需要放到类路径当中才行。
url: 这种方式是一种绝对路径的方式,这种方式不要求配置文件必须放到类路径当中,哪里都行,只要提供一个绝对路径就行。这种方式使用极少,因为移植性太差。
class: 这个位置提供的是mapper接口的全限定接口名,必须带有包名的。
思考:mapper标签的作用是指定SqlMapper.xml文件的路径,指定接口名有什么用呢?
<mapper class="com.powernode.mybatis.mapper.CarMapper"/>
如果你class指定是:com.powernode.mybatis.mapper.CarMapper
那么mybatis框架会自动去com/powernode/mybatis/mapper目录下查找CarMapper.xml文件。
注意:也就是说:如果你采用这种方式,那么你必须保证CarMapper.xml文件和CarMapper接口必须在同一个目录下。并且名字一致。
CarMapper接口-> CarMapper.xml
LogMapper接口-> LogMapper.xml
....
提醒!!!!!!!!!!!!!!!!!!!!!!!
在IDEA的resources目录下新建多重目录的话,必须是这样创建:
com/powernode/mybatis/mapper
不能这样:
com.powernode.mybatis.mapper
十、MyBatis参数处理
-
模块名:mybatis-006-param
-
表:t_stu
-
表中数据:
-
pojo类:
package com.powernode.mybatis.pojo; import java.util.Date; /** * 学生信息封装类 * @author ShiningSong * @version 1.0 * @since 1.0 */ public class Student { private Long id; private String name; private Integer age; private Double height; private Character sex; private Date birth; @Override public String toString() { return "Student{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + ", height=" + height + ", sex=" + sex + ", birth=" + birth + '}'; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Double getHeight() { return height; } public void setHeight(Double height) { this.height = height; } public Character getSex() { return sex; } public void setSex(Character sex) { this.sex = sex; } public Date getBirth() { return birth; } public void setBirth(Date birth) { this.birth = birth; } public Student() { } public Student(Long id, String name, Integer age, Double height, Character sex, Date birth) { this.id = id; this.name = name; this.age = age; this.height = height; this.sex = sex; this.birth = birth; } }
10.1 单个简单类型参数
简单类型包括:
- byte short int long float double char
- Byte Short Integer Long Float Double Character
- String java.util.Date
- java.sql.Date
需求:
-
根据name查
-
根据id查
-
根据birth查
-
根据sex查
StudentMapper接口
package com.powernode.mybatis.mapper; import com.powernode.mybatis.pojo.Student; import java.util.Date; import java.util.List; /** * 学生表映射接口 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface StudentMapper { /** * 根据学生名查询学生信息 * @param name * @return */ List<Student> selectByName(String name); /** * 根据id查询学生信息 * @param id * @return */ Student selectById(Long id); /** * 根据生日birth查找学生信息 * @param birth * @return */ List<Student> selectByBirth(Date birth); /** * 根据性别sex查询学生信息 * @param sex * @return */ List<Student> selectBySex(Character sex); }
StudentMapper.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.powernode.mybatis.mapper.StudentMapper"> <select id="selectByName" resultType="Student"> select * from t_stu where name=#{name} </select> <select id="selectById" resultType="Student"> select * from t_stu where id=#{id} </select> <select id="selectByBirth" resultType="Student"> select * from t_stu where birth=#{birth} </select> <select id="selectBySex" resultType="Student"> select * from t_stu where sex=#{sex} </select> </mapper>
测试类:
package com.powernode.mybatis.test; import com.powernode.mybatis.mapper.StudentMapper; import com.powernode.mybatis.pojo.Student; import com.powernode.mybatis.utils.SqlSessionUtil; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.List; /** * 测试类:测试学生映射方法 */ public class StudentMapperTest { @Test public void testSelectBySex(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.selectBySex('男'); students.forEach(student -> System.out.println(student)); sqlSession.close(); } @Test public void testSelectByBirth(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); try { List<Student> students = mapper.selectByBirth(new SimpleDateFormat("yyyy-MM-dd").parse("2022-08-16")); students.forEach(student -> System.out.println(student)); } catch (ParseException e) { throw new RuntimeException(e); } sqlSession.close(); } @Test public void testSelectById(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectById(2L); System.out.println(student); sqlSession.close(); } @Test public void testSelectByName(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.selectByName("张三"); students.forEach(student -> System.out.println(student)); sqlSession.close(); } }
执行结果正常。
-
通过测试得知,简单类型对于mybatis来说都是可以自动类型识别的:
- 也就是说对于mybatis来说,它是可以自动推断出ps.setXxxx()方法的。ps.setString()还是 ps.setInt()。它可以自动推断。
-
其实SQL映射文件中的配置比较完整的写法是:
<select id="selectByName" resultType="Student" parameterType="java.lang.String"> select * from t_stu where name=#{name} </select>
其中sql语句中的javaType,jdbcType,以及select标签中的parameterType属性,都是用来帮助 mybatis进行类型确定的。不过这些配置多数是可以省略的。因为mybatis它有强大的自动类型推断机制。
- javaType:可以省略
- jdbcType:可以省略
- parameterType:可以省略
-
如果参数只有一个的话,#{} ⾥面的内容就随便写了。对于 ${} 来说,注意加单引号。
10.2 Map参数
需求:根据name和age查询
StudentMapper接口
/**
* 利用Map根据name和age查询学生信息
* @param paramMap
* @return
*/
List<Student> selectByParaMap(Map<String, Object> paramMap);
StudentMapper.xml
<select id="selectByParaMap" resultType="Student">
select * from t_stu where name=#{nameKey} and age=#{ageKey}
</select>
测试类:
@Test
public void testSelectByParaMap(){
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Map<String, Object> map = new HashMap<>();
map.put("nameKey", "张三");
map.put("ageKey", 20);
List<Student> students = mapper.selectByParaMap(map);
students.forEach(student -> System.out.println(student));
sqlSession.close();
}
执行结果正常。
这种方式是手动封装Map集合,将每个条件以key和value的形式存放到集合中。然后在使用的时候通过#{map集合的key}来取值。
10.3 实体类参数
需求:插入一条Student数据
StudentMapper接口
/**
* 插入学生信息
* @param student
* @return
*/
int insertByPOJO(Student student);
StudentMapper.xml
<insert id="insertByPOJO">
insert into t_stu
values (null, #{name}, #{age}, #{height}, #{birth}, #{sex})
</insert>
测试类:
@Test
public void testInsertByPOJO(){
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
Student student = new Student(null, "李四", 18, 1.70, '男', new Date());
int count = mapper.insertByPOJO(student);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
执行结果正常。
这⾥需要注意的是:#{} ⾥面写的是属性名字。这个属性名其本质上是:set/get方法名去掉set/get之后的名字。
10.4 多参数
需求:通过name和sex查询
StudentMapper接口
/**
* 多参数查询,根据name和sex查询学生信息
* @return
*/
List<Student> selectByMultiParam(String name, Character sex);
StudentMapper.xml
<select id="selectByMultiParam" resultType="Student">
select * from t_stu
where name=#{name} and sex=#{sex}
</select>
测试类:
@Test
public void testSelectByMultiParam(){
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.selectByMultiParam("张三", '女');
students.forEach(student -> System.out.println(student));
sqlSession.close();
}
执行结果:
异常信息描述了:name参数找不到,可用的参数包括[arg1, arg0, param1, param2]
修改StudentMapper.xml配置文件:尝试使用[arg1, arg0, param1, param2]去参数
StudentMapper.xml
<select id="selectByMultiParam" resultType="Student">
select * from t_stu
where name=#{arg0} and sex=#{arg1}
</select>
执行结果:
通过测试可以看到:
- arg0 是第一个参数
- param1是第一个参数
- arg1 是第二个参数
- param2是第二个参数
采用param1, param2, … 的方式也可以,甚至可以arg*和param*混用,但是要记住,arg*标记是从0开始,param*标记是从1开始
**实现原理:**实际上在mybatis底层会创建一个map集合,以arg0/param1为key,以方法上的参数为 value,例如以下代码:
Map<String, Object> map = new HashMap<>();
map.put("arg0", name);
map.put("arg1", sex);
map.put("param1", name);
map.put("param2", sex);
// 所以可以这样取值:#{arg0} #{arg1} #{param1} #{param2}
// 其本质就是#{map集合的key}
注意:使用mybatis3.4.2之前的版本时:要用#{0}和#{1}这种形式。
10.5 @Param注解(命名参数)
-
可以不用arg0 arg1 param1 param2吗?这个map集合的key我们自定义可以吗?
- 当然可以。使用 @Param注解即可。这样可以增强可读性。
-
需求:根据name和age查询
StudentMapper接口
/** * 使用@Param注解,根据name和age查询学生信息 * @return */ List<Student> selectByParamAnnotation(@Param("name")String name, @Param("age")Integer age);
StudentMapper.xml
<select id="selectByParamAnnotation" resultType="Student"> select * from t_stu where name=#{name} and age=#{age} </select>
测试类:
@Test public void testSelectByParamAnnotation(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.selectByParamAnnotation("张三", 20); students.forEach(student -> System.out.println(student)); sqlSession.close(); }
执行结果正常。
-
核心:@Param(“这⾥填写的其实就是map集合的key”)
10.6 @Param源码分析
十一、MyBatis查询语句专题
- 模块名:mybatis-007-select
- 打包方式:jar
- 引入依赖:mysql驱动依赖、mybatis依赖、logback依赖、junit依赖。
- 引入配置文件:jdbc.properties、mybatis-config.xml、logback.xml
- 创建pojo类:Car
- 创建Mapper接口:CarMapper
- 创建Mapper接口对应的映射文件:com/powernode/mybatis/mapper/CarMapper.xml
- 创建单元测试:CarMapperTest
- 拷贝工具类:SqlSessionUtil
11.1 返回Car
-
当查询的结果,有对应的实体类,并且查询结果只有一条时:
CarMapper接口:
package com.powernode.mybatis.mapper; import com.powernode.mybatis.pojo.Car; /** * 汽车表t_car映射接口 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface CarMapper { /** * 根据id查询车辆信息 * @param id * @return */ Car selectById(Long id); }
CarMapper.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.powernode.mybatis.mapper.CarMapper"> <select id="selectById" resultType="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> </mapper>
测试类:
package com.powernode.mybatis.test; import com.powernode.mybatis.mapper.CarMapper; import com.powernode.mybatis.pojo.Car; import com.powernode.mybatis.utils.SqlSessionUtil; import org.apache.ibatis.session.SqlSession; import org.junit.Test; public class CarMapperTest { @Test public void testSelectById(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectById(2L); System.out.println(car); sqlSession.close(); } }
测试结果:
-
查询结果是一条的话可以使用List集合接收吗?
- 当然可以。
CarMapper接口
/** * 根据id查询车辆信息放入List中 * @param id * @return */ List<Car> selectByIdToList(Long id);
CarMapper.xml
<select id="selectByIdToList" resultType="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>
测试类:
@Test public void testSelectByIdToList(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectByIdToList(1L); System.out.println(cars); sqlSession.close(); }
执行结果:
11.2 返回List
当查询的记录条数是多条的时候,必须使用集合接收。如果使用单个实体类接收会出现异常。
CarMapper接口
/** * 查询所有车辆信息 * @return */ List<Car> selectAll();
CarMapper.xml
<select id="selectAll" resultType="Car"> select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car </select>
测试类:
@Test public void testSelectAll(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectAll(); cars.forEach(car -> System.out.println(car)); sqlSession.close(); }
执行结果:
-
如果返回多条记录,采用单个实体类接收会怎样?
CarMapper接口
/** * 将查询结果返回单个POJO类 * 如果查询结果超过一个会报异常 * @return */ Car selectAllToPOJO();
CarMapper.xml
<select id="selectAllToPOJO" resultType="Car"> select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car </select>
测试类:
@Test public void testSelectAllToPOJO(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = mapper.selectAllToPOJO(); System.out.println(car); sqlSession.close(); }
执行结果:
- 报错,因为查询结果不止一个对象,利用一个实体类对象是无法接收的
11.3 返回Map
-
当返回的数据,没有合适的实体类对应的话,可以采用Map集合接收。
-
字段名做key,字段值做value。 查询如果可以保证只有一条数据,则返回一个Map集合即可。
CarMapper接口
/** * 通过id查询一条记录,返回Map集合 * @param id * @return */ Map<String, Object> selectByIdRetMap(Long id);
resultMap=“map”,这是因为mybatis内置了很多别名。【参见mybatis开发手册】
CarMapper.xml
<select id="selectByIdRetMap" resultType="map"> 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>
测试类:
@Test public void testSelectByIdRetMap(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Map<String, Object> car = mapper.selectByIdRetMap(1L); System.out.println(car); sqlSession.close(); }
执行结果:
-
当然,如果返回一个Map集合,可以将Map集合放到List集合中吗?
- 当然可以,这⾥就不再测试了。
-
反过来,如果返回的不是一条记录,是多条记录的话,只采用单个Map集合接收,这样同样会出现之前的异常:TooManyResultsException
11.4 返回List<Map>
-
查询结果条数大于等于1条数据,则可以返回一个存储Map集合的List集合。List等同于List
CarMapper接口
/** * 查询所有车辆信息,返回一个List集合,List集合中存储的是保存车辆信息的Map集合 * @return */ List<Map<String, Object>> selectAllRetListMap();
CarMapper.xml
<select id="selectAllRetListMap" resultType="map"> select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car </select>
测试类:
@Test public void testSelectAllRetListMap(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Map<String, Object>> cars = mapper.selectAllRetListMap(); System.out.println(cars); sqlSession.close(); }
执行结果:
[ {carType=燃油车, carNum=100, guidePrice=41.00, produceTime=2022-09-01, id=1, brand=宝马520Li}, {carType=电车, carNum=101, guidePrice=54.00, produceTime=2022-08-01, id=2, brand=奔驰E300L}, {carType=燃油车, carNum=1003, guidePrice=30.00, produceTime=2000-10-11, id=4, brand=丰田霸道}, ......, {carType=燃油车, carNum=9999, guidePrice=920.00, produceTime=2018-09-10, id=40, brand=劳斯莱斯幻影} ]
11.5 返回Map<String, Map>
-
拿Car的id做key,以后取出对应的Map集合时更方便。
-
使用 @MapKey(“id”); 注解实现
CarMapper接口
/** * 查询所有车辆信息,返回一个Map集合, * Map集合中存储的是保存车辆信息的Map集合, * 大Map的key是小map的id,使用@MapKey注解实现 * @return */ @MapKey("id") Map<Long, Map<String, Object>> selectAllRetMap();
CarMapper.xml
<select id="selectAllRetMap" resultType="map"> select id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType from t_car </select>
测试类:
@Test public void testSelectAllRetMap(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Map<Long, Map<String, Object>> cars = mapper.selectAllRetMap(); System.out.println(cars); sqlSession.close(); }
执行结果:
{ 1={carType=燃油车, carNum=100, guidePrice=41.00, produceTime=2022-09-01, id=1, brand=宝马520Li}, 2={carType=电车, carNum=101, guidePrice=54.00, produceTime=2022-08-01, id=2, brand=奔驰E300L}, 4={carType=燃油车, carNum=1003, guidePrice=30.00, produceTime=2000-10-11, id=4, brand=丰田霸道}, ......, 40={carType=燃油车, carNum=9999, guidePrice=920.00, produceTime=2018-09-10, id=40, brand=劳斯莱斯幻影} }
11.6 resultMap结果映射
查询结果的列名和java对象的属性名对应不上怎么办?
- 第一种方式:as 给列起别名
- 第二种方式:使用resultMap进行结果映射
- 第三种方式:是否开启驼峰命名自动映射(配置settings)
使用resultMap进行结果映射
CarMapper接口
/**
* 查询所有Car,使用resultMap进行结果映射
* @return
*/
List<Car> selectAllByResultMap();
CarMapper.xml
<!--
resultMap:
id: 这个是结果映射的标识,作为select标签的resultMap属性的值
type: 结果集要映射的类,可以使用别名
-->
<resultMap id="carResultMap" type="Car">
<!--对象的唯一标识,官方的解释是:为了提高mybatis的性能,建议协商-->
<id property="id" column="id"/>
<result property="carNum" column="car_num"/>
<!--当属性名和数据库列名一致时,可以省略,但建议都写上。-->
<!--javaType用来指定属性类型,jdbcType用来指定列类型,一般可以省略-->
<result property="brand" column="brand" javaType="String" jdbcType="VARCHAR"/>
<result property="guidePrice" column="guide_price"/>
<result property="produceTime" column="produce_time"/>
<result property="carType" column="car_type"/>
</resultMap>
<!--resultMap属性的值必须和resultMap标签中id属性值一致-->
<select id="selectAllByResultMap" resultMap="carResultMap">
select * from t_car
</select>
测试类:
@Test
public void testSelectAllByResultMap(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAllByResultMap();
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
执行结果正常。
是否开启驼峰命名自动映射
- 使用这种方式的前提是:属性名遵循Java的命名规范,数据库表的列名遵循SQL的命名规范。
- Java命名规范:首字母小写,后面每个单词首字母大写,遵循驼峰命名方式。
- SQL命名规范:全部小写,单词之间采用下划线分割。
- 比如以下的对应关系:
实体类中的属性名 | 数据库表的列名 |
---|---|
carNum | car_num |
guidePrice | guide_price |
produceTime | produce_time |
carType | car_type |
-
如何启用该功能,在mybatis-config.xml文件中进行配置:
<!--放在properties标签后面--> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>
CarMapper接口
/** * 查询所有车辆信息,启用驼峰命名自动映射 * @return */ List<Car> selectAllByMapUnderscoreToCamelCase();
CarMapper.xml
<select id="selectAllByMapUnderscoreToCamelCase" resultType="Car"> select * from t_car </select>
测试类:
@Test public void testSelectAllByMapUnderscoreToCamelCase(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectAllByMapUnderscoreToCamelCase(); cars.forEach(car -> System.out.println(car)); sqlSession.close(); }
执行结果正常。
11.7 返回总记录条数
需求:查询总记录条数
CarMapper接口
/**
* 获取总记录条数
* @return
*/
Long selectTotal();
CarMapper.xml
<select id="selectTotal" resultType="long">
select count(*) from t_car
</select>
测试类:
@Test
public void testSelectTotal(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Long count = mapper.selectTotal();
System.out.println("总记录条数为:" + count);
sqlSession.close();
}
执行结果:
十二、动态SQL
有的业务场景,也需要SQL语句进行动态拼接,例如:
delete from t_car where id in(1,2,3,4,5,6,......这⾥的值是动态的,根据用户选择的id不同,值是不同的);
-
多条件查询
select * from t_car where brand like '丰⽥%' and guide_price > 30 and .....;
-
创建模块:mybatis-008-dynamic-sql
-
打包方式:jar
-
引入依赖:mysql驱动依赖、mybatis依赖、junit依赖、logback依赖
-
pojo:com.powernode.mybatis.pojo.Car
-
mapper接口:com.powernode.mybatis.mapper.CarMapper
-
引入配置文件:mybatis-config.xml、jdbc.properties、logback.xml
-
mapper配置文件:com/powernode/mybatis/mapper/CarMapper.xml
-
编写测试类:com.powernode.mybatis.test.CarMapperTest
-
拷贝工具类:SqlSessionUtil
12.1 if标签
-
需求:多条件查询。
-
可能的条件包括:品牌(brand)、指导价格(guide_price)、汽车类型(car_type)
CarMapper接口
package com.powernode.mybatis.mapper; import com.powernode.mybatis.pojo.Car; import org.apache.ibatis.annotations.Param; import java.util.List; /** * 汽车表映射接口 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface CarMapper { /** * 多条件查询车辆信息 * 条件包括:品牌(brand)、指导价格(guide_price)、汽车类型(car_type) * @param brand * @param guidePrice * @param carType * @return */ List<Car> selectByMultiCondition(@Param("brand")String brand, @Param("guidePrice")Double guidePrice, @Param("carType")String carType); }
CarMapper.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.powernode.mybatis.mapper.CarMapper"> <select id="selectByMultiCondition" resultType="Car"> select * from t_car where <if test="brand != null and brand != ''"> brand like "%"#{brand}"%" </if> <if test="guidePrice != null and guidePrice != ''"> and guide_price >= #{guidePrice} </if> <if test="carType != null and carType != ''"> and car_type = #{carType} </if> </select> </mapper>
测试类:
package com.powernode.mybatis.test; import com.powernode.mybatis.mapper.CarMapper; import com.powernode.mybatis.pojo.Car; import com.powernode.mybatis.utils.SqlSessionUtil; import org.apache.ibatis.session.SqlSession; import org.junit.Test; import java.util.List; public class CarMapperTest { @Test public void testSelectByMultiCondition(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectByMultiCondition("丰田", 10.0, "燃油车"); cars.forEach(car -> System.out.println(car)); sqlSession.close(); } }
执行结果:
-
如果第一个条件为空,剩下两个条件不为空,会是怎样呢?
List<Car> cars = mapper.selectByMultiCondition(null, 10.0, "燃油车");
执行结果:
报错了,SQL语法有问题,where后面出现了and。这该怎么解决呢?
-
可以where后面添加一个恒成⽴的条件。
CarMapper.xml
<select id="selectByMultiCondition" resultType="Car"> <!--where后面加上任何一个恒等式都可以--> select * from t_car where 1=1 <if test="brand != null and brand != ''"> brand like "%"#{brand}"%" </if> <if test="guidePrice != null and guidePrice != ''"> and guide_price >= #{guidePrice} </if> <if test="carType != null and carType != ''"> and car_type = #{carType} </if> </select>
执行结果:
-
如果三个条件都是空,有影响吗?
List<Car> cars = mapper.selectByMultiCondition("", null, "");
执行结果:
-
三个条件都不为空呢?
- 第一个条件前也要加 and
执行结果:
12.2 where标签
where标签的作用:让where子句更加动态智能。
- 所有条件都为空时,where标签保证不会生成where子句。
- 自动去除某些条件前面多余的and或or。
继续使用if标签中的需求。
CarMapper接口
/**
* 多条件查询车辆信息
* 条件包括:品牌(brand)、指导价格(guide_price)、汽车类型(car_type)
* 使用<where>标签实现
* @param brand
* @param guidePrice
* @param carType
* @return
*/
List<Car> selectByMultiConditionWithWhere(@Param("brand")String brand, @Param("guidePrice")Double guidePrice, @Param("carType")String carType);
CarMapper.xml
<select id="selectByMultiConditionWithWhere" resultType="Car">
select * from t_car
<where>
<if test="brand != null and brand != ''">
and brand like "%"#{brand}"%"
</if>
<if test="guidePrice != null and guidePrice != ''">
and guide_price >= #{guidePrice}
</if>
<if test="carType != null and carType != ''">
and car_type = #{carType}
</if>
</where>
</select>
测试类:
@Test
public void testSelectByMultiConditionWithWhere() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 10.0, "燃油车");
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
执行结果:
-
如果所有条件都是空呢?
测试类:
List<Car> cars = mapper.selectByMultiCondition("", null, "");
执行结果:
-
它可以自动去掉前面多余的and,那可以自动去掉前面多余的or吗?
测试类:
List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 10.0, "燃油车");
CarMapper.xml
<select id="selectByMultiConditionWithWhere" resultType="Car"> select * from t_car <where> <if test="brand != null and brand != ''"> or brand like "%"#{brand}"%" </if> <if test="guidePrice != null and guidePrice != ''"> and guide_price >= #{guidePrice} </if> <if test="carType != null and carType != ''"> and car_type = #{carType} </if> </where> </select>
测试结果:
-
它可以自动去掉前面多余的and,那可以自动去掉后面多余的and吗?
测试类:
// 设置最后一个参数为空 List<Car> cars = mapper.selectByMultiConditionWithWhere("丰田", 10.0, "");
CarMapper.xml
<select id="selectByMultiConditionWithWhere" resultType="Car"> select * from t_car <where> <if test="brand != null and brand != ''"> brand like "%"#{brand}"%" and </if> <if test="guidePrice != null and guidePrice != ''"> guide_price >= #{guidePrice} and </if> <if test="carType != null and carType != ''"> car_type = #{carType} </if> </where> </select>
测试结果:
很显然,后面多余的and是不会被去除的。
12.3 trim标签
trim标签的属性:
- prefix:在trim标签中的语句前添加内容
- suffix:在trim标签中的语句后添加内容
- prefixOverrides:前缀覆盖掉(去掉)
- suffixOverrides:后缀覆盖掉(去掉)
CarMapper接口
/**
* 多条件查询车辆信息
* 条件包括:品牌(brand)、指导价格(guide_price)、汽车类型(car_type)
* 使用<trim>标签实现
* @param brand
* @param guidePrice
* @param carType
* @return
*/
List<Car> selectByMultiConditionWithTrim(@Param("brand")String brand, @Param("guidePrice")Double guidePrice, @Param("carType")String carType);
CarMapper.xml
<select id="selectByMultiConditionWithTrim" resultType="Car">
select * from t_car
<trim prefix="where" suffixOverrides="and|or">
<if test="brand != null and brand != ''">
brand like "%"#{brand}"%" and
</if>
<if test="guidePrice != null and guidePrice != ''">
guide_price >= #{guidePrice} and
</if>
<if test="carType != null and carType != ''">
car_type = #{carType}
</if>
</trim>
</select>
测试类:
@Test
public void testSelectByMultiConditionWithTrim() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByMultiConditionWithTrim("丰田", 10.0, "");
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
测试结果:
-
如果所有条件为空,where会被加上吗?
List<Car> cars = mapper.selectByMultiConditionWithTrim("", null, "");
执行结果:
12.4 set标签
-
主要使用在update语句当中,用来生成set关键字,同时去掉最后多余的“,”
-
比如我们只更新提交的不为空的字段,如果提交的数据是空或者"",那么这个字段我们将不更新。
CarMapper接口
/** * 使用<set>标签更新车辆信息 * @param car * @return */ int updateWithSet(Car car);
CarMapper.xml
<update id="updateWithSet"> update t_car <set> <if test="carNum != null and carNum != ''"> car_num = #{carNum}, </if> <if test="brand != null and brand != ''"> brand = #{brand}, </if> <if test="guidePrice != null and guidePrice != ''"> guide_price = #{guidePrice}, </if> <if test="produceTime != null and produceTime != ''"> produce_time = #{produceTime}, </if> <if test="carType != null and carType != ''"> car_type = #{carType}, </if> </set> where id = #{id} </update>
测试类:
@Test public void testUpdateWithSet(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); Car car = new Car(12L, "1010", "特斯拉model3", 26.0, "", null); int count = mapper.updateWithSet(car); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
测试结果:
12.5 choose when otherwise
这三个标签是在一起使用的:
<choose>
<when></when>
<when></when>
<when></when>
<otherwise></otherwise>
</choose>
等同于:
if(){
}else if(){
}else if(){
}else if(){
}else{
}
只有一个分支会被选择!!!!
-
需求:先根据品牌查询,如果没有提供品牌,再根据指导价格查询,如果没有提供指导价格,就根据生 产日期查询。
CarMapper接口
/** * 使用<choose>、<when>、<otherwise>标签查询车辆信息 * 先根据品牌查询,如果没有提供品牌,再根据指导价格查询,如果没有提供指导价格,就根据生产日期查询。 * @param brand * @param guidePrice * @param produceTime * @return */ List<Car> selectByChoose(@Param("brand")String brand, @Param("guidePrice")Double guidePrice, @Param("produceTime")String produceTime);
CarMapper.xml
<select id="selectByChoose" resultType="Car"> select * from t_car <where> <choose> <when test="brand != null and brand != ''"> brand like "%"#{brand}"%" </when> <when test="guidePrice != null and guidePrice != ''"> guide_price >= #{guidePrice} </when> <otherwise> produce_time >= #{produceTime} </otherwise> </choose> </where> </select>
测试类:
@Test public void testSelectByChoose(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectByChoose("丰田霸道", 20.0, "2000-10-10"); cars.forEach(car -> System.out.println(car)); sqlSession.close(); }
执行结果:
测试类:
List<Car> cars = mapper.selectByChoose("", 20.0, "2000-10-10");
执行结果:
测试类:
List<Car> cars = mapper.selectByChoose("", null, "2000-10-10");
执行结果:
测试类:
List<Car> cars = mapper.selectByChoose("", null, "");
执行结果:
12.6 foreach标签
-
循环数组或集合,动态生成sql,比如这样的SQL:
- 批量删除:
delete from t_car where id in (1, 2, 3); delete from t_car where id=1 or id=2 or id=3;
- 批量添加:
insert into t_car values (null, '1001', '凯美瑞', 35.0, '2010-10-11', '燃油车') (null, '1002', '比亚迪唐', 31.0, '2020-11-11', '新能源') (null, '1003', '比亚迪宋', 32.0, '2020-10-11', '新能源')
批量删除
-
用in来删除
CarMapper接口
/** * 根据id批量删除车辆信息,用in * 使用<foreach>标签 * @param ids * @return */ int deleteBatchByForeachWithIn(@Param("ids")Long[] ids);
CarMapper.xml
<delete id="deleteBatchByForeachWithIn"> delete from t_car where id in <foreach collection="ids" item="id" separator="," open="(" close=")"> #{id} </foreach> </delete>
测试类:
@Test public void testDeleteBatchByForeachWithIn(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); int count = mapper.deleteBatchByForeachWithIn(new Long[]{5L, 6L, 7L}); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
执行结果:
-
用or来删除
CarMapper接口
/** * 根据id批量删除车辆信息,用or * 使用<foreach>标签 * @param ids * @return */ int deleteBatchByForeachWithOr(@Param("ids")Long[] ids);
CarMapper.xml
<delete id="deleteBatchByForeachWithOr"> delete from t_car where <foreach collection="ids" item="id" separator="or"> id = #{id} </foreach> </delete>
测试类:
@Test public void testDeleteBatchByForeachWithOr(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); int count = mapper.deleteBatchByForeachWithOr(new Long[]{5L, 6L, 7L}); System.out.println(count); sqlSession.commit(); sqlSession.close(); }
执行结果:
批量添加
CarMapper接口
/**
* 批量添加车辆信息
* 使用<foreach>标签
* @param cars
* @return
*/
int insertBatchByForeach(@Param("cars")List<Car> cars);
CarMapper.xml
<insert id="insertBatchByForeach">
insert into t_car values
<foreach collection="cars" item="car" separator=",">
(null, #{car.carNum}, #{car.brand}, #{car.guidePrice}, #{car.produceTime}, #{car.carType})
</foreach>
</insert>
测试类:
@Test
public void testInsertBatchByForeach(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car1 = new Car(null, "1001", "凯美瑞", 35.0, "2010-10-11", "燃油车");
Car car2 = new Car(null, "1002", "比亚迪唐", 31.0, "2020-11-11", "新能源");
Car car3 = new Car(null, "1003", "比亚迪宋", 32.0, "2020-10-11", "新能源");
List<Car> cars = Arrays.asList(car1, car2, car3);
int count = mapper.insertBatchByForeach(cars);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
执行结果:
12.7 sql标签与include标签
-
sql标签用来声明sql片段
-
include标签用来将声明的sql片段包含到某个sql语句当中
-
作用:代码复用。易维护。
CarMapper接口
/** * 查询所有汽车信息 * @return */ List<Car> selectAll();
CarMapper.xml
<sql id="carCols"> id, car_num as carNum, brand, guide_price as guidePrice, produce_time as produceTime, car_type as carType </sql> <select id="selectAll" resultType="com.powernode.mybatis.pojo.Car"> select <include refid="carCols"></include> from t_car </select>
测试类:
@Test public void testSelectAll(){ SqlSession sqlSession = SqlSessionUtil.openSession(); CarMapper mapper = sqlSession.getMapper(CarMapper.class); List<Car> cars = mapper.selectAll(); cars.forEach(car -> System.out.println(car)); sqlSession.close(); }
执行结果:
十三、MyBatis的高级映射及延迟加载
-
模块名:mybatis-009-advanced-mapping
-
打包方式:jar
-
依赖:mybatis依赖、mysql驱动依赖、junit依赖、logback依赖
-
配置文件:mybatis-config.xml、logback.xml、jdbc.properties
-
拷贝工具类:SqlSessionUtil
-
准备数据库表:一个班级对应多个学生。
- 班级表:t_clazz:
- 学生表:t_student:
-
创建pojo:Student、Clazz
Student.java
package com.powernode.mybatis.pojo; /** * 学生类 * @author ShiningSong * @version 1.0 * @since 1.0 */ public class Student { private Long sid; private String sname; ...... }
Clazz.java
package com.powernode.mybatis.pojo; /** * 班级类 * @author ShiningSong * @version 1.0 * @since 1.0 */ public class Clazz { private Long cid; private String cname; ...... }
-
创建mapper接口:StudentMapper、ClazzMapper
-
创建mapper映射文件:StudentMapper.xml、ClazzMapper.xml
13.1 多对一
多种方式,常见的包括三种:
- 第一种方式:一条SQL语句,级联属性映射。
- 第二种方式:一条SQL语句,association。
- 第三种方式:两条SQL语句,分步查询。(这种方式常用:优点一是可复用。优点二是支持懒加 载。)
第一种方式:级联属性映射
-
pojo类Student中添加一个属性:Clazz clazz; 表示学生关联的班级对象。
package com.powernode.mybatis.pojo; /** * 学生类 * @author ShiningSong * @version 1.0 * @since 1.0 */ public class Student { private Long sid; private String sname; private Clazz clazz; @Override public String toString() { return "Student{" + "sid=" + sid + ", sname='" + sname + '\'' + ", clazz=" + clazz + '}'; } public Long getSid() { return sid; } public void setSid(Long sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } public Clazz getClazz() { return clazz; } public void setClazz(Clazz clazz) { this.clazz = clazz; } public Student(Long sid, String sname, Clazz clazz) { this.sid = sid; this.sname = sname; this.clazz = clazz; } public Student() { } }
StudentMapper接口
package com.powernode.mybatis.mapper; import com.powernode.mybatis.pojo.Student; /** * 学生表映射接口 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface StudentMapper { /** * 通过sid查询学生信息 * @param sid * @return */ Student selectBySid(Long sid); }
StudentMapper.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.powernode.mybatis.mapper.StudentMapper"> <resultMap id="studentResultMap" type="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <result property="clazz.cid" column="cid"/> <result property="clazz.cname" column="cname"/> </resultMap> <select id="selectBySid" resultMap="studentResultMap"> select s.*, c.* from t_student s left join t_clazz c on s.cid = c.cid where sid = #{sid} </select> </mapper>
测试类:
package com.powernode.mybatis.test; import com.mysql.cj.xdevapi.SqlUpdateResult; import com.powernode.mybatis.mapper.StudentMapper; import com.powernode.mybatis.pojo.Student; import com.powernode.mybatis.utils.SqlSessionUtil; import org.apache.ibatis.session.SqlSession; import org.junit.Test; public class AdvancedMappingTest { @Test public void testSelectBySid(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectBySid(2L); System.out.println(student); sqlSession.close(); } }
执行结果:
第二种方式:association
-
其他位置都不需要修改,只需要修改resultMap中的配置:association即可。
StudentMapper.xml
<resultMap id="studentResultMap" type="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <!--<result property="clazz.cid" column="cid"/> <result property="clazz.cname" column="cname"/>--> <association property="clazz" javaType="Clazz"> <id property="cid" column="cid"/> <result property="cname" column="cname"/> </association> </resultMap>
-
association翻译为:关联。
-
学生对象关联一个班级对象。
第三种方式:分步查询
-
其他位置不需要修改,只需要修改以及添加以下三处:
- 第一处:在ClazzMapper接口中添加方法
ClazzMapper接口
package com.powernode.mybatis.mapper; import com.powernode.mybatis.pojo.Clazz; /** * 班级表映射接口 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface ClazzMapper { /** * 根据cid查询班级信息 * @param cid * @return */ Clazz selectByCid(Long cid); }
- 第二处:在ClazzMapper.xml文件中进行配置
ClazzMapper.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.powernode.mybatis.mapper.ClazzMapper"> <select id="selectByCid" resultType="Clazz"> select * from t_clazz where cid = #{cid} </select> </mapper>
- 第三处:association中select位置填写sqlId。sqlId=namespace.id。其中column属性作为这条子sql语句的条件。
StudentMapper.xml
<resultMap id="studentResultMap" type="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <!--<result property="clazz.cid" column="cid"/> <result property="clazz.cname" column="cname"/>--> <!--<association property="clazz" javaType="Clazz"> <id property="cid" column="cid"/> <result property="cname" column="cname"/> </association>--> <association property="clazz" select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid" column="cid"/> </resultMap> <select id="selectBySid" resultMap="studentResultMap"> select s.* from t_student s where sid=#{sid} </select>
执行结果:可以很明显看到先后有两条sql语句执行
分步优点:
- 第一个优点:代码复用性增强。
- 第二个优点:支持延迟加载。【暂时访问不到的数据可以先不查询。提高程序的执行效率。】
13.2 多对一延迟加载
-
要想支持延迟加载,非常简单,只需要在association标签中添加fetchType="lazy"即可。
-
修改StudentMapper.xml文件:
StudentMapper.xml
<resultMap id="studentResultMap" type="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <association property="clazz" select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid" column="cid" fetchType="lazy"/> </resultMap>
测试类:
@Test public void testSelectBySid(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectBySid(2L); // System.out.println(student); // 只获取学生姓名 System.out.println("学生姓名:" + student.getSname()); sqlSession.close(); }
执行结果:
如果后续需要使用到学生所在班级的名称,这个时候才会执行关联的sql语句,修改测试程序:
@Test public void testSelectBySid(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectBySid(2L); // System.out.println(student); // 只获取学生姓名 System.out.println("学生姓名:" + student.getSname()); // 到这里想要获取班级名字 System.out.println("该学生的班级名称:" + student.getClazz().getCname()); sqlSession.close(); }
通过以上的执行结果可以看到,只有当使用到班级名称之后,才会执行关联的sql语句,这就是延迟加载。
-
在mybatis中如何开启全局的延迟加载呢?需要setting配置,如下:
mybatis-config.xml
<settings> <setting name="lazyLoadingEnabled" value="true"/> </settings>
把fetchType="lazy"去掉。
测试类:
@Test public void testSelectBySid(){ SqlSession sqlSession = SqlSessionUtil.openSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); Student student = mapper.selectBySid(2L); // System.out.println(student); // 只获取学生姓名 System.out.println("学生姓名:" + student.getSname()); // 到这里想要获取班级名字 System.out.println("该学生的班级名称:" + student.getClazz().getCname()); sqlSession.close(); }
执行结果:
-
通过以上的测试可以看出,我们已经开启了全局延迟加载策略。
-
开启全局延迟加载之后,所有的sql都会支持延迟加载,如果某个sql你不希望它支持延迟加载怎么办呢?
- 将fetchType设置为eager:
StudentMapper.xml
<resultMap id="studentResultMap" type="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> <association property="clazz" select="com.powernode.mybatis.mapper.ClazzMapper.selectByCid" column="cid" fetchType="eager"/> </resultMap>
执行结果:
这样的话,针对某个特定的sql,你就关闭了延迟加载机制。
-
后期我们要不要开启延迟加载机制,主要看实际的业务需求是怎样的。
13.3 一对多
-
一对多的实现,通常是在一的一方中有List集合属性。
-
在Clazz类中添加List stus; 属性。
Clazz.java
package com.powernode.mybatis.pojo; import java.util.List; /** * 班级类 * @author ShiningSong * @version 1.0 * @since 1.0 */ public class Clazz { private Long cid; private String cname; private List<Student> students; @Override public String toString() { return "Clazz{" + "cid=" + cid + ", cname='" + cname + '\'' + ", students=" + students + '}'; } public Long getCid() { return cid; } public void setCid(Long cid) { this.cid = cid; } public String getCname() { return cname; } public void setCname(String cname) { this.cname = cname; } public List<Student> getStudents() { return students; } public void setStudents(List<Student> students) { this.students = students; } public Clazz(Long cid, String cname, List<Student> students) { this.cid = cid; this.cname = cname; this.students = students; } public Clazz() { } }
-
一对多的实现通常包括两种实现方式:
- 第一种方式:collection
- 第二种方式:分步查询
第一种方式:collection
ClazzMapper接口
/**
* 根据cid查询班级信息
* 同时也要查询班级的学生信息
* @param cid
* @return
*/
Clazz selectClazzAndStudentsByCid(Long cid);
ClazzMapper.xml
<resultMap id="clazzResultMap" type="Clazz">
<id property="cid" column="cid"/>
<result property="cname" column="cname"/>
<collection property="students" ofType="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
</collection>
</resultMap>
<select id="selectClazzAndStudentsByCid" resultMap="clazzResultMap">
select *
from t_clazz c
left join t_student s
on c.cid = s.cid
where c.cid = #{cid}
</select>
测试类:
@Test
public void testSelectClazzAndStudentsByCid(){
SqlSession sqlSession = SqlSessionUtil.openSession();
ClazzMapper mapper = sqlSession.getMapper(ClazzMapper.class);
Clazz clazz = mapper.selectClazzAndStudentsByCid(1001L);
System.out.println(clazz);
sqlSession.close();
}
执行结果:
Clazz{cid=1001,
cname='高三1班',
students=[
Student{sid=1, sname='张三', clazz=null},
Student{sid=2, sname='李四', clazz=null},
Student{sid=3, sname='王五', clazz=null}
]
}
第二种方式:分步查询
-
修改以下三个位置即可:
StudentMapper接口
/** * 根据cid查询学生信息返回List集合 * @param cid * @return */ List<Student> selectByCid(Long cid);
StudentMapper.xml
<select id="selectByCid" resultType="Student"> select * from t_student where cid = #{cid} </select>
ClazzMapper.xml
<resultMap id="clazzResultMap" type="Clazz"> <id property="cid" column="cid"/> <result property="cname" column="cname"/> <!--<collection property="students" ofType="Student"> <id property="sid" column="sid"/> <result property="sname" column="sname"/> </collection>--> <collection property="students" select="com.powernode.mybatis.mapper.StudentMapper.selectByCid" column="cid"/> </resultMap> <select id="selectClazzAndStudentsByCid" resultMap="clazzResultMap"> select * from t_clazz c where c.cid = #{cid} </select>
测试类:
@Test public void testSelectClazzAndStudentsByCid(){ SqlSession sqlSession = SqlSessionUtil.openSession(); ClazzMapper mapper = sqlSession.getMapper(ClazzMapper.class); Clazz clazz = mapper.selectClazzAndStudentsByCid(1001L); System.out.println(clazz); sqlSession.close(); }
执行结果:
13.4 一对多延迟加载
- 一对多延迟加载机制和多对一是一样的。同样是通过两种方式:
- 第一种:fetchType=“lazy”
- 第二种:修改全局的配置setting,lazyLoadingEnabled=true,如果开启全局延迟加载,想让某个 sql不使用延迟加载:fetchType=“eager”
十四、MyBatis的缓存
- 缓存:cache
- 缓存的作用:通过减少IO的方式,来提高程序的执行效率。
- mybatis的缓存:将select语句的查询结果放到缓存(内存)当中,下一次还是这条select语句的话,直 接从缓存中取,不再查数据库。一方面是减少了IO。另一方面不再执行繁琐的查找算法。效率大大提升。
- mybatis缓存包括:
- 一级缓存:将查询到的数据存储到SqlSession中。
- 二级缓存:将查询到的数据存储到SqlSessionFactory中。
- 或者集成其它第三方的缓存:比如EhCache【Java语⾔开发的】、Memcache【C语⾔开发的】 等。
- 缓存只针对于DQL语句,也就是说缓存机制只对应select语句。
14.1 一级缓存
-
一级缓存默认是开启的。不需要做任何配置。
-
原理:只要使用同一个SqlSession对象执行同一条SQL语句,就会走缓存。
-
模块名:mybatis-010-cache
CarMapper接口
package com.powernode.mybatis.mapper; import com.powernode.mybatis.pojo.Car; /** * 汽车表映射接口 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface CarMapper { /** * 根据id获取Car信息 * @return */ Car selectById(Long id); }
CarMapper.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.powernode.mybatis.mapper.CarMapper"> <select id="selectById" resultType="Car"> select * from t_car where id=#{id} </select> </mapper>
测试类:
package com.powernode.mybatis.test; import com.powernode.mybatis.mapper.CarMapper; import com.powernode.mybatis.pojo.Car; 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 org.junit.Test; import java.io.IOException; public class CacheTest { @Test public void testSelectById() throws IOException { // 不能使用工具类 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml")); SqlSession sqlSession1 = sqlSessionFactory.openSession(); CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class); CarMapper mapper2 = sqlSession1.getMapper(CarMapper.class); Car car1 = mapper1.selectById(12L); System.out.println(car1); Car car2 = mapper2.selectById(12L); System.out.println(car2); SqlSession sqlSession2 = sqlSessionFactory.openSession(); CarMapper mapper3 = sqlSession2.getMapper(CarMapper.class); CarMapper mapper4 = sqlSession2.getMapper(CarMapper.class); Car car3 = mapper3.selectById(12L); System.out.println(car3); Car car4 = mapper4.selectById(12L); System.out.println(car4); sqlSession1.close(); sqlSession2.close(); } }
执行结果:
-
什么情况下不走缓存?
- 第一种:不同的SqlSession对象。
- 第二种:查询条件变化了。
-
一级缓存失效情况包括两种:
- 第一种:第一次查询和第二次查询之间,手动清空了一级缓存。
Car car1 = mapper1.selectById(12L); System.out.println(car1); // 手动清空 sqlSession1.clearCache(); Car car2 = mapper2.selectById(12L); System.out.println(car2);
执行结果:
- 第二种:第一次查询和第二次查询之间,执行了增删改操作。【这个增删改和哪张表没有关系,只要有insert delete update操作,一级缓存就失效。】
CarMapper接口
/** * 插入账户信息 * 任务:在一级缓存之间做一个CUD操作 */ void insertAccount();
CarMapper.xml
<insert id="insertAccount"> insert into t_act values (3, 'act003', 10000) </insert>
测试类:
Car car1 = mapper1.selectById(12L); System.out.println(car1); // 插入一次insert操作(对其他表也行) mapper1.insertAccount(); Car car2 = mapper2.selectById(12L); System.out.println(car2);
执行结果:
14.2 二级缓存
-
二级缓存的范围是SqlSessionFactory。
-
使用二级缓存需要具备以下几个条件:
- <setting name=“cacheEnabled” value=“true”>全局性地开启或关闭所有映射器配置文件中已配置 的任何缓存。默认就是true,无需设置。
- 在需要使用二级缓存的SqlMapper.xml文件中添加配置:<cache/>
- 使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
- SqlSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
-
测试二级缓存:
Car.java
package com.powernode.mybatis.pojo; import java.io.Serializable; /** * 封装汽车的相关信息的pojo类,普通的java类 * @author ShiningSong * @version 1.1 * @since 1.0 */ public class Car implements Serializable { // 实现Serializable接口 ...... }
CarMapper.xml
<cache/>
测试类:
@Test public void testSelectById2() throws IOException { // 不能使用工具类 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml")); SqlSession sqlSession1 = sqlSessionFactory.openSession(); CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class); Car car1 = mapper1.selectById(12L); System.out.println(car1); // 关键一步 sqlSession1.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); CarMapper mapper3 = sqlSession2.getMapper(CarMapper.class); Car car3 = mapper3.selectById(12L); System.out.println(car3); sqlSession2.close(); }
执行结果:
二级缓存的失效:只要两次查询之间出现了增删改操作。二级缓存就会失效。【一级缓存也会失效】
二级缓存的相关配置:
- eviction:指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略。
- LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
- FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。
- SOFT:软引用。淘汰软引用指向的对象。具体算法和JVM的垃圾回收算法有关。
- WEAK:弱引用。淘汰弱引用指向的对象。具体算法和JVM的垃圾回收算法有关。
- flushInterval:
- 二级缓存的刷新时间间隔。单位毫秒。如果没有设置。就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据。除非执行了增删改。
- readOnly:
- true:多条相同的sql语句执行之后返回的对象是共享的同一个。性能好。但是多线程并发可能会存在安全问题。
- false:多条相同的sql语句执行之后返回的对象是副本,调用了clone方法。性能一般。但安全。
- size:
- 设置二级缓存中最多可存储的java对象数量。默认值1024。
14.3 MyBatis集成EhCache
-
集成EhCache是为了代替mybatis自带的二级缓存。一级缓存是无法替代的。
-
mybatis对外提供了接口,也可以集成第三方的缓存组件。比如EhCache、Memcache等。都可以。
-
EhCache是Java写的。Memcache是C语⾔写的。所以mybatis集成EhCache较为常见,按照以下步骤操 作,就可以完成集成:
- 第一步:pom.xml引入mybatis整合ehcache的依赖。
<dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.2</version> </dependency> <!---ehcache需要slf4j的日志组件,log4j不好使--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency>
- 第二步:在类的根路径下新建echcache.xml文件,并提供以下配置信息。
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <!--磁盘存储:将缓存中暂时不使用的对象,转移到硬盘,类似于Windows系统的虚拟内存--> <diskStore path="e:/ehcache"/> <!--defaultCache:默认的管理策略--> <!--eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断--> <!--maxElementsInMemory:在内存中缓存的element的最大数目--> <!--overflowToDisk:如果内存中数据超过内存限制,是否要缓存到磁盘上--> <!--diskPersistent:是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false--> <!--timeToIdleSeconds:对象空闲时间(单位:秒),指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问--> <!--timeToLiveSeconds:对象存活时间(单位:秒),指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问--> <!--memoryStoreEvictionPolicy:缓存的3种清空策略--> <!--FIFO:first in first out (先进先出)--> <!--LFU:Less Frequently Used (最少使用).意思是一直以来最少被使用的。缓存的元素有一个hit 属性,hit 值最小的将会被清出缓存--> <!--LRU:Least Recently Used(最近最少使用). (ehcache 默认值).缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存--> <defaultCache eternal="false" maxElementsInMemory="1000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="0" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU"/> </ehcache>
- 第三步:修改SqlMapper.xml文件中的标签,添加type属性。
CarMapper.xml
<cache type="org.mybatis.caches.ehcache.EhcacheCache" />
测试类:
@Test public void testSelectById2() throws IOException { SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml")); SqlSession sqlSession1 = sqlSessionFactory.openSession(); CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class); Car car1 = mapper1.selectById(12L); System.out.println(car1); sqlSession1.close(); SqlSession sqlSession2 = sqlSessionFactory.openSession(); CarMapper mapper3 = sqlSession2.getMapper(CarMapper.class); Car car3 = mapper3.selectById(12L); System.out.println(car3); sqlSession2.close(); }
执行结果:
十五、MyBatis的逆向工程
- 所谓的逆向工程是:根据数据库表逆向生成Java的pojo类,SqlMapper.xml文件,以及Mapper接口类等。
- 要完成这个工作,需要借助别⼈写好的逆向工程插件。
- 思考:使用这个插件的话,需要给这个插件配置哪些信息?
- pojo类名、包名以及生成位置。
- SqlMapper.xml文件名以及生成位置。
- Mapper接口名以及生成位置。
- 连接数据库的信息。
- 指定哪些表参与逆向工程。
- …
15.1 逆向工程配置与生成
第一步:基础环境准备
- 新建模块:mybatis-011-generator
- 打包方式:jar
第二步:在pom中添加逆向工程插件
<!--定制构建过程-->
<build>
<!--可配置多个插件-->
<plugins>
<!--其中的一个插件:mybatis逆向工程插件-->
<plugin>
<!--插件的GAV坐标-->
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.1</version>
<!--允许覆盖-->
<configuration>
<overwrite>true</overwrite>
</configuration>
<!--插件的依赖-->
<dependencies>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
第三步:配置generatorConfig.xml
-
该文件名必须叫做:generatorConfig.xml
-
该文件必须放在类的根路径下。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration> <!-- targetRuntime有两个值: MyBatis3Simple:生成的是基础版,只有基本的增删改查。 MyBatis3:生成的是增强版,除了基本的增删改查之外还有复杂的增删改查。 --> <context id="DB2Tables" targetRuntime="MyBatis3"> <!--防止生成重复代码--> <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin"/> <commentGenerator> <!--是否去掉生成日期--> <property name="suppressDate" value="true"/> <!--是否去除注释--> <property name="suppressAllComments" value="true"/> </commentGenerator> <!--连接数据库信息--> <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/powernode" userId="root" password="root"> </jdbcConnection> <!-- 生成pojo包名和位置 --> <javaModelGenerator targetPackage="com.powernode.mybatis.pojo" targetProject="src/main/java"> <!--是否开启子包--> <property name="enableSubPackages" value="true"/> <!--是否去除字段名的前后空白--> <property name="trimStrings" value="true"/> </javaModelGenerator> <!-- 生成SQL映射文件的包名和位置 --> <sqlMapGenerator targetPackage="com.powernode.mybatis.mapper" targetProject="src/main/resources"> <!--是否开启子包--> <property name="enableSubPackages" value="true"/> </sqlMapGenerator> <!-- 生成Mapper接口的包名和位置 --> <javaClientGenerator type="xmlMapper" targetPackage="com.powernode.mybatis.mapper" targetProject="src/main/java"> <property name="enableSubPackages" value="true"/> </javaClientGenerator> <!-- 表名和对应的实体类名--> <table tableName="t_car" domainObjectName="Car"/> </context> </generatorConfiguration>
第四步:运行插件
15.2 测试逆向工程生成的是否好用
第一步:环境准备
- 依赖:mybatis依赖、mysql驱动依赖、junit依赖、logback依赖
- jdbc.properties
- mybatis-config.xml
- logback.xml
第二步:编写测试程序
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.pojo.CarExample;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.math.BigDecimal;
import java.util.List;
public class GeneratorCompleteTest {
// CarExample类负责封装查询条件的。
@Test
public void testSelect(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 执行查询
// 1. 查询一个
Car car = mapper.selectByPrimaryKey(12L);
System.out.println(car);
// 2. 查询所有(selectByExample,根据条件查询,如果条件是null表示没有条件。)
List<Car> cars = mapper.selectByExample(null);
cars.forEach(car1 -> System.out.println(car1));
System.out.println("=========================================");
// 3. 按照条件进行查询
// QBC 风格:Query By Criteria 一种查询方式,比较面向对象,看不到sql语句。
// 封装条件,通过CarExample对象来封装查询条件
CarExample carExample = new CarExample();
// 调用carExample.createCriteria()方法来创建查询条件
carExample.createCriteria()
.andBrandLike("帕萨特")
.andGuidePriceGreaterThan(new BigDecimal(20.0));
// 添加or
carExample.or().andCarTypeEqualTo("燃油车");
// 执行查询
List<Car> cars2 = mapper.selectByExample(carExample);
cars2.forEach(car2 -> System.out.println(car2));
sqlSession.close();
}
}
十六、MyBatis使用PageHelper
16.1 limit分页
- mysql的limit后面两个数字:
- 第一个数字:startIndex(起始下标。下标从0开始。)
- 第二个数字:pageSize(每页显示的记录条数)
- 假设已知页码pageNum,还有每页显示的记录条数pageSize,第一个数字可以动态的获取吗?
- startIndex = (pageNum - 1) * pageSize
- 所以,标准通用的mysql分页SQL:
select
*
from
tableName .......
limit
(pageNum - 1) * pageSize, pageSize
- 使用mybatis应该怎么做?
- 模块名:mybatis-012-page
CarMapper接口
package com.powernode.mybatis.mapper;
import com.powernode.mybatis.pojo.Car;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 汽车表映射接口
*/
public interface CarMapper {
/**
* 通过分页的方式获取汽车列表信息
* @param startIndex 页码
* @param pageSize 每页显示记录条数
* @return
*/
List<Car> selectAllPage(@Param("startIndex")Integer startIndex, @Param("pageSize")Integer pageSize);
}
CarMapper.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.powernode.mybatis.mapper.CarMapper">
<select id="selectAllPage" resultType="Car">
select * from t_car
limit #{startIndex}, #{pageSize}
</select>
</mapper>
测试类:
package com.powernode.mybatis.test;
import com.powernode.mybatis.mapper.CarMapper;
import com.powernode.mybatis.pojo.Car;
import com.powernode.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class PageTest {
@Test
public void testSelectAllPage(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 页码
Integer pageNum = 2;
// 每页显示记录条数
Integer pageSize = 3;
// 起始下标
Integer startIndex = (pageNum - 1) * pageSize;
List<Car> cars = mapper.selectAllPage(startIndex, pageSize);
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
}
执行结果:
- 获取数据不难,难的是获取分页相关的数据比较难。可以借助mybatis的PageHelper插件。
16.3 PageHelper插件
- 使用PageHelper插件进行分页,更加的便捷。
第一步:引入依赖
<!--PageHelper依赖-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.1</version>
</dependency>
第二步:在mybatis-config.xml文件中配置插件
- typeAliases标签下面进行配置:
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
第三步:编写Java代码
CarMapper接口
/**
* 查询所有车辆信息
* @return
*/
List<Car> selectAll();
CarMapper.xml
<select id="selectAll" resultType="Car">
select * from t_car
</select>
- 关键点:
- 在查询语句之前开启分页功能。
- 在查询语句之后封装PageInfo对象。(PageInfo对象将来会存储到request域当中。在页面上展示。)
测试类:
@Test
public void testSelectAll(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 开启分页
PageHelper.startPage(2,3);
// 执行查询语句
List<Car> cars = mapper.selectAll();
// 获取分页信息对象
PageInfo<Car> pageInfo = new PageInfo<>(cars, 5);
System.out.println(pageInfo);
sqlSession.close();
}
执行结果:
PageInfo{
pageNum=2, pageSize=3, size=3, startRow=4, endRow=6, total=24, pages=8,
list=Page{count=true, pageNum=2, pageSize=3, startRow=3, endRow=6, total=24, pages=8, reasonable=false, pageSizeZero=false}
[
Car{id=8, carNum='1003', brand='丰田霸道', guidePrice=30.0, produceTime='2000-10-11', carType='燃油车'},
Car{id=9, carNum='102', brand='比亚迪汉', guidePrice=30.23, produceTime='2018-09-10', carType='电车'},
Car{id=10, carNum='1003', brand='丰田霸道', guidePrice=30.0, produceTime='2000-10-11', carType='燃油车'}
],
prePage=1, nextPage=3, isFirstPage=false, isLastPage=false, hasPreviousPage=true, hasNextPage=true,
navigatePages=5, navigateFirstPage=1, navigateLastPage=5, navigatepageNums=[1, 2, 3, 4, 5]
}
十七、MyBatis的注解式开发
- mybatis中也提供了注解式开发方式,采用注解可以减少Sql映射文件的配置。
- 当然,使用注解式开发的话,sql语句是写在java程序中的,这种方式也会给sql语句的维护带来成本。 官方是这么说的:
使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅⼒不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
-
使用注解编写复杂的SQL是这样的:
-
原则:简单sql可以注解。复杂sql使用xml。
-
模块名:mybatis-013-annotation
-
打包方式:jar
-
依赖:mybatis,mysql驱动,junit,logback
-
配置文件:jdbc.properties、mybatis-config.xml、logback.xml
-
pojo:com.powernode.mybatis.pojo.Car
-
mapper接口:com.powernode.mybatis.mapper.CarMapper
17.1 @Insert
CarMapper接口
/**
* 使用@Insert()注解插入车辆信息
* @return
*/
@Insert(value = "insert into t_car values(null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType})")
int insert(Car car);
测试类:
@Test
public void testInsert(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(null, "1112", "卡罗拉", 30.0, "2000-10-10", "燃油车");
int count = mapper.insert(car);
System.out.println("插入了几条数据:" + count);
sqlSession.commit();
sqlSession.close();
}
17.2 @Delete
CarMapper接口
/**
* 使用@Delete()注解删除车辆信息
* @param id
* @return
*/
@Delete("delete from t_car where id = #{id}")
int deleteById(Long id);
测试类:
@Test
public void testDeleteById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
int count = mapper.deleteById(29L);
System.out.println("删除了几条记录:" + count);
sqlSession.commit();
sqlSession.close();
}
17.3 @Update
CarMapper接口
/**
* 使用@Update()注解更新车辆信息
* @param car
* @return
*/
@Update("update t_car set car_num=#{carNum}, brand=#{brand}, guide_price=#{guidePrice}, produce_time=#{produceTime}, car_type=#{carType} where id = #{id}")
int updateById(Car car);
测试类:
@Test
public void testUpdateById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(44L, "1001", "凯美瑞", 30.5, "2000-11-10", "新能源");
int count = mapper.updateById(car);
System.out.println("更新了几条数据:" + count);
sqlSession.commit();
sqlSession.close();
}
17.4 @Select
CarMapper接口
/**
* 使用@Select()、@Results({@Result()...})注解更新车辆信息
* @param id
* @return
*/
@Select("select * from t_car where id = #{id}")
@Results({
@Result(column = "id", property = "id", id = true),
@Result(column = "car_num", property = "carNum"),
@Result(column = "brand", property = "brand"),
@Result(column = "guide_price", property = "guidePrice"),
@Result(column = "produce_time", property = "produceTime"),
@Result(column = "car_type", property = "carType"),
})
Car selectById(Long id);
测试类:
@Test
public void testSelectById(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = mapper.selectById(44L);
System.out.println(car);
sqlSession.close();
}
执行结果:
五、手写MyBatis框架(掌握原理)
- 警示:该部分内容有难度,基础较弱的程序员可能有些部分是听不懂的,如果无法跟下来,可直接跳 过,不影响后续知识点的学习。
- 当然,如果你要能够跟下来,必然会让你加深对MyBatis框架的理解。 我们给自己的框架起个名:GodBatis(起名灵感来源于:my god!!! 我的天呢!)
5.1 dom4j解析XML文件
-
该部分内容不再赘述,不会解析XML的,请观看⽼杜前面讲解的dom4j解析XML文件的视频。
-
模块名:parse-xml-by-dom4j(普通的Java Maven模块)
-
第一步:引入dom4j的依赖
<?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>org.group</groupId> <artifactId>parse-xml-by-dom4j</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <!--dome4j依赖--> <dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>2.1.3</version> </dependency> <!--jaxen依赖--> <dependency> <groupId>jaxen</groupId> <artifactId>jaxen</artifactId> <version>1.2.0</version> </dependency> <!--junit依赖--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> </project>
-
第二步:编写配置文件godbatis-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> <environments default="powernodeDB"> <environment id="powernodeDB"> <transactionManager type="MANAGED"/> <dataSource type="UNPOOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/powernode"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> <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> <mapper resource="CarMapper.xml"/> </mappers> </configuration>
-
第三步:解析godbatis-config.xml
@Test public void testParseMyBatisConfigXML() throws DocumentException { // 创建SAXReader对象 SAXReader reader = new SAXReader(); // 获取输入流 InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml"); // 读取XML文件 Document document = reader.read(is); // // 获取文档当中的根标签 // Element rootElt = document.getRootElement(); // String rootEltName = rootElt.getName(); // System.out.println("根节点的名字:" + rootEltName); // 获取default默认的环境id // xpath是做标签路径匹配的,能够让我们快速定位XML文件中的元素。 // 以下的xpath代表了:从根下开始找configuration标签,然后找configuration标签下的子标签environments String xpath = "/configuration/environments"; Element environments = (Element) document.selectSingleNode(xpath); // Element是Node类的子类,方法更多,使用更便捷。 // 获取属性的值 String defaultEnvironmentId = environments.attributeValue("default"); System.out.println("默认环境的id:" + defaultEnvironmentId); // 具体的环境environment xpath = "/configuration/environments/environment[@id='" + defaultEnvironmentId + "']"; // System.out.println(xpath); Element environment = (Element) document.selectSingleNode(xpath); // 获取environment节点下的transactionManager节点(Element的element()方法用来获取孩子节点) Element transactionManager = environment.element("transactionManager"); String transactionType = transactionManager.attributeValue("type"); System.out.println("事务管理器的类型:" + transactionType); // 获取dataSource节点 Element dataSource = environment.element("dataSource"); String dataSourceType = dataSource.attributeValue("type"); System.out.println("数据源的类型:" + dataSourceType); // 获取dataSource节点下的所有子节点 List<Element> propertyElts = dataSource.elements(); // 遍历 propertyElts.forEach(propertyElt -> { String name = propertyElt.attributeValue("name"); String value = propertyElt.attributeValue("value"); System.out.println(name + "=" + value); }); // 获取所有的mapper标签 // 不想从根下开始获取,你想从任意位置开始,获取所有的某个标签,xpath该这样写 xpath = "//mapper"; List<Node> mappers = document.selectNodes(xpath); // 遍历 mappers.forEach(mapper -> { Element mapperElt = (Element) mapper; String resource = mapperElt.attributeValue("resource"); System.out.println(resource); }); }
执行结果:
-
第四步:编写配置文件sqlmapper.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="car"> <insert id="insertCar"> insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType}) </insert> <select id="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> </mapper>
-
第五步:解析sqlmapper.xml
@Test public void testParseSqlMapperXML() throws DocumentException { SAXReader reader = new SAXReader(); InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("CarMapper.xml"); Document document = reader.read(is); // 获取namespace String xpath = "/mapper"; Element mapper = (Element) document.selectSingleNode(xpath); String namespace = mapper.attributeValue("namespace"); System.out.println(namespace); // 获取mapper节点下的所有子节点 List<Element> elements = mapper.elements(); // 遍历 elements.forEach(element -> { // 获取sqlId String id = element.attributeValue("id"); System.out.print(id+"\t"); // 获取resultType String resultType = element.attributeValue("resultType"); System.out.println(resultType); // 获取标签中的sql语句(表示获取标签中的文本内容,而且去除前后空白) String sql = element.getTextTrim(); System.out.println(sql); // insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType}) // insert into t_car values(null,?,?,?,?,?) // mybatis封装了jdbc。早晚要执行带有?的sql语句 // 转换 String newSql = sql.replaceAll("#\\{[0-9A-Za-z_$]*}", "?"); System.out.println(newSql); }); }
执行结果:
5.2 GodBatis
-
手写框架之前,如果没有思路,可以先参考一下mybatis的客户端程序,通过客户端程序来逆推需要的 类,参考代码:
@Test public void testInsert(){ SqlSession sqlSession = null; try { // 1.创建SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSession FactoryBuilder(); // 2.创建SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.bui ld(Resources.getResourceAsStream("mybatis-config.xml")); // 3.创建SqlSession对象 sqlSession = sqlSessionFactory.openSession(); // 4.执行SQL Car car = new Car(null, "111", "宝⻢X7", "70.3", "2010-10-11", "燃油车"); int count = sqlSession.insert("insertCar",car); System.out.println("更新了几条记录:" + count); // 5.提交 sqlSession.commit(); } catch (Exception e) { // 回滚 if (sqlSession != null) { sqlSession.rollback(); } e.printStackTrace(); } finally { // 6.关闭 if (sqlSession != null) { sqlSession.close(); } } } @Test public void testSelectOne(){ SqlSession sqlSession = null; try { // 1.创建SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); // 2.创建SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml")); // 3.创建SqlSession对象 sqlSession = sqlSessionFactory.openSession(); // 4.执行SQL Car car = (Car)sqlSession.selectOne("selectCarByCarNum", "111"); System.out.println(car); // 5.提交 sqlSession.commit(); } catch (Exception e) { // 回滚 if (sqlSession != null) { sqlSession.rollback(); } e.printStackTrace(); } finally { // 6.关闭 if (sqlSession != null) { sqlSession.close(); } } }
第一步:IDEA中创建模块
-
模块:godbatis(创建普通的Java Maven模块,打包方式jar),引入相关依赖
<?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>org.god</groupId> <artifactId>godbatis</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.dom4j</groupId> <artifactId>dom4j</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>jaxen</groupId> <artifactId>jaxen</artifactId> <version>1.2.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> </project>
第二步:资源工具类,方便获取指向配置文件的输入流
Resources.java
package org.god.core;
import java.io.InputStream;
/**
* 资源工具类
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public class Resources {
/**
* 从类路径中获取配置文件的输入流
* @param config
* @return 输入流,该输入流执行类路径中的配置文件
*/
public static InputStream getResourceAsStream(String config){
return Thread.currentThread().getContextClassLoader().getResourceAsStream(config);
}
}
第三步:定义SqlSessionFactoryBuilder类
-
提供一个无参数构造方法,再提供一个build方法,该build方法要返回SqlSessionFactory对象
package org.god.core; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; import javax.sql.DataSource; import java.io.InputStream; import java.util.HashMap; import java.util.Map; /** * SqlSessionFactory对象构建器 * * @author ShiningSong * @version 1.0 * @since 1.0 */ public class SqlSessionFactoryBuilder { /** * 创建构建器对象 */ public SqlSessionFactoryBuilder() { } /** * 获取SqlSessionFactory对象 * 该方法的主要功能是:读取godbatis核心配置文件,并构建SqlSessionFactory对象 * @param inputStream 指向核心配置文件的输入流 * @return SqlSessionFactory对象 */ public SqlSessionFactory build(InputStream inputStream) throws DocumentException { // 解析配置文件,创建数据源对象 // 解析配置文件,创建事务管理器对象 // 解析配置文件,获取所有的SQL映射对象 // 将以上信息封装到SqlSessionFactory对象中 // 返回 return null; } }
第四步:分析SqlSessionFactory类中有哪些属性
- 事务管理器
- GodJDBCTransaction
- SQL映射对象集合
- Map<String, GodMappedStatement>>
第五步:定义GodJDBCTransaction
-
事务管理器最好是定义一个接口,然后每一个具体的事务管理器都实现这个接口。
TransactionManager接口
package org.god.core; import java.sql.Connection; /** * 事务管理器接口 * @author ShiningSong * @version 1.0 * @since 1.0 */ public interface TransactionManager { /** * 提交事务 */ void commit(); /** * 回滚事务 */ void rollback(); /** * 关闭事务 */ void close(); /** * 开启连接 */ void openConnection(); /** * 获取连接对象 * @return */ Connection getConnection(); }
GodJDBCTransaction.java
package org.god.core; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; /** * JDBC事务管理器 * @author ShiningSong * @version 1.0 * @since 1.0 */ public class GodJDBCTransaction implements TransactionManager{ /** * 连接对象,控制事务时需要 */ private Connection conn; /** * 数据源对象 */ private DataSource dataSource; /** * 自动提交标志 * true表示自动提交 * false表示手动提交 */ private boolean autoCommit; /** * 构造事务管理器对象 * @param dataSource * @param autoCommit */ public GodJDBCTransaction(DataSource dataSource, boolean autoCommit) { this.dataSource = dataSource; this.autoCommit = autoCommit; } @Override public void commit() { try { conn.commit(); } catch (SQLException e) { throw new RuntimeException(e); } } @Override public void rollback() { try { conn.rollback(); } catch (SQLException e) { throw new RuntimeException(e); } } @Override public void close() { try { conn.close(); } catch (SQLException e) { throw new RuntimeException(e); } } @Override public void openConnection() { try { this.conn = dataSource.getConnection(); this.conn.setAutoCommit(this.autoCommit); } catch (SQLException e) { throw new RuntimeException(e); } } @Override public Connection getConnection() { return conn; } }
第六步:事务管理器中需要数据源,定义GodUNPOOLEDDataSource
package org.god.core;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 数据源实现类,不使用连接池
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public class GodUNPOOLEDDataSource implements javax.sql.DataSource{
private String url;
private String username;
private String password;
public GodUNPOOLEDDataSource(String driver, String url, String username, String password) {
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
this.url = url;
this.username = username;
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
第七步:定义GodMappedStatement
package org.god.core;
/**
* SQL映射实体类
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public class GodMappedStatement {
private String sqlId;
private String resultType;
private String sql;
private String parameterType;
private String sqlType;
@Override
public String toString() {
return "GodMappedStatement{" +
"sqlId='" + sqlId + '\'' +
", resultType='" + resultType + '\'' +
", sql='" + sql + '\'' +
", parameterType='" + parameterType + '\'' +
", sqlType='" + sqlType + '\'' +
'}';
}
public GodMappedStatement() {
}
public GodMappedStatement(String sqlId, String resultType, String sql, String parameterType, String sqlType) {
this.sqlId = sqlId;
this.resultType = resultType;
this.sql = sql;
this.parameterType = parameterType;
this.sqlType = sqlType;
}
public String getSqlId() {
return sqlId;
}
public void setSqlId(String sqlId) {
this.sqlId = sqlId;
}
public String getResultType() {
return resultType;
}
public void setResultType(String resultType) {
this.resultType = resultType;
}
public String getSql() {
return sql;
}
public void setSql(String sql) {
this.sql = sql;
}
public String getParameterType() {
return parameterType;
}
public void setParameterType(String parameterType) {
this.parameterType = parameterType;
}
public String getSqlType() {
return sqlType;
}
public void setSqlType(String sqlType) {
this.sqlType = sqlType;
}
}
第八步:完善SqlSessionFactory类
package org.god.core;
import java.util.Map;
/**
* SqlSession工厂对象,使用SqlSessionFactory可以获取会话对象
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactory {
private TransactionManager transactionManager;
private Map<String, GodMappedStatement> mappedStatemens;
public SqlSession openSession(){
transactionManager.openConnection();
SqlSession sqlSession = new SqlSession(transactionManager, mappedStatemens);
return sqlSession;
}
public SqlSessionFactory() {
}
public SqlSessionFactory(TransactionManager transactionManager, Map<String, GodMappedStatement> mappedStatemens) {
this.transactionManager = transactionManager;
this.mappedStatemens = mappedStatemens;
}
public TransactionManager getTransactionManager() {
return transactionManager;
}
public void setTransactionManager(TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public Map<String, GodMappedStatement> getMappedStatemens() {
return mappedStatemens;
}
public void setMappedStatemens(Map<String, GodMappedStatement> mappedStatemens) {
this.mappedStatemens = mappedStatemens;
}
}
第九步:完善SqlSessionFactoryBuilder中的build方法
package org.god.core;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import javax.sql.DataSource;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/**
* SqlSessionFactory对象构建器
*
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public class SqlSessionFactoryBuilder {
/**
* 创建构建器对象
*/
public SqlSessionFactoryBuilder() {
}
/**
* 获取SqlSessionFactory对象
* 该方法的主要功能是:读取godbatis核心配置文件,并构建SqlSessionFactory对象
* @param inputStream 指向核心配置文件的输入流
* @return SqlSessionFactory对象
*/
public SqlSessionFactory build(InputStream inputStream) throws DocumentException {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(inputStream);
Element environmentsElt = (Element) document.selectSingleNode("/configuration/environments");
String defaultEnv = environmentsElt.attributeValue("default");
Element environmentElt = (Element) document.selectSingleNode("/configuration/environments/environment[@id='" + defaultEnv + "']");
// 解析配置文件,创建数据源对象
Element dataSourceElt = environmentElt.element("dataSource");
DataSource dataSource = getDataSource(dataSourceElt);
// 解析配置文件,创建事务管理器对象
Element transactionManagerElt = environmentElt.element("transactionManager");
TransactionManager transactionManager = getTransactionManager(transactionManagerElt, dataSource);
// 解析配置文件,获取所有的SQL映射对象
// Element mappersElt = environmentsElt.element("mappers");
Element mappersElt = (Element) document.selectSingleNode("/configuration/mappers");
Map<String, GodMappedStatement> mappedStatements = getMappedStatements(mappersElt);
// 将以上信息封装到SqlSessionFactory对象中
SqlSessionFactory sqlSessionFactory = new SqlSessionFactory(transactionManager, mappedStatements);
// 返回
return sqlSessionFactory;
}
/**
* 通过mapperElt获取SQL映射实体类
* @param mappersElt
* @return
*/
private Map<String, GodMappedStatement> getMappedStatements(Element mappersElt){
Map<String, GodMappedStatement> mappedStatements = new HashMap<>();
mappersElt.elements().forEach(mapperElt -> {
try {
String resource = mapperElt.attributeValue("resource");
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(Resources.getResourceAsStream(resource));
Element mapper = (Element) document.selectSingleNode("/mapper");
String namespace = mapper.attributeValue("namespace");
mapper.elements().forEach(sqlMapper -> {
String sqlId = sqlMapper.attributeValue("id");
String resultType = sqlMapper.attributeValue("resultType");
String sql = sqlMapper.getTextTrim();
String parameterType = sqlMapper.attributeValue("parameterType");
String sqlType = sqlMapper.getName().toLowerCase();
GodMappedStatement godMappedStatement = new GodMappedStatement(sqlId, resultType, sql, parameterType, sqlType);
mappedStatements.put(namespace + "." + sqlId, godMappedStatement);
});
} catch (DocumentException e) {
throw new RuntimeException(e);
}
});
return mappedStatements;
}
/**
* 通过transactionManagerElt和数据源信息创建事务
* @param transactionManagerElt
* @param dataSource
* @return
*/
private TransactionManager getTransactionManager(Element transactionManagerElt, DataSource dataSource) {
String type = transactionManagerElt.attributeValue("type").toUpperCase();
TransactionManager transactionManager = null;
if("JDBC".equals(type)){
// 使用JDBC事务
transactionManager = new GodJDBCTransaction(dataSource, false);
}else if ("MANAGED".equals(type)){
// 使用MANAGED事务,事务管理器是交给JEE容器的
}
return transactionManager;
}
/**
* 通过dataSourceElt获取数据源信息
* @param dataSourceElt
* @return
*/
private DataSource getDataSource(Element dataSourceElt) {
// 获取所有的数据源的属性配置
Map<String, String> dataSourceMap = new HashMap<>();
dataSourceElt.elements().forEach(propertyElt -> {
dataSourceMap.put(propertyElt.attributeValue("name"), propertyElt.attributeValue("value"));
});
String dataSourceType = dataSourceElt.attributeValue("type").toUpperCase();
DataSource dataSource = null;
if ("UNPOOLED".equals(dataSourceType)) {
dataSource = new GodUNPOOLEDDataSource(dataSourceMap.get("driver"), dataSourceMap.get("url"), dataSourceMap.get("username"),dataSourceMap.get("password"));
} else if ("POOLED".equals(dataSourceType)) {
} else if ("JNDI".equals(dataSourceType)){
}
return dataSource;
}
}
- MappedStatement类的封装:
第十步:在SqlSessionFactory中添加openSession方法
public SqlSession openSession(){
transactionManager.openConnection();
SqlSession sqlSession = new SqlSession(transactionManager, mappedStatemens);
return sqlSession;
}
第十一步:编写SqlSession类中commit rollback close方法
package org.god.core;
import jdk.nashorn.internal.ir.CallNode;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
/**
* 创建数据库会话对象
*
* @author ShiningSong
* @version 1.0
* @since 1.0
*/
public class SqlSession {
private TransactionManager transactionManager;
private Map<String, GodMappedStatement> mappedStatements;
public SqlSession(TransactionManager transactionManager, Map<String, GodMappedStatement> mappedStatements) {
this.transactionManager = transactionManager;
this.mappedStatements = mappedStatements;
}
public void commit() {
try {
transactionManager.getConnection().commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void rollback() {
try {
transactionManager.getConnection().rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public void close() {
try {
transactionManager.getConnection().close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
第十二步:编写SqlSession类中的insert方法
/**
* 插入数据
* @param sqlId 要执行的sqlId
* @param obj 要插入的数据
* @return
*/
public int insert(String sqlId, Object obj) {
GodMappedStatement godMappedStatement = mappedStatements.get(sqlId);
Connection connection = transactionManager.getConnection();
// 获取sql语句
String godbatisSql = godMappedStatement.getSql();
String sql = godbatisSql.replaceAll("#\\{[a-z0-9A-Z_\\$]*}", "?");
//重点一步
Map<Integer, String> map = new HashMap<>();
int index = 1;
while (godbatisSql.indexOf("#") >= 0) {
int beginIndex = godbatisSql.indexOf("#") + 2;
int endIndex = godbatisSql.indexOf("}");
map.put(index++, godbatisSql.substring(beginIndex, endIndex).trim());
godbatisSql = godbatisSql.substring(endIndex + 1);
}
final PreparedStatement ps;
try {
ps = connection.prepareStatement(sql);
// 给?赋值
map.forEach((k, v) -> {
try {
String getMethodName = "get" + v.toUpperCase().charAt(0) + v.substring(1);
Method getMethod = obj.getClass().getDeclaredMethod(getMethodName);
ps.setString(k, getMethod.invoke(obj).toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
int count = ps.executeUpdate();
ps.close();
return count;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
第十三步:编写SqlSession类中的selectOne方法
/**
* 查询一个对象
* @param sqlId
* @param parameterObj
* @return
*/
public Object selectOne(String sqlId, Object parameterObj) {
GodMappedStatement godMappedStatement = mappedStatements.get(sqlId);
Connection connection = transactionManager.getConnection();
// 获取sql语句
String godbatisSql = godMappedStatement.getSql();
String sql = godbatisSql.replaceAll("#\\{[a-z0-9A-Z_\\$]*}", "?");
// 执行sql
PreparedStatement ps = null;
ResultSet rs = null;
Object obj = null;
try {
ps = connection.prepareStatement(sql);
ps.setString(1, parameterObj.toString());
rs = ps.executeQuery();
if (rs.next()) {
// 通过反射,将结果集封装对象
String resultType = godMappedStatement.getResultType();
Class<?> aClass = Class.forName(resultType);
Constructor<?> con = aClass.getDeclaredConstructor();
obj = con.newInstance();
// 给对象obj属性赋值
ResultSetMetaData rsmd = rs.getMetaData();
int columnCount = rsmd.getColumnCount();
for (int i = 1; i < columnCount+1; i++) {
String columnName = rsmd.getColumnName(i);
String setMethodName = "set" + columnName.toUpperCase().charAt(0) + columnName.substring(1);
Method setMethod = aClass.getDeclaredMethod(setMethodName, aClass.getDeclaredField(columnName).getType());
setMethod.invoke(obj, rs.getString(columnName));
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
try {
ps.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return obj;
}
5.3 GodBatis使用Maven打包
-
查看本地仓库中是否已经有jar包:
5.4 使用GodBatis
-
使用GodBatis就和使用MyBatis是一样的。
-
第一步:准备数据库表t_user
-
第二步:创建模块,普通的Java Maven模块:godbatis-test
-
第三步:引入依赖pom.xml
<?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.powernode</groupId> <artifactId>godbatis-test</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.god</groupId> <artifactId>godbatis</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> </project>
-
第四步:编写pojo类 User.java
package com.powernode.godbatis.pojo; /** * 用户类 */ public class User { private String id; private String name; private String email; private String address; @Override public String toString() { return "User{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", email='" + email + '\'' + ", address='" + address + '\'' + '}'; } public User() { } public User(String id, String name, String email, String address) { this.id = id; this.name = name; this.email = email; this.address = address; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
-
第五步:编写核心配置文件:godbatis-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"/> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="lazyLoadingEnabled" value="true"/> </settings> <typeAliases> <package name="com.powernode.mybatis.pojo"/> </typeAliases> <environments default="dev"> <environment id="dev"> <transactionManager type="JDBC"/> <dataSource type="UNPOOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/powernode"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <!--<package name="com.powernode.mybatis.mapper"/>--> <mapper resource="UserMapper.xml"/> </mappers> </configuration>
-
第六步:编写sql映射文件:UserMapper.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="User"> <insert id="insertUser"> insert into t_user values(#{id}, #{name}, #{email}, #{address}) </insert> <select id="selectById" resultType="com.powernode.godbatis.pojo.User"> select * from t_user where id = #{id} </select> </mapper>
-
第七步:编写测试类
package com.powernode.godbatis.test; import com.powernode.godbatis.pojo.User; import org.dom4j.DocumentException; import org.god.core.Resources; import org.god.core.SqlSession; import org.god.core.SqlSessionFactory; import org.god.core.SqlSessionFactoryBuilder; import org.junit.Test; import java.util.List; public class GodbatisTest { @Test public void testSelectById() throws DocumentException { SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("godbatis-config.xml")); SqlSession sqlSession = sqlSessionFactory.openSession(); User user = (User) sqlSession.selectOne("User.selectById", 1); System.out.println(user); sqlSession.close(); } @Test public void testInsertUser() throws DocumentException { SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("godbatis-config.xml")); SqlSession sqlSession = sqlSessionFactory.openSession(); User user = new User("1", "zhangsan", "zhangsan@1234.com", "北京大兴区"); int count = sqlSession.insert("User.insertUser", user); System.out.println("插入了几条数据:" + count); sqlSession.commit(); sqlSession.close(); } }
-
执行结果:
- insertUser
- selectById