[Java]Mybatis学习笔记(动力节点老杜)

这篇具有很好参考价值的文章主要介绍了[Java]Mybatis学习笔记(动力节点老杜)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

文章修改记录:
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

  • image.png

  • [Java]Mybatis学习笔记(动力节点老杜)

  • [Java]Mybatis学习笔记(动力节点老杜)

  • [Java]Mybatis学习笔记(动力节点老杜)

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(邻域模型)。
    [Java]Mybatis学习笔记(动力节点老杜)

Mybatis 与 ORM

  • Mybatis可以实现Java对象与数据库表中一条记录的映射。
  • MyBatis属于半⾃动化ORM框架。MyBatis中SQL语句需要我们自己编写。
  • Hibernate属于全⾃动化的ORM框架。Hibernate中SQL语句不需要我们自己编写,SQL语句可以自动生成。

[Java]Mybatis学习笔记(动力节点老杜)

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 的形式

image.png

设置项目采用的编码

image.png

设置项目的Maven

image.png

设置项目使用的JDK

image.png
image.png

创建第一个 MyBatis 模块

  • 快捷键:(选中要新建模块的项目)ALT + Ins

image.png
image.png

项目模块目录分析

image.png

  • src目录下存放源程序
  • main目录下存放源程序的主程序
  • test目录下存放源程序的测试程序
  • main/java目录下编写主程序的Java代码
  • test/java目录下编写测试程序的Java代码
  • resources目录用于存放配置文件或资源文件
    • 放在该目录下的文件,相当于放到了类的根路径下。
类根路径
  • 不管是 maven 还是普通的 java 模块,只要是 idea 文件夹中变成蓝色的部分,就是可以理解为类路径的起始地点,之后的编译就是由蓝色文件夹开始算类路径,自定义文件夹后用 idea 标记为 resource 后的文件夹也会被最终打包到类路径下
  • 图中的蓝色Java目录和resource目录都相当于类的根路径,就是打包后,包的根路径
    • image.png
    • image.png
    • image.png
  • 由于test目录中的文件主要是测试程序,测试程序一般不会被打包到JAR包,test目录下的子目录和文件会被单独打包到另外的文件夹下
    • image.png
    • image.png
    • image.png

设置模块的打包方式

<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

  • image.png
  • 每个基于 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>

image.png

  • 注意:
    • 第一:这个文件名不是必须叫做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>

image.png

  • 注意:
    • 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();
    }
}

image.png

关于第一个程序的小细节

  • 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。
  • 重点:
    • 以后注意了,只要你的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语句是怎样的。但是没有详细的日期,线程名字等。
    • [Java]Mybatis学习笔记(动力节点老杜)
  • 如果想可以看到更加丰富的日志信息,可以集成第三方的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>

[Java]Mybatis学习笔记(动力节点老杜)

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();
    }
}

[Java]Mybatis学习笔记(动力节点老杜)

使用 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();
    }
}

[Java]Mybatis学习笔记(动力节点老杜)

使用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>

[Java]Mybatis学习笔记(动力节点老杜)

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()
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();
}

[Java]Mybatis学习笔记(动力节点老杜)

  • 注意:如果占位符只有一个,那么#{}的大括号里可以随意。但是最好见名知意。

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();
}

[Java]Mybatis学习笔记(动力节点老杜)

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();
}

[Java]Mybatis学习笔记(动力节点老杜)

查多条数据
<!-- 
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();
}

[Java]Mybatis学习笔记(动力节点老杜)

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();
}

[Java]Mybatis学习笔记(动力节点老杜)

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();
}

[Java]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>
    <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语句,则提交一次
  • dataSource:指定数据源
    • type属性:用来指定具体使用的数据库连接池的策略,可选值包括三个
      • UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
        • property可以是:
          • driver 这是 JDBC 驱动的 Java 类全限定名。
          • url 这是数据库的 JDBC URL 地址。
          • username 登录数据库的用户名。
          • password 登录数据库的密码。
          • defaultTransactionIsolationLevel 默认的连接事务隔离级别。
          • defaultNetworkTimeout 等待数据库操作完成的默认网络超时时间(单位:毫秒)
      • POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现。
        • property可以是(除了包含UNPOOLED中之外):
          • poolMaximumActiveConnections 在任意时间可存在的活动(正在使用)连接数量,默认值:10
          • poolMaximumIdleConnections 任意时间可能存在的空闲连接数。
          • 其它…
      • JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。如果不是web或者maven的war工程,JNDI是不能使用的。
        • property可以是(最多只包含以下两个属性):
          • initial_context 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
          • data_source 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
  • 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 属性指定的数据库环境
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

需求描述

[Java]Mybatis学习笔记(动力节点老杜)

数据库表的设计和准备数据

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);

[Java]Mybatis学习笔记(动力节点老杜)

环境搭建

  • IDEA中使用Maven原型创建WEB应用
  • image.png
  • image.png
  • 使用Maven原型创建的web应用默认没有java和resources目录,包括两种解决方案
    • image.png
    • 第一种:自己手动加上。
      • image.png
    • 第二种:找到原型jar包所在的位置,修改maven-archetype-webapp-1.4.jar中的配置文件
      [Java]Mybatis学习笔记(动力节点老杜)
      [Java]Mybatis学习笔记(动力节点老杜)
  • 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。
    • image.png
  • 确定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目录下(相当于放到类的根路径下)
    • image.png
  • 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架构模式)

image.png

工具类 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
  • image.png
  • image.png

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);
    }
    
}

image.png

使用 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();
}

image.png

使用 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);
}

image.png

工具类 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);
}

image.png

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);
}

image.png

面向接口进行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注入的风险。
  • ${}:底层使用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

image.png

${} 小结
  • 如果需要SQL语句的关键字放到SQL语句中,只能使用${},因为#{}是以值的形式放到SQL语句当中的。

${} 的使用场景 - 拼接表名

  • 向SQL语句当中拼接表名,就需要使用${}
  • 现实业务当中,可能会存在分表存储数据的情况。因为一张表存的话,数据量太大。查询效率比较低。可以将这些数据有规律的分表存储,这样在查询的时候效率就比较高。因为扫描的数据量变少了。
    • 如,日志表:专门存储日志信息的。如果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();
}

image.png

注意点
  • 别名不缺分大小写
<select id="selectByCarType" resultType="caR">

image.png

  • 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">

image.png

  • 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">

image.png

别名机制:package

  • 在起别名时,我们可以直接指定要起别名的类所在的包,MyBatis会自动为这个包下的所有的类全部起别名,别名就是类简名,不区分大小写。
<!-- 起别名 -->
<typeAliases>
  <!-- 为 cw.mybatis.pojo 包下面的所有类起别名,别名默认为类的简名 -->
  <package name="cw.mybatis.pojo"/>
</typeAliases>
<select id="selectByCarType" resultType="cAR">

image.png

核心配置文件中 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目录下没有建包的概念,只有创建目录的概念
    • [Java]Mybatis学习笔记(动力节点老杜)
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>

image.png
image.png

测试使用新建的MyBatis核心配置文件模版

image.png
image.png

在 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>

image.png

测试使用新建的SqlMapper配置文件模版

image.png
image.png
image.png

插入数据时获取自动生成的主键

业务场景说明
  • 存在业务场景,当我们向一张表中插入了一条数据,由于主键一般是自增的,在插入数据时,一般不指定主键,但是此时还有另外一张表,与新插入数据的表有关联关系,该表的外键需要使用新插入的数据的主键值,建立数据之间的关联关系,此时我们就需要获取自动生成的主键值
  • 业务背景:
    • [Java]Mybatis学习笔记(动力节点老杜)
    • 一个用户有多个角色:插入一个用户数据的同时需要给该用户分配角色:需要将生成的用户的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();
}

image.png

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();
}

image.png

实体类 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();
}

image.png

多参数

  • 如果是操作数据库的方法的形参列表是多个参数的话,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();
}

image.png

@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();
}

image.png

注意点
  • 使用了@Param注解之后,arg0和arg1失效了
<select id="selectByNameAndSexAnnotation" resultType="student">
  select * from t_student
  where `name` = #{arg0} and sex = #{arg1};
</select>

image.png

  • 使用了@Param注解之后,param1和param2还可以用
<select id="selectByNameAndSexAnnotation" resultType="student">
  select * from t_student
  where `name` = #{param1} and sex = #{param2};
</select>

image.png

@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;
    }
}
图示分析

image.png

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();
}

image.png

  • 如果返回的结果集可能有多条记录,则不能使用一个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();
}

image.png

  • 如果返回的结果集只有一条记录,可以使用集合进行接收
  • 如果返回的结果集有多条记录,只能使用集合进行接收,如果返回的结果集只有一条记录,可以使用集合进行接收也可以使用一个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();
}

image.png

返回 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();
}

image.png

返回 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();
}

image.png

开启驼峰命名自动映射
  • 驼峰命名自动映射这种方式的前提是: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
    • image.png
<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();
}

image.png

返回总记录条数

/**
 * 查询汽车信息的总记录条数
 * 
 * @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();
}

image.png

动态 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对象上为高级映射
  • [Java]Mybatis学习笔记(动力节点老杜)
  • [Java]Mybatis学习笔记(动力节点老杜)

数据库表

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>

项目结构

image.png

实体类

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中的主对象
  • 多对一,多个学生对应一个班级,学生表为主表,学生对象为主对象,班级对象为副对象,要建立两个对象之间的关系,通过学生对象可以找到班级对象,所以在学生类中添加对班级对象的引用属性
  • 多对一的映射实体类设计:
  • [Java]Mybatis学习笔记(动力节点老杜)
  • 修改学生类,添加对班级对象的引用属性,修改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);
}

image.png

第二种方式: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);
}

image.png

第三种方式:分步查询 & 多对一延迟加载
  • 先使用一条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);
}

image.png

分步查询分析
  • 分步查询的优点:
    • 第一:复用性增强。可以重复利用。(大步拆成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());
}

image.png

全局开启延迟加载
  • 在实际的开发中,大部分都是需要使用延迟加载的,所以建议开启全部的延迟加载机制:
    • 在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>

image.png

一对多

  • 多对一的映射实体类设计:
    • 一对多的实现,通常是在一的一方中有List集合属性。
    • 一个班级对应多个学生
    • image.png
  • 在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);
}

image.png

第二种方式:分步查询 & 一对多延迟加载
  • 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);
}

image.png

开启延迟加载
  • 一对多延迟加载机制和多对一是一样的。同样是通过两种方式:
    • 第一种: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>

image.png

多对多

  • 多对多,可以分解为两个一对多进行处理
  • 多对多,可以多一个存储两个表之间数据关系的表,将多对多转化为两个一对多或者两个多对一

MyBatis的缓存

[Java]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();
}

image.png

不走一级缓存的情况
  • 什么情况下不走缓存?
    • 第一种:不同的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();
}

image.png

一级缓存失效的情况
  • 一级缓存失效情况包括两种:
    • 第一种:第一次查询和第二次查询之间,手动清空了一级缓存。sqlSession.clearCache();
    • 第二种:第一次查询和第二次查询之间,执行了增删改操作。这个增删改和哪张表没有关系,只要有insert delete update操作,一级缓存就失效,清空一级缓存,保证查出来的数据是对的

二级缓存

  • 二级缓存的范围是SqlSessionFactory。
二级缓存的使用
  • 使用二级缓存需要具备以下几个条件:
    1. <setting name="cacheEnabled" value="true"> 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。默认就是true,无需设置。
      1. 二级缓存默认是开启的
    2. 在需要使用二级缓存的SqlMapper.xml文件中添加配置:<cache />
<!--
    默认情况下,二级缓存机制是开启的。
    只需要在对应的SqlMapper.xml文件中添加以下标签。
		表示要在该SqlMapper.xml文件中使用该二级缓存。
-->
<cache/>
  1. 使用二级缓存的实体类对象必须是可序列化的,也就是必须实现java.io.Serializable接口
  2. SqlSession对象关闭或提交之后,一级缓存中的数据才会被写入到二级缓存当中。此时二级缓存才可用。
    1. 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();
}

image.png

二级缓存失效的情况
  • 只要两次查询之间出现了增删改操作,二级缓存就会失效,一级缓存也会失效
二级缓存的相关配置
  • [Java]Mybatis学习笔记(动力节点老杜)
  • eviction(剔除):指定从缓存中移除某个对象的淘汰算法。默认采用LRU策略。
    • LRU:Least Recently Used。最近最少使用。优先淘汰在间隔时间内使用频率最低的对象。(其实还有一种淘汰算法LFU,最不常用。)
      • 最近最少使用,不一定最少使用,可能只是最近使用少,但是常用
      • LFU,从对象存在开始,最不常用
    • FIFO:First In First Out。一种先进先出的数据缓存器。先进入二级缓存的对象最先被淘汰。先进先出
    • SOFT:软引用。淘汰软引用指向的对象。具体算法和JVM的垃圾回收算法有关。
    • WEAK:弱引用。淘汰弱引用指向的对象。具体算法和JVM的垃圾回收算法有关。
  • 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>

image.png

  • 第二步:在类的根路径下新建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();
}

image.png

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>
  • 第四步:运行插件

[Java]Mybatis学习笔记(动力节点老杜)
文章来源地址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
  • 分页查询图示:
    • image.png
  • 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是这样的:
    • [Java]Mybatis学习笔记(动力节点老杜)
  • 原则:简单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模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Vue笔记第七章vue3【动力节点】

    vue3官网地址 https://cn.vuejs.org/ vue3发布时间 2020年9月18日。 翻译: 今天,我们很自豪地宣布Vue.js 3.0“海贼王”正式发布。这个新的主要版本的框架提供了改进的性能 斜体样式 、更小的捆绑包大小、更好的TypeScript集成、用于处理大规模用例的新API,以及为框架未来的长期迭代

    2024年02月02日
    浏览(32)
  • 动力节点rabbitmq笔记-12-17RabbitMQ消息Confirm模式

    动力节点最新rabbitMQ视频 消息的confirm确认机制,是指生产者投递消息后,到达了消息服务器Broker里面的exchange交换机,则会给生产者一个应答,生产者接收到应答,用来确定这条消息是否正常的发送到Broker的exchange中,这也是消息可靠性投递的重要保障; 1 配置文件applicatio

    2024年02月01日
    浏览(27)
  • 动力节点RocketMQ笔记第三章RocketMQ集成SpringBoot

    22.1.1 创建项目,完整的pom.xml 22.1.2 修改配置文件application.yml 22.1.3 我们在测试类里面测试发送消息 往powernode主题里面发送一个简单的字符串消息 运行后查看控制台 22.1.4 查看rocketMq的控制台 查看消息细节

    2024年02月04日
    浏览(27)
  • RabbitMQ笔记一7-11章RabbitMQ工作模型【动力节点】

    broker 相当于mysql服务器,virtual host相当于数据库(可以有多个数据库) queue相当于表,消息相当于记录。   消息队列有三个核心要素:  消息生产者 、 消息队列 、 消息消费者 ; 生产者(Producer):发送消息的应用;(java程序,也可能是别的语言写的程序) 消费者(Con

    2023年04月26日
    浏览(23)
  • 【动力节点】杜老师Vue笔记第七章vue3

    vue3官网地址 https://cn.vuejs.org/ vue3发布时间 2020年9月18日。 翻译: 今天,我们很自豪地宣布Vue.js 3.0“海贼王”正式发布。这个新的主要版本的框架提供了改进的性能、更小的捆绑包大小、更好的TypeScript集成、用于处理大规模用例的新API,以及为框架未来的长期迭代奠定了坚实

    2024年02月05日
    浏览(34)
  • 动力节点最新RocketMq笔记第一章RocketMQ基本操作

    MQ====Message Queue 官网: http://rocketmq.apache.org/ RocketMQ是阿里巴巴2016年MQ中间件,使用Java语言开发,RocketMQ 是一款开源的 分布式消息系统 ,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。同时,广泛应用于多个领域,包括异步通信解耦、企业解决方

    2024年02月07日
    浏览(38)
  • 动力节点最新Redis7笔记-第七章Redis缓存

    7.1.1 Jedis简介 Jedis是一个基于java的Redis客户端连接工具,旨在提升性能与易用性。 7.1.2 创建工程 首先创建一个普通的Maven工程01-jedis,然后在POM文件中添加Jedis与Junit依赖。 7.1.3 使用Jedis实例 Jedis基本使用十分简单,其提供了非常丰富的操作Redis的方法,而这些方法名几乎与R

    2024年02月07日
    浏览(50)
  • 动力节点RabbitMQ笔记-1-6章What is RabbitMQ?

    笔记出自: 动力节点最新RabbitMQ视频 RabbitMQ是一个广泛使用的消息服务器,采用Erlang语言编写,是一种开源的实现 AMQP(高级消息队列协议)的消息中间件; RabbitMQ最初起源于金融系统,它的性能及稳定性都非常出色; AMQP协议(http://www.amqp.org),即 Advanced Message Queuing Proto

    2023年04月25日
    浏览(35)
  • RocketMQ视频笔记第三章RocketMQ集成SpringBoot(动力节点)

    本篇文章是RocketMQ视频笔记的第三章,重点介绍了如何将RocketMQ集成到Spring Boot框架中。通过学习该文章,读者能够掌握如何使用RocketMQ和Spring Boot进行消息传递和处理。

    2024年02月05日
    浏览(42)
  • 【动力节点】最新rabbitmq笔记-1-6章What is RabbitMQ?

    动力节点最新RabbitMQ视频 RabbitMQ是一个广泛使用的消息服务器,采用Erlang语言编写,是一种开源的实现 AMQP(高级消息队列协议)的消息中间件; RabbitMQ最初起源于金融系统,它的性能及稳定性都非常出色; AMQP协议(http://www.amqp.org),即 Advanced Message Queuing Protocol,高级消息

    2024年02月03日
    浏览(32)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包