文章修改记录:
2023-02-01 第一版:https://www.yuque.com/u27599042/un32ge/mfpui3w5mgiugbhp
2023-06-04 完成重构 第二版:https://www.yuque.com/u27599042/un32ge/kggx3zvyg7mztgvd
手写 MyBatis 框架[GodBatis]第五章节单独抽取了出来:https://www.yuque.com/u27599042/un32ge/ru8czamo6trse3rl
文章汇总归纳于:https://www.yuque.com/u27599042/un32ge
老杜MyBatis原版笔记:https://www.yuque.com/zuihoudewu/java_note/mt2812
资料
1、javaWeb 链接:https://pan.baidu.com/s/1T3ouoZuZCMCAwPRv1Tahdg?pwd=5u25 提取码:5u25
2、主流框架 链接:https://pan.baidu.com/s/10HGe7wP1aed2HUCihc3-yQ?pwd=afjd 提取码:afjd
3、微服务架构 链接:https://pan.baidu.com/s/14RCkZXWyRP5hlOpDpp_ESQ?pwd=g0at 提取码:g0at
4、互联网生态 链接:https://pan.baidu.com/s/1IlM4LAU2gQqUMeN_B48t8w?pwd=egl7 提取码:egl7
6、架构师必会 链接:https://pan.baidu.com/s/10fPzIzSskuecnSxs4F4FRQ?pwd=s8tg 提取码:s8tg
MyBatis下载
-
MyBatis GitHub 网址:https://github.com/mybatis/mybatis-3
-
-
-
-
MyBatis 文档
- MyBatis 官方文档:https://mybatis.org/mybatis-3/
- MyBatis 中文文档:https://mybatis.net.cn/
MyBatis 概述
- MyBatis本质上就是对JDBC的封装,我们可以通过MyBatis完成CRUD。
- MyBatis在三层架构中负责持久层的,属于持久层框架。
ORM思想
- ORM:对象关系映射
- O(Object):Java虚拟机中的Java对象
- R(Relational):关系型数据库
- M(Mapping):映射,将Java虚拟机中的Java对象映射到数据库表中⼀⾏记录,或是将数据库表中⼀⾏记录映射成Java虚拟机中的⼀个Java对象。
- 在ORM思想中,一个Java类对应数据库中一个表,Java类中的属性对应数据库表中的字段,该Java类实例化出来的对象对应数据库表中的一条记录,Java对象的属性对应数据库表中相应记录的字段值,其中Java对象到数据库表中每条记录的对应关系为映射。
- 在ORM中,数据库表对应的Java类被称为pojo(普通Java类)、javabean(咖啡豆)或domain(邻域模型)。
Mybatis 与 ORM
- Mybatis可以实现Java对象与数据库表中一条记录的映射。
- MyBatis属于半⾃动化ORM框架。MyBatis中SQL语句需要我们自己编写。
- Hibernate属于全⾃动化的ORM框架。Hibernate中SQL语句不需要我们自己编写,SQL语句可以自动生成。
MyBatis框架特点
- ⽀持定制化 SQL、存储过程、基本映射以及⾼级映射
- 避免了⼏乎所有的 JDBC 代码中⼿动设置参数以及获取结果集
- ⽀持XML开发,也⽀持注解式开发。【为了保证sql语句的灵活,所以mybatis⼤部分是采⽤XML⽅式开发。】
- 将接⼝和 Java 的 POJOs(Plain Ordinary Java Object,简单普通的Java对象)映射成数据库中的记录
- 体积⼩好学:两个jar包,两个XML配置⽂件。
- 完全做到sql解耦合。
- 提供了基本映射标签。
- 提供了⾼级映射标签。
- 提供了XML标签,⽀持动态SQL的编写。
- …
MyBatis 入门程序
数据库表的准备
- 汽⻋表 t_car
- id:主键(⾃增)【bigint】
- car_num:汽⻋编号【varchar】
- brand:品牌【varchar】
- guide_price:⼚家指导价【decimal类型,专⻔为财务数据准备的类型】
- produce_time:⽣产时间【char,年⽉⽇即可,10个⻓度,‘2022-10-11’】
- car_type:汽⻋类型(燃油⻋、电⻋、氢能源)【varchar】
- 补充:数据库中的命名规范,数据库中字段名全部字母采用小写,如果字段名由多个单词组成,单词之间使用下划线进行分割
# 创建数据库
CREATE DATABASE mybatis_study;
# 使用数据库
USE mybatis_study;
# 创建表 汽⻋表t_car
CREATE TABLE t_car(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '自然主键',
car_num VARCHAR(255) COMMENT '汽车编号',
brand VARCHAR(255) COMMENT '汽车品牌',
guide_price DECIMAL(10, 2) COMMENT '厂家指导价',
produce_time CHAR(10) COMMENT '生产时间 如:2022-10-11',
car_type VARCHAR(255) COMMENT '汽车类型'
);
# 添加数据
INSERT INTO t_car(car_num, brand, guide_price, produce_time, car_type)
VALUES ('1001', '宝马520Li', 10.00, '2022-10-11', '燃油车'),
('1002', '奔驰E300L', 55.00, '2022-11-11', '新能源');
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eR8ThlDz-1685870936754)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg-blog.csdnimg.cn%2Fd860610f0184434e9077fb5027e9e597.png&sign=98ee78e42953c41fb0b78df7a3357393b05f83cceb62b7a4aa71fc680c7a3509#from=url&id=CPSPR&originHeight=179&originWidth=1135&originalType=binary&ratio=1.25&rotation=0&showTitle=false&status=done&style=none&title=)]
创建空项目
- 这里采用在工程 project 中创建模块 model 的形式
设置项目采用的编码
设置项目的Maven
设置项目使用的JDK
创建第一个 MyBatis 模块
- 快捷键:(选中要新建模块的项目)ALT + Ins
项目模块目录分析
- src目录下存放源程序
- main目录下存放源程序的主程序
- test目录下存放源程序的测试程序
- main/java目录下编写主程序的Java代码
- test/java目录下编写测试程序的Java代码
- resources目录用于存放配置文件或资源文件
- 放在该目录下的文件,相当于放到了类的根路径下。
- 放在该目录下的文件,相当于放到了类的根路径下。
类根路径
- 不管是 maven 还是普通的 java 模块,只要是 idea 文件夹中变成蓝色的部分,就是可以理解为类路径的起始地点,之后的编译就是由蓝色文件夹开始算类路径,自定义文件夹后用 idea 标记为 resource 后的文件夹也会被最终打包到类路径下
- 图中的蓝色Java目录和resource目录都相当于类的根路径,就是打包后,包的根路径
- 由于test目录中的文件主要是测试程序,测试程序一般不会被打包到JAR包,test目录下的子目录和文件会被单独打包到另外的文件夹下
设置模块的打包方式
<groupId>cw</groupId>
<artifactId>mybatis-study-001</artifactId>
<version>1.0-SNAPSHOT</version>
<!--
设置打包方式为 jar,
MyBatis 是封装 JDBC 的框架
不创建web项目所以不需要使用war的打包方式
-->
<packaging>jar</packaging>
引入依赖
- 由于MyBatis是封装JDBC的框架,用于操作数据库,且这里使用的数据库为MySQL
- 因此我们需要引入的依赖有:MyBatis依赖、MySQL驱动依赖
<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>
</dependencies>
编写mybatis核心配置文件:mybatis-config.xml
- 每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
- 通过官方文档中的这句话,我们可以得到:
- 第一:在MyBatis中一定是有一个很重要的对象,SqlSessionFactory对象。
- 第二:SqlSessionFactory对象需要通过SqlSessionFactoryBuilder实例加载读取XML配置文件来进行创建。
- 第三:MyBatis 需要一个配置文件,这个配置文件的文件格式为XML。
- 由于 SqlSessionFactory 的实例是 MyBatis 的核心,所以与其创建相关的 xml 配置文件即为 MyBatis 的核心配置文件
- 在类根路径下创建 MyBatis 的核心配置文件: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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!-- 修改链接数据库的配置信息 -->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_study"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--sql映射⽂件创建好之后,需要将该⽂件路径配置到这⾥-->
<mapper resource=""/>
</mappers>
</configuration>
- 注意:
- 第一:这个文件名不是必须叫做mybatis-config.xml,可以用其他的名字。只是大家都采用这个名字。
- 第二:这个文件存放的位置也不是固定的,可以随意,但一般情况下,会放到类的根路径下。(打包后在jar包的根目录下)
- MyBatis 包含一个名叫 Resources 的工具类,该工具类中有静态方法,可以从类路径加载资源文件。
- mybatis-config.xml 文件中的配置信息不理解没关系,先把连接数据库的信息修改以下即可。
编写 XxxxMapper.xml 文件
- MyBatis 是封装 JDBC 的框架,并且MyBatis是一个半自动化ORM框架,也就是需要我们自己编写SQL语句,在MyBatis中我们主要在 XxxxMapper.xml 文件中进行SQL语句的编写。所以我们还需要一个 XxxxMapper.xml 配置文件
- 在类根路径下,新建 CarMapper.xml 文件
- 因为在该配置文件中,我们主要写操作t_car这个数据库表的SQL语句,所以文件名为 XxxxMapper.xml
- 与t_car数据库表相关的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">
<!--namespace先随意写⼀个-->
<mapper namespace="car">
<!--insert sql:保存⼀个汽⻋信息-->
<!-- id 是这条SQL语句的唯一标识,这个id就代表了这条SQL语句 -->
<insert id="insertCar">
insert into t_car
(id,car_num,brand,guide_price,produce_time,car_type)
values
(null,'102','丰⽥mirai',40.30,'2014-10-05','氢能源')
</insert>
</mapper>
- 注意:
- sql语句最后结尾可以不写“;”
- CarMapper.xml文件的名字不是固定的。可以使用其它名字。
- CarMapper.xml文件的位置也是随意的。
- 这里选择放在resources根下,相当于放到了类的根路径下。便于未来程序进行移植,同时也便于对该配置文件进行配置
在 mybatis-config.xml 文件中指定 XxxxMapper.xml 文件的路径
- 由于SQL语句,是我们自己在自己创建的配置文件中编写的,MyBatis并没有强制规定配置文件的存放路径和位置,也没有强制规定配置文件的文件名,所以我们需要通过配置告诉MyBatis框架我们编写SQL语句的配置文件存放的位置,以及配置文件的文件名
- 在前面,我们已经确定了MyBatis核心配置文件的配置,可以将其配置进MyBatis程序,所以我们可以通过将编写SQL的配置文件的路径和文件配置在核心配置文件中来告知MyBatis
- 修改MyBatis核心配置文件的 mapper 标签:
<mappers>
<!-- sql映射⽂件创建好之后,需要将该⽂件路径配置到这⾥ -->
<!-- 和 resource 相关的路径配置,默认都是从类根路径开始 -->
<mapper resource="CarMapper.xml"/>
</mappers>
- 注意:resource属性会自动从类的根路径下开始查找资源。
mybatis 中的两个主要配置文件
- mybatis-config.xml:这是核心配置文件,主要用于配置连接数据库的信息等
- 该配置文件,在MyBatis中只需要一个
- XxxxMapper.xml:这个文件是专门用来编写SQL语句的配置文件。
- 该配置文件,一般一个表对应一个 XxxxMapper.xml
- t_user表,一般会对应一个UserMapper.xml
- t_student表,一般会对应一个StudentMapper.xml
编写MyBatis程序
- 接下来,我们使用mybatis的类库,编写mybatis程序,连接数据库,做增删改查就行了。
- 在MyBatis当中,负责执行SQL语句的那个对象叫做什么呢?
- SqlSession
- SqlSession是专门用来执行SQL语句的,是一个Java程序和数据库之间的一次会话。
- 要想获取SqlSession对象,我们就需要先获取SqlSessionFactory对象,通过SqlSessionFactory工厂对象来生产SqlSession对象。
- 怎么获取SqlSessionFactory对象呢?
- 需要首先获取SqlSessionFactoryBuilder对象。
- 通过SqlSessionFactoryBuilder对象的build方法,来获取一个SqlSessionFactory对象。
- mybatis的核心对象包括:
- SqlSessionFactoryBuilder:用于创建SqlSessionFactory对象
- SqlSessionFactory:用于创建SqlSession对象
- SqlSession:Java程序和数据库之间进行会话的对象
- SqlSessionFactoryBuilder --> SqlSessionFactory --> SqlSession
/**
* ClassName: Main
* Package: cw.mybatis.study.main
* Description:
*
* @Author tcw
* @Create 2023-05-25 23:55
* @Version 1.0
*/
public class Main {
public static void main(String[] args) throws IOException {
// 获取SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 获取 SqlSessionFactory 对象
// 获取 SqlSessionFactory 对象,需要读取配置文件,根据配置文件中的信息创建 SqlSessionFactory 对象
// Resources.getResourceAsStream 默认就是从类的根路径下开始查找资源。
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
// 以流的形式加载资源的方式二
//InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
// 需要传入核心配置文件对应的输入流,一般情况下都是一个数据库对应一个 SqlSessionFactory 对象。
// 根据配置文件中的信息创建 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
// 通过 SqlSessionFactory 对象获取 SqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession(); // 开启与数据库之间的会话
// 执行 SQL 语句
// 传入SQL语句对应的id,执行配置文件中id对应的SQL语句,
// 返回值是影响数据库表当中的记录条数。
int count = sqlSession.insert("insertCar");
System.out.println("插入了几条记录:" + count);
// 手动提交事务
// 注意:默认采用的事务管理器是:JDBC。
// JDBC事务默认是不提交的,需要手动提交。
sqlSession.commit();
// 关闭本次与数据库的会话
sqlSession.close();
}
}
关于第一个程序的小细节
- mybatis中sql语句的结尾 “;” 可以省略。
-
Resources.getResourceAsStream
- 小技巧:以后凡是遇到resource这个单词,大部分情况下,这种加载资源的方式就是从类的根路径下开始加载(开始查找)
- 优点:采用这种方式,从类路径当中加载资源,项目的移植性很强。项目从windows移植到linux,代码不需要修改,因为这个资源文件一直都在类路径当中。
-
InputStream is = new FileInputStream("d:\\mybatis-config.xml");
采用这种方式也可以。- 缺点:可移植性太差,程序不够健壮。可能会移植到其他的操作系统当中。导致以上路径无效,还需要修改java代码中的路径。这样违背了OCP原则。
- 已经验证了:
- mybatis核心配置文件的名字,不一定是:mybatis-config.xml。可以是其它名字。
- mybatis核心配置文件存放的路径,也不一定是在类的根路径下。可以放到其它位置。但为了项目的移植性,健壮性,最好将这个配置文件放到类路径下面。
- 因为我们在通过核心配置文件mybatis-config.xml创建SqlSessionFactory对象时,配置文件的路径和文件名都是我们自己在程序中指定的
-
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
- ClassLoader.getSystemClassLoader() 获取系统的类加载器。
- 系统类加载器有一个方法叫做:getResourceAsStream,它就是从类路径当中加载资源的。
- 通过源代码分析发现:
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
,底层的源代码其实就是:InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("mybatis-config.xml");
- CarMapper.xml文件的名字是固定的吗?CarMapper.xml文件的路径是固定的吗?
- 都不是固定的。
-
<mapper resource="CarMapper.xml"/>
resource属性:这种方式是从类路径当中加载资源。 -
<mapper url="file:///d:/CarMapper.xml"/>
url属性:这种方式是从绝对路径当中加载资源。(绝对路径前需要加上file:///
) - 由于CarMapper.xml文件的文件路径和文件名都是我们指定的,所以都不是固定的
关于 mybatis 的事务管理机制(剖析)
- 在mybatis-config.xml文件中,可以通过以下的配置进行mybatis的事务管理
<transactionManager type="JDBC"/>
- type属性的值包括两个:
- JDBC(jdbc)
- MANAGED(managed)
- type后面的值,只有以上两个值可选,不区分大小写。
- 在mybatis中提供了两种事务管理机制:
- 第一种:JDBC事务管理器
- 第二种:MANAGED事务管理器
JDBC事务管理器
- mybatis框架自己管理事务,自己采用原生的JDBC代码去管理事务:
conn.setAutoCommit(false); // 开启事务。
// ....业务处理...
conn.commit(); // 手动提交事务
// 获取SqlSession对象
// 如果使用的事务管理器是JDBC的话,底层实际上会执行:conn.setAutoCommit(false);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行SQL语句
// 返回值是影响数据库表当中的记录条数。
int count = sqlSession.insert("insertCar");
System.out.println("插入了几条记录:" + count);
// 手动提交
// 如果使用的事务管理器是JDBC的话,底层实际上还是会执行conn.commit();
sqlSession.commit();
- 使用JDBC事务管理器的话,底层创建的事务管理器对象:JdbcTransaction对象。
- 如果你编写的代码是下面的代码:
-
SqlSession sqlSession = sqlSessionFactory.openSession(true);
,表示没有开启事务。- 因为这种方式压根不会执行:
conn.setAutoCommit(false);
- 因为这种方式压根不会执行:
- 在JDBC事务中,没有执行
conn.setAutoCommit(false);
那么autoCommit就是true。如果autoCommit是true,就表示没有开启事务(事务自动提交)。只要执行任意一条DML语句就提交一次。
MANAGED事务管理器
- mybatis不再负责事务的管理了。事务管理交给其它容器来负责。
- 例如:spring。我不管事务了,你来负责吧。
- 对于我们当前的单纯的只有mybatis的情况下,如果配置为:MANAGED,那么事务这块是没人管的。没有人管理事务表示事务压根没有开启。
- 没有人管理事务就是没有事务。不开启事务,autoCommit是true,就表示没有事务。只要执行任意一条DML语句就提交一次。
- JDBC中的事务:
- 如果你没有在JDBC代码中执行:
conn.setAutoCommit(false);
的话,默认的autoCommit是true。
- 如果你没有在JDBC代码中执行:
- 重点:
- 以后注意了,只要你的autoCommit是true,就表示没有开启事务(事务自动提交)。
- 只有你的autoCommit是false的时候,就表示开启了事务。
MyBatis第一个程序比较完整的代码写法
package cw.mybatis.study.main;
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;
import java.io.InputStream;
/**
* ClassName: Main
* Package: cw.mybatis.study.main
* Description:
*
* @Author tcw
* @Create 2023-05-25 23:55
* @Version 1.0
*/
public class Main {
public static void main(String[] args){
SqlSession sqlSession = null; // 与数据库会话的对象
try {
// 获取SqlSessionFactoryBuilder对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 获取 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 通过 SqlSessionFactory 对象获取 SqlSession 对象
// 开启与数据库之间的会话
sqlSession = sqlSessionFactory.openSession();
// 执行 SQL 语句
int count = sqlSession.insert("insertCar");
System.out.println("插入了几条记录:" + count);
// 到这里没有报错,说明执行成功,可以提交本次事务
// 手动提交事务
sqlSession.commit();
} catch (Exception e) {
e.printStackTrace();
// 由异常说明事务执行过程中出现错误
// 回滚事务
if (sqlSession != null) sqlSession.rollback();
} finally {
// 关闭本次与数据库的会话(释放资源)
if (sqlSession != null) sqlSession.close();
}
}
}
引入 Junit
- Junit 是专⻔做单元测试的组件。
- 在实际开发中,单元测试⼀般是由我们 Java 程序员来完成的。我们要对我们⾃⼰写的每⼀个业务⽅法负责任,要保证每个业务⽅法在进⾏测试的时候都能通过。
- 测试的过程中涉及到两个概念:
- 期望值:执行了这个业务方法之后,你期望的执行结果是什么
- 实际值:被测试的业务方法的真正执行结果
- 期望值和实际值相同表示测试通过,期望值和实际值不同则单元测试执⾏时会报错。
- 这里引⼊ Junit 是为了代替 main ⽅法
第一步:引入依赖
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
第二步:使用 Junit 进行测试
- 编写单元测试类,对我们的代码进行测试,测试类中的每⼀个测试⽅法上使用
@Test
注解进⾏标注 - 关于测试类的一写规范:
- 测试类的类名:
要进行测试的类 + Test
- 测试类中的测试方法:一般是一个业务方法对应一个测试方式,测试方法的命名为
test + 要测试方法的方法名
,public void testXxxx() {}
,每个测试方法需要使用@Test
注解进行标注,表示该方法是一个单元测试方法
- 测试类的类名:
package com.powernode.junit.service;
import org.junit.Assert;
import org.junit.Test;
// 测试类的类名:要进行测试的类 + Test
public class MathServiceTest {
@Test
public void testSum(){
// 单元测试中有两个重要的概念:
// 一个是:实际值(被测试的业务方法的真正执行结果)
// 一个是:期望值(执行了这个业务方法之后,你期望的执行结果是多少)
MathService mathService = new MathService();
// 获取实际值
int actual = mathService.sum(1, 2);
// 期望值
//int expected = 3;
int expected = 30;
// 加断言进行测试,期望值与实际值不一致会报错
Assert.assertEquals(expected, actual);
}
@Test
public void testSub(){
MathService mathService = new MathService();
// 实际值
int actual = mathService.sub(10, 5);
// 期望值
int expected = 5;
// 添加断言机制,期望值与实际值一致则不会报错
Assert.assertEquals(expected, actual);
}
}
关于 MyBatis 集成日志组件
日志组件简述
- mybatis 常见的集成的日志组件有哪些呢?
- SLF4J(沙拉风):沙拉风是一个日志标准,其中有一个日志框架叫做logback,它实现了沙拉风规范。
- LOG4J
- LOG4J2
- STDOUT_LOGGING
- …
- log4j、log4j2、logback都是同一个作者开发的。
- STDOUT_LOGGING 是标准日志,mybatis 已经实现了这种标准日志,mybatis 框架本身已经实现了这种标准,只要开启即可。
开启 MyBatis 本身的日志功能
- 在 mybatis-config.xml 文件中使用settings标签进行配置开启。
- 可参考mybatis⼿册:https://mybatis.net.cn/configuration.html#settings
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
- 这个标签在编写的时候要注意,它应该出现在 environments 标签之前,注意顺序。
- 当然,不需要记忆这个顺序,因为有dtd文件进行约束,我们只要参考dtd约束即可,标签的顺序不正确会报错
- MyBatis 本身自己实现的日志框架也是可以的,可以看到一些信息,比如:连接对象什么时候创建,什么时候关闭,sql语句是怎样的。但是没有详细的日期,线程名字等。
- 如果想可以看到更加丰富的日志信息,可以集成第三方的log组件。
集成 logback 日志框架。
- logback日志框架实现了slf4j标准。(沙拉风:日志门面、日志标准)
- 在MyBatis使用标准日志 STDOUT_LOGGING 之外的其他日志框架,可以不在mybatis的配置文件中指定 MyBatis 所用日志的具体实现,未指定时将会自动查找,只需要引入相应日志框架的依赖以及日志框架的配置文件即可。
- 第一步:引入logback的依赖。
<!-- 引入logback依赖,logback日志框架实现了slf4j规范 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
- 第二步:引入logback所必须的xml配置文件。
- 这个配置文件的名字必须叫做:logback.xml或者logback-test.xml,不能是其它的名字。
- 这个配置文件必须放到类的根路径下,不能是其他位置。
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>[%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!--mybatis log configure-->
<logger name="com.apache.ibatis" level="TRACE"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
<!-- 级别越低输出的信息越多 -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
MyBatis 工具类 SqlSessionUtil 的封装
package cw.mybatis.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
/**
* Mybatis工具类
*/
public class SqlSessionUtils {
// 工具类的构造方法一般为私有,防止实例化对象
// 工具类中的方法都是静态的,可以直接采用类名进行调用,不需要new对象,方便调用
private SqlSessionUtils() {}
private static SqlSessionFactory sqlSessionFactory;
// 类加载时执行
// SqlSessionUtil工具类在进行第一次加载的时候,解析mybatis-config.xml文件。创建SqlSessionFactory对象。
// SqlSessionFactory对象:一个SqlSessionFactory对应一个environment,一个environment通常是一个数据库。
static {
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
try {
sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 获取数据库会话对象
* @return 数据库会话对象
*/
public static SqlSession openSession() {
// SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// SqlSessionFactory对象:一个SqlSessionFactory对应一个environment,一个environment通常是一个数据库。
// SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession = sqlSessionFactory.openSession();
return sqlSession;
}
}
- 测试:
package cw.mybatis.test;
import cw.mybatis.utils.SqlSessionUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
public class SqlSessionUtilsTest {
@Test
public void testOpenSession() {
SqlSession sqlSession = SqlSessionUtils.openSession();
int count = sqlSession.insert("insertCar");
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
}
使用 MyBatis 完成 CRUD
- CRUD
- C: Create 增
- R: Retrieve 查(检索)
- U: Update 改
- D: Delete 删
insert
MyBatis 中 SQL 的编写
<!--namespace先随便写-->
<mapper namespace="car">
<!-- 在insert标签中写向数据库中插入数据的SQL -->
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type)
values('103', '奔驰E300L', 50.3, '2022-01-01', '燃油车')
</insert>
</mapper>
- 这样写的问题是:插入数据库的值写死到配置文件中的。这个在实际开发中是不存在的。一定是前端的form表单提交过来数据。然后将值传给sql语句。
- JDBC 的代码的写法
String sql = "insert into t_car(id,car_num,brand,guide_price,produce_time,car_type) values(null,?,?,?,?,?)";
ps.setString(1, xxx);
ps.setString(2, yyy);
....
- 在JDBC当中占位符采用的是?,在mybatis当中是什么呢?
- 和?等效的写法是:#{}
- 在mybatis当中不能使用?占位符,必须使用 #{} 来代替JDBC当中的 ?
- #{} 和 JDBC当中的 ? 是等效的。
<!--namespace先随便写-->
<mapper namespace="car">
<insert id="insertCar">
insert into t_car(car_num,brand,guide_price,produce_time,car_type)
values(#{}, #{}, #{}, #{}, #{})
</insert>
</mapper>
使用Map集合传参
- java程序中使用Map可以给SQL语句的占位符传值,占位符中写map集合的key,如果key不存在,获取的是null,一般map集合的key起名的时候要见名知意。
<mapper namespace="car">
<!--insert sql:保存⼀个汽⻋信息-->
<insert id="insertCar">
insert into t_car
(id,car_num,brand,guide_price,produce_time,car_type)
values
(null, #{k1}, #{k2}, #{k3}, #{k4}, #{k5})
</insert>
</mapper>
package cw.mybatis;
import cw.mybatis.utils.SqlSessionUtils;
import org.apache.ibatis.session.SqlSession;
import java.util.HashMap;
import java.util.Map;
public class CarMapperTest {
@Test
public void testInsertCar() {
// 使用map集合进行数据的封装。
Map<String, Object> map = new HashMap<>();
map.put("k1", "1111");
map.put("k2", "比亚迪汉");
map.put("k3", 10.0);
map.put("k4", "2020-11-11");
map.put("k5", "电车");
// 获取与数据库的会话对象
SqlSession sqlSession = SqlSessionUtils.openSession();
// 执行SQL语句
// insert SQL 使用 insert方法
// insert方法的参数:
// 第一个参数:sqlId,从CarMapper.xml文件中复制。
// 第二个参数:封装数据的对象。
sqlSession.insert("insertCar", map);
sqlSession.commit();
sqlSession.close();
}
}
使用POJO(简单普通的java对象)传参
- 第一步:定义一个pojo类Car,提供相关属性
package cw.mybatis.pojo;
/**
* 封装汽车相关信息的pojo类。普通的java类。
*/
public class Car {
// 数据库表当中的字段应该和pojo类的属性一一对应。
// 建议使用包装类,这样可以防止null的问题。
private Long id;
private String carNum;
private String brand;
private Double guidePrice;
private String produceTime;
private String carType;
public Car() {
}
public Car(Long id, String carNum, String brand, Double guidePrice, String produceTime, String carType) {
this.id = id;
this.carNum = carNum;
this.brand = brand;
this.guidePrice = guidePrice;
this.produceTime = produceTime;
this.carType = carType;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCarNum() {
return carNum;
}
public void setCarNum(String carNum) {
this.carNum = carNum;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Double getGuidePrice() {
return guidePrice;
}
public void setGuidePrice(Double guidePrice) {
this.guidePrice = guidePrice;
}
public String getProduceTime() {
return produceTime;
}
public void setProduceTime(String produceTime) {
this.produceTime = produceTime;
}
public String getCarType() {
return carType;
}
public void setCarType(String carType) {
this.carType = carType;
}
@Override
public String toString() {
return "Car{" + "id=" + id + ", carNum='" + carNum + '\'' + ", brand='" + brand + '\'' + ", guidePrice=" + guidePrice + ", produceTime='" + produceTime + '\'' + ", carType='" + carType + '\'' + '}';
}
}
- 第二步:Java程序
@Test
public void testInsertCarPOJO() {
// 封装数据
Car car = new Car(null, "3333", "比亚迪", 30.0, "2020-11-11", "新能源");
SqlSession sqlSession = SqlSessionUtils.openSession();
// 执行sql
sqlSession.insert("insertCar", car);
sqlSession.commit();
sqlSession.close();
}
- 第三步:SQL语句
<mapper namespace="car">
<!--insert sql:保存⼀个汽⻋信息-->
<!-- 占位符 #{} 中写POJO的属性名 -->
<insert id="insertCar">
insert into t_car
(id,car_num,brand,guide_price,produce_time,car_type)
values
(null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType})
</insert>
</mapper>
MyBatis 如何获取 POJO 属性值分析
- 如果把SQL语句写成如下这个德行:
insert into t_car(id,car_num,brand,guide_price,produce_time,car_type)
values(null,#{xyz},#{brand},#{guidePrice},#{produceTime},#{carType})
- 出现了什么问题呢?
There is no getter for property named 'xyz' in 'class com.powernode.mybatis.pojo.Car'
- mybatis去找:Car类中的getXyz()方法去了。没找到。报错了。
- 怎么解决的?
- 可以在Car类中提供一个getXyz()方法。这样问题就解决了。
- 通过这个测试,得出一个结论:
- 严格意义上来说:如果使用POJO对象传递值的话,#{}这个大括号中到底写什么?
- 写的是get方法的方法名去掉get,然后将剩下的单词首字母小写,然后放进去。
- 例如:getUsername() --> #{username}
- 例如:getEmail() --> #{email}
- …
- 也就是说mybatis在底层给?传值的时候,先要获取值,怎么获取的?
- 调用了pojo对象的get方法。例如:car.getCarNum(),car.getCarType(),car.getBrand()
- 调用了pojo对象的get方法。例如:car.getCarNum(),car.getCarType(),car.getBrand()
SQL 语句标签的 parameterType 属性
- 其实传参数的时候有一个属性parameterType,这个属性用来指定传参的数据类型,不过这个属性是可以省略的
<insert id="insertCar" parameterType="java.util.Map">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
<insert id="insertCarByPOJO" parameterType="com.powernode.mybatis.pojo.Car">
insert into t_car(car_num,brand,guide_price,produce_time,car_type) values(#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})
</insert>
delete
- 需求:根据id删除数据
- SQL语句
<!-- 当sql语句中的占位符只有一个时,占位符中的内容可以任意写,但是不能不写,一般采用见名知意的命名 -->
<!-- 只有一个占位符时,mybatis可以知道数据填放的位置 -->
<delete id="deleteById">
delete from t_car where id = #{id}
</delete>
- java程序
@Test
public void testDeleteById() {
SqlSession sqlSession = SqlSessionUtils.openSession();
// 第二个参数会被自动装箱成相应的类型
sqlSession.delete("deleteById", 10);
sqlSession.commit();
sqlSession.close();
}
- 注意:如果占位符只有一个,那么#{}的大括号里可以随意。但是最好见名知意。
update
- 需求:根据id修改某条记录。
<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>
@Test
public void testUpdateById() {
SqlSession sqlSession = SqlSessionUtils.openSession();
// 封装数据
Car car = new Car(4L, "9999", "凯美瑞", 30.3, "1999-11-10", "燃油车");
int count = sqlSession.update("updateById", car);
System.out.println(count);
sqlSession.commit();
sqlSession.close();
}
select
- select语句查询会有一个结果集
查一条数据
- 需求:根据id查询。
- 查询时需要告诉mybatis查询结果集封装成的java对象类型,select标签中resultType属性,这个属性用来告诉mybatis,查询结果集封装成什么类型的java对象。
- select标签中resultType通常写的是:全限定类名。
- 如果表的字段名与相应的java类的属性名不一致可以使用别名为查询语句的字段指定别名,来解决不能为java对象属性赋值的问题
<!--
sql语句中如果表的字段名与相应的java类的属性名不一致需要使用别名为查询语句的字段指定别名,
否则对应字段的查询为null
-->
<select id="selectById" resultType="cw.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>
@Test
public void testSelectById() {
SqlSession sqlSession = SqlSessionUtils.openSession();
// 执行DQL语句。查询。根据id查询。返回结果一定是一条。
// mybatis底层执行了select语句之后,一定会返回一个结果集对象:ResultSet
// JDBC中叫做ResultSet,接下来就是mybatis应该从ResultSet中取出数据,封装java对象。
Object car = sqlSession.selectOne("selectById", 1);
System.out.println(car);
sqlSession.close();
}
查多条数据
<!--
resultType还是指定要封装的结果集的类型。
不是指定List类型,是指定List集合中元素的类型。
-->
<select id="selectAll" resultType="cw.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>
@Test
public void testSelectAll() {
SqlSession sqlSession = SqlSessionUtils.openSession();
// selectList方法:mybatis通过这个方法就可以得知你需要一个List集合。
// 它会自动给你返回一个List集合。
List<Car> cars = sqlSession.selectList("selectAll");
cars.forEach(System.out::println);
sqlSession.close();
}
SQL Mapper 的 namespace
- 在sql mapper.xml文件当中有一个namespace,这个属性是用来指定命名空间的。用来防止id重复。
- 当不同的 SQLMapper 映射文件中,存在 sqlId 相同的两个SQL语句,若使用SQL语句时不使用命名空间,则会报错
- 命名空间的使用
namespace.sqlId
(MyBatis中sqlId的完整写法) - 实际上,本质上,mybatis中的sqlId的完整写法:namespace.id
- 创建CarMapper2.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="car2">
<select id="selectCarAll" 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>
- CarMapper.xml和CarMapper2.xml文件中都有 id=“selectCarAll”
- 将CarMapper2.xml配置到mybatis-config.xml文件中。
<mappers>
<mapper resource="CarMapper.xml"/>
<mapper resource="CarMapper2.xml"/>
</mappers>
- Java代码
@Test
public void testSelectAll() {
SqlSession sqlSession = SqlSessionUtils.openSession();
List<Car> cars = sqlSession.selectList("selectAll");
cars.forEach(System.out::println);
sqlSession.close();
}
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.IllegalArgumentException:
selectCarAll is ambiguous in Mapped Statements collection (try using the full name including the namespace, or rename one of the entries)
【翻译】selectCarAll在Mapped Statements集合中不明确(请尝试使用包含名称空间的全名,或重命名其中一个条目)
【大致意思是】selectCarAll重名了,你要么在selectCarAll前添加一个名称空间,要有你改个其它名字。
- Java代码修改如下:
@Test
public void testSelectAll() {
SqlSession sqlSession = SqlSessionUtils.openSession();
// SQL id:namespace.id
List<Car> cars = sqlSession.selectList("car.selectAll");
cars.forEach(System.out::println);
sqlSession.close();
}
MyBatis核心配置文件详解
老杜原版
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<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="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="CarMapper.xml"/>
<mapper resource="CarMapper2.xml"/>
</mappers>
</configuration>
- configuration:根标签,表示配置信息。
- environments:环境(多个),以“s”结尾表示复数,也就是说mybatis的环境可以配置多个数据源。
- default属性:表示默认使用的是哪个环境,default后面填写的是environment的id。default的值只需要和environment的id值一致即可。
- environment:具体的环境配置(主要包括:事务管理器的配置 + 数据源的配置)
- id:给当前环境一个唯一标识,该标识用在environments的default后面,用来指定默认环境的选择。
- transactionManager:配置事务管理器
- type属性:指定事务管理器具体使用什么方式,可选值包括两个
- JDBC:使用JDBC原生的事务管理机制。底层原理:事务开启conn.setAutoCommit(false); …处理业务…事务提交conn.commit();
- MANAGED:交给其它容器来管理事务,比如WebLogic、JBOSS等。如果没有管理事务的容器,则没有事务。没有事务的含义:只要执行一条DML语句,则提交一次。
- type属性:指定事务管理器具体使用什么方式,可选值包括两个
- dataSource:指定数据源
- type属性:用来指定具体使用的数据库连接池的策略,可选值包括三个
-
UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
- property可以是:
- driver 这是 JDBC 驱动的 Java 类全限定名。
- url 这是数据库的 JDBC URL 地址。
- username 登录数据库的用户名。
- password 登录数据库的密码。
- defaultTransactionIsolationLevel 默认的连接事务隔离级别。
- defaultNetworkTimeout 等待数据库操作完成的默认网络超时时间(单位:毫秒)
- property可以是:
-
POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现。
- property可以是(除了包含UNPOOLED中之外):
- poolMaximumActiveConnections 在任意时间可存在的活动(正在使用)连接数量,默认值:10
- poolMaximumIdleConnections 任意时间可能存在的空闲连接数。
- 其它…
- property可以是(除了包含UNPOOLED中之外):
-
JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。如果不是web或者maven的war工程,JNDI是不能使用的。
- property可以是(最多只包含以下两个属性):
- initial_context 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
- data_source 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
- property可以是(最多只包含以下两个属性):
-
UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
- type属性:用来指定具体使用的数据库连接池的策略,可选值包括三个
- mappers:在mappers标签中可以配置多个sql映射文件的路径。
- mapper:配置某个sql映射文件的路径
- resource属性:使用相对于类路径的资源引用方式
- url属性:使用完全限定资源定位符(URL)方式
各标签拆分解释说明版
文档类型说明
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
- 文档类型说明中的 configuration,是根标签的名称,一个文档只有一个根标签
- http://mybatis.org/dtd/mybatis-3-config.dtd 是xml文档的dtd约束,约束文档中可以出现什么标签、标签能有什么子标签、标签中可以有什么属性、标签属性可以具有的值以及标签出现的顺序
根标签
<configuration>
...
</configuration>
- configuration:根标签,表示配置信息
properties
- java.util.Properties类,是一个Map集合,key和value都是String类型
- property标签中的name属性为key,value属性为value
- 在properties标签中可以配置很多属性
- 在properties下面的标签中可以使用property配置的属性值,使用
${property的name属性}
取出相应的值
<properties>
<!-- <property name="属性名" value="属性值"/> -->
<property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="jdbc.username" value="root"/>
<property name="jdbc.password" value="root"/>
</properties>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
从指定路径下开始查找配置文件
- properties中的property可以配置到配置文件中
<!-- resource,从类路径下开始查找资源 -->
<properties resource="jdbc.properties" />
<!-- 从绝对路径当中加载资源 -->
<!-- 绝对路径怎么写?file:///路径 -->
<properties url="file:///d:/jdbc.properties" />
environments
<environments default="powernodeDB">
...
</environments>
- 在 environments 标签中配置数据库环境,可以配置多个数据库环境
- default 表示默认使用的数据库环境
- 当你使用mybatis创建SqlSessionFactory对象的时候,没有指定数据库环境的话,默认使用 default 属性指定的数据库环境
- 当你使用mybatis创建SqlSessionFactory对象的时候,没有指定数据库环境的话,默认使用 default 属性指定的数据库环境
environment
- environments 的子标签,在 environment 标签中配置每一个数据库环境
- 一般一个数据库会对应一个SqlSessionFactory对象,一个环境environment会对应一个SqlSessionFactory对象,所以一个数据库一般对应一个环境
<!-- 第一个数据库环境 -->
<environment id="powernodeDB">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="poolMaximumActiveConnections" value="10"/>
<property name="poolTimeToWait" value="2000"/>
<property name="poolMaximumCheckoutTime" value="10000"/>
<property name="poolMaximumIdleConnections" value="5"/>
</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>
默认方式获取 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
- 这种方式就是获取默认数据库环境
- 该 SqlSessionFactory 对象对应在 environments 标签的 default 属性中指定的数据库环境
通过数据库环境id获取 SqlSessionFactory 对象
SqlSessionFactory sqlSessionFactory1 =
sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "powernodeDB");
- 第二个参数为数据库环境id
- 这种方式就是通过数据库环境id来使用指定的数据库环境
- 通过该方式获取的 SqlSessionFactory 对象对应 id 相应的数据库环境
transactionManager
<transactionManager type="JDBC"/>
- 作用:配置事务管理器,指定mybatis具体使用什么方式去管理事务。
- type属性有两个值:
- 第一个:JDBC: 使用原生的JDBC代码来管理事务。
conn.setAutoCommit(false);
...
conn.commit();
- 第二个:MANAGED:mybatis不再负责事务的管理,将事务管理交给其它的JEE(JavaEE)容器来管理。
- 例如:spring
- 大小写无所谓。不缺分大小写。但是不能写其他值。只能是二选一: jdbc、managed
- 在mybatis中提供了一个事务管理器接口:Transaction
- 该接口下有两个实现类:
- JdbcTransaction
- ManagedTransaction
- 该接口下有两个实现类:
- 如果type=“JDBC”,那么底层会实例化JdbcTransaction对象。
- 如果type=“MANAGED”,那么底层会实例化ManagedTransaction
dataSource
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
- dataSource被称为数据源。
- dataSource的作用:为程序提供Connection对象。
- 凡是给程序提供Connection对象的,都叫做数据源。
- 数据源实际上是一套规范。
- JDK中有这套规范:javax.sql.DataSource,这个数据源的规范,这套接口是JDK规定的。
- 我们自己也可以编写数据源组件,只要实现javax.sql.DataSource接口就行,实现接口当中所有的方法。
- 常见的数据源组件有哪些呢【常见的数据库连接池有哪些呢】?
- 阿里巴巴的德鲁伊连接池:druid
- c3p0
- dbcp
- …
- type属性用来指定数据源的类型,就是指定具体使用什么方式来获取Connection对象:
- type属性有三个值:必须是三选一。
- type=“[UNPOOLED|POOLED|JNDI]”
- UNPOOLED:不使用数据库连接池技术。每一次请求过来之后,都是创建新的Connection对象。
- POOLED:使用mybatis自己实现的数据库连接池。
- JNDI:集成其它第三方的数据库连接池。
- JNDI是一套规范,大部分的web容器都实现了JNDI规范,例如:Tomcat、Jetty、WebLogic、WebSphere,这些服务器(容器)都实现了JNDI规范。
- JNDI是:java命名目录接口。Tomcat服务器实现了这个规范。
- 就好比,WEB容器实现了这个规范,为第三方数据库连接池提供了一个接入到web项目的“接口”,第三方数据库连接池可以通过该“接口”接入web项目,web项目中可以直接通过JNDI来使用第三方数据库连接池
- 数据源的type属性指定不同的值,可以配置不同的属性 https://mybatis.net.cn/configuration.html#environments
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 以下配置数据库连接池的参数 -->
<!--提醒:正常使用连接池的话,池中有很多参数是需要设置的。设置好参数,可以让连接池发挥的更好。事半功倍的效果。-->
<!--具体连接池当中的参数如何配置呢?需要反复的根据当前业务情况进行测试。-->
<!--poolMaximumActiveConnections:连接池当中最多的正在使用的连接对象的数量上限。最多有多少个连接可以活动。默认值10-->
<property name="poolMaximumActiveConnections" value="10"/>
<!--每隔2秒打印日志,并且尝试获取连接对象-->
<property name="poolTimeToWait" value="2000"/>
<!--强行让某个连接空闲,超时时间的设置-->
<property name="poolMaximumCheckoutTime" value="10000"/>
<!--最多的空闲数量-->
<!--
假设最多的连接数量为10个,最多空闲数量为5个,现在已经空闲5个了,马上第六个也要空闲了
如果第六个空闲下来,连接池为了保证空闲的数量最多5个,会真正关闭多余的空闲连接对象
可以节省系统资源
-->
<property name="poolMaximumIdleConnections" value="5"/>
</dataSource>
mappers
- 在mappers标签中可以配置多个sql映射文件的路径
<mappers>
<!-- mapper:配置某个sql映射文件的路径 -->
<!-- resource属性:使用相对于类路径的资源引用方式 -->
<!-- url属性:使用完全限定资源定位符(URL)方式 -->
<mapper resource="CarMapper.xml"/>
</mappers>
代码文档注释版
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 文档类型说明中的 configuration,是根标签的名称,一个文档一个根标签 -->
<!--
http://mybatis.org/dtd/mybatis-3-config.dtd
xml文档的dtd约束,约束文档中可以出现什么标签、标签能有什么子标签、标签中可以有什么属性以及标签出现的顺序
-->
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- configuration:根标签,表示配置信息。 -->
<configuration>
<!--java.util.Properties类。是一个Map集合。key和value都是String类型-->
<!-- property标签中的name属性为key,value属性为value -->
<!--在properties标签中可以配置很多属性-->
<!-- 在下面的标签中可以使用property配置的属性值,使用 ${property的name属性} 取出相应的值 -->
<!--<properties>-->
<!--这是其中的一个属性-->
<!--<property name="属性名" value="属性值"/>-->
<!--<property name="jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbc.url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="jdbc.username" value="root"/>
<property name="jdbc.password" value="root"/>-->
<!--</properties>-->
<!-- properties中的property可以配置到配置文件中 -->
<!--resource,一定是从类路径下开始查找资源-->
<properties resource="jdbc.properties" />
<!--从绝对路径当中加载资源。绝对路径怎么写?file:///路径-->
<!--<properties url="file:///d:/jdbc.properties" />-->
<!-- environments:环境(多个),以“s”结尾表示复数,也就是说mybatis的环境可以配置多个数据源。 -->
<!--default表示默认使用的环境。-->
<!--默认环境什么意思?当你使用mybatis创建SqlSessionFactory对象的时候,没有指定环境的话,默认使用哪个环境。-->
<environments default="powernodeDB">
<!--其中的一个环境。连接的数据库是powernode-->
<!--一般一个数据库会对应一个SqlSessionFactory对象。-->
<!--一个环境environment会对应一个SqlSessionFactory对象-->
<!-- 一个数据库对应一个环境 -->
<!--
// 这种方式就是获取的默认环境
SqlSessionFactory sqlSessionFactory =
sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
// 这种方式就是通过环境id来使用指定的环境,第二个参数为环境id
SqlSessionFactory sqlSessionFactory1 =
sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"), "powernodeDB");
-->
<environment id="powernodeDB">
<!--
transactionManager标签:
1.作用:配置事务管理器。指定mybatis具体使用什么方式去管理事务。
2.type属性有两个值:
第一个:JDBC: 使用原生的JDBC代码来管理事务。
conn.setAutoCommit(false);
....
conn.commit();
第二个:MANAGED:mybatis不再负责事务的管理,将事务管理交给其它的JEE(JavaEE)容器来管理。例如:spring
3. 大小写无所谓。不缺分大小写。但是不能写其他值。只能是二选一:
jdbc、managed
4. 在mybatis中提供了一个事务管理器接口:Transaction
该接口下有两个实现类:
JdbcTransaction
ManagedTransaction
如果type="JDBC",那么底层会实例化JdbcTransaction对象。
如果type="MANAGED",那么底层会实例化ManagedTransaction
-->
<transactionManager type="JDBC"/>
<!--
dataSource配置:
1.dataSource被称为数据源。
2.dataSource作用是什么?为程序提供Connection对象。(但凡是给程序提供Connection对象的,都叫做数据源。)
3.数据源实际上是一套规范。JDK中有这套规范:javax.sql.DataSource(这个数据源的规范,这套接口实际上是JDK规定的。)
4.我们自己也可以编写数据源组件,只要实现javax.sql.DataSource接口就行了。实现接口当中所有的方法。这样就有了自己的数据源。
比如你可以写一个属于自己的数据库连接池(数据库连接池是提供连接对象的,所以数据库连接池就是一个数据源)。
5.常见的数据源组件有哪些呢【常见的数据库连接池有哪些呢】?
阿里巴巴的德鲁伊连接池:druid
c3p0
dbcp
....
6. type属性用来指定数据源的类型,就是指定具体使用什么方式来获取Connection对象:
type属性有三个值:必须是三选一。
type="[UNPOOLED|POOLED|JNDI]"
UNPOOLED:不使用数据库连接池技术。每一次请求过来之后,都是创建新的Connection对象。
POOLED:使用mybatis自己实现的数据库连接池。
JNDI:集成其它第三方的数据库连接池。
JNDI是一套规范。谁实现了这套规范呢?大部分的web容器都实现了JNDI规范:
例如:Tomcat、Jetty、WebLogic、WebSphere,这些服务器(容器)都实现了JNDI规范。
JNDI是:java命名目录接口。Tomcat服务器实现了这个规范。
-->
<!-- 数据源的type属性指定不同的值,需要配置不太的属性 https://mybatis.net.cn/configuration.html#environments -->
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 以下配置数据库连接池的参数 -->
<!--提醒:正常使用连接池的话,池中有很多参数是需要设置的。设置好参数,可以让连接池发挥的更好。事半功倍的效果。-->
<!--具体连接池当中的参数如何配置呢?需要反复的根据当前业务情况进行测试。-->
<!--poolMaximumActiveConnections:连接池当中最多的正在使用的连接对象的数量上限。最多有多少个连接可以活动。默认值10-->
<property name="poolMaximumActiveConnections" value="10"/>
<!--每隔2秒打印日志,并且尝试获取连接对象-->
<property name="poolTimeToWait" value="2000"/>
<!--强行让某个连接空闲,超时时间的设置-->
<property name="poolMaximumCheckoutTime" value="10000"/>
<!--最多的空闲数量-->
<!--
假设最多的连接数量为10个,最多空闲数量为5个,现在已经空闲5个了,马上第六个也要空闲了
如果第六个空闲下来,连接池为了保证空闲的数量最多5个,会真正关闭多余的空闲连接对象
可以节省系统资源
-->
<property name="poolMaximumIdleConnections" value="5"/>
</dataSource>
</environment>
<!--这是mybatis的另一个环境,也就是连接的数据库是另一个数据库mybatis-->
<environment id="mybatisDB">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- mappers:在mappers标签中可以配置多个sql映射文件的路径。 -->
<mappers>
<!-- mapper:配置某个sql映射文件的路径 -->
<!-- resource属性:使用相对于类路径的资源引用方式 -->
<!-- url属性:使用完全限定资源定位符(URL)方式 -->
<mapper resource="CarMapper.xml"/>
</mappers>
</configuration>
手写MyBatis框架
- 手写 MyBatis 框架[GodBatis]:https://www.yuque.com/u27599042/un32ge/ru8czamo6trse3rl
在 WEB 中应用 MyBatis(MVC架构模式)
- 实现功能:银行账户转账
- 使用技术:HTML + Servlet + MyBatis
- WEB应用的名称:bank
需求描述
数据库表的设计和准备数据
USE dbtest;
DROP TABLE IF EXISTS t_act;
CREATE TABLE t_act (
id int PRIMARY KEY AUTO_INCREMENT,
actno VARCHAR(255),
balance DECIMAL(10, 2)
);
INSERT INTO t_act(actno, balance) VALUES ('act001', 50000);
INSERT INTO t_act(actno, balance) VALUES ('act002', 0);
环境搭建
- IDEA中使用Maven原型创建WEB应用
- 使用Maven原型创建的web应用默认没有java和resources目录,包括两种解决方案
- 第一种:自己手动加上。
- 第二种:找到原型jar包所在的位置,修改maven-archetype-webapp-1.4.jar中的配置文件
- web.xml文件的版本较低,可以从tomcat的样例文件中复制,然后修改
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
</web-app>
- 删除index.jsp文件,因为我们这个项目不使用JSP,使用html。
- 确定pom.xml文件中的打包方式是war包。
<packaging>war</packaging>
- 引入相关依赖
<dependencies>
<!-- MyBatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!-- MySQL驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- servlet依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<!-- logback依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
- 编译器版本修改为17
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
- IDEA配置Tomcat,并部署应用到tomcat,应用名 bank
- 准备相关配置文件,放到resources目录下(相当于放到类的根路径下)
- mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"/>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="AccountMapper.xml"/>
</mappers>
</configuration>
- AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="account">
</mapper>
- logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!--mybatis log configure-->
<logger name="com.apache.ibatis" level="TRACE"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
- jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/dbtest
jdbc.username=root
jdbc.password=root
前端页面 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行账户转账</title>
</head>
<body>
<!--/bank是应用的根,部署web应用到tomcat的时候一定要注意这个名字-->
<form action="/bank/transfer" method="post">
转出账户:<input type="text" name="fromActno"/><br>
转入账户:<input type="text" name="toActno"/><br>
转账金额:<input type="text" name="money"/><br>
<input type="submit" value="转账"/>
</form>
</body>
</html>
创建软件包(MVC架构模式)
工具类 SqlSessionUtil
package cw.study.mybatis.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
/**
* ClassName: SqlSessionUtil
* Package: cw.study.mybatis.utils
* Description:
*
* @Author tcw
* @Create 2023-05-28 14:32
* @Version 1.0
*/
public class SqlSessionUtil {
private SqlSessionUtil(){}
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 获取会话对象。
* @return 会话对象
*/
public static SqlSession openSession(){
return sqlSessionFactory.openSession();
}
}
定义实体类 Account
public class Account {
private Long id;
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public Account() {
}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
后端代码实现
@WebServlet({"/transfer"})
public class AccountServlet extends HttpServlet {
// 为了让这个对象在其他方法中也可以用。声明为实例变量。
private AccountService accountService = new AccountServiceImpl();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取数据
String fromActno = request.getParameter("fromActno");
String toActno = request.getParameter("toActno");
double money = Double.parseDouble(request.getParameter("money"));
// 调用业务层
try {
accountService.transfer(fromActno, toActno, money);
// 到这转账成功
// 调用视图完成结果展示
response.sendRedirect(request.getContextPath() + "/success.html");
} catch (MoneyNotEnoughException e) {
response.sendRedirect(request.getContextPath() + "/error1.html");
} catch (TransferException e) {
response.sendRedirect(request.getContextPath() + "/error2.html");
} catch (Exception e) {
response.sendRedirect(request.getContextPath() + "/error2.html");
}
}
}
/**
* 注意:业务类当中的业务方法的名字在起名的时候,最好见名知意,能够体现出具体的业务是做什么的。
* 账户业务类
*/
public interface AccountService {
/**
* 转账业务
* @param fromActno 转出账户
* @param toActno 转入账户
* @param money 转账金额
*/
void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException;
}
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromActno, String toActno, double money)
throws MoneyNotEnoughException, TransferException {
// 1. 判断转出账户的余额是否充足(select)
Account fromAct = accountDao.selectByActno(fromActno);
// 2. 如果转出账户余额不足,提示用户
if (fromAct.getBalance() < money) { // 转出账户余额不足
throw new MoneyNotEnoughException("余额不足");
}
// 3. 如果转出账户余额充足,更新转出账户余额(update)
// 先更新java内存中对象的余额再更新数据库
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.updateByAccount(fromAct);
// 4. 更新转入账户余额(update)
count += accountDao.updateByAccount(toAct);
if (count != 2) {
throw new TransferException("转账失败");
}
}
}
/**
* 账户的DAO对象。负责t_act表中数据的CRUD.
* 强调一下:DAO对象中的任何一个方法和业务不挂钩。没有任何业务逻辑在里面。
* DAO中的方法就是做CRUD的。所以方法名大部分是:insertXXX deleteXXX updateXXX selectXXX
*/
public interface AccountDao {
/**
* 根据账号查询账户信息。
* @param actno 账号
* @return 账户信息
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act 被更新的账户对象
* @return 1表示更新成功,其他值表示失败。
*/
int updateByAccount(Account act);
}
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
sqlSession.close();
return account;
}
@Override
public int updateByAccount(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.updateByAccount", act);
sqlSession.commit();
sqlSession.close();
return count;
}
}
public class MoneyNotEnoughException extends Exception{
public MoneyNotEnoughException() {
}
public MoneyNotEnoughException(String message) {
super(message);
}
}
public class TransferException extends Exception{
public TransferException() {
}
public TransferException(String message) {
super(message);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>转账成功!</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>余额不足!!!</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>转账失败,未知原因!!!</h1>
</body>
</html>
使用 ThreadLocal 进行事务控制
- 在之前的转账业务中,更新了两个账户,我们需要保证它们的同时成功或同时失败,这个时候就需要使用事务机制,在transfer方法开始执行时开启事务,直到两个更新都成功之后,再提交事务
- 如果在转账业务中,更新两个账户的操作在两个事务之中,如果中间出现异常,可能会导致钱丢失,只更新了一个账户的数据,另一个账户的数据未进行更新。
- 前面代码的实现,可能会存在上述的问题,**主要是因为service和dao中使用的SqlSession对象不是同一个,**不能将更新两个账户的操作控制在一个事务之内
- 为了解决可能存在的事务问题,为了保证service和dao中使用的SqlSession对象是同一个,将更新两个账户的操作控制在一个事务之内,我们可以将SqlSession对象存放到ThreadLocal当中,保证一个线程都使用同一个SqlSession对象,把更新两个账户的操作控制在一个事务之内
- tomcat服务器是支持多线程的,对应不同的来自客户端的请求,tomcat会使用线程池中的一个线程来处理该请求,将SqlSession对象存放到ThreadLocal当中,为当前线程绑定一个属于该线程的SqlSession对象,可以实现只要是同一个线程中的操作,使用的就是同一个SqlSession对象
- 修改SqlSessionUtil工具类:
public class SqlSessionUtil {
private SqlSessionUtil(){}
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 全局的,服务器级别的,一个服务器当中定义一个即可。
// 为什么把SqlSession对象放到ThreadLocal当中呢?
// 为了保证一个线程对应一个SqlSession。可以把更新两个账户的操作控制在一个事务内
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
/**
* 获取会话对象。
* @return 会话对象
*/
public static SqlSession openSession(){
SqlSession sqlSession = local.get();
if (sqlSession == null) {
sqlSession = sqlSessionFactory.openSession();
// 将sqlSession对象绑定到当前线程上。
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭SqlSession对象(从当前线程中移除SqlSession对象。)
* @param sqlSession
*/
public static void close(SqlSession sqlSession) {
if (sqlSession != null) {
sqlSession.close();
// 注意移除SqlSession对象和当前线程的绑定关系。
// 因为Tomcat服务器支持线程池。也就是说:用过的线程对象t1,可能下一次还会使用这个t1线程。
local.remove();
}
}
}
- 修改service中的方法:
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromActno, String toActno, double money)
throws MoneyNotEnoughException, TransferException {
// 添加事务控制代码
SqlSession sqlSession = SqlSessionUtil.openSession();
// 1. 判断转出账户的余额是否充足(select)
Account fromAct = accountDao.selectByActno(fromActno);
// 2. 如果转出账户余额不足,提示用户
if (fromAct.getBalance() < money) { // 转出账户余额不足
throw new MoneyNotEnoughException("余额不足");
}
// 3. 如果转出账户余额充足,更新转出账户余额(update)
// 先更新java内存中对象的余额再更新数据库
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.updateByAccount(fromAct);
String s = null;
s.toUpperCase();
// 4. 更新转入账户余额(update)
count += accountDao.updateByAccount(toAct);
if (count != 2) {
throw new TransferException("转账失败");
}
// 提交事务
sqlSession.commit();
// 关闭事务
SqlSessionUtil.close(sqlSession);
}
}
- 修改dao中的方法:AccountDaoImpl中所有方法中的提交commit和关闭close代码全部删除。
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
// sqlSession.close();
return account;
}
@Override
public int updateByAccount(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.updateByAccount", act);
// sqlSession.commit();
// sqlSession.close();
return count;
}
}
MyBatis 三大核心对象的作用域
SqlSessionFactoryBuilder
- 这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。
- SqlSessionFactoryBuilder 对象的作用就是创建 SqlSessionFactory 对象,只要有了 SqlSessionFactory 对象就不在需要 SqlSessionFactoryBuilder 对象,其就可以被垃圾回收器回收了
- 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
static {
try {
// 获取默认数据库环境对应的SqlSessionFactory对象
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
- 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory
- SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。
- 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。
- 因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
// 默认数据库环境对应的SqlSessionFactory
private static SqlSessionFactory sqlSessionFactory = null;
static {
try {
// 获取默认数据库环境对应的SqlSessionFactory对象
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
SqlSession
- 每个线程都应该有它自己的 SqlSession 实例。
- SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
- 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。
- 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。
- 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}
使用 javassist 生成类
依赖
<dependencies>
<!-- javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.1-GA</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
运行参数配置
- 出现下列报错,需要进行运行参数的配置,JDK高版本的问题,低版本没有java.base这个模块,但是高版本添加了这个模块
java.lang.reflect.InaccessibleObjectException:
Unable to make protected final java.lang.Class
java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
throws java.lang.ClassFormatError accessible:
module java.base does not "opens java.lang" to unnamed module @28c97a5
- –add-opens java.base/java.lang=ALL-UNNAMED
- –add-opens java.base/sun.net.util=ALL-UNNAMED
-
Javassist的使用
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.reflect.Method;
/**
* ClassName: Test
* Package: PACKAGE_NAME
* Description:
*
* @Author tcw
* @Create 2023-05-31 15:54
* @Version 1.0
*/
public class Test {
@org.junit.Test
public void tset01() throws Exception {
// 获取类池,用于生成class
ClassPool pool = ClassPool.getDefault();
// 使用类池制造类
// 方法参数为类的全类名
CtClass ctClass = pool.makeClass("cw.mybatis.bank.dao.impl.AccountDaoImpl");
// 类中方法的代码
String methodCode = "public void insert() { System.out.println(\"数据库正在新增信息...\");}";
// 制造类中的方法
// 参数一:类中方法的代码;参数二:为哪个类创建方法
CtMethod ctMethod = CtMethod.make(methodCode, ctClass);
// 将创建的方法添加到类中
ctClass.addMethod(ctMethod);
// 在内存中动态生成class,将类加载到JVM中
// 得到类对应的Class对象
// Class<?> aClass = ctClass.toClass();
ctClass.toClass();
// 加载类到JVM中
Class<?> aClass = Class.forName("cw.mybatis.bank.dao.impl.AccountDaoImpl");
// 利用动态生成的类进行对象的创建(使用无参构造方法)
Object o = aClass.getDeclaredConstructor().newInstance();
// 获取类中的方法
Method insertMethod = aClass.getDeclaredMethod("insert");
// 调用新创建的对象的insert方法
insertMethod.invoke(o);
}
}
使用 javassist 动态生成类并实现接口
public interface AccountDao {
void delete();
}
@org.junit.Test
public void testGenerateImpl() throws Exception {
// 获取类型用于类的制造
ClassPool pool = ClassPool.getDefault();
// 制造类
CtClass AccountDaoImpl = pool.makeClass("cw.study.mybatis.dao.impl.AccountDaoImpl");
// 制造接口
CtClass AccountDao = pool.makeInterface("cw.study.mybatis.dao.AccountDao");
// 添加接口到类中
AccountDaoImpl.addInterface(AccountDao);
// 实现接口中的方法
// 方法的代码
String methodCode = "public void delete() {System.out.println(\"数据库正在删除信息...\");}";
// 制造AccountDaoImpl类的方法
CtMethod method = CtMethod.make(methodCode, AccountDaoImpl);
// 将方法添加到类中
AccountDaoImpl.addMethod(method);
// 在内存中生成类,并将类加载到JVM中
Class<?> AccountDaoImplClass = AccountDaoImpl.toClass();
// 创建对象
AccountDao accountDao = (AccountDao) AccountDaoImplClass.getDeclaredConstructor().newInstance();
// 调用方法
accountDao.delete();
}
使用 javassist 动态生成类并实现接口中所有方法
public interface AccountDao {
void delete();
int insert(String actno);
int update(String actno, Double balance);
String selectByActno(String actno);
}
@org.junit.Test
public void testGenerateAccountDaoImpl() throws Exception {
// 获取类池
ClassPool pool = ClassPool.getDefault();
// 制造类
CtClass ctClass = pool.makeClass("cw.study.mybatis.dao.impl.AccountDaoImpl");
// 制造接口
CtClass ctInterface = pool.makeInterface("cw.study.mybatis.dao.AccountDao");
// 类实现接口
ctClass.addInterface(ctInterface);
// 获取接口中的所有方法
Method[] methods = AccountDao.class.getDeclaredMethods();
// 遍历接口中的每个方法实现接口方法
Arrays.stream(methods).forEach(method -> {
// 实现接口方法
// public void update(String actno, Double balance) {}
StringBuilder builder = new StringBuilder();
// 添加访问权限修饰符
builder.append("public ");
// 添加返回值类型
builder.append(method.getReturnType().getName());
builder.append(" ");
// 追加方法名
builder.append(method.getName());
builder.append("(");
// 获取方法的形参列表
Parameter[] parameters = method.getParameters();
// 遍历追加形参列表
for (int i = 0; i < parameters.length; i++) {
// 追加形参列表中形参的类型
builder.append(parameters[i].getType().getName());
builder.append(" ");
// 追加形参名 parameters[i].getName() 获取的形参名为 arg0 arg1 ...
builder.append(parameters[i].getName());
if (i != parameters.length - 1) {
builder.append(", ");
}
}
builder.append(") {");
// 添加方法体的方法
builder.append("System.out.println(\"" + method.getName() + "\");");
// 动态添加return语句
// 获取返回类型的简类名
String returnTypeSimpleName = method.getReturnType().getSimpleName();
if ("void".equals(returnTypeSimpleName)) {
} else if ("int".equals(returnTypeSimpleName)) {
builder.append("return 111;");
} else if ("String".equals(returnTypeSimpleName)) {
builder.append("return \"method return string\";");
}
builder.append("}");
// System.out.println(builder);
try {
// 制造ctClass的方法
CtMethod method1 = CtMethod.make(builder.toString(), ctClass);
// 将方法添加到类中
ctClass.addMethod(method1);
} catch (CannotCompileException e) {
e.printStackTrace();
}
});
// 在内存中生成类并将类加载到JVM
Class<?> aClass = ctClass.toClass();
// 创建对象(接口的实现类可以进行强转)
AccountDao accountDao = (AccountDao) aClass.getDeclaredConstructor().newInstance();
// 调用方法
accountDao.delete();
accountDao.insert("");
accountDao.selectByActno("");
accountDao.update("", 12.0);
}
工具类 GenerateDaoProxy 的实现
- 工具类 GenerateDaoProxy 可以动态生成接口的实现类
- 在MyBatis中对javassist进行了二次包装,所以可以在MyBatis中直接使用javassist
- 由于SQL语句的id是框架的使用者提供的,所以框架的开发者不可能知道SQL语句的id,因此框架的开发者就规定SQL语句的id必须时dao接口中的方法名,namespace必须是dao接口的全限定类名,否则不能使用框架提供的动态代理生成接口的实现类的功能
package cw.study.mybatis.utils;
import org.apache.ibatis.javassist.CannotCompileException;
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.lang.reflect.Parameter;
import java.util.Arrays;
/**
* ClassName: GenerateDaoProxy
* Package: cw.study.mybatis.utils
* Description:
* 该工具类用于动态生成实现Dao接口的代理类
* GenerateDaoProxy 这个工具类是MyBatis框架提供的
*
* @Author tcw
* @Create 2023-05-31 17:42
* @Version 1.0
*/
public class GenerateDaoProxy {
/**
* 工具类的构造方法私有化,防止创建对象
*/
private GenerateDaoProxy() {}
/**
* 动态生成Dao接口的代理类对象
*
* @param sqlSession 与数据库的会话对象,由工具类的使用这传递
* @param daoInterfaceClass Dao接口
* @return Dao接口的代理类对象
*/
public static Object generate(SqlSession sqlSession, Class daoInterfaceClass) {
// 获取类池
ClassPool pool = ClassPool.getDefault();
// 制造接口的代理类
CtClass daoInterfaceImpl = pool.makeClass(daoInterfaceClass.getName() + "Impl");
// 制造接口
CtClass daoInterface = pool.makeInterface(daoInterfaceClass.getName());
// 接口的代理类实现接口
daoInterfaceImpl.addInterface(daoInterface);
// 接口的代理类实现接口中的方法
// 获取接口中所有的方法
Method[] daoInterfaceClassDeclaredMethods = daoInterfaceClass.getDeclaredMethods();
// 遍历接口中的所有的方法,逐个进行实现,并添加到接口的代理类中
Arrays.stream(daoInterfaceClassDeclaredMethods).forEach(method -> {
// 实现接口中的方法
try {
// 接口方法的实现方法的代码
StringBuilder methodCode = new StringBuilder();
// public int update(String actno, Double balance) {代码;}
methodCode.append("public ");
// 获取接口方法的返回值类型,添加到实现方法中
methodCode.append(method.getReturnType().getName());
methodCode.append(" ");
// 获取接口方法的方法名,添加到实现方法中
methodCode.append(method.getName());
methodCode.append("(");
// 获取接口方法的形参列表,添加到实现方法中
Parameter[] methodParameters = method.getParameters();
for (int i = 0; i < methodParameters.length; i++) {
// 获取形参的类型
methodCode.append(methodParameters[i].getType().getName());
methodCode.append(" ");
// 获取形参的名 arg0 arg1 arg2 ...
methodCode.append(methodParameters[i].getName());
// 如果不是最后一个形参需要在后面添加逗号
if (i != methodParameters.length - 1) {
methodCode.append(", ");
}
}
methodCode.append(")");
methodCode.append("{\n");
// 接口方法的具体实现
// 获取与数据库的会话对象
// javassist 需要知道类是哪个包下的所以要使用全限定包名
methodCode.append("\torg.apache.ibatis.session.SqlSession sqlSession = cw.study.mybatis.utils" +
".SqlSessionUtil.openSession();\n");
// 执行SQL语句,我们需要知道SQL语句的类型才可以调用相应的方法执行SQL
// getConfiguration() 获取MyBatis核心配置文件中根标签configuration中的内容
// getMappedStatement(SQL语句的Id) 根据SQL语句的id获取相应的SQL语句
// getSqlCommandType() 获取SQL语句的类型,获取到SQL语句的类型是一个枚举值
// 由于SQL语句的id是框架的使用者提供的,所以框架的开发者不可能知道SQL语句的id
// 因此框架的开发者就规定SQL语句的id必须时dao接口中的方法名,namespace必须是dao接口的全限定类名
// 否则不能使用框架提供的动态代理生成接口的实现类的功能
// SQL id = namespace + . +SQL id
String SqlId = daoInterfaceClass.getName() + "." + method.getName();
SqlCommandType sqlCommandType = sqlSession.getConfiguration().getMappedStatement(SqlId).getSqlCommandType();
// 根据SQL语句的类型的不同调用相应的方法
if (sqlCommandType == SqlCommandType.INSERT) {
} else if (sqlCommandType == SqlCommandType.DELETE) {
} else if (sqlCommandType == SqlCommandType.UPDATE) {
methodCode.append("\treturn sqlSession.update(\"" + SqlId + "\", arg0);\n");
} else if (sqlCommandType == SqlCommandType.SELECT) {
// by java.lang.reflect.InvocationTargetException
methodCode.append("\treturn (" + method.getReturnType().getName() + ") sqlSession.selectOne(\"" + SqlId +
"\", arg0);\n");
}
methodCode.append("}");
// System.out.println(methodCode);
// 制造接口方法对应的实现方法
CtMethod ctMethod = CtMethod.make(methodCode.toString(), daoInterfaceImpl);
// 将制造的实现方法添加到动态生成的代理类中
daoInterfaceImpl.addMethod(ctMethod);
} catch (Exception e) {
e.printStackTrace();
}
});
// 接口动态代理类的对象
Object daoInterfaceProxyObj = null;
try {
// 在内存中动态生成类,并且将动态生成的类添加到JVM中
Class<?> daoInterfaceImplClass = daoInterfaceImpl.toClass();
// 创建接口代理类的对象
daoInterfaceProxyObj = daoInterfaceImplClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
// 返回接口动态代理类的对象
return daoInterfaceProxyObj;
}
}
工具类 GenerateDaoProxy 测试
- 要想使用工具类GenerateDaoProxy这种自动生成Dao接口实现类的机制: namespace必须是dao接口的全限定名称,id必须是dao接口的方法名。
<!-- 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>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
package cw.study.mybatis.pojo;
/**
* ClassName: Account
* Package: cw.study.mybatis.pojo
* Description:
*
* @Author tcw
* @Create 2023-05-28 14:33
* @Version 1.0
*/
public class Account {
private Long id;
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public Account() {
}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
package cw.study.mybatis.dao;
import cw.study.mybatis.pojo.Account;
/**
* ClassName: AccountDao
* Package: cw.study.mybatis.dao
* Description:
*
* @Author tcw
* @Create 2023-05-31 16:44
* @Version 1.0
*/
public interface AccountDao {
Account selectByActno(String actno);
}
<?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="cw.study.mybatis.dao.AccountDao">
<select id="selectByActno" resultType="cw.study.mybatis.pojo.Account">
select * from t_act where actno = #{actno}
</select>
</mapper>
@org.junit.Test
public void testGenerateDaoProxy() {
SqlSession sqlSession = SqlSessionUtil.openSession();
AccountDao accountDao = (AccountDao) GenerateDaoProxy.generate(sqlSession, AccountDao.class);
Account act001 = accountDao.selectByActno("act001");
System.out.println(act001);
}
MyBatis 的 getMapper 方法(MyBatis 的接口代理机制)
- 在MyBatis中提供了相关机制,可以为我们动态生成dao接口的代理实现类,即工具类 GenerateDaoProxy 不用我们编写
- MyBatis中实际上采用了代理机制,在内存中生成dao接口的代理实现类,并创建代理类的实例对象
- 如果要使用MyBatis提供的dao接口代理机制,SQL语句的id必须时dao接口中的方法名,namespace必须是dao接口的全限定类名
- MyBatis提供的dao接口代理机制的使用如下:
@org.junit.Test
public void testGetMapper() {
SqlSession sqlSession = SqlSessionUtil.openSession();
// GenerateDaoProxy.generate(sqlSession, AccountDao.class);
// 需要一个sqlSession用于获取SQL id,AccountDao.class 需要代理实现的接口
// sqlSession.getMapper(AccountDao.class)
// 调用该方法的就是用于获取SQL id的sqlSession,AccountDao.class 需要代理实现的接口
// 该方法返回dao接口的代理实现类对象实例
// 要使用该方法,SQL语句的id必须时dao接口中的方法名,namespace必须是dao接口的全限定类名
AccountDao accountDaoProxy = sqlSession.getMapper(AccountDao.class);
Account act001 = accountDaoProxy.selectByActno("act001");
System.out.println(act001);
}
面向接口进行CRUD
工具类 SqlSessionUtil
package cw.mybatis.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
/**
* ClassName: SqlSessionUtil
* Package: cw.mybatis.utils
* Description:
*
* @Author tcw
* @Create 2023-05-23 21:40
* @Version 1.0
*/
public class SqlSessionUtil {
private static SqlSessionFactory sqlSessionFactory = null; // 默认使用数据库环境对应的SqlSessionFactory
private static ThreadLocal<SqlSession> local = new ThreadLocal<>(); // 保证一个线程对应一个SqlSession
static {
try {
// 获取默认使用数据库环境对应的SqlSessionFactory对象
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config" +
".xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 私有化构造器,防止创建工具类实例对象
*/
private SqlSessionUtil() {}
/**
* 使用工厂对象开启本线程和数据库的会话
*
* @return 与数据库的会话对象
*/
public static SqlSession openSession() {
SqlSession sqlSession = local.get(); // 获取当前线程对应的数据库会话对象
// 如果当前线程没有对应的数据库会话对象,则创建一个会话对象
if (sqlSession == null) {
sqlSession = sqlSessionFactory.openSession();
// 保存本线程对应的数据库会话对象
// 将sqlSession对象绑定到当前线程上。
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭会话对象
* 关闭SqlSession对象并从当前线程中移除SqlSession对象
*/
public static void close() {
SqlSession sqlSession = local.get(); // 获取当前线程对应的数据库会话对象
// 如果当前线程有相应的数据库会话对象就进行关闭
if (sqlSession != null) {
sqlSession.close();
// 注意移除SqlSession对象和当前线程的绑定关系。
// 因为Tomcat服务器支持线程池。也就是说:用过的线程对象t1,可能下一次还会使用这个t1线程。
local.remove();
}
}
}
Car(pojo)
package cw.mybatis.pojo;
/**
* ClassName: Car
* Package: cw.mybatis.pojo
* Description:
* 封装汽车相关信息的pojo类,普通的java类。
*
* @Author tcw
* @Create 2023-05-23 21:59
* @Version 1.0
*/
public class Car {
private Long id;
private String carNum;
private String brand;
private Double guidePrice;
private String produceTime;
private String carType;
public Car() {
}
public Car(Long id, String carNum, String brand, Double guidePrice, String produceTime, String carType) {
this.id = id;
this.carNum = carNum;
this.brand = brand;
this.guidePrice = guidePrice;
this.produceTime = produceTime;
this.carType = carType;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCarNum() {
return carNum;
}
public void setCarNum(String carNum) {
this.carNum = carNum;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
public Double getGuidePrice() {
return guidePrice;
}
public void setGuidePrice(Double guidePrice) {
this.guidePrice = guidePrice;
}
public String getProduceTime() {
return produceTime;
}
public void setProduceTime(String produceTime) {
this.produceTime = produceTime;
}
public String getCarType() {
return carType;
}
public void setCarType(String carType) {
this.carType = carType;
}
@Override
public String toString() {
return "Car{" + "id=" + id + ", carNum='" + carNum + '\'' + ", brand='" + brand + '\'' + ", guidePrice=" + guidePrice + ", produceTime='" + produceTime + '\'' + ", carType='" + carType + '\'' + '}';
}
}
CarMapper
- 操作数据库的接口,在MyBatis中一般包名命名为mapper,就是MVC架构模式中的dao
- 在MyBatis中操作数据库的接口名,一般叫XxxxMapper,而不为XxxxDao
package cw.mybatis.mapper;
import cw.mybatis.pojo.Car;
import java.util.List;
/**
* ClassName: CarMapper
* Package: cw.mybatis.mapper
* Description:
*
* @Author tcw
* @Create 2023-05-23 21:57
* @Version 1.0
*/
public interface CarMapper {
/**
* 新增 car
*
* @param car 需要新增的 car 信息
* @return 影响数据库中的条数
*/
int insert(Car car);
/**
* 根据 id 删除汽车信息
*
* @param id 汽车信息对应的 id
* @return 影响数据库中的条数
*/
int deleteById(Long id);
/**
* 修改汽车信息
*
* @param car 修改之后的汽车信息
* @return 影响数据库中的条数
*/
int update(Car car);
/**
* 根据汽车 id 查询汽车信息
*
* @param id 汽车信息的 id
* @return 汽车信息
*/
Car selectById(Long id);
/**
* 查询所有的汽车信息
*
* @return 所有的汽车信息
*/
List<Car> selectAll();
}
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">
<!-- 如果要使用面向接口的方式进行CRUD,则namespace的属性值必须为相应接口的全类名 -->
<!-- SQL语句的id必须为方法名 -->
<mapper namespace="cw.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="update">
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="cw.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="cw.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 cw.mybatis.mapper;
import cw.mybatis.pojo.Car;
import cw.mybatis.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
import static org.junit.Assert.*;
/**
* ClassName: CarMapperTest
* Package: cw.mybatis.mapper
* Description:
*
* @Author tcw
* @Create 2023-05-23 22:15
* @Version 1.0
*/
public class CarMapperTest {
@Test
public void insert() {
SqlSession sqlSession = SqlSessionUtil.openSession();
// 面向接口,获取接口的代理对象
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 数据对应的对象
Car car = new Car(null, "4444", "奔驰C200", 32.0, "2000-10-10", "新能源");
// 调用定义的新增car的方法
int insert = mapper.insert(car);
System.out.println(insert);
sqlSession.commit();
SqlSessionUtil.close();
}
@Test
public void deleteById() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
int i = mapper.deleteById(1L);
System.out.println(i);
sqlSession.commit();
SqlSessionUtil.close();
}
@Test
public void update() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(20L, "4444", "奔驰C200&&&", 32.0, "2000-10-10", "新能源");
int update = mapper.update(car);
System.out.println(update);
sqlSession.commit();
SqlSessionUtil.close();
}
@Test
public void selectById() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = mapper.selectById(20L);
System.out.println(car);
SqlSessionUtil.close();
}
@Test
public void selectAll() {
SqlSession sqlSession = SqlSessionUtil.openSession();
List<Car> cars = sqlSession.getMapper(CarMapper.class).selectAll();
cars.forEach(System.out::println);
SqlSessionUtil.close();
}
}
MyBatis 小技巧
#{} 与 ${}
接口方法
/**
* 根据汽车类型获取汽车信息
*
* @param carType 汽车类型
* @return 汽车信息组成的列表
*/
List<Car> selectByCarType(String carType);
SQL
<select id="selectByCarType" resultType="cw.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>
<select id="selectByCarType" resultType="cw.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>
测试
@Test
public void test01() {
SqlSession sqlSession = SqlSessionUtil.openSession();
// 获取CarMapper接口的代理类对象
// 底层为CarMapper接口生成了字节码,同时创建了实现类的对象
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByCarType("新能源");
cars.forEach(System.out::println);
SqlSessionUtil.close();
}
#{} 结果
- 先对SQL语句进行编译,采用 ?作为占位符,然后使用查询参数填充占位符
[main] DEBUG cw.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 cw.mybatis.mapper.CarMapper.selectByCarType - ==>
Parameters: 新能源(String)
[main] DEBUG cw.mybatis.mapper.CarMapper.selectByCarType - <==
Total: 2
查询出来的数据:
Car{id=2, carNum='1002', brand='奔驰E300L', guidePrice=55.0, produceTime='2022-11-11', carType='新能源'}
Car{id=20, carNum='4444', brand='奔驰C200&&&', guidePrice=32.0, produceTime='2000-10-10', carType='新能源'}
${} 结果
- 直接将查询参数的数值直接采用字符串拼接的方式,拼接到SQL语句中,然后再进行SQL语句的编译
[main] DEBUG cw.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 cw.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注入的风险。
- 特点:先进行SQL语句的编译,然后给SQL语句的占位符问号?传值。
- ${}:底层使用Statement。
- 特点:先进行SQL语句的拼接,然后再对SQL语句进行编译。
- 存在SQL注入的风险。
- 优先使用#{},这是原则。避免SQL注入的风险。#{}不能实现在考虑${}
${} 的使用场景 - 升序或降序排序
- 从数据库查询数据时,进行排序
接口方法
/**
* 查询所有的汽车信息,并进行升序或降序排序
*
* @param ascOrDesc 升序或降序排序
* @return 排序后的汽车信息组成的集合
*/
List<Car> selectAllAscOrDesc(String ascOrDesc);
SQL
<select id="selectAllAscOrDesc" resultType="cw.mybatis.pojo.Car">
select id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from t_car
order by produce_time #{ascOrDesc};
</select>
<select id="selectAllAscOrDesc" resultType="cw.mybatis.pojo.Car">
select id,
car_num as carNum,
brand,
guide_price as guidePrice,
produce_time as produceTime,
car_type as carType
from t_car
order by produce_time ${ascOrDesc};
</select>
测试
@Test
public void test02() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 查询所有的汽车信息,并降序排序
List<Car> cars = mapper.selectAllAscOrDesc("desc");
cars.forEach(System.out::println);
SqlSessionUtil.close();
}
#{} 结果
- 会先进行SQL语句的编译,将#{}的位置使用占位符?替换,然后再使用查询参数进行填充
[main] DEBUG cw.mybatis.mapper.CarMapper.selectAllAscOrDesc - ==>
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 ?;
[main] DEBUG cw.mybatis.mapper.CarMapper.selectAllAscOrDesc - ==>
Parameters: desc(String)
填充后的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 produce_time 'desc';
存在语法错误
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause:
java.sql.SQLSyntaxErrorException:
You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for the right syntax to use near ''desc'' at line 8
### 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 order by produce_time ?;
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''desc'' at line 8
${} 结果
- 先将查询参数与SQL语句进行字符串拼接,然后再进行SQL语句的编译
[main] DEBUG cw.mybatis.mapper.CarMapper.selectAllAscOrDesc - ==>
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 desc;
符合预期,可以进行数据的查询并进行排序
[main] DEBUG cw.mybatis.mapper.CarMapper.selectAllAscOrDesc - ==> Parameters:
[main] DEBUG cw.mybatis.mapper.CarMapper.selectAllAscOrDesc - <== Total: 17
${} 小结
- 如果需要SQL语句的关键字放到SQL语句中,只能使用${},因为#{}是以值的形式放到SQL语句当中的。
${} 的使用场景 - 拼接表名
- 向SQL语句当中拼接表名,就需要使用${}
- 现实业务当中,可能会存在分表存储数据的情况。因为一张表存的话,数据量太大。查询效率比较低。可以将这些数据有规律的分表存储,这样在查询的时候效率就比较高。因为扫描的数据量变少了。
- 如,日志表:专门存储日志信息的。如果t_log只有一张表,这张表中每一天都会产生很多log,慢慢的,这个表中数据会很多。我们可以每天生成一个新表。每张表以当天日期作为名称,例如:t_log_20220901、t_log_20220902…当我们需要知道某一天的日志信息,假设今天是20220901,那么我们可以直接查:t_log_20220901的表即可。
- 如,日志表:专门存储日志信息的。如果t_log只有一张表,这张表中每一天都会产生很多log,慢慢的,这个表中数据会很多。我们可以每天生成一个新表。每张表以当天日期作为名称,例如:t_log_20220901、t_log_20220902…当我们需要知道某一天的日志信息,假设今天是20220901,那么我们可以直接查:t_log_20220901的表即可。
数据库表准备
use dbtest;
create table t_log_20220901
(
id int primary key auto_increment,
log varchar(255),
time datetime
);
INSERT INTO `t_log_20220901` (`id`, `log`, `time`)
VALUES (1, '日志信息日志信息日志信息', '2022-09-01 11:06:31');
INSERT INTO `t_log_20220901` (`id`, `log`, `time`)
VALUES (2, '安全警告安全警告安全警告', '2022-09-01 11:07:20');
create table `t_log_20220902`
(
id int primary key auto_increment,
log varchar(255),
time datetime
);
INSERT INTO `t_log_20220902` (`id`, `log`, `time`)
VALUES (1, '插入数据', '2022-09-02 11:08:31');
INSERT INTO `t_log_20220902` (`id`, `log`, `time`)
VALUES (2, '删除数据', '2022-09-02 11:08:45');
数据库表对应的Java类
package cw.mybatis.pojo;
/**
* ClassName: Log
* Package: cw.mybatis.pojo
* Description:
*
* @Author tcw
* @Create 2023-05-25 11:24
* @Version 1.0
*/
public class Log {
private Integer id;
private String log;
private String time;
public Log() {
}
public Log(Integer id, String log, String time) {
this.id = id;
this.log = log;
this.time = time;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLog() {
return log;
}
public void setLog(String log) {
this.log = log;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
@Override
public String toString() {
return "Log{" + "id=" + id + ", log='" + log + '\'' + ", time='" + time + '\'' + '}';
}
}
接口声明
public interface LogMapper {
/**
* 根据日期查询不同的数据库表中的全部信息
*
* @param date 日期字符串
* @return 与日期对应的数据表中全部信息组成的集合
*/
List<Log> selectAllByTable(String date);
}
SQL映射文件
<mapper namespace="cw.mybatis.mapper.LogMapper">
<select id="selectAllByTable" resultType="cw.mybatis.pojo.Log">
select id, log, `time`
from t_log_${date}
</select>
</mapper>
修改 MyBatis 核心配置文件
<mappers>
<mapper resource="CarMapper.xml"/>
<mapper resource="LogMapper.xml"/>
</mappers>
测试
@Test
public void selectAllByTable() {
SqlSession sqlSession = SqlSessionUtil.openSession();
// 获取代理对象
LogMapper mapper = sqlSession.getMapper(LogMapper.class);
// 查询数据
List<Log> logs20220901 = mapper.selectAllByTable("20220901");
logs20220901.forEach(System.out::println); // 打印信息
List<Log> logs20220902 = mapper.selectAllByTable("20220902");
logs20220902.forEach(System.out::println); // 打印信息
// 关闭资源
SqlSessionUtil.close();
}
直接将日期字符串以字符串拼接的方式与SQL语句进行拼接,然后再进行SQL语句的编译
[main] DEBUG cw.mybatis.mapper.LogMapper.selectAllByTable - ==>
Preparing: select id, log, `time` from t_log_20220901
[main] DEBUG cw.mybatis.mapper.LogMapper.selectAllByTable - ==> Parameters:
[main] DEBUG cw.mybatis.mapper.LogMapper.selectAllByTable - <== Total: 2
Log{id=1, log='日志信息日志信息日志信息', time='2022-09-01 11:06:31'}
Log{id=2, log='安全警告安全警告安全警告', time='2022-09-01 11:07:20'}
[main] DEBUG cw.mybatis.mapper.LogMapper.selectAllByTable - ==>
Preparing: select id, log, `time` from t_log_20220902
[main] DEBUG cw.mybatis.mapper.LogMapper.selectAllByTable - ==> Parameters:
[main] DEBUG cw.mybatis.mapper.LogMapper.selectAllByTable - <== Total: 2
Log{id=1, log='插入数据', time='2022-09-02 11:08:31'}
Log{id=2, log='删除数据', time='2022-09-02 11:08:45'}
如果是 #{} 会先进行SQL语句的编译,
编译后的SQL语句为:Preparing: select id, log, `time` from t_log_?
使用日期字符串填充后,SQL语句变为:Preparing: select id, log, `time` from t_log_'20220902'
${} 的使用场景 - 批量删除(一次删除多条记录)
- 批量删除的SQL语句有两种写法:
-- 第一种 or :
delete from t_car where id=1 or id=2 or id=3;
-- 第二种 in :
delete from t_car where id in(1,2,3);
接口声明
/**
* 根据id批量删除数据库表中的信息
*
* @param ids 需要进行删除操作的信息的id组成的字符串
* @return 删除记录的条数
*/
int deleteBatch(String ids);
SQL
<delete id="deleteBatch">
delete from t_car
where id in (${ids});
</delete>
<!--
如果要采用 delete from t_car where id=1 or id=2 or id=3;
这种形式则需要使用动态SQL
-->
测试
@Test
public void test03() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
int deleteCount = mapper.deleteBatch("2, 3, 4");
System.out.println(deleteCount);
sqlSession.commit()
SqlSessionUtil.close();
}
先进行字符串拼接,然后再进行SQL语句的编译
[main] DEBUG cw.mybatis.mapper.CarMapper.deleteBatch - ==>
Preparing: delete from t_car where id in (2, 3, 4);
[main] DEBUG cw.mybatis.mapper.CarMapper.deleteBatch - ==> Parameters:
[main] DEBUG cw.mybatis.mapper.CarMapper.deleteBatch - <== Updates: 3
如果使用 #{} 先进行SQL语句的编译
编译后的SQL为 delete from t_car where id in (?);
使用查询参数填充占位符后SQL为 delete from t_car where id in ('2, 3, 4');
会报语法错误
模糊查询(like)
- 需求:根据汽车品牌进行模糊查询
- SQL如下:
select * from t_car where brand like '%奔驰%';
select * from t_car where brand like '%比亚迪%';
可以采取的方式分析
- 由于#{}是先对SQL语句进行编译,将#{}的位置使用JDBC中的占位符?进行替换,然后再使用参数来填充占位符,如果用于填充占位符的参数为字符串类型,则填充后会携带有引号;${}是先采用字符串拼接的方式,将参数和SQL语句字符串进行拼接,然后再对SQL语句进行编译,所以用于填充占位符的参数为字符串类型,与SQL语句拼接后不会有引号
- 注意:SQL语句中,引号内的 ? ,如
'%?%'
不会被认为是JDBC的占位符,而会被认为是字符串的一部分 - 因此可以有如下四种方案:
- 第一种方案:
'%${brand}%'
,字符串拼接后为'%brand%'
- 第二种方案:concat函数,这个是mysql数据库当中的一个函数,专门进行字符串拼接
concat('%',#{brand},'%')
,SQL编译后为concat('%', ?,'%')
- 第三种方案:
concat('%','${brand}','%')
,字符串拼接后为concat('%','brand','%')
- 第四种方案:
"%"#{brand}"%"
,SQL编译后为"%"?"%"
- 让JDBC可以识别出充当占位符的 ?
- MySQL会自动进行字符串的拼接
- 也可以在传入的字符串中将模糊匹配写好,如
'%宝马%'
,将这一整个字符串传递
- 第一种方案:
接口声明
/**
* 根据汽车的品牌进行模糊查询
*
* @param brand 汽车品牌字符串
* @return 查询出来的汽车信息
*/
List<Car> selectByBrandLike(String brand);
SQL
<select id="selectByBrandLike" resultType="cw.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 concat('%', #{brand}, '%');
</select>
测试
@Test
public void test04() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByBrandLike("比亚迪");
cars.forEach(System.out::println);
SqlSessionUtil.close();
}
先进行SQL语句的编译
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 concat('%', ?, '%');
使用数据填充占位符(采用 #{} )
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 concat('%', '比亚迪', '%');
[main] DEBUG cw.mybatis.mapper.CarMapper.selectByBrandLike - ==>
Preparing:
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 concat('%', ?, '%');
[main] DEBUG cw.mybatis.mapper.CarMapper.selectByBrandLike - ==>
Parameters: 比亚迪(String)
[main] DEBUG cw.mybatis.mapper.CarMapper.selectByBrandLike - <== Total: 5
Car{id=9, carNum='1111', brand='比亚迪汉', guidePrice=10.0, produceTime='2020-11-11', carType='电车'}
Car{id=15, carNum='1333', brand='比亚迪汉', guidePrice=10.0, produceTime='2020-11-11', carType='电车'}
Car{id=16, carNum='1333', brand='比亚迪汉', guidePrice=10.0, produceTime='2020-11-11', carType='电车'}
Car{id=17, carNum='1333', brand='比亚迪汉', guidePrice=10.0, produceTime='2020-11-11', carType='电车'}
Car{id=18, carNum='1333', brand='比亚迪汉', guidePrice=10.0, produceTime='2020-11-11', carType='电车'}
别名机制:typeAliases
- 我们之前在XxxxMapper.xml文件书写查询语句,指定返回结果集中数据元素的类型时,每次都需要写全类名,太过于复杂
- resultType属性用来指定查询结果集的封装类型,这个名字太长
<select id="selectByCarType" resultType="cw.mybatis.pojo.Car"><select/>
<select id="selectAllAscOrDesc" resultType="cw.mybatis.pojo.Car"><select/>
<select id="selectByBrandLike" resultType="cw.mybatis.pojo.Car"><select/>
在核心配置文件中配置别名
- MyBatis中为我们提供了别名机制,使用别名机制,我们可以为查询语句中指定的返回值类型的全类名取别名
- 在MyBatis核心配置文件中,在
typeAliases
标签中使用typeAliase
标签起别名- 注意:settings标签写在properties标签后,typeAliases标签写在settings标签后
- typeAliases标签中的typeAlias可以写多个
<!-- 起别名 -->
<typeAliases>
<!-- 为 cw.mybatis.pojo.Car 起别名,别名为 Car -->
<typeAlias type="cw.mybatis.pojo.Car" alias="Car"/>
</typeAliases>
在XxxxMapper.xml文件中使用别名
- 在 CarMapper.xml 文件中使用别名
<select id="selectByCarType" 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 car_type = #{carType}
</select>
- 测试
@Test
public void test01() {
SqlSession sqlSession = SqlSessionUtil.openSession();
// 获取CarMapper接口的代理类对象
// 底层为CarMapper接口生成了字节码,同时创建了实现类的对象
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByCarType("新能源");
cars.forEach(System.out::println);
SqlSessionUtil.close();
}
注意点
- 别名不缺分大小写
<select id="selectByCarType" resultType="caR">
- namespace不能使用别名机制,必须写全限定接口名称,带有包名的。
<!-- 起别名 -->
<typeAliases>
<!-- 为 cw.mybatis.pojo.Car 起别名,别名为 Car -->
<typeAlias type="cw.mybatis.pojo.Car" alias="Car"/>
<typeAlias type="cw.mybatis.mapper.CarMapper" alias="CarMapper"/>
</typeAliases>
<mapper namespace="CarMapper">
- typeAlias 标签中的 alias 属性是可以省略的,有默认的别名,省略alias之后,别名就是类的简名,比如:cw.mybatis.pojo.Car 的别名就是 Car/car/cAR/cAr,不区分大小写。
<!-- 起别名 -->
<typeAliases>
<!-- 为 cw.mybatis.pojo.Car 起别名,别名为 Car -->
<typeAlias type="cw.mybatis.pojo.Car"/>
</typeAliases>
<select id="selectByCarType" resultType="caR">
别名机制:package
- 在起别名时,我们可以直接指定要起别名的类所在的包,MyBatis会自动为这个包下的所有的类全部起别名,别名就是类简名,不区分大小写。
<!-- 起别名 -->
<typeAliases>
<!-- 为 cw.mybatis.pojo 包下面的所有类起别名,别名默认为类的简名 -->
<package name="cw.mybatis.pojo"/>
</typeAliases>
<select id="selectByCarType" resultType="cAR">
核心配置文件中 mappers 标签的配置
mapper 标签中指定配置文件的三种方式
- 在MyBatis核心配置文件mappers标签中的mapper标签指定XxxxMapper.xml配置文件位置的方式有三种:
-
<mapper resource="CarMapper.xml"/>
- 要求类的根路径下必须有:CarMapper.xml
- 使用 resource 属性指定配置文件的存放位置,这种方式是从类的根路径下开始查找资源。
- 采用这种方式的话,你的配置文件需要放到类路径当中才行。
-
<mapper url="file:///d:/CarMapper.xml"/>
- 要求在d:/下有CarMapper.xml文件
- 使用 url 属性指定配置文件的存放位置,这种方式是一种绝对路径的方式
- 这种方式不要求配置文件必须放到类路径当中,哪里都行,只要提供一个绝对路径就行。
- 这种方式使用极少,移植性太差。
-
<mapper class="全限定接口名,带有包名"/>
- class:该属性的属性值需要我们提供的是 mapper 接口的全限定接口名,必须带有包名的。
- 使用 class 属性指定全限定接口名,MyBatis就会去该接口所在包下查找与接口相对应与接口同名的 XxxMapper.xml SQL映射配置文件
- 如:
<mapper class="com.powernode.mybatis.mapper.CarMapper"/>
,mybatis框架会自动去com/powernode/mybatis/mapper目录下查找CarMapper.xml文件 - 如果你采用这种方式,那么必须保证CarMapper.xml文件和CarMapper接口必须在同一个目录下,并且名字必须一致。
- CarMapper接口 -> CarMapper.xml
- LogMapper接口 -> LogMapper.xml
- …
-
接口和配置文件的存放目录
- 将类和配置文件放在同一个目录下,可以采用如下方式建包,因为resources目录相当于类的根路径,就是打包后包的根路径,蓝色的java目录也相当于类的根路径,所以打包完成后 resources 下与 java 下在相同名字的文件夹下的文件会被放在一起
- 注意:在IDEA的 resources 目录下如果要一次新建多重目录的话,在创建目录时必须采用如下的形式:
- com/powernode/mybatis/mapper
- 不能这样采用如下的形式,如下的形式只能用于软件包的创建:
- com.powernode.mybatis.mapper
- 如果采用上述的形式,则
com.powernode.mybatis.mapper
这个会被整个当成一个目录名,resources目录下没有建包的概念,只有创建目录的概念 -
- 注意:在IDEA的 resources 目录下如果要一次新建多重目录的话,在创建目录时必须采用如下的形式:
mapper 直接指定接口及配置文件所在的包
- 采用 mapper 的 class 属性指定接口对应的配置文件的存放位置,可以直接指定接口所在包名,需要使用相应配置文件中的SQL语句时,MyBatis就会去接口所在的包自动查询同名的配置文件
- 这种方式在实际开发中是使用的,必须满足XML文件必须和接口放在一起,并且名字必须一致。
<mappers>
<!-- 指定 xml 文件放在 com.powernode.mybatis.mapper 包下 -->
<package name="com.powernode.mybatis.mapper"/>
</mappers>
在 IDEA 中配置 MyBatis 配置文件模版
新建 MyBatis 核心配置文件模版
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource=""/>
<typeAliases>
<package name=""/>
</typeAliases>
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name=""/>
</mappers>
</configuration>
测试使用新建的MyBatis核心配置文件模版
在 IDEA 中配置 SqlMapper 配置文件模版
新建 SqlMapper 配置文件模版
<?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="">
</mapper>
测试使用新建的SqlMapper配置文件模版
插入数据时获取自动生成的主键
业务场景说明
- 存在业务场景,当我们向一张表中插入了一条数据,由于主键一般是自增的,在插入数据时,一般不指定主键,但是此时还有另外一张表,与新插入数据的表有关联关系,该表的外键需要使用新插入的数据的主键值,建立数据之间的关联关系,此时我们就需要获取自动生成的主键值
- 业务背景:
- 一个用户有多个角色:插入一个用户数据的同时需要给该用户分配角色:需要将生成的用户的id插入到角色表的user_id字段上
- 实现方式有两种:
- 第一种方式:可以先插入用户数据,再写一条查询语句获取新插入用户数据的id,然后再在角色表插入user_id字段。【比较麻烦】
- 第二种方式:mybatis提供了一种方式更加便捷。
接口声明
/**
* 插入汽车信息,同时使用生成的主键值
*
* @param car 汽车信息
* @return 影响数据库的条数
*/
int insertCarUseGeneratedKey(Car car);
SQL
<!--
useGeneratedKeys="true" 开启使用自动生成的主键
keyProperty="id" 指定要将自动生成的主键值保存到用于
传递数据给SQL语句的对象的哪个属性上
-->
<insert id="insertCarUseGeneratedKey" useGeneratedKeys="true" keyProperty="id">
insert into t_car
values (null, #{carNum}, #{brand}, #{guidePrice}, #{produceTime}, #{carType});
</insert>
测试
@Test
public void testInsertCarUseGeneratedKey() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = new Car(null, "9199", "凯美瑞", 32.0, "2020-11-12", "电车");
int count = mapper.insertCarUseGeneratedKey(car);
System.out.println("影响数据库的条数:" + count);
System.out.println(car);
SqlSessionUtil.close();
}
Mybatis 参数处理
- 数据库表:t_student
USE dbtest;
DROP TABLE IF EXISTS t_student;
CREATE TABLE t_student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255),
age INT,
height DOUBLE,
birth DATE,
sex CHAR(1)
);
INSERT INTO t_student(name, age, height, birth, sex) VALUES ('张三', 20, 1.77, '2001-01-02', '男');
INSERT INTO t_student(name, age, height, birth, sex) VALUES ('李四', 20, 1.67, '2001-11-22', '女');
- 学生类:Student
package cw.study.mybatis.pojo;
import java.util.Date;
/**
* ClassName: Student
* Package: cw.study.mybatis.pojo
* Description:
*
* @Author tcw
* @Create 2023-05-28 11:49
* @Version 1.0
*/
public class Student {
private Long id;
private String name;
private Integer age;
private Double height;
private Character sex;
private Date birth;
// constructor
// setter and getter
// toString
}
单个简单类型参数
- 简单类型包括:
- byte short int long float double char
- Byte Short Integer Long Float Double Character
- String
- java.util.Date
- java.sql.Date
- select 标签中有个 parameterType属性:告诉mybatis框架,我这个方法的参数类型是什么类型。
- 明确指明类型,MyBatis 可以不用进行类型推断,可以提高效率
<!--
select 标签中有个 parameterType属性
告诉mybatis框架,我这个方法的参数类型是什么类型。
mybatis框架自身带有类型自动推断机制,所以大部分情况下parameterType属性都是可以省略不写的。
-->
<select id="selectById" resultType="Student" parameterType="java.lang.Long">
select * from t_student where id = #{id}
</select>
- mybatis框架自身带有类型自动推断机制,所以大部分情况下parameterType属性都是可以省略不写的。
SQL语句编译后:select * from t_student where id = ?
JDBC代码是一定要给 ? 传值的,会调用 ps.setXxx(第几个问号, 传什么值);
ps.setLong(1, 1L);
ps.setString(1, "zhangsan");
ps.setDate(1, new Date());
ps.setInt(1, 100);
...
- mybatis底层到底调用setXxx的哪个方法,取决于parameterType属性的值。
- 注意:mybatis框架实际上内置了很多别名,可以参考开发手册:https://mybatis.net.cn/configuration.html#typeAliases
<!-- mybatis框架实际上内置了很多别名,可以参考开发手册 -->
<select id="selectByName" resultType="Student" parameterType="string">
select * from t_student where name = #{name}
</select>
- 在#{}中,可以明确告诉mybatis框架,该参数会用什么类型的数据进行填充,对应数据库中的数据类是什么
- 明确指明类型,MyBatis 可以不用进行类型推断,可以提高效率
<select id="selectByName" resultType="Student" parameterType="string">
select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR}
</select>
- 根据id查询、name查询、birth查询、sex查询
package cw.study.mybatis.mapper;
import cw.study.mybatis.pojo.Student;
import java.util.Date;
import java.util.List;
/**
* ClassName: StudentMapper
* Package: cw.study.mybatis.mapper
* Description:
*
* @Author tcw
* @Create 2023-05-28 11:55
* @Version 1.0
*/
public interface StudentMapper {
// 根据id查询
List<Student> selectById(Long id);
// name查询
List<Student> selectByName(String name);
// birth查询
List<Student> selectByBirth(Date birth);
// sex查询
List<Student> selectBySex(Character sex);
}
<?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="cw.study.mybatis.mapper.StudentMapper">
<!--
select 标签中有个 parameterType属性
告诉mybatis框架,我这个方法的参数类型是什么类型。
mybatis框架自身带有类型自动推断机制,所以大部分情况下parameterType属性都是可以省略不写的。
-->
<select id="selectById" resultType="Student" parameterType="java.lang.Long">
select * from t_student where id = #{id}
</select>
<!-- mybatis框架实际上内置了很多别名,可以参考开发手册 -->
<select id="selectByName" resultType="Student" parameterType="string">
select * from t_student where name = #{name, javaType=String, jdbcType=VARCHAR}
</select>
<select id="selectByBirth" resultType="Student" parameterType="date">
select * from t_student where birth = #{birth}
</select>
<select id="selectBySex" resultType="Student">
select * from t_student where sex = #{sex}
</select>
</mapper>
/**
* ClassName: StudentMapperTest
* Package: cw.study.mybatis.test
* Description:
*
* @Author tcw
* @Create 2023-05-28 11:59
* @Version 1.0
*/
public class StudentMapperTest {
@Test
public void testSelectById() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.selectById(1L);
students.forEach(System.out::println);
SqlSessionUtil.close();
}
@Test
public void testSelectByName() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.selectByName("李四");
students.forEach(System.out::println);
SqlSessionUtil.close();
}
@Test
public void testSelectByBirth() throws ParseException {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
List<Student> students = mapper.selectByBirth(simpleDateFormat.parse("2001-11-22"));
students.forEach(System.out::println);
SqlSessionUtil.close();
}
@Test
public void testSelectBySex() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.selectBySex('男');
students.forEach(System.out::println);
SqlSessionUtil.close();
}
}
Map 集合
- 使用Map集合进行参数的传递,将每个条件以key和value的形式存放到集合中,在使用集合中的数据的时候需要通过
#{map集合的key}
来取值。
/**
* 通过集合传递要插入数据库中的学生信息
*
* @param map 学生信息
* @return 影响数据库的条数
*/
int insertStudentByMap(Map<String, Object> map);
<!--
parameterType="map" 可以省略, 可以自动推出传入的参数为map集合
在使用集合中的数据的时候需要通过#{map集合的key}来取值。
-->
<insert id="insertStudentByMap" parameterType="map">
insert into t_student(id, name, age, sex, height, birth)
values (null, #{name}, #{age}, #{sex}, #{height}, #{birth});
</insert>
@Test
public void testInsertStudentByMap() {
// 封装数据
HashMap<String, Object> map = new HashMap<>();
// name, age, sex, height, birth
map.put("name", "张三");
map.put("age", 20);
map.put("sex", '男');
map.put("height", 1.80);
map.put("birth", new Date());
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int count = mapper.insertStudentByMap(map);
System.out.println(count);
sqlSession.commit();
SqlSessionUtil.close();
}
实体类 POJO
- 使用实体类pojo进行参数的传递,将数据封装到实体类中,在使用实体类中的数据的时候需要通过
#{get方法去掉get首字母小写}
来取值。
/**
* 通过实体类传递要插入数据库的学生信息
*
* @param student 学生信息
* @return 影响数据库记录的条数
*/
int insertStudentByPojo(Student student);
<!--
parameterType="student" 可以省略,可以自动推出类型为Student类
可以使用 student 是因为在MyBatis核心配置文件中配置了别名
在使用集合中的数据的时候需要通过#{get方法去掉get首字母小写}来取值。
-->
<insert id="insertStudentByPojo" parameterType="student">
insert into t_student(id, `name`, age, sex, height, birth)
values (null, #{name}, #{age}, #{sex}, #{height}, #{birth});
</insert>
@Test
public void testInsertStudentByPojo() {
// 封装数据
Student student = new Student(null, "李四", 23, 1.78, '男', new Date());
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
int count = mapper.insertStudentByPojo(student);
System.out.println(count);
sqlSession.commit();
SqlSessionUtil.close();
}
多参数
- 如果是操作数据库的方法的形参列表是多个参数的话,mybatis框架会自动创建一个Map集合。并且Map集合是以这种方式存储参数的:
map.put("arg0", name);
map.put("arg1", sex);
map.put("param1", name);
map.put("param2", sex);
- 注意:低版本的mybatis中,使用的是:#{0}和#{1},以及#{2}…,使用mybatis3.4.2之前的版本时:要用#{0}和#{1}这种形式。
- 高版本的mybatis中,使用的是:
// arg 从 0 开始
#{arg0}
#{arg1}
#{arg2}
// param 从 1 开始
#{param1}
#{param2}
#{param3}
- 对于操作数据库的方法的形参列表是多个参数的情况,在SQL语句中取数据不能直接使用形参的参数名,会报错
/**
* 根据学生的姓名和性别进行学生信息的查询
*
* @param name 学生的姓名
* @param sex 学生的性别
* @return 学生信息组成的集合
*/
List<Student> selectByNameAndSex(String name, Character sex);
<!-- parameterType="" 不用写了 -->
<select id="selectByNameAndSex" resultType="student">
select * from t_student
where `name` = #{arg0} and sex = #{param2};
</select>
@Test
public void testSelectByNameAndSex() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.selectByNameAndSex("张三", '男');
students.forEach(System.out::println);
SqlSessionUtil.close();
}
@Param(命名参数)
- 对于操作数据库的方法中形参列表的参数为多个的情况,如果使用arg0 arg1 param1 param2这种方式的话,代码的可读性太差。
- 在MyBatis中,为我们提供了@Param注解,使我们可以不用arg0 arg1 param1 param2,@Param注解使我们可以自定义存放参数的map集合的key,可以增强代码的可读性。
@Param 的使用
/**
* 根据学生的姓名和性别进行学生信息的查询
*
* @param name 学生的姓名
* @param sex 学生的性别
* @return 学生信息组成的集合
*/
List<Student> selectByNameAndSexAnnotation(
// 使用@Param注解指定参数在map集合中的key
// @Param注解中的value属性用于指定参数在map集合中的key
@Param("name") String name,
@Param("sex") Character sex
);
- 使用Param注解后,mybatis框架底层的实现原理:
map.put("name", name);
map.put("sex", sex);
<select id="selectByNameAndSexAnnotation" resultType="student">
select * from t_student
where `name` = #{name} and sex = #{sex};
</select>
@Test
public void testSelectByNameAndSexAnnotation() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = mapper.selectByNameAndSexAnnotation("张三", '男');
students.forEach(System.out::println);
SqlSessionUtil.close();
}
注意点
- 使用了@Param注解之后,arg0和arg1失效了
<select id="selectByNameAndSexAnnotation" resultType="student">
select * from t_student
where `name` = #{arg0} and sex = #{arg1};
</select>
- 使用了@Param注解之后,param1和param2还可以用
<select id="selectByNameAndSexAnnotation" resultType="student">
select * from t_student
where `name` = #{param1} and sex = #{param2};
</select>
@Param源码分析
源码跟踪
// mapper 实际上指向了代理对象
StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
// selectByNameAndSexAnnotation 是代理对象 mapper 的代理方法
// 执行到这时,会调用代理对象mapper的代理方法
List<Student> students = mapper.selectByNameAndSexAnnotation("张三", '男');
- 调用代理对象mapper的代理方法,就是执行 MapperProxy 的 invoke 方法
// Object proxy:代理对象
// Method method:目标方法
// Object[] args:目标方法的参数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}
- 接下来执行 this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession)
// MapperProxy 内部类 PlainMethodInvoker 的方法
public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
return this.mapperMethod.execute(sqlSession, args);
}
- 执行 MapperMethod 的 execute 方法进行 SQL 语句类和代理目标方法返回值类型的匹配
// MapperMethod 的方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
// this.command.getType() 获取 SQL 语句的类型(insert|update|select|delete)
switch (this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
// this.method.returnsVoid() 我们定义的Mapper接口中的方法无返回值
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
// 我们定义的Mapper接口中的方法返回多条记录
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
- SQL 语句的类型以及返回结果的类型匹配完成后,进行Mapper接口代理方法的执行
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
// 将Mapper接口参数组成的数组转换为SQL语句中占位符填充的参数
// 转换成 param1 = value1 param2 = value2 ... 的形式
// 或者 arg1 = value1 arg2 = value2 ... 的形式
// 或者 'name' = value1 'sex' = value2 ... 的形式
Object param = this.method.convertArgsToSqlCommandParam(args);
List result;
if (this.method.hasRowBounds()) {
RowBounds rowBounds = this.method.extractRowBounds(args);
result = sqlSession.selectList(this.command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(this.command.getName(), param);
}
if (!this.method.getReturnType().isAssignableFrom(result.getClass())) {
return this.method.getReturnType().isArray() ? this.convertToArray(result) : this.convertToDeclaredCollection(sqlSession.getConfiguration(), result);
} else {
return result;
}
}
public Object convertArgsToSqlCommandParam(Object[] args) {
return this.paramNameResolver.getNamedParams(args);
}
- 调用 ParamNameResolver 类的 getNamedParams(args) 方法进行转换
public Object getNamedParams(Object[] args) {
// private final SortedMap<Integer, String> names;
// 是一个Map集合,key为第几个参数,value为参数对应的名字
int paramCount = this.names.size();
if (args != null && paramCount != 0) {
// 判断方法参数上是否有Param注解
if (!this.hasParamAnnotation && paramCount == 1) {
Object value = args[(Integer)this.names.firstKey()];
return wrapToMapIfCollection(value, this.useActualParamName ? (String)this.names.get(0) : null);
} else { // 有Param注解走该分支
// 有Param注解会新建一个Map集合 param集合
Map<String, Object> param = new MapperMethod.ParamMap();
int i = 0;
// 遍历存储参数名字的Map集合
for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
Map.Entry<Integer, String> entry = (Map.Entry)var5.next();
// 向param集合中添加数据,key为接口方法参数的名字 entry.getValue()
// value为接口方法参数的参数值 args[(Integer)entry.getKey()]
param.put(entry.getValue(), args[(Integer)entry.getKey()]);
// genericParamName 通用参数名
String genericParamName = "param" + (i + 1); // param1 param2 ...
// 集合是否包含通用参数名 param1 param2 ...
if (!this.names.containsValue(genericParamName)) {
// 这个就是可以使用 param1 param2 获取参数值
// 向集合中添加 param1=value1 param2=value2
param.put(genericParamName, args[(Integer)entry.getKey()]);
}
}
return param;
}
} else {
return null;
}
}
图示分析
MyBatis查询语句专题
返回 POJO(一条记录)
public interface CarMapper {
/**
* 根据id查询汽车信息
*
* @param id 汽车信息的id
* @return 汽车信息
*/
Car selectById(Long id);
}
<!--
如果SQL语句中不使用别名,
则读取数据后字段名与对象的属性名不一致的话
不一致的属性的值会为null
-->
<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>
@org.junit.Test
public void testSelectById() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Car car = mapper.selectById(5L);
System.out.println(car);
SqlSessionUtil.close();
}
- 如果返回的结果集可能有多条记录,则不能使用一个pojo对象进行接收,否则会报TooManyResultsException,返回的结果集可能有多条数据记录,则需要使用集合进行接收
返回 List(多条记录)
/**
* 查询所有的汽车信息
*
* @return 所有的汽车信息
*/
List<Car> selectAll();
<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>
@org.junit.Test
public void testSelectAll() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAll();
cars.forEach(System.out::println);
SqlSessionUtil.close();
}
- 如果返回的结果集只有一条记录,可以使用集合进行接收
- 如果返回的结果集有多条记录,只能使用集合进行接收,如果返回的结果集只有一条记录,可以使用集合进行接收也可以使用一个pojo进行接收
返回 Map
- 当返回的数据,没有合适的实体类对应的话,可以采用Map集合接收,字段名做key,字段值做value,如果SQL语句中使用了别名,则使用指定的别名做key
- 查询如果可以保证只有一条数据,则返回一个Map集合即可。如果返回的数据存在多条,则使用一个Map集合接收会报错,得使用Map组成的List集合进行接收
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bZqbNusc-1685870936829)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg-blog.csdnimg.cn%2F63cfeac812cf4a99a1381937b72343bf.png&sign=2b687760bd7f818d690380fb59baf7e89531b0ad5e79ba1e9630c4d78948a06f#from=url&height=225&id=N4UXP&originHeight=313&originWidth=316&originalType=binary&ratio=1.25&rotation=0&showTitle=false&status=done&style=none&title=&width=227)]
/**
* 根据id查询汽车信息
*
* @param id 汽车信息的id
* @return 汽车信息封装成的Map
*/
Map<String, Object> selectByIdReturnMap(Long id);
<!-- MyBatis 中 map 为 java.util.Map 的别名 -->
<select id="selectByIdReturnMap" 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>
@org.junit.Test
public void testSelectByIdReturnMap() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Map<String, Object> map = mapper.selectByIdReturnMap(5L);
System.out.println(map);
SqlSessionUtil.close();
}
返回 List<Map>
- 查询结果条数大于等于1条数据,没有合适的实体类对应的话,则可以返回一个存储Map集合的List集合。
-
List<Map>
相当于List<Car>
- 查询出来的数据有多条或者只有一条都可以使用存储Map集合的List集合进行接收
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ET4Etots-1685870936830)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg-blog.csdnimg.cn%2F41b7c6ebf4454d82b5a8e61d75f06698.png&sign=164d10c96d9a58c55ad0984c21efae7c611f5c3cdc9f43da16de37ccc6fb26bb#from=url&height=381&id=Gbi57&originHeight=733&originWidth=477&originalType=binary&ratio=1.25&rotation=0&showTitle=false&status=done&style=none&title=&width=248)]
/**
* 查询所有的汽车信息
*
* @return 由汽车信息封装在Map中组成的List集合
*/
List<Map<String, Object>> selectAllReturnListMap();
<!-- MyBatis 中 map 为 java.util.Map 的别名 -->
<!-- 返回的还是 map,只有最后数据多个map封装在list中 -->
<select id="selectAllReturnListMap" 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>
@org.junit.Test
public void testSelectAllReturnListMap() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Map<String, Object>> maps = mapper.selectAllReturnListMap();
maps.forEach(System.out::println);
SqlSessionUtil.close();
}
返回 Map<String, Map>
- 当查询结果集中存在多条数据记录,使用
List<Map>
进行接收,不利于我们对结果集中的数据进行查询,需要顺序遍历查询 - 为了可以更快的查询我们所需的数据,我们可以拿数据记录的id(主键)做key,由整条数据记录封装而成的Map作为value,则以后取出对应的所需数据的Map集合时更方便。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XHoQiYeX-1685870936832)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg-blog.csdnimg.cn%2F78116385c1a742ac9862719ae9d2635d.png&sign=f48795a8beb121c5ffb9d5e63f0f160f78d5104ed3b2f7c3b863e2ea13231504#from=url&height=311&id=zwM3Y&originHeight=736&originWidth=787&originalType=binary&ratio=1.25&rotation=0&showTitle=false&status=done&style=none&title=&width=332.4000244140625)]
/**
* 查询所有的汽车信息
*
* @return 由汽车信息封装在Map中组成的Map集合
*/
@MapKey("id") // @MapKey 注解的属性 value 指定使用数据哪个字段作为Map 的 key
// 作为大Map key 的数据类型要与@MapKey 注解指定的字段的数据类型一致
Map<Long, Map<String, Object>> selectAllReturnMapMap();
<!-- MyBatis 中 map 为 java.util.Map 的别名 -->
<!-- 返回的还是 map,只有最后数据多个map封装在Map中 -->
<select id="selectAllReturnMapMap" 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>
@org.junit.Test
public void testSelectAllReturnMapMap() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Map<Long, Map<String, Object>> map = mapper.selectAllReturnMapMap();
System.out.println(map.get(5L));
System.out.println(map.get(7L));
SqlSessionUtil.close();
}
结果映射
- 当查询结果的列名和 java 对象的属性名对应不上时的解决方式:
- 第一种方式:as 给列起别名
- 第二种方式:使用resultMap进行结果映射
- 第三种方式:是否开启驼峰命名自动映射(配置settings)
使用 resultMap 进行结果映射
- resultMap 可以实现查询结果集中字段名和Java对象的属性名之间的映射
/**
* 查询所有的汽车信息
* 使用 ResultMap 标签进行结果映射
*
* @return 汽车信息组成的集合
*/
List<Car> selectAllByResultMap();
<!--
定义一个结果映射
在这个结果映射中指定数据库表字段和Java类的属性名之间的对应关系
type="":用于指定Java类的类名
id="":指定 resultMap 的唯一标识,这个id要在select标签中进行使用
-->
<!-- 在Mybatis核心配置文件中指定了别名,所以可以直接使用car -->
<resultMap id="carResultMap" type="car">
<!-- 如果数据库表中有主键,建议配一个id标签,提高Mybatis的执行效率 -->
<!-- 当然 id 也可以使用 result 标签,就是效率较低 -->
<id property="id" column="id"/>
<!--
property="":填写Java类的属性名
column="":填写数据库表的字段名
javaType:指定Java类属性的类型,不用Mybatis自动类型推断,提高效率(可以使用别名)
jdbcType:指定数据库表字段的类型,不用Mybatis自动类型推断,提高效率
-->
<result property="carNum"
column="car_num"
javaType="java.lang.String"
jdbcType="VARCHAR"
/>
<!-- 如果column和property的属性值一样可以省略 -->
<!-- <result property="brand" column="brand"/> -->
<result property="guidePrice" column="guide_price"/>
<result property="produceTime" column="produce_time"/>
<result property="carType"
column="carType"
javaType="string"
jdbcType="VARCHAR"
/>
</resultMap>
<!--
使用结果映射,在select标签中不使用resultType
而是使用 resultMap,其属性值为 resultMap 的 id
使用 resultMap 指定结果映射
-->
<select id="selectAllByResultMap" resultMap="carResultMap">
select * from t_car;
</select>
@org.junit.Test
public void testSelectAllByResultMap() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAllByResultMap();
cars.forEach(System.out::println);
SqlSessionUtil.close();
}
开启驼峰命名自动映射
-
驼峰命名自动映射这种方式的前提是:Java类的属性名遵循Java的命名规范,数据库表的列名遵循SQL的命名规范。
- Java命名规范:首字母小写,后面每个单词首字母大写,遵循驼峰命名方式。
- SQL命名规范:全部小写,单词之间采用下划线分割。
-
Java类的属性名与数据库表的列名的对应关系:
| 实体类中的属性名 | 数据库表的列名 |
| — | — |
| carNum | car_num |
| carType | car_type |
| produceTime | produce_time | -
开启驼峰命名自动映射,在MyBatis的核心配置文件中的settings标签中进行配置
- MyBatis 中文网 settings 配置说明:https://mybatis.net.cn/configuration.html#settings
<settings>
<!-- 开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
/**
* 查询所有汽车信息
* 使用驼峰命名自动映射
*
* @return 汽车信息组成的集合
*/
List<Car> selectAllByMapUnderscoreToCamelCase();
<select id="selectAllByMapUnderscoreToCamelCase" resultType="car">
select * from t_car;
</select>
@org.junit.Test
public void testSelectAllByMapUnderscoreToCamelCase() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectAllByMapUnderscoreToCamelCase();
cars.forEach(System.out::println);
SqlSessionUtil.close();
}
返回总记录条数
/**
* 查询汽车信息的总记录条数
*
* @return 汽车信息的总记录条数
*/
Long selectTotal();
<!-- long 是 java.util.Long 的别名 -->
<select id="selectTotal" resultType="long">
select count(*) from t_car;
</select>
@org.junit.Test
public void testSelectTotal() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
Long total = mapper.selectTotal();
System.out.println(total);
SqlSessionUtil.close();
}
动态 SQL
业务场景(为什么需要动态SQL)
- 我们需要进行数据的批量删除
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2HD5LXTw-1685870936837)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg-blog.csdnimg.cn%2F7dcef7f1f04e44bf8f7431c7a5e87bdd.png&sign=9532cda95213becdabf7a702b5cadecf196a083af802d074aa92c11792567342#from=url&height=109&id=sPcVI&originHeight=168&originWidth=232&originalType=binary&ratio=1.25&rotation=0&showTitle=false&status=done&style=none&title=&width=150)]
-
delete from t_car where id in(1,2,3,4,5,6,...);
,其中,SQL语句中的in()
中的内容是动态的,根据用户选择的id不同,SQL语句in()
中的内容是不同的
- 我们需要进行多条件查询
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzjhAMts-1685870936838)(https://www.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg-blog.csdnimg.cn%2Fd56eb25cb21b43a39ad5fb938d6659d0.png&sign=4557fedb9e295ef872fcccf3a5b57265041fcbeeb0a4d48807994f5cf1be35fa#from=url&height=92&id=Pr2m3&originHeight=159&originWidth=706&originalType=binary&ratio=1.25&rotation=0&showTitle=false&status=done&style=none&title=&width=410.4000244140625)]
-
select * from t_car where brand like '丰田%' and guide_price > 30 and .....;
,其中,SQL语句中where后面条件的个数是不确定的,是动态的,用户选择不同的条件,SQL语句中条件的个数不同
if 标签
- 可能的条件包括:品牌(brand)、指导价格(guide_price)、汽车类型(car_type)
/**
* 多条件查询
* 可能的条件包括:品牌(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
);
<select id="selectByMultiCondition" resultType="Car">
<!-- where 1 = 1 保证条件都为空时sql语法可以通过 -->
<!--
如果条件都为空,并且没有1 = 1,则SQL语句会变为select * from t_car where,
会报语法错误
-->
select * from t_car where 1 = 1
<!--
1. if标签中test属性是必须的。
2. if标签中test属性的值是false或者true。
if标签的test属性中可以写表达式,值为false或者true
3. 如果test是true,则if标签中的sql语句就会拼接。反之,则不会拼接。
4. 在test属性中使用接口方法中参数的方式:(与#{}中使用接口方法中参数一样)
1)当使用了@Param注解,那么test中要出现的是@Param注解指定的参数名。
@Param("brand"),那么这里只能使用brand
2)当没有使用@Param注解,那么test中要出现的是:
param1 param2 param3 arg0 arg1 arg2....
3)当使用了POJO,那么test中出现的是POJO类的属性名。
5. 在mybatis的动态SQL当中,不能使用&&,只能使用and。
-->
<!-- 第一个条件添加and防止“where 1 = 1 brand like "%"#{brand}"%"”语法错误 -->
<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>
</select>
@Test
public void testSelectByMultiCondition(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 假设三个条件都不是空
//List<Car> cars = mapper.selectByMultiCondition("比亚迪", 2.0, "新能源");
// 假设三个条件都是空
//List<Car> cars = mapper.selectByMultiCondition("", null, "");
// 假设后两个条件不为空,第一个条件为空
//List<Car> cars = mapper.selectByMultiCondition("", 2.0, "新能源");
// 假设第一个条件不是空,后两个条件是空
List<Car> cars = mapper.selectByMultiCondition("比亚迪", null, "");
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
where 标签
- where标签的作用:让where子句更加动态智能。当所有条件都为空时,where标签保证不会生成where子句,可以自动去除某些条件前面多余的and或or,但是不能自动去除某些条件后面多余的and或or
/**
* 使用where标签,让where子句更加的智能。
* @param brand
* @param guidePrice
* @param carType
* @return
*/
List<Car> selectByMultiConditionWithWhere(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
<select id="selectByMultiConditionWithWhere" resultType="Car">
select * from t_car
<!--
where标签是专门负责where子句动态生成的。
如果where标签中的条件都不成立,则SQL语句中不会有where子句
-->
<where>
<!-- 可以自动去除条件前面多余的and或or -->
<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("比亚迪", 2.0, "新能源");
// 三个条件都是空
//List<Car> cars = mapper.selectByMultiConditionWithWhere("", null, "");
// 如果第一个条件是空
//List<Car> cars = mapper.selectByMultiConditionWithWhere("", 2.0, "新能源");
// 后面两个条件是空
List<Car> cars = mapper.selectByMultiConditionWithWhere("比亚迪", null, "");
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
trim 标签
<select id="selectByMultiConditionWithTrim" resultType="Car">
select * from t_car
<!--
prefix:加前缀
suffix:加后缀
prefixOverrides:删除前缀
suffixOverrides:删除后缀
-->
<!--
prefix="where" 是在trim标签内容的前面添加 where,
这个添加是动态的
如果有条件,会动态添加where,没有则不添加
-->
<!--
suffixOverrides="and|or"
把trim标签中动态判断完后的内容多余的后缀and或or去掉
-->
<trim prefix="where" suffixOverrides="and|or">
<if test="brand != null and brand != ''">
brand like "%"#{brand}"%" or
</if>
<if test="guidePrice != null and guidePrice != ''">
guide_price > #{guidePrice} and
</if>
<if test="carType != null and carType != ''">
car_type = #{carType}
</if>
</trim>
</select>
set 标签
- 主要使用在update语句当中,用来动态生成set关键字,同时去掉最后多余的“,”
- 需求:只更新提交的不为空的字段,如果提交的数据是空或者"“,那么这个字段我们将不更新,即字段不为空或者”"就动态拼接其对应的SQL子句,否则不动态拼接其对应的SQL子句
/**
* 更新信息,使用set标签
* @param car
* @return
*/
int updateWithSet(Car car);
<update id="updateBySet">
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>
choose when otherwise
这三个标签是必须在一起使用的:
<choose>
<when></when>
<when></when>
<when></when>
<otherwise></otherwise>
</choose>
等同于:
if(){
}else if(){
}else if(){
}else if(){
}else{
}
只有一个分支会被选择!!!!
需求:先根据品牌查询,如果没有提供品牌,再根据指导价格查询,如果没有提供指导价格,就根据汽车类型查询。
/**
* 使用choose when otherwise标签查询
* @param brand
* @param guidePrice
* @param produceTime
* @return
*/
List<Car> selectWithChoose(@Param("brand") String brand, @Param("guidePrice") Double guidePrice, @Param("carType") String carType);
<select id="selectByChoose" resultType="Car">
select * from t_car
<where>
<!--
先根据品牌查询,没有传品牌,在根据价格查询,
都没有最后根据汽车类型查询
-->
<choose>
<!--只有一个分支会执行,所以不需要and or关键字-->
<when test="brand != null and brand != ''">
brand like "%"#{brand}"%"
</when>
<when test="guidePrice != null and guidePrice != ''">
guide_price > #{guidePrice}
</when>
<!--都为null,还是执行这个分支,car_type = null,只是查不到数据-->
<otherwise>
car_type = #{carType}
</otherwise>
</choose>
</where>
</select>
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()
/**
* 批量删除。foreach标签
* @param ids
* @return
*/
int deleteByIds(@Param("ids") Long[] ids);
<!--
foreach标签的属性:
collection:指定数组或者集合
item:代表数组或集合中的元素
separator:循环之间的分隔符,自动添加分隔符,会自动在分隔符的两边添加空格
open: foreach循环拼接的所有sql语句的最前面以什么开始。
close: foreach循环拼接的所有sql语句的最后面以什么结束。
-->
<delete id="deleteByIds">
<!--
delete from t_car where id in(
<foreach collection="ids" item="aaaaaaa" separator=",">
#{aaaaaaa}
</foreach>
)
没有使用@Param注解,上面的SQL报错了,错误信息是:[array, arg0]
因为没有使用@Param注解时,参数在map集合中存储,默认使用array, arg0作为key
map.put("array", 数组);
map.put("arg0", 数组);
-->
delete from t_car where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</delete>
or
/**
* 根据id批量删除 使用or关键字。
* @param ids
* @return
*/
int deleteByIds2(@Param("ids") Long[] ids);
<delete id="deleteByIds2">
delete from t_car where
<!-- 会自动在分隔符的两边添加空格 -->
<foreach collection="ids" item="id" separator="or">
id=#{id}
</foreach>
</delete>
批量插入
/**
* 批量插入,一次插入多条Car信息
* @param cars
* @return
*/
int insertBatch(@Param("cars") List<Car> cars);
<insert id="insertBatch">
insert into t_car values
<!-- cars 汽车信息组成的list集合,car一个汽车类的对象,取属性,对象.属性 -->
<foreach collection="cars" item="car" separator=",">
(null,#{car.carNum},#{car.brand},#{car.guidePrice},#{car.produceTime},#{car.carType})
</foreach>
</insert>
sql 标签与 include 标签
- sql标签用来声明sql片段,可以使需要重复编写的重复的SQL语句片段可以得到复用
- include标签用来将声明的sql片段包含到某个sql语句当中
- 作用:代码复用,易维护。
<!--
声明sql片段
将重复的SQL片段提取出来
-->
<sql id="carCols">
id,
car_num carNum,
brand,
guide_price guidePrice,
produce_time produceTime,
car_type carType
</sql>
<select id="selectAllRetMap" resultType="map">
<!-- 将声明的sql片段包含进来 -->
select
<include refid="carCols"/>
from t_car
</select>
<select id="selectAllRetListMap" resultType="map">
select
<include refid="carCols"/>
carType from t_car
</select>
<select id="selectByIdRetMap" resultType="map">
select
<include refid="carCols"/>
from t_car
where id = #{id}
</select>
MyBatis的高级映射及延迟加载
- 低级映射(基础映射):一个数据库表对应一个Java对象
- 高级映射:数据存储在数据库中两张不同的表中,并两个表之间的数据之间具有关系,此时映射到Java对象上为高级映射
-
数据库表
use mybatis_study;
drop table t_clazz;
create table t_clazz(
cid int primary key,
cname varchar(255)
);
drop table t_student;
create table t_student(
sid int primary key,
sname varchar(255),
cid int
);
insert into t_clazz
values (1000, '高三一班'),
(1001, '高三二班');
insert into t_student
values (1, '张三', 1000),
(2, '李四', 1000),
(3, '王五', 1000),
(4, '赵六', 1001),
(5, '钱七', 1001);
依赖
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</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.4.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
项目结构
实体类
package cw.study.mybatis.pojo;
/**
* ClassName: Clazz
* Package: cw.study.mybatis.pojo
* Description:
* 班级信息
*
* @Author tcw
* @Create 2023-06-04 11:38
* @Version 1.0
*/
public class Clazz {
private Integer cid;
private String cname;
public Clazz() {
}
public Clazz(Integer cid, String cname) {
this.cid = cid;
this.cname = cname;
}
@Override
public String toString() {
return "Clazz{" + "cid=" + cid + ", cname='" + cname + '\'' + '}';
}
public Integer getCid() {
return cid;
}
public void setCid(Integer cid) {
this.cid = cid;
}
public String getCname() {
return cname;
}
public void setCname(String cname) {
this.cname = cname;
}
}
package cw.study.mybatis.pojo;
/**
* ClassName: Student
* Package: cw.study.mybatis.pojo
* Description:
* 学生信息
*
* @Author tcw
* @Create 2023-06-04 11:38
* @Version 1.0
*/
public class Student {
private Integer sid;
private String sname;
public Student() {
}
public Student(Integer sid, String sname) {
this.sid = sid;
this.sname = sname;
}
@Override
public String toString() {
return "Student{" + "sid=" + sid + ", sname='" + sname + '\'' + ", " + '}';
}
public Integer getSid() {
return sid;
}
public void setSid(Integer sid) {
this.sid = sid;
}
public String getSname() {
return sname;
}
public void setSname(String sname) {
this.sname = sname;
}
}
多对一
- 主表与副表:谁在前,谁就是主表
- 多对一,多为主表
- 一对多,一为主表
- 主表对应Java中的主对象
- 多对一,多个学生对应一个班级,学生表为主表,学生对象为主对象,班级对象为副对象,要建立两个对象之间的关系,通过学生对象可以找到班级对象,所以在学生类中添加对班级对象的引用属性
- 多对一的映射实体类设计:
- 修改学生类,添加对班级对象的引用属性,修改setter、getter、toString
/**
* 学生信息
*/
public class Student { // Student是多的一方
private Integer sid;
private String sname;
private Clazz clazz; // Clazz是一的一方。
......
}
/**
* 班级信息
*/
public class Clazz {
private Integer cid;
private String cname;
......
}
多对一映射的实现方式
- 多对一映射的多种方式,常见的包括三种:
- 第一种方式:一条SQL语句,级联属性映射。
- 第二种方式:一条SQL语句,association。
- 第三种方式:两条SQL语句,分步查询。(这种方式常用:优点一是可复用。优点二是支持懒加载。)
第一种方式:级联属性映射
/**
* 根据id获取学生信息。同时获取学生关联的班级信息。
* @param id 学生的id
* @return 学生对象,但是学生对象当中含有班级对象。
*/
Student selectById(Integer id);
<!--多对一映射的第一种方式:一条SQL语句,级联属性映射。-->
<resultMap id="studentResultMap" type="student">
<!-- column 查询结果的列名,赋值到 property 对象的属性上 -->
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<!-- 为clazz(班级对象)的属性赋值 -->
<result property="clazz.cid" column="cid"/>
<result property="clazz.cname" column="cname"/>
</resultMap>
<!-- 查询结果会按配置的结果集进行赋值 -->
<!-- 哪个表在前哪个表为主表,多对一,多为主表 -->
<select id="selectById" resultMap="studentResultMap">
select s.sid, s.sname, c.cid, c.cname
from t_student s left join t_clazz c
on s.cid = c.cid
where s.sid = #{id};
</select>
@org.junit.Test
public void test01() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.selectById(1);
System.out.println(student);
}
第二种方式:association
/**
* 一条SQL语句,association
* @param id
* @return
*/
Student selectByIdAssociation(Integer id);
<!-- type 查询结果映射到类型 -->
<resultMap id="studentResultMapAssociation" type="Student">
<!-- column 查询结果的列名,赋值到 property 对象的属性上 -->
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<!--
association:翻译为关联。一个Student对象关联一个Clazz对象
property:提供要映射的POJO类的属性名。关联对象引用的属性名
javaType:用来指定要映射的java类型。
-->
<association property="clazz" javaType="Clazz">
<!-- column 查询结果的列名,赋值到 property 对象的属性上 -->
<id property="cid" column="cid"/>
<result property="cname" column="cname"/>
</association>
</resultMap>
<select id="selectByIdAssociation" resultMap="studentResultMapAssociation">
select s.sid, s.sname, c.cid, c.cname
from t_student s left join t_clazz c
on s.cid = c.cid
where s.sid = #{id};
</select>
@org.junit.Test
public void test02() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.selectByIdAssociation(4);
System.out.println(student);
}
第三种方式:分步查询 & 多对一延迟加载
- 先使用一条SQL查询出学生的信息,再使用一条SQL查询出学生信息对应的额班级信息
- 分步优点:
- 第一个优点:代码复用性增强。
- 第二个优点:支持延迟加载。暂时访问不到的数据可以先不查询。提高程序的执行效率。
- StudentMapper接口中的方法
/**
* 分部查询第一步:先根据学生的sid查询学生的信息。
* @param sid
* @return
*/
Student selectByIdStep1(Integer sid);
- ClazzMapper接口中的方法
/**
* 分步查询第二步:根据cid获取班级信息。
* @param cid
* @return
*/
Clazz selectByIdStep2(Integer cid);
<!--分步查询第二步:根据cid获取班级信息。-->
<select id="selectByIdStep2" resultType="Clazz">
select cid, cname
from t_clazz
where cid = #{cid};
</select>
<!-- 两条SQL语句,完成多对一的分步查询 -->
<!-- 第一步:根据学生的id查询学生的所有信息。这些信息当中含有班级id(cid) -->
<select id="selectByIdStep1" resultMap="studentResultMapByStep">
select sid, sname, cid as clazzId
from t_student
where sid = #{sid};
</select>
<resultMap id="studentResultMapByStep" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<!--
第二步:根据班级的id查询班级信息
第一步中查询出来的cid(有起别名使用别名)通过column标签属性
传给cw.study.mybatis.mapper.ClazzMapper.selectByIdStep2对应的SQL语句
进行第二步查询,根据班级的id查询班级信息
-->
<association
property="clazz"
select="cw.study.mybatis.mapper.ClazzMapper.selectByIdStep2"
column="clazzId"
></association>
</resultMap>
@org.junit.Test
public void test03() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.selectByIdStep1(3);
System.out.println(student);
}
分步查询分析
- 分步查询的优点:
- 第一:复用性增强。可以重复利用。(大步拆成N多个小碎步。每一个小碎步更加可以重复利用。)
- 第二:采用这种分步查询,可以充分利用他们的延迟加载/懒加载机制。
- 延迟加载(懒加载)的核心原理是:用的时候再执行查询语句。不用的时候不查询。
- 作用:提高性能。尽可能的不查,或者说尽可能的少查。来提高效率。
- 作用:提高性能。尽可能的不查,或者说尽可能的少查。来提高效率。
局部懒加载
- 在mybatis当中怎么开启延迟加载呢?
- association标签中添加 fetchType=“lazy”
- 注意:默认情况下是没有开启延迟加载的。需要设置:fetchType=“lazy”
- 这种在association标签中配置fetchType=“lazy”,是局部的设置,只对当前的association关联的sql语句起作用。
<resultMap id="studentResultMapByStep" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<!-- 使用fetchType="lazy"开启局部懒加载 -->
<!-- fetchType="eager"不开启懒加载 -->
<association
property="clazz"
select="cw.study.mybatis.mapper.ClazzMapper.selectByIdStep2"
column="clazzId"
fetchType="lazy"
></association>
</resultMap>
@org.junit.Test
public void test03() {
SqlSession sqlSession = SqlSessionUtil.openSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.selectByIdStep1(3);
// 没有使用到第二条SQL语句查询的信息,不会执行第二条SQL
System.out.println(student.getSid());
System.out.println(student.getSname());
// 需要使用第二条SQL查询的信息时,才会执行
System.out.println(student.getClazz());
}
全局开启延迟加载
- 在实际的开发中,大部分都是需要使用延迟加载的,所以建议开启全部的延迟加载机制:
- 在mybatis核心配置文件中添加全局配置:lazyLoadingEnabled=true
- 实际开发中的模式:把全局的延迟加载打开。如果某一步不需要使用延迟加载,请设置:fetchType=“eager”
<settings>
<!--延迟加载的全局开关。默认值false不开启。-->
<!--什么意思:所有只要但凡带有分步的,都采用延迟加载。-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
<resultMap id="studentResultMapByStep" type="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
<association
property="clazz"
select="cw.study.mybatis.mapper.ClazzMapper.selectByIdStep2"
column="clazzId"
></association>
</resultMap>
一对多
- 多对一的映射实体类设计:
- 一对多的实现,通常是在一的一方中有List集合属性。
- 一个班级对应多个学生
- 在Clazz类中添加
List<Student> stus;
属性。
/**
* 班级信息
*/
public class Clazz {
private Integer cid;
private String cname;
private List<Student> stus;
......
}
/**
* 学生信息
*/
public class Student { // Student是多的一方
private Integer sid;
private String sname;
private Clazz clazz; // Clazz是一的一方。
......
}
一对多映射的实现方式
- 一对多的实现通常包括两种实现方式:
- 第一种方式:collection 标签
- 第二种方式:分步查询
第一种方式:collection 标签
/**
* 根据班级编号查询班级信息。
* @param cid
* @return
*/
Clazz selectByCollection(Integer cid);
<select id="selectByCollection" resultMap="clazzResultMap">
select c.cid, c.cname, s.sid, s.sname
from t_clazz c left join t_student s
on c.cid = s.cid
where c.cid = #{cid};
</select>
<resultMap id="clazzResultMap" type="Clazz">
<id property="cid" column="cid"/>
<result property="cname" column="cname"/>
<!--
一对多,使用collection
ofType:指定集合中的元素的类型
-->
<!--
不能为Student对象的clazz属性赋值,
否则会死循环Student中有clazz,clazz中有Student
-->
<collection property="stus" ofType="Student">
<id property="sid" column="sid"/>
<result property="sname" column="sname"/>
</collection>
</resultMap>
@org.junit.Test
public void test04() {
SqlSession sqlSession = SqlSessionUtil.openSession();
ClazzMapper clazzMapper = sqlSession.getMapper(ClazzMapper.class);
Clazz clazz = clazzMapper.selectByCollection(1000);
System.out.println(clazz.getCid() + " " + clazz.getCname());
clazz.getStus().forEach(System.out::println);
}
第二种方式:分步查询 & 一对多延迟加载
- ClazzMapper接口中的方法
/**
* 分步查询。第一步:根据班级编号获取班级信息。
* @param cid 班级编号
* @return
*/
Clazz selectByStep1(Integer cid);
- StudentMapper接口中的方法
/**
* 根据班级编号查询学生信息。
* @param cid
* @return
*/
List<Student> selectByCidStep2(Integer cid);
<!-- 根据班级编号查询学生信息。 -->
<select id="selectByCidStep2" resultType="Student">
select sid, sname from t_student where cid = #{cid}
</select>
<!--分步查询第一步:根据班级的cid获取班级信息。-->
<select id="selectByStep1" resultMap="clazzResultMapStep">
select cid, cname from t_clazz where cid = #{cid};
</select>
<resultMap id="clazzResultMapStep" type="Clazz">
<id property="cid" column="cid"/>
<result property="cname" column="cname"/>
<!--
第二步:根据班级的id查询学生信息
第一步中查询出来的cid(有起别名使用别名)通过column标签属性
传给cw.study.mybatis.mapper.StudentMapper.selectByCidStep2对应的SQL语句
进行第二步查询,根据班级的id查询学生信息
-->
<association
property="stus"
select="cw.study.mybatis.mapper.StudentMapper.selectByCidStep2"
column="cid"
/>
</resultMap>
@org.junit.Test
public void test05() {
SqlSession sqlSession = SqlSessionUtil.openSession();
ClazzMapper clazzMapper = sqlSession.getMapper(ClazzMapper.class);
Clazz clazz = clazzMapper.selectByStep1(1001);
System.out.println(clazz.getCid() + " " + clazz.getCname());
clazz.getStus().forEach(System.out::println);
}
开启延迟加载
- 一对多延迟加载机制和多对一是一样的。同样是通过两种方式:
- 第一种:fetchType=“lazy”
- 第二种:修改全局的配置setting,lazyLoadingEnabled=true,如果开启全局延迟加载,想让某个sql不使用延迟加载:fetchType=“eager”
<resultMap id="clazzResultMapStep" type="Clazz">
<id property="cid" column="cid"/>
<result property="cname" column="cname"/>
<association
property="stus"
select="cw.study.mybatis.mapper.StudentMapper.selectByCidStep2"
column="cid"
fetchType="lazy"
/>
</resultMap>
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
多对多
- 多对多,可以分解为两个一对多进行处理
- 多对多,可以多一个存储两个表之间数据关系的表,将多对多转化为两个一对多或者两个多对一
MyBatis的缓存
- 缓存:cache
- 缓存的作用:把数据保存到内存(缓存)中,下一次要使用的时候,直接从缓存中获取,通过减少IO的方式,来提高程序的执行效率。
- mybatis的缓存:将select语句的查询结果放到缓存(内存)当中,下一次还是这条select语句的话,直接从缓存中取,不再查数据库,不再去硬盘上找数据
- 一方面是减少了IO。另一方面不再执行繁琐的查找算法。效率大大提升。
- 如果执行完查询的SQL后再执行增删改的SQL,mybatis会将缓存进行清空,再执行相同的查询SQL时,不会再去查找缓存,会从硬盘文件中重新读取,保证使用的数据与数据库一致
- mybatis缓存包括:
- 一级缓存:将查询到的数据存储到SqlSession中。
- 二级缓存:将查询到的数据存储到SqlSessionFactory中。
- 或者集成其它第三方的缓存:比如EhCache【Java语言开发的】、Memcache【C语言开发的】等。
- 缓存只针对于DQL语句,也就是说缓存机制只对应select语句。
一级缓存
- 一级缓存默认是开启的。不需要做任何配置。
- 原理:只要使用同一个SqlSession对象执行同一条SQL语句,就会走缓存。
/**
* 根据汽车id查询汽车信息
* @param id
* @return
*/
Car selectById(Long id);
<select id="selectById" resultType="Car">
select * from t_car where id = #{id};
</select>
@org.junit.Test
public void test01() {
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper carMapper = sqlSession.getMapper(CarMapper.class);
Car car1 = carMapper.selectById(5L);
System.out.println(car1);
Car car2 = carMapper.selectById(5L);
System.out.println(car2);
SqlSessionUtil.close();
}
不走一级缓存的情况
- 什么情况下不走缓存?
- 第一种:不同的SqlSession对象。
- 第二种:查询条件变化了。
@org.junit.Test
public void test02() throws IOException {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession1 = sqlSessionFactory.openSession();
CarMapper carMapper1 = sqlSession1.getMapper(CarMapper.class);
System.out.println(carMapper1.selectById(5L));
sqlSession1.close();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
CarMapper carMapper2 = sqlSession2.getMapper(CarMapper.class);
System.out.println(carMapper2.selectById(5L));
sqlSession2.close();
}
一级缓存失效的情况
- 一级缓存失效情况包括两种:
- 第一种:第一次查询和第二次查询之间,手动清空了一级缓存。
sqlSession.clearCache();
- 第二种:第一次查询和第二次查询之间,执行了增删改操作。这个增删改和哪张表没有关系,只要有insert delete update操作,一级缓存就失效,清空一级缓存,保证查出来的数据是对的
- 第一种:第一次查询和第二次查询之间,手动清空了一级缓存。
二级缓存
- 二级缓存的范围是SqlSessionFactory。
二级缓存的使用
- 使用二级缓存需要具备以下几个条件:
-
<setting name="cacheEnabled" value="true">
全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是true,无需设置。- 二级缓存默认是开启的
- 在需要使用二级缓存的SqlMapper.xml文件中添加配置:
<cache />
-
<!--
默认情况下,二级缓存机制是开启的。
只需要在对应的SqlMapper.xml文件中添加以下标签。
表示要在该SqlMapper.xml文件中使用该二级缓存。
-->
<cache/>
- 使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
- SqlSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
- SqlSession对象不关闭或不提交,那么一级缓存中的数据不会被写到二级缓存中
public class Car implements Serializable {}
@org.junit.Test
public void test02() throws IOException {
// 这里只有一个SqlSessionFactory对象,二级缓存对应的就是SqlSessionFactory。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
// 这行代码执行结束之后,实际上数据是缓存到一级缓存当中了。(sqlSession1是一级缓存。)
Car car1 = mapper1.selectById(5L);
System.out.println(car1);
// 如果执行了这行代码,sqlSession1的一级缓存中的数据会放到二级缓存当中。
sqlSession1.close();
// 由于当前执行的SQL和查询条件与二级缓存中的已有的缓存数据一样
// 直接从二级缓存中获取,不再执行SQL语句进行查询
Car car2 = mapper2.selectById(5L);
System.out.println(car2);
// 程序执行到这里的时候,会将sqlSession2这个一级缓存中的数据写入到二级缓存当中。
sqlSession2.close();
}
二级缓存失效的情况
- 只要两次查询之间出现了增删改操作,二级缓存就会失效,一级缓存也会失效
二级缓存的相关配置
- eviction(剔除):指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略。
- LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
- 最近最少使用,不一定最少使用,可能只是最近使用少,但是常用
- LFU,从对象存在开始,最不常用
- FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。先进先出
- SOFT:软引用。淘汰软引用指向的对象。具体算法和JVM的垃圾回收算法有关。
- WEAK:弱引用。淘汰弱引用指向的对象。具体算法和JVM的垃圾回收算法有关。
- LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
- flushInterval:
- 二级缓存的刷新时间间隔。单位毫秒。
- 如果没有设置。就代表不刷新缓存,只要内存足够大,一直会向二级缓存中缓存数据。除非执行了增删改。
- 缓存一刷新,缓存也是失效的
- readOnly:
- true:多条相同的sql语句执行之后返回的对象是共享的同一个。性能好。但是多线程并发可能会存在安全问题。多个线程操作同一个对象
- false:多条相同的sql语句执行之后返回的对象是副本,调用了clone方法。性能一般。但安全。
- size:
- 设置二级缓存中最多可存储的java对象数量。默认值1024。
- 缓存中的对象数量超过size,就会对对象开始驱逐(eviction)
MyBatis集成EhCache
- 老杜原版笔记集成EhCache步骤:【https://www.yuque.com/zuihoudewu/java_note/mt2812#IB2eW】
- 集成EhCache是为了代替mybatis自带的二级缓存,一级缓存是无法替代的。
- mybatis对外提供了接口,也可以集成第三方的缓存组件。比如EhCache、Memcache等。都可以。
- EhCache是Java写的。Memcache是C语言写的。所以mybatis集成EhCache较为常见,按照以下步骤操作,就可以完成集成:
- 第一步:引入mybatis整合ehcache的依赖。
<!--mybatis集成ehcache的组件-->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.2.2</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属性。
<!-- <cache/> 表示使用mybatis自己的缓存 -->
<!-- 集成使用第三方EhCache缓存 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
- 第四步:编写测试程序使用。
@org.junit.Test
public void test02() throws IOException {
// 这里只有一个SqlSessionFactory对象,二级缓存对应的就是SqlSessionFactory。
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
CarMapper mapper1 = sqlSession1.getMapper(CarMapper.class);
CarMapper mapper2 = sqlSession2.getMapper(CarMapper.class);
// 这行代码执行结束之后,实际上数据是缓存到一级缓存当中了。(sqlSession1是一级缓存。)
Car car1 = mapper1.selectById(5L);
System.out.println(car1);
// 如果执行了这行代码,sqlSession1的一级缓存中的数据会放到二级缓存当中。
sqlSession1.close();
// 由于当前执行的SQL和查询条件与二级缓存中的已有的缓存数据一样
// 直接从二级缓存中获取,不再执行SQL语句进行查询
Car car2 = mapper2.selectById(5L);
System.out.println(car2);
// 程序执行到这里的时候,会将sqlSession2这个一级缓存中的数据写入到二级缓存当中。
sqlSession2.close();
}
文章来源:https://www.toymoban.com/news/detail-464930.html
MyBatis的逆向工程
- 所谓的逆向工程是:根据数据库表逆向动态生成Java的pojo类、SqlMapper.xml文件以及Mapper接口类等。
- 要完成这个工作,需要借助别人写好的逆向工程插件。
- 思考:使用这个插件的话,需要给这个插件配置哪些信息?
- 1.连接数据库的信息。
- 2.指定哪些表参与逆向工程。
- 3.pojo类名、包名以及生成位置。
- 4.SqlMapper.xml文件名以及生成位置。
- 5.Mapper接口名以及生成位置。
- …
逆向工程的使用(基础版)
- 第一步:基础环境准备,新建模块,打包方式
- 第二步:在pom中添加配置逆向工程的插件(project标签下)
<!--定制构建过程-->
<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 生成配置
- 该文件名必须叫做:generatorConfig.xml
- 该文件必须放在类的根路径下。
- 使用这个插件需要给这个插件配置哪些信息?
- 1.连接数据库的信息。
- 2.指定哪些表参与逆向工程。
- 3.pojo类名、包名以及生成位置。
- 4.SqlMapper.xml文件名以及生成位置。
- 5.Mapper接口名以及生成位置。
- …
<?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="MyBatis3Simple">
<!--防止生成重复代码-->
<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">
<!--是否开启子包-->
<!--如果没有开启子包,com.powernode.mybatis.pojo整个会被作为一个文件夹名-->
<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>
- 第四步:运行插件
文章来源地址https://www.toymoban.com/news/detail-464930.html
逆向工程的使用(增强版)
- generatorConfig.xml配置文件中targetRuntime的值选择MyBatis3,生成的是增强版,除了基本的增删改查之外还有复杂的增删改查。
<!--
targetRuntime有两个值:
MyBatis3Simple:生成的是基础版,只有基本的增删改查。
MyBatis3:生成的是增强版,除了基本的增删改查之外还有复杂的增删改查。
-->
<context id="DB2Tables" targetRuntime="MyBatis3">
- 生成的XxxxExample类负责封装查询条件,where字句中的查询条件
public class CarMapperTest {
// CarExample类负责封装查询条件的。
@Test
public void testSelect(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 执行查询
// 1. 查询一个
Car car = mapper.selectByPrimaryKey(165L);
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语句。
// QBC 面向对象查询
// 封装条件,通过CarExample对象来封装查询条件
CarExample carExample = new CarExample();
// 调用carExample.createCriteria()方法来创建查询条件
// 继续向后调用方法追加查询条件
carExample.createCriteria()
.andBrandLike("帕萨特") // 模糊查询
.andGuidePriceGreaterThan(new BigDecimal(20.0)); // 并且价格大于20
// 添加or
// 上面添加的为and条件,or()开始添加or条件
// (... and ... 前面的查询条件) or (...) 生成后的查询条件
carExample.or().andCarTypeEqualTo("燃油车");
// 执行查询
// 更加封装的条件进行查询
List<Car> cars2 = mapper.selectByExample(carExample);
cars2.forEach(car2 -> System.out.println(car2));
sqlSession.close();
}
}
MyBatis 使用 PageHelper
limit分页
limit
- 分页查询图示:
- mysql的limit的语法格式:
limit 开始下标, 显示的记录条数
- mysql的limit后面两个数字:
- 第一个数字:startIndex,起始下标,下标从0开始,第一条记录的下标是0
- 第二个数字:pageSize,每页显示的记录条数,从startIndex开始查询出pageSize条记录
- limit后面只写一个数字,
limit 显示的记录条数
,相当于limit 0, 显示的记录条数
,即从第一条记录开始显示指定数目的记录条数 - 假设已知页码pageNum,还有每页显示的记录条数pageSize,第一个数字可以动态的获取吗?
- startIndex = (pageNum - 1) * pageSize
- 所以,标准通用的mysql分页SQL:
select
*
from
tableName ......
limit
(pageNum - 1) * pageSize, pageSize
mybatis 利用 limit 实现分页查询
- 使用mybatis应该怎么做?
/**
* 分页查询
* @param startIndex 起始下标
* @param pageSize 每页显示的记录条数
* @return
*/
List<Car> selectByPage(@Param("startIndex") int startIndex, @Param("pageSize") int pageSize);
<select id="selectByPage" resultType="Car">
select * from t_car limit #{startIndex}, #{pageSize}
</select>
@Test
public void testSelectByPage(){
// 获取每页显示的记录条数
int pageSize = 3;
// 显示第几页:页码
int pageNum = 3;
// 计算开始下标
int startIndex = (pageNum - 1) * pageSize;
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
List<Car> cars = mapper.selectByPage(startIndex, pageSize);
cars.forEach(car -> System.out.println(car));
sqlSession.close();
}
PageHelper 插件
- PageHelper插件,为mybatis写的分页插件
- 获取数据不难,难的是获取分页相关的数据比较难(是否还有下一页,是否有下一页等),可以借助mybatis的PageHelper插件。
- 使用PageHelper插件进行分页,更加的便捷。
- 第一步:引入依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.1</version>
</dependency>
- 第二步:在mybatis-config.xml文件中配置插件
- typeAliases标签下面进行配置:
<!-- 配置mybatis分页拦截器插件,使用PageHelper插件的分页拦截器插件 -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
- 第三步:编写Java代码
/**
* 查询所有的Car,通过分页查询插件PageHelper完成。
* @return
*/
List<Car> selectAll();
<!-- 使用了PageHelper插件,分页不用我们进行处理,直接查询数据即可 -->
<select id="selectAll" resultType="Car">
select * from t_car
</select>
- 要使用PageHelper插件的分页功能,必须在执行DQL语句之前,开启分页功能
@Test
public void testSelectAll(){
SqlSession sqlSession = SqlSessionUtil.openSession();
CarMapper mapper = sqlSession.getMapper(CarMapper.class);
// 一定一定一定要注意:在执行DQL语句之前。开启分页功能。
int pageNum = 2;
int pageSize = 3;
// 开启分页功能,参数一:页码,参数二:页面显示的数据条数
PageHelper.startPage(pageNum, pageSize);
List<Car> cars = mapper.selectAll();
//cars.forEach(car -> System.out.println(car));
// 查询完之后创建PageInfo对象获取分页的相关信息
// 封装分页信息对象 new PageInfo()
// PageInfo对象是PageHelper插件提供的,用来封装分页相关的信息的对象。
// 第一个参数为查询出来的数据,
// 第二个参数为导航选项的个数(导航有几页)有几个数字页码可以点击直接跳转
// 页面跳转导航页码数字的个数
// navigatePages=3, navigateFirstPage=1, navigateLastPage=3, navigatepageNums=[1, 2, 3]
PageInfo<Car> carPageInfo = new PageInfo<>(cars, 3);
System.out.println(carPageInfo);
sqlSession.close();
/*
PageInfo{
pageNum=2, // 页码,第几页
pageSize=3, // 每页几条记录
size=3,
startRow=4, // 整个数据表从第几条数据开始
endRow=6, // 整个数据表从第几条数据结束
total=7, // 整个数据表数据条数
pages=3, // 页数的个数,有几页
list = Page{
count=true,
pageNum=2,
pageSize=3,
startRow=3,
endRow=6,
total=7,
pages=3,
reasonable=false,
pageSizeZero=false
}
// 本次查询出来的数据,当前页的数据
[Car{id=168, carNum='1204', brand='奥迪Q7', guidePrice=3.0, produceTime='2009-10-11', carType='燃油车'},
Car{id=169, carNum='1205', brand='朗逸', guidePrice=4.0, produceTime='2001-10-11', carType='新能源'},
Car{id=170, carNum='1206', brand='奔驰E300L', guidePrice=50.0, produceTime='2003-02-03', carType='新能源'}],
prePage=1, // 上一页的页码
nextPage=3, // 下一页的页码
isFirstPage=false, // 是否是第一页
isLastPage=false, // 是否是最后一页
hasPreviousPage=true, // 是否有上一页
hasNextPage=true, // 是否有下一页
navigatePages=3, // 导航页码的个数
navigateFirstPage=1, // 导航页码开始页码
navigateLastPage=3, // 导航页码结束页码
navigatepageNums=[1, 2, 3] // 导航页码数字
}
*/
}
MyBatis的注解式开发
- mybatis中也提供了注解式开发方式,采用注解可以减少Sql映射xml文件的配置。
- 当然,使用注解式开发的话,sql语句是写在java程序中的,这种方式也会给sql语句的维护带来成本。
- 官方是这么说的:
- 使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让你本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。
- 使用注解编写复杂的SQL是这样的:
- 原则:简单sql可以注解,复杂sql使用xml。
@Insert
@Insert("insert into t_car values(null,#{carNum},#{brand},#{guidePrice},#{produceTime},#{carType})")
int insert(Car car);
@Delete
@Delete("delete from t_car where id = #{id}")
int deleteById(Long id);
@Update
@Update("update t_car set car_num=#{carNum},brand=#{brand},guide_price=#{guidePrice},produce_time=#{produceTime},car_type=#{carType} where id=#{id}")
int update(Car car);
@Select
// 有启动字段名和属性名自动映射
@Select("select * from t_car where id = #{id}")
Car selectById(Long id);
@Results
@Select("select * from t_car where id = #{id}")
// 没有启动字段名和属性名自动映射
// 定义属性名与字段名的对应关系
@Results({
@Result(property = "id", column = "id"),
@Result(property = "carNum", column = "car_num"),
@Result(property = "brand", column = "brand"),
@Result(property = "guidePrice", column = "guide_price"),
@Result(property = "produceTime", column = "produce_time"),
@Result(property = "carType", column = "car_type")
})
Car selectById(Long id);
到了这里,关于[Java]Mybatis学习笔记(动力节点老杜)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!