mybatis

发布时间 2024-01-07 19:00:00作者: PursueExcellence

学习环境说明

  • jdk 8 +
  • MySQL 5.7.19
  • maven-3.6.1
  • IDEA

学习前需要掌握:

  • JDBC
  • MySQL
  • Java 基础
  • Maven
  • Junit

简介

什么是MyBatis

  • MyBatis 是一款优秀的持久层框架
  • MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程
  • MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 实体类 【Plain Old Java Objects,普通的 Java对象】映射成数据库中的记录。
  • MyBatis 本是apache的一个开源项目ibatis, 2010年这个项目由apache 迁移到了google code,并且改名为MyBatis 。
  • 2013年11月迁移到Github .

获取Mybatis(jar包):

持久化

持久化是将程序数据在持久状态和瞬时状态间转换的机制。

  • 即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的对象存储在数据库中,或者存储在磁盘文件中、XML数据文件中等等。(内存:断电即失)
  • JDBC就是一种持久化机制。文件IO也是一种持久化机制。
  • 在生活中 : 将鲜肉冷藏,吃的时候再解冻的方法也是。将水果做成罐头的方法也是。

为什么需要持久化服务呢?那是由于内存本身的缺陷引起的

  • 有一些对象,不能让他丢掉
  • 内存太贵,用不起

持久层

Dao层、Service层、Controller层

  • 完成持久化工作的代码块
  • 层次界限明显

为什么需要Mybatis

  • 方便

  • 传统的jdbc复杂。简化、框架

  • 所有的事情,不用Mybatis依旧可以做到,只是用了它,所有实现会更加简单!技术没有高低之分,只有使用这个技术的人有高低之别

  • 优点

    • 简单易学
    • 灵活
    • 解除sql与程序代码的耦合
    • 提供xml标签,支持编写动态sql
  • 最重要的一点,使用的人多!公司需要!

MyBatis第一个程序

思路流程:搭建环境-->导入Mybatis--->编写代码--->测试

搭建环境

搭建实验数据库

CREATE DATABASE `mybatis`;
USE `mybatis`;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(20) NOT NULL,
`name` varchar(30) DEFAULT NULL,
`pwd` varchar(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert  into `user`(`id`,`name`,`pwd`) values (1,'狂神','123456'),(2,'张三','abcdef'),(3,'李四','987654');

新建maven项目

  1. 普通maven项目

  2. 检查maven配置是否改变

    image-20230419235434360
  3. 删除src目录(用来当作父工程)

    image-20230419235712321

导入MyBatis相关 jar 包

<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.2</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

创建一个模块

image-20230420000740860

编写MyBatis核心配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/><!--事务管理使用的是JDBC-->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
</configuration>

编写MyBatis工具类

// sqlSessionFactory --> sqlSession
public class MybatisUtils {

    private static SqlSessionFactory sqlSessionFactory;

    static {
        // 获取sqlSessionFactory对象
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    public static SqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }
}

编写代码

实体类

package com.jjh.pojo;

public class User {

    private int id;
    private String name;
    private String pwd;

    public User() {
    }

    public User(int id, String name, String pwd) {
        this.id = id;
        this.name = name;
        this.pwd = pwd;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}

Dao接口

package com.jjh.dao;

import com.jjh.pojo.User;

import java.util.List;

public interface UserDao {

    List<User> selectUser();
}

接口实现类

由原来的UserDaoImpl转变为一个Mapper

<?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绑定一个Dao/Mapper接口-->
<mapper namespace="com.jjh.dao.UserDao">
    <select id="selectUser" resultType="com.jjh.pojo.User">
        select * from user
    </select>
</mapper>

测试

写在测试包里面

image-20230420021030544

编写测试类

package com.jjh.dao;

import com.jjh.pojo.User;
import com.jjh.util.MybatisUtils;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;

import java.util.List;

public class UserDaoTest {

    @Test
    public void test(){
        // 获取SqlSession对象
        SqlSession sqlSession = MybatisUtils.getSqlSession();

        // 执行SQL方式1(推荐使用)
        UserDao userDao = sqlSession.getMapper(UserDao.class);
        List<User> userList = userDao.selectUser();
        
         // 执行SQL方式2(不推荐使用)
        // List<User> userList = sqlSession.selectList("com.jjh.dao.UserDao.selectUser");
        
        for (User user : userList) {
            System.out.println(user);
        }

        // 关闭SqlSession
        sqlSession.close();

    }
}

测试运行

可能遇到的问题

mapper.xml文件未进行注册

org.apache.ibatis.binding.BindingException: Type interface com.jjh.dao.UserDao is not known to the MapperRegistry.

解决:

resource:值使用的是文件路径(/)

<mappers>
      <!--每一个mapper.xml文件都需要在Mybatis核心配置文件中注册-->
      <mapper resource="com/jjh/dao/UserMapper.xml"></mapper>
</mappers>
Maven静态资源过滤问题

由于maven约定大于配置,默认的静态资源文件在resource文件夹下,在此处,UserMapper.xml不在导致没有加载此文件

image-20230420022855937

解决:

在父工程pom.xml和子工程pom.xml中进行如下配置(父子工程都配置,保证一定生效)

<!--在buildzhong配置resources,防止我们资源导出失败的问题-->
<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>
连接数据库服务失败
Error querying database.  Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communicat

解决:

image-20230420025618544

useSSL=false和true的区别:

SSL(Secure Sockets Layer 安全套接字协议),在mysql进行连接的时候,如果mysql的版本是5.7之后的版本必须要加上useSSL=false,mysql5.7以及之前的版本则不用进行添加useSSL=false,会默认为false,一般情况下都是使用useSSL=false,尤其是在将项目部署到linux上时,一定要使用useSSL=false!!!,useSSL=true是进行安全验证,一般通过证书或者令牌什么的,useSSL=false就是通过账号密码进行连接,通常使用useSSL=false

CURD操作

namespace

配置文件中namespace中的名称为对应Mapper接口或者Dao接口的完整包名,必须一致!

select

  • 查询语句
  • id:对应的Mapper(Dap)接口中的方法名
  • resultType:Sql语句执行的返回值
  • parameterType:参数类型

Mapepr接口

//查询全部用户
List<User> selectUser();

//根据id查询用户
User selectUserById(int id);

Mapper.xml文件

<select id="selectUser" resultType="com.jjh.pojo.User">
    select * from mybatis.user
</select>

<select id="selectUserById" parameterType="int" resultType="com.jjh.pojo.User">
    select * from user where id = #{id}
</select>

Test类

@Test
public void selectUser(){
    List<User> userList=null;
    SqlSession sqlSession=null;
    try{
        // 获取SqlSession对象
        sqlSession = MybatisUtils.getSqlSession();
        // 执行SQL
        UserMapper userDao = sqlSession.getMapper(UserMapper.class);
        userList = userDao.selectUser();
        // 执行SQL方式2(不推荐使用)
        //List<User> userList = sqlSession.selectList("com.jjh.dao.UserDao.selectUser");
    } catch (Exception e){
        e.printStackTrace();
    } finally {
        // 关闭SqlSession
        sqlSession.close();
    }
    for (User user : userList) {
        System.out.println(user);
    }
}
@Test
public void selectUserById() {
    SqlSession session = MybatisUtils.getSqlSession();  //获取SqlSession连接
    UserMapper mapper = session.getMapper(UserMapper.class);
    User user = mapper.selectUserById(1);
    System.out.println(user);
    session.close();
}

insert、update、delete

更新操作(增删改)需要提交事务

Mapepr接口

    //添加用户
    int addUser(User user);

    //修改用户
    int updateUser(User user);

    //删除用户
    int deleteUser(int id);

Mapper.xml文件

<!--对象中的属性,可以直接取出来-->
<insert id="addUser" parameterType="com.jjh.pojo.User">
    insert into user (id,name,pwd) values (#{id},#{name},#{pwd});
</insert>

<update id="updateUser" parameterType="com.jjh.pojo.User">
    update user set name=#{name},pwd=#{pwd} where id = #{id};
</update>

<delete id="deleteUser" parameterType="int">
    delete from user where id=#{id};
</delete>

Test测试类(修改和删除操作类似)

// 更新操作(增删改)需要提交事务
@Test
public void addUser() {
    SqlSession session = MybatisUtils.getSqlSession();  //获取SqlSession连接
    UserMapper mapper = session.getMapper(UserMapper.class);

    int res = mapper.addUser(new User(4, "哈哈", "123333"));
    if(res>0){
        System.out.println("添加成功");
    }

    // 提交事务
    session.commit();
    session.close();
}

万能Map

假设,我们的实体类,或者数据库中的表,字段或者参数过多,我们应当考虑使用Map

Mapepr接口

//万能Map
int addUser2(Map<String,Object> map);

Mapper.xml文件

<!--万能Map-->
<insert id="addUser2" parameterType="map">
    insert into user (id,name,pwd) values (#{userId},#{userName},#{password});
</insert>

Test测试类

@Test
public void addUser2() {
    SqlSession session = MybatisUtils.getSqlSession();  //获取SqlSession连接
    UserMapper mapper = session.getMapper(UserMapper.class);

    HashMap<String, Object> map = new HashMap<>();
    map.put("userId",4);
    map.put("userName","Hello");
    map.put("password","123456");
    int res = mapper.addUser2(map);
    if(res>0){
        System.out.println("添加成功");
    }

    // 提交事务
    session.commit();
    session.close();
}

总结

  • Map传递参数,直接在sql中取出key即可【parameterType="map" 取值用#{id}】
  • 对象传递参数,直接在sql中取出对象属性即可【parameterType="com.jjh.pojo.User" 取值用#{userId}】
  • 只有一个基本类型参数的情况下,可以直接在sql中取到 【parameterType="int" 可无parameterType,取值用#{id}】
  • 多个参数使用Map,或者注解

模糊查询

Mapper接口

// 模糊查询
List<User> getUserLike(String value);

Java代码执行的时候,传递通配符%%

Mapper.xml文件

<select id="getUserLike" resultType="com.jjh.pojo.User">
	select * from user where name like #{value}
</select>

Test测试类

@Test
public void getUserLike() {
    SqlSession session = MybatisUtils.getSqlSession();  //获取SqlSession连接
    UserMapper mapper = session.getMapper(UserMapper.class);
    
    List<User> userList = mapper.getUserLike("%李%"); // 传递通配符%%,不建议这样写
    for (User user : userList) {
        System.out.println(user);
    }
    
    session.close();
}

在sql语句中拼接通配符,会引起sql注入

Mapper.xml文件

<select id="getUserLike" resultType="com.jjh.pojo.User">
	select * from user where name like "%"#{value}"%"
</select>

Test测试类

List<User> userList = mapper.getUserLike("李");

使用mysql自带的函数(推荐)

<select id="getUserLike" resultType="com.jjh.pojo.User">
	select * from user where name like concat('%',#{studentName},'%')
</select>

#和$的区别

${}:字符串拼接,可能造成SQL注入,需要手动拼接单引号''

{}:占位符赋值,使用的就是JDBC中给预编译sql进行赋值的操作preparedStatement.setString()方法

#{}为什么可以防止SQL注入?

  • {}通过setString()方法预编译sql的占位符进行赋值,所以赋值之后会自动带上''

  • 如果给占位符赋的值是字符串,会将字符串的引号进行转义\'

#和$的使用时机

  • 大多数情况建议直接使用#{},尽量避免使用
  • 当我们需要拼接的变量上不能带单引号时,就必须使用${}通过字符串拼接方式,比如
    • 当sql中表名是从参数中取的情况,表名不用加引号,所以使用${}
    • order by排序语句中,因为后边必须跟字段名,这个字段名不能带引号,如果带引号会被识别会字符串

配置解析

核心配置文件

  • mybatis-config.xml 系统核心配置文件
  • MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。
configuration(配置)
properties(属性)
settings(设置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境配置)
environment(环境变量)
transactionManager(事务管理器)
dataSource(数据源)
databaseIdProvider(数据库厂商标识)
mappers(映射器)
<!-- 注意元素节点的顺序!顺序不对会报错 -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

环境配置(environments元素)

  • MyBatis 可以配置成适应多种环境

  • 不过要记住:尽管可以配置多个环境,但每个SqlSessionFactory 实例只能选择一种环境。

  • 事务管理器(transactionManager)

    在 MyBatis 中有两种类型的事务管理器,使用 Spring + MyBatis,则没有必要配置事务管理器,因为Spring模块会使用自带的管理器来覆盖前面的配置。

    • JDBC:提交和回滚功能
    • MANAGED:这个配置几乎没做什么,它从不提交或回滚一个连接
  • 数据源(dataSource)

    用来连接数据库

    MyBatis有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):

    • UNPOOLED:这个数据源的实现会每次请求时打开和关闭连接,没有池子的概念,链接不回收
    • POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,连接数据库的链接,用完可回收
    • JNDI:正常连接,不经常用

    数据源也有很多第三方的实现,比如dbcp,c3p0,druid等等

  • Mybatis默认的事务管理器就是JDBC,连接池:POOLED

属性(properties)

我们可以通过properties属性来实现引用配置文件

数据库这些属性都是可外部配置且可动态替换的,既可以在典型的 Java 属性文件(.properties)中配置,亦可通过 properties 元素的子元素来传递。

在xml中,所有的标签都可以规定其顺序

image-20230421003831654

编写配置文件

在资源目录下新建一个db.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf8"
username=root
password=123456

在核心配置文件中引入

<!--引入外部配置文件-->
<properties resource="db.properties">
    <property name="username" value="root"/>
    <property name="password" value="111111"/>
</properties>

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/><!--事务管理使用的是JDBC-->
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/> <!--引用外部配置文件属性值-->
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
</environments>
  • 可以直接引用外部配置文件
  • 可以在其中添加一些属性配置
  • 如果两个文件有同一个字段,优先引用外部配置文件的

类型别名(typeAliases)

  • 为 Java 类型设置一个短的名字
  • 存在的意义仅在于用来减少类完全限定名的冗余。

方式一

需要每个java类型分别进行配置

<!--实体类起别名-->
<typeAliases>
    <typeAlias type="com.jjh.pojo.User" alias="User"/>
</typeAliases>

方式2

指定包名,MyBatis 会在包名下面搜索需要的 Java Bean

<!--实体类起别名-->
<typeAliases>
    <package name="com.jjh.pojo"/>
</typeAliases>

在实体类较少的情况下推荐使用第一种方式

如果实体类非常多推荐使用第二种方式

方式1和方式2的区别

  • 方式1可以自定义别名
  • 方式2默认定义的别名是类名首字母小写(也可以是类名,Mybatis推荐使用首字母小写的别名)

方式3

扫描包的方式下,在实体类上加注解,实现自定义类型别名

实体类

@Alias("Hello")
public class User {

    private int id;
    private String name;
    private String pwd;

    public User() {
    }
    ...
}

Maper.xml

<select id="selectUser" resultType="Hello">
	select * from user
</select>

默认别名

  • 基本数据类型,别名是"_(基本数据类型)" 比如:int 的别名是 _int
  • 基本的引用数据类型是简写小写 比如:Integer→int、Double→double、Map→map、List→list

设置(settings)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为

  • 懒加载
  • 日志实现
  • 缓存开启关闭
  • 驼峰命名
  • 等等...

完整的settings元素

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
  <setting name="useColumnLabel" value="true"/>
  <setting name="useGeneratedKeys" value="false"/>
  <setting name="autoMappingBehavior" value="PARTIAL"/>
  <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
  <setting name="defaultExecutorType" value="SIMPLE"/>
  <setting name="defaultStatementTimeout" value="25"/>
  <setting name="defaultFetchSize" value="100"/>
  <setting name="safeRowBoundsEnabled" value="false"/>
  <setting name="mapUnderscoreToCamelCase" value="false"/>
  <setting name="localCacheScope" value="SESSION"/>
  <setting name="jdbcTypeForNull" value="OTHER"/>
  <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
  <setting name="logImpl " value="SLF4J,LOG4J(3.5.9 起废弃),LOG4J2,JDK_LOGGING..."/>  
</settings>

缓存(cacheEnabled)

全局性地开启或关闭所有映射器配置文件中已配置的任何缓存

默认:false

懒加载(lazyLoadingEnabled)

延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。特定关联关系中可通过设置 fetchType属性来覆盖该项的开关状态。

驼峰命名(mapUnderscoreToCamelCase)

默认:false

数据库字段 A_COLUMN→aColumn

举例:user_name→userName、USER_NAME→userName

日志实现(logImpl)

值:

  • SLF4J
  • LOG4J(3.5.9 起废弃)
  • LOG4J2
  • JDK_LOGGING
  • COMMONS_LOGGING
  • STDOUT_LOGGING
  • NO_LOGGING

指定 MyBatis 所用日志的具体实现,未指定时将自动查找

默认:未设置,将自动查找

其他配置

  • 类型处理器(typeHandlers)

    • 无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。

    • 你可以重写类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。【了解即可】

  • 对象工厂(ObjectFactory)

    • MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。
    • 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认构造方法,要么在参数映射存在的时候通过有参构造方法来实例化。
    • 如果想覆盖对象工厂的默认行为,则可以通过创建自己的对象工厂来实现。【了解即可】
  • 插件(plugins)

    • mybatis-generator-core
    • mybatis-plus
    • 通用mapper

映射器(mappers)

MapperRegistry:注册绑定我们的mapper.xml文件

使用相对于类路径的资源引用

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <!-- 放到包目录下 -->  
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
  <!-- 放到resources目录下 -->    
  <mapper resource="UserMapper.xml"></mapper>
</mappers>

使用完全限定资源定位符

很少使用,了解即可

<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>

完全限定类名

  • 文件名称和接口名称一致
  • 位于同一目录下
<!--使用映射器接口实现类的完全限定类名需要配置文件名称和接口名称一致,并且位于同一目录下-->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>

映射器接口实现全部注册为映射器

配置

  • 文件名称和接口名称一致
  • 位于同一目录下
<!--将包内的映射器接口实现全部注册为映射器,但是需要配置文件名称和接口名称一致,并且位于同一目录下-->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

举例

核心配置文件中配置mapper

<mappers>
    <package name="com.jjh.dao"></package>
</mappers>
方式一

image-20231202190849289

方式二

image-20231202191057071

查看编译之后的文件位置

image-20231202191141832

生命周期和作用域

作用域(Scope)和生命周期类是至关重要的,因为错误的使用会导致非常严重的并发问题

Mybatis的执行过程

image-20230421231216156

作用域理解

image-20230421232214236

这里面的每一个Mapper对象,代表一个具体的业务(执行一个具体的sql)

SqlSessionFactoryBuilder

  • 一旦创建了 SqlSessionFactory,就不再需要它了
  • 实例的最佳作用域是方法作用域(也就是局部方法变量)

SqlSessionFactory

  • 说白了,可以想象成为数据库连接池
  • 旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例
  • 最简单的就是使用单例模式或者静态单例模式。
  • 实例的最佳作用域是应用作用域(全局变量)

SqlSession

  • 连接到连接池的一个请求
  • SqlSession的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域
  • 用完之后需要关闭,否则资源被占用

解决属性名和数据库字段不一致的情况

实体类属性名和数据库字段不一致时,实体类无法准确获得返回值

解决方式

数据库字段起别名

resultMap

<!--结果集映射-->
<resultMap id="UserMap" type="User">
    <result property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="pwd" column="pwd"/>
</resultMap>
  • MyBatis 中最重要最强大的元素。
  • 设计思想是,简单的语句根本不需要配置显示的结果映射,复杂一点的语句,只需要描述语句之间的关系就行了。
  • resultMap最优秀的地方在于,你对它已经相当了解了,就根本不需要显示的配置他们(那个字段不一样就配置那个)

日志

日志工厂

如果一个数据库操作,出现了异常,我们需要排错,日志就是最好的助手

曾经:sout、debug

现在:日志工厂

image-20230422001342173

  • SLF4J
  • LOG4J(3.5.9 起废弃) 【掌握】
  • LOG4J2
  • JDK_LOGGING
  • COMMONS_LOGGING
  • STDOUT_LOGGING 【掌握】
  • NO_LOGGING

具体使用哪一个日志实现,在设置中设定

STDOUT_LOGGING

STDOUT_LOGGING标准日志输出,不用导包,直接使用

mybatis-config.xml文件:

<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

结果打印:

image-20230422002722886

LOG4J

  • Log4j是Apache的一个开源项目
  • 通过使用Log4j,我们可以控制日志信息输送的目的地:控制台,文本,GUI组件....
  • 我们也可以控制每一条日志的输出格式;
  • 通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

导包

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

日志配置文件

在资源目录下(classPath路径)创建日志配置文件log4j.properties

log4j.properties:

#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file

#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n

#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n

#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

配置日志的实现

Mybatis核心配置文件

<settings>
    <!--<setting name="logImpl" value="STDOUT_LOGGING"/>-->
    <setting name="logImpl" value="LOG4J"/>
</settings>

Log4j使用

直接测试运行刚才的查询

image-20230422004545351

简单使用:

  1. 在要使用Log4j的类中,导入import org.apache.log4j.Logger;
  2. 日志对象,加载参数为当前类的class
public static Logger logger = Logger.getLogger(UserDaoTest.class);
  1. 日志级别
@Test
public void testtLog4j(){
    logger.info("info: 进入了testtLog4j方法");
    logger.debug("debug: 进入了testtLog4j方法");
    logger.error("error: 进入了testtLog4j方法");
}

日志级别

ERROR

# 在日志配置文件中进行下面的配置,表示将ERROR级别的日志输出到控制台和日志文件中
log4j.rootLogger=ERROR,console,file

只能输出ERROR级别的日志信息

INFO

配置INFO级别的日志,能输出INFO和ERROR级别日志信息

DEBUG

配置DEBUG级别的日志,能输出DEBUG、INFO、ERROR三个级别的日志信息

分页

减少数据的处理量

select * from user limit stratIndex(从0开始),pagesize(查询条数)

select * from user limit 3; #[0,n]

方式1

通过sql实现分页

Mapper接口

// 实现分页
List<User> getUserByLimit(Map map);

Mapper.xml文件

<!--实现分页-->
<select id="getUserByLimit" parameterType="map" resultType="user">
    select * from user limit #{startIndex},#{pageSize}
</select>

Test测试类

@Test
public void getUserByLimit(){
    SqlSession session = MybatisUtils.getSqlSession();  //获取SqlSession连接
    UserMapper userMapper = session.getMapper(UserMapper.class);

    Map<String, Integer> map = new HashMap<>();
    map.put("startIndex",0);
    map.put("pageSize",2);
    List<User> userList = userMapper.getUserByLimit(map);
    for (User user : userList) {
        System.out.println(user);
    }
    
    session.close();
}

方式2

RowBounds分页

Mapper接口

// RowBounds实现分页
List<User> getUserByRowBounds();

Mapper.xml文件

<!--RowBounds实现分页-->
<select id="getUserByRowBounds" resultType="user">
    select * from user
</select>

Test测试类

@Test
public void getUserByRowBounds(){
    SqlSession session = MybatisUtils.getSqlSession();  //获取SqlSession连接
    UserMapper userMapper = session.getMapper(UserMapper.class);

    // RowBounds实现
    RowBounds rowBounds = new RowBounds(1,2);
    List<User> userList = session.selectList("com.jjh.dao.UserMapper.getUserByRowBounds", null, rowBounds);
    for (User user : userList) {
        System.out.println(user);
    }

    session.close();
}

方式3

分页插件:PageHelper

https://pagehelper.github.io/docs/howtouse/

使用注解开发

面向接口编程优点:解耦

实现

  1. 注解在接口上实现

    //查询全部用户
    @Select("select * from user")
    List<User> getUsers();
    
  2. 需要在核心配置文件中绑定接口

    <mappers>
        <!--每一个mapper.xml文件都需要在Mybatis核心配置文件中注册-->
        <mapper class="com.jjh.dao.UserMapper"></mapper>
    </mappers>
    
  3. 测试

    @Test
    public void getUsers(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        // 底层主要使用反射
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    
        List<User> userList = mapper.getUsers();
        for (User user : userList) {
            System.out.println(user);
        }
    
        sqlSession.close();
    }
    

本质:反射机制实现

底层:动态代理

image-20230423005330814

Mybatis详细执行流程

image-20230423011744471

注解实现CURD

可以在工具类中设置事务的自动提交,这样进行数据库的过更新操作的时候就不用手动提交事务了

注意:核心配置文件要绑定接口,目的是为了找到sql

MybatisUtils

public class MybatisUtils {
    private static SqlSessionFactory sqlSessionFactory;

    static {
        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    }

    public static SqlSession getSqlSession() {
    	// 参数设置为true,进行自动事务的提交,默认时false
        return sqlSessionFactory.openSession(true);
    }

}

Mapper接口

//根据id查询用户
@Select("select * from user where id=#{id} and name=#{name}")
User getUserByIdAndName(@Param("id") String id,@Param("name") String name);

测试

@Test
public void selectUserById() {
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);

    User user = mapper.getUserByIdAndName("1", "张三");
    System.out.println(user);

}

@Param注解

用于给方法参数起一个名字:

  • 参数只有一个,不需要加(基本数据类型,基本引用类型,String类型建议加上)
  • 参数有多个,一定要加

#与$的区别

  • #{} 的作用主要是替换预编译语句(PrepareStatement)中的占位符? 【推荐使用】

    INSERT INTO user (name) VALUES (#{name});
    INSERT INTO user (name) VALUES (?);
    
  • ${} 的作用是直接进行字符串替换

    INSERT INTO user (name) VALUES ('${name}');
    INSERT INTO user (name) VALUES ('kuangshen');
    
  • 能用#{}就用#{},能有效防止sql注入

  • 举例

    Test测试类

    List<User> userList = mapper.getUserByIdAndName1("1 or 1=1");
    for (User user : userList) {
        System.out.println(user);
    }
    

    使用${}

    @Select("select * from user where id=${id}")
    List<User> getAllUserByAnnotateImpl(@Param("id") String id);
    

    结果

    image-20231203133644213

    使用#{}

    @Select("select * from user where id=#{id}")
    List<User> getAllUserByAnnotateImpl(@Param("id") String id);
    

    结果

    image-20231203133748042

Lombok

通过使用注解自动生成pojo实体类的set、get、构造等方法

idea安装插件

image-20230423111705497

导入Lombok的jar包

子工程导入即可

image-20230423112051340

使用注解

@Getter and @Setter
@FieldNameConstants
@ToString
@EqualsAndHashCode
@AllArgsConstructor, @RequiredArgsConstructor and @NoArgsConstructor
@Log, @Log4j, @Log4j2, @Slf4j, @XSlf4j, @CommonsLog, @JBossLog, @Flogger, @CustomLog
@Data
@Builder
@SuperBuilder
@Singular
@Delegate
@Value
@Accessors
@Wither
@With
@SneakyThrows
@val
@var
experimental @var
@UtilityClass
Lombok config system
  • @Data:无参构造、set、get、toString、hashCode、equals
  • @AllArgsConstructor:有参构造
  • @NoArgsConstructor:无参构造

缺点:

  • 无法实现多个构造方法的重载

一对一处理

多对一的理解:

  • 多个学生对应一个老师
  • 如果对于学生这边,即从学生这边关联一个老师!【多对一】
  • 如果对于老师而言,即一个老师,有多个学生【就是一对多现象】

自身理解:

  • 不存在多以一现象,只是从不同对象角度出发
  • 所谓的多对一,就是每一个学生对象只有一个老师对象,所以使用association,代表一个老师对象
  • 所谓的一对多,就是每一个学生有多个老师对象,所以使用collection,代表多个老师对象
  • 从老师的角度亦然

准备工作

创建教师表、学生表

-- 教师表
CREATE TABLE `teacher` (
  `id` INT(10) NOT NULL,
  `name` VARCHAR(30) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4

INSERT INTO teacher(`id`, `name`) VALUES (1, '秦老师');

-- 学生表
CREATE TABLE `student` (
  `id` INT(10) NOT NULL,
  `name` VARCHAR(30) DEFAULT NULL,
  `tid` INT(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fktid` (`tid`),
  CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4

INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('1', '小明', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('2', '小红', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('3', '小张', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('4', '小李', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('5', '小王', '1');

测试环境搭建

  1. 导入lombok
  2. 新建实体类Teacher、Student
  3. 建立Mapper接口(如果采用注解的实现方式,那么.xml文件中就不能有该方法的sql映射,只能保留一个)
  4. 建立Mapper.xml文件
  5. 在核心配置文件中绑定Mapper.xml文件
  6. 测试查询

按照查询嵌套处理

本质,先查询学生的信息,然后根据学生表的tid再次查询老师的信息【嵌套查询(子查询)】

Student学生类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private Integer id;
    private String name;
    private Integer tid;
    
    // 老师的信息
    private Teacher teacher;
}

StudentMapper接口

// 查询所有的学生
List<Student> getStudentTeacher();
// 根据tid查询老师的信息
Teacher getTeacherById(Integer tid);

StudentMapper.xml文件

<mapper namespace="com.jjh.dao.StudentMapper">

    <resultMap id="getStudentTeacherMap" type="com.jjh.pojo.Student">
        <result property="id" column="id"/>
        <result property="name" column="name"/>

        <!--复杂的属性字段映射进行特殊处理 teacher属性代表一个老师对象-->
        <!--对象:association 集合:collection-->
        <association property="teacher" javaType="com.jjh.pojo.Teacher" column="tid" 						select="getTeacherById">
            <result property="id" column="id"/>
            <result property="name" column="name"/>
        </association>
    </resultMap>
	
    <!--查询所有学生-->
    <select id="getStudentTeacher" resultMap="getStudentTeacherMap">
        select * from student
    </select>
	
    <!--根据数据库的tid字段查询老师信息-->
    <select id="getTeacherById" parameterType="int" resultType="com.jjh.pojo.Teacher">
        select * from teacher where id=#{tid}
    </select>

</mapper>

补充:

想象下,上述sql是根据学生表的tid查询老师的信息,只有一个tid参数,如果涉及到多个参数(比如要根据老师的id和name进行查询),实现方式如下

<resultMap id="StudentTeacher" type="Student">
   <association property="teacher"  column="{id=tid,name=tid}" javaType="Teacher" 						select="getTeacher"/>
</resultMap>
<!--
这里传递过来的id,只有一个属性的时候,下面可以写任何值
association中column多参数配置:
   column="{key=value,key=value}"
   其实就是键值对的形式,key是传给下个sql的取值名称,value是片段一中sql查询的字段名。
-->
<select id="getTeacher" resultType="teacher">
  select * from teacher where id = #{id} and name = #{name}
</select>

Test测试类

@Test
public void getStudentTeacher(){
    SqlSession sqlSession = MybatisUtils.getSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

    List<Student> studentList = mapper.getStudentTeacher();
    for (Student student : studentList) {
        System.out.println(student);
    }

    sqlSession.close();
}

结果

image-20230423161549210

按照结果嵌套处理

先将想要的结果查询出来,然后再进行属性和字段的映射【连表查询】

StudentMapper接口

// 按照结果嵌套处理
List<Student> getStudentList();

StudentMapper.xml文件

<resultMap id="getStudentListMap" type="com.jjh.pojo.Student">
    <result property="id" column="sid"/>
    <result property="name" column="sname"/>
    <result property="tid" column="stid"/>
    <association property="teacher" javaType="com.jjh.pojo.Teacher">
        <result property="name" column="tname"/>
    </association>
</resultMap>

<!--结果嵌套处理-->
<select id="getStudentList" resultMap="getStudentListMap">
    select s.id sid,s.name sname,s.tid stid,t.name tname
    from student s
    inner join teacher t on s.tid=t.id
</select>

Test测试类

@Test
public void getStudentList(){
    SqlSession sqlSession = MybatisUtils.getSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);

    List<Student> studentList = mapper.getStudentList();
    for (Student student : studentList) {
        System.out.println(student);
    }

    sqlSession.close();
}

结果

image-20230423182616662

回顾mysql多对一查询方式:

  • 子查询
  • 连表查询

一对多处理

准备工作

和10一样

按照查询嵌套处理

数据库数据预览

image-20230424001109653 image-20230424001146817

先查询出所有老师的信息,在根据老师的id查询学生集合

Mapper接口

/**
 * 按照查询嵌套处理
 */
// 查询所有老师
List<Teacher> getTeacherList();

// 根据teacherId查询studentList
List<Student> getStudentListByTeacherId(Integer teacherId);

Mapper.xml文件

<resultMap id="getTeacherListMap" type="com.jjh.pojo.Teacher">
    <result property="id" column="id"/>
    <result property="name" column="name"/>
    <!--
      JavaType和ofType都是用来指定对象类型的
      JavaType是用来指定pojo中属性的类型
      ofType指定的是映射到list集合属性中pojo的类型。
    -->
    <collection property="studentList"
                javaType="ArrayList"
                ofType="com.jjh.pojo.Student"
                column="id"
                select="getStudentListByTeacherId">
        <result property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="tid" column="tid"/>
    </collection>
</resultMap>

<select id="getTeacherList" resultMap="getTeacherListMap">
    select id,name from teacher
</select>

<select id="getStudentListByTeacherId" parameterType="int"
        resultType="com.jjh.pojo.Student">
    select id,name,tid from student where tid=#{teacherId}
</select>

测试结果:

image-20230424001449669

按照结果嵌套处理

先关联表统一查询,然后进行属性字段映射配置

Mapper接口:

/**
 * 按照结果嵌套处理
 */
List<Teacher> getTeacherListByLianBiao();

Mapper.xml文件:

<resultMap id="getTeacherListByLianBiaoMap" type="com.jjh.pojo.Teacher">
    <result property="id" column="tid"/>
    <result property="name" column="tname"/>
    <collection property="studentList" ofType="com.jjh.pojo.Student">
        <result property="id" column="sid"/>
        <result property="name" column="sname"/>
        <result property="tid" column="stid"/>
    </collection>
</resultMap>

<!--根据结果嵌套处理-->
<select id="getTeacherListByLianBiao" resultMap="getTeacherListByLianBiaoMap">
    select t.id tid,t.name tname,s.id sid,s.name sname,s.tid stid
    from student s,teacher t
    where s.tid=t.id
</select>

测试结果:

image-20230424001831564

小结:

  1. 关联:association 【一对一】
  2. 集合:collection 【一对多】
  3. JavaType 和 ofType
    • JavaType 用来指定实体类中属性的类型
    • ofType 用来指定映射到List或者集合中的pojo类型,泛型中的约束类型

注意点:

  • 保证sql的可读性,尽量保证通俗易懂
  • 注意一对一,一对多中,属性和字段的问题
  • 如果问题不好排查,可以使用日志,建议用Log4j

避免慢SQL

面试高频

  • Mysql引擎
  • InnoDB底层原理
  • 索引
  • 索引优化

动态SQL

动态SQL指的是根据不同的查询条件 , 生成不同的Sql语句.

环境搭建

数据库:

CREATE TABLE `blog` (
  `id` varchar(50) NOT NULL COMMENT '博客id',
  `title` varchar(100) NOT NULL COMMENT '博客标题',
  `author` varchar(30) NOT NULL COMMENT '博客作者',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `views` int(30) NOT NULL COMMENT '浏览量'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

创建基础工程

  • 导包

  • 编写配置文件

  • 编写实体类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Blog {
       private String id;
       private String title;
       private String author;
       private Date createTime; // 属性和数据库字段不一致,开启驼峰命名配置
       private int views;
    }
    
  • 编写实体类对应的Mapper接口和Mappr.xml文件

if 语句

Mapper接口:

List<Blog> queryBlogIf(Map map);

Mapper.xml文件:

<select id="queryBlogIf" parameterType="map" resultType="blog">
    select * from blog where 1=1
    <if test="title != null and title != ''">
        and title = #{title}
    </if>
    <if test="author != null and author != ''">
        and author = #{author}
    </if>
</select>

where、set、trim

where

  • 如果有子句返回,插入where元素,没有子句返回,去掉where元素
  • 若where后面返回的第一个子句有and或者or,where元素会将他们去掉
<select id="queryBlogIf" parameterType="map" resultType="blog">
    select * from blog
    <where>
        <if test="title != null and title != ''">
            and title = #{title}
        </if>
        <if test="author != null and author != ''">
            and author = #{author}
        </if>
    </where>
</select>

set

  • 动态前置set元素,如果有子句返回,就添加set元素,执行修改操作,如果没有子句返回,去掉set元素导致sql报错
  • 删除set的子句中的最后一个","号

Mapper接口:

int updateBolgSet(Map map);

Mapper.xml文件:

<update id="updateBolgSet" parameterType="map">
    update blog
    <set>
        <if test="title != null and title != ''">
            title = #{title},
        </if>
        <if test="author != null and author != ''">
            author = #{author},
        </if>
    </set>
    where id='b54052ac-01b2-4ed9-a561-84a98ec9f519'
</update>

Test测试类:

@Test
public void updateBolgSet(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);
	
	// 没有子句返回
    HashMap<String, String> map = new HashMap<String, String>();
    int i = mapper.updateBolgSet(map);
    System.out.println(i);
    
    sqlSession.close();
}

测试结果:

  • 没有子句返回的情况:

    image-20230424225117768

  • 有子句返回的情况

    image-20230424225332716

    image-20230424225414857

trim

用来定制其他元素的功能

  • prefix 指定前缀
  • prefixOverrides 指定覆盖的前缀
  • suffixOverrides 指定覆盖的后缀

where元素的使用效果等价于:

<trim prefix="WHERE" prefixOverrides="AND|OR">
  ...
</trim>
  • 指定的前缀是WHERE
  • 如果没有子句返回,去掉WHERE
  • 如果有子句返回,去掉子句中的第一个AND或者OR

set元素的使用效果等价于:

<trim prefix="SET" suffixOverrides =",">
  ...
</trim>
  • 指定的前缀是SET
  • 如果没有子句返回,去掉SET
  • 如果有子句返回,去掉子句中的最后一个","号

choose、when、otherwise

  • 在多个子句中选择第一个符合条件的子句执行
  • 如果所有的子句都不符合条件,选择otherwise子句
  • 类似java中的switch

Mapper接口:

List<Blog> queryBlogChoose(Map map);

Mapper.xml文件:

<select id="queryBlogChoose" parameterType="map" resultType="blog">
    select * from blog where 1=1
    <choose>
        <when test="title != null and title != ''">
            and title = #{title}
        </when>
        <when test="author != null and author != ''">
            and author = #{author}
        </when>
        <otherwise>
            and title is null
        </otherwise>
    </choose>
</select>

Test测试类:

@Test
public void queryBlogChoose(){
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);

    HashMap<String, String> map = new HashMap<String, String>();
    List<Blog> blogs = mapper.queryBlogChoose(map);
    for (Blog blog : blogs) {
        System.out.println(blog);
    }

    sqlSession.close();
}

测试结果:

image-20230424223741174

SQL片段

将公用的SQL抽取出来,方便复用

<!--抽取公用SQL-->
<sql id="baseSqlFiled">
    <if test="title != null and title != ''">
        and title = #{title}
    </if>
    <if test="author != null and author != ''">
        and author = #{author}
    </if>
</sql>

<select id="queryBlogIf" parameterType="map" resultType="blog">
    select * from blog
    <where>
        <!--引用公用SQL-->
        <include refid="baseSqlFiled"/>
    </where>
</select>

注意:

  • 最好基于单表来定义SQL片段
  • 不要存在where标签

foreach

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  <where>
    <foreach item="item" index="index" collection="list"
        open="ID in (" separator="," close=")" nullable="true">
          #{item}
    </foreach>
  </where>
</select>
  • 指定一个集合,对集合进行遍历,通常在构建IN语句的时候
  • 指定开头与结尾的字符串以及集合项迭代之间的分隔符
  • 可以在元素体内使用的集合项(item)和索引(index)变量
  • 任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach
    • 使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素
    • 当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值

数据库数据:

image-20230424233613763

要实现SQL:

select * from blog where ( id = 1 or id = 2 or id = 3 or id = 4 ) 

Mapper接口:

List<Blog> queryBlogForeach(Map map);

Mapper.xml文件:

<select id="queryBlogForeach" parameterType="map" resultType="blog">
    select * from blog where
    <!--
	1.ids 是传递的Map中的键名
	2.如果键对应的值是个没有数据的集合(size=0,集合不能为null),去掉则foreach中指定的sql。
	3.因为如果键ids的对应的值(集合)中没有数据,此SQL会成为select * from blog where,导致报错,所以要使用<where>元素
	-->
    <foreach collection="ids" open="(" close=")" index="index" item="item" separator="or">
        id = #{item}
    </foreach>
</select>

Test测试类:

@Test
public void queryBlogForeach() {
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    BlogMapper mapper = sqlSession.getMapper(BlogMapper.class);

    HashMap<String, List<Integer>> map = new HashMap<String,List<Integer>>();

    List<Integer> idsList = new ArrayList<Integer>();

    idsList.add(1);
    idsList.add(2);
    idsList.add(3);
    idsList.add(4);
	
    map.put("ids",idsList);

    List<Blog> blogList = mapper.queryBlogForeach(map);
    for (Blog blog : blogList) {
        System.out.println(blog);
    }

    sqlSession.close();
}

测试结果:

image-20230425000555827

bind

在表达式以外创建一个使用OGNL表达式的变量,并将其绑定到当前的上下文

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

script

在Mapper接口中,实现动态SQL,可以使用script 元素

@Update({"<script>",
"update Author",
"  <set>",
"    <if test='username != null'>username=#{username},</if>",
"    <if test='password != null'>password=#{password},</if>",
"    <if test='email != null'>email=#{email},</if>",
"    <if test='bio != null'>bio=#{bio}</if>",
"  </set>",
"where id=#{id}",
"</script>"})
void updateAuthorValues(Author author);

总结

动态SQL指的是根据不同的查询条件 , 生成不同的Sql语句.

所谓的动态SQL,本质还是SQL语句,只是我们可以在SQL层面,去执行一个逻辑代码

动态SQL就是在拼接SQL语句,我们只要保证SQL的正确性,按照SQL的格式,去排列组合就可以了

建议:

现在Mysql中写出完整的SQL,再对应去修改成为我们的动态SQL实现通用即可

缓存

简介

查询:要连接数据库,频繁的查询数据库耗资源

优化:一次查询的结果,给他暂存在一个可以直接取到的地方 --> 内存:缓存

再次查询:当我们再次查询相同的数据的时候,直接走缓存,这样就不用走数据库了

定义

  • 存在内存中的临时数据。
  • 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题。

为什么使用缓存

  • 减少和数据库的交互次数,减少系统开销,提高系统效率。

什么样的数据能使用缓存

  • 经常查询并且不经常改变的数据

Mybatis缓存

  • MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
  • MyBatis系统中默认定义了两级缓存:一级缓存二级缓存
    • 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
    • 二级缓存需要手动开启和配置,他是基于namespace级别的缓存(Mapper接口级别)
    • 为了提高扩展性,MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存

一级缓存

一级缓存

一级缓存也叫本地缓存

  • 与数据库同一次会话期间查询到的数据会放在本地缓存中。
  • 以后如果需要获取相同的数据,直接从缓存中拿,没必须再去查询数据库

测试

在mybatis中加入日志,方便测试结果

Mapper接口:

//根据id查询用户
User queryUserById(@Param("id") int id);

Mapper.xml文件:

<select id="queryUserById" resultType="user">
    select * from user where id = #{id}
</select>

Test测试:

@Test
public void testQueryUserById(){
    SqlSession session = MybatisUtils.getSqlSession();
    UserMapper mapper = session.getMapper(UserMapper.class);

    User user = mapper.queryUserById(1);
    System.out.println(user);
    User user2 = mapper.queryUserById(1);
    System.out.println(user2);
    System.out.println(user==user2);

    session.close();
}

测试结果:

image-20230425011254947

一级缓存失效的四种情况

  • sqlSession相同,查询条件不同

  • sqlSession相同,两次查询之间执行了增删改操作!

  • sqlSession相同,手动清除一级缓存

    Test测试类:

    @Test
    public void testQueryUserById(){
        SqlSession session = MybatisUtils.getSqlSession();
        UserMapper mapper = session.getMapper(UserMapper.class);
    
        User user = mapper.queryUserById(1);
        System.out.println(user);
    
        // 手动清理缓存
        session.clearCache();
    
        User user2 = mapper.queryUserById(1);
        System.out.println(user2);
        System.out.println(user==user2);
    
        session.close();
    }
    

    测试结果:

    image-20230425012340969

  • sqlSession不同

关闭一级缓存

一级缓存在默认的情况下是开启的,建议也是开启一级缓存

在mybatis的核心配置文件中进行如下的配置即可关闭一级缓存

<settings>
    <!--关闭一级缓存,默认情况下开启,建议也是开启的-->
    <!--STATEMENT表示只对当前的语句有效,执行完毕后将被清空-->
    <setting name="localCacheScope" value="STATEMENT"/>
</settings>

小结:

一级缓存默认是开启的,只在一次SqlSession中有效,也就是拿到连接到关闭连接这个区间段

一级缓存就是一个Map

二级缓存

  • 二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存
  • 基于namespace级别的缓存,一个名称空间,对应一个二级缓存;
  • 工作机制
    • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中;
    • 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据被保存到二级缓存中;
    • 新的会话查询信息,就可以从二级缓存中获取内容;
    • 不同的mapper查出的数据会放在自己对应的缓存(map)中;

使用步骤

  1. 开启全局缓存 【mybatis-config.xml】

    <setting name="cacheEnabled" value="true"/>
    
  2. 去每个mapper.xml中配置使用二级缓存,这个配置非常简单;【xxxMapper.xml】

    <!--默认,加个标签即可-->
    <cache/>
    <!--自定义缓存配置-->
    <cache
     eviction="FIFO"
     flushInterval="60000"
     size="512"
     readOnly="true"/>
    <!--
    这个更高级的配置创建了一个 FIFO 缓存,
    每隔 60 秒刷新,
    最多可以存储结果对象或列表的 512 个引用,
    而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
    -->
    
  3. 代码测试

    所有的实体类先实现序列化接口

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class User implements Serializable {
        private Integer id;
        private String name;
        private String pwd;
    }
    

    Test测试类:

    @Test
    public void testQueryUserByIdCache2() {
        SqlSession session = MybatisUtils.getSqlSession();
        SqlSession session2 = MybatisUtils.getSqlSession();
    
        UserMapper mapper = session.getMapper(UserMapper.class);
        UserMapper mapper2 = session2.getMapper(UserMapper.class);
    
        User user = mapper.queryUserById(1);
        System.out.println(user);
        session.close();
    
        User user2 = mapper2.queryUserById(1);
        System.out.println(user2);
        System.out.println(user == user2);
    
        session2.close();
    }
    
  4. 测试结果:

    image-20230425015503925

小结:

  • 只要开启了二级缓存,在同一个mapper下就有效
  • 所有的数据都会先放到一级缓存中
  • 只有当会话提交或者关闭的时候,才会提交到二级缓存

缓存原理

用户查询需要的数据

  • 先去二级缓存中找,如果二级缓存中没有,再去一级缓存中查找,如果没有,则打开数据库连接,查询数据库

image-20230425020916157

自定义缓存(EhCache)

Ehcache是一种广泛使用的java分布式缓存,用于通用缓存

导包

<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-ehcache</artifactId>
    <version>1.1.0</version>
</dependency>

在Mapper.xml中指定

指定之后,mybatis就会使用我们指定的二级缓存

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jjh.dao.UserMapper">

    <!--配置二级缓存-->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

    <select id="queryUserById" resultType="user">
        select * from user where id = #{id}
    </select>
</mapper>

编写ehcache.xml文件

在resources目录下创建编写ehcache.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">
    <!--
       diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
       user.home – 用户主目录
       user.dir – 用户当前工作目录
       java.io.tmpdir – 默认临时文件路径
     -->
    <diskStore path="./tmpdir/Tmp_EhCache"/>

    <defaultCache
            eternal="false"
            maxElementsInMemory="10000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="259200"
            memoryStoreEvictionPolicy="LRU"/>

    <cache
            name="cloud_user"
            eternal="false"
            maxElementsInMemory="5000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="1800"
            memoryStoreEvictionPolicy="LRU"/>
 <!--
    defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
  -->
 <!--
   name:缓存名称。
   maxElementsInMemory:缓存最大数目
   maxElementsOnDisk:硬盘最大缓存个数。
   eternal:对象是否永久有效,一但设置了,timeout将不起作用。
   overflowToDisk:是否保存到磁盘,当系统宕机时
   timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,	   	可选属性,默认值是0,也就是可闲置时间无穷大。
   timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当		  	 eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
   diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the 	      Virtual Machine. The default value is false.
   diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己	   	的一个缓冲区。
   diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
   memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认	      策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
   clearOnFlush:内存数量最大时是否清除。
   memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次	    数)。
   FIFO,first in first out,这个是大家最熟的,先进先出。
   LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓	      存的元素有一个hit属性,hit值最小的将会被清出缓存。
   LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存	   	  新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
 -->

</ehcache>

测试

@Test
public void testQueryUserByIdCache2() {
    SqlSession session = MybatisUtils.getSqlSession();
    SqlSession session2 = MybatisUtils.getSqlSession();

    UserMapper mapper = session.getMapper(UserMapper.class);
    UserMapper mapper2 = session2.getMapper(UserMapper.class);

    User user = mapper.queryUserById(1);
    System.out.println(user);
    session.close();

    User user2 = mapper2.queryUserById(1);
    System.out.println(user2);
    System.out.println(user == user2);

    session2.close();
}

测试结果:

image-20230425023530565

参数传递

Mapper和xml文件参数传递的对应格式

Mapper接口

public interface BuProductCheckMapper extends BaseMapper<BuProductCheck> {

/** 用@Param注解 */
BuProductCheck getEditCheckInfo(@Param("hazardId") String hazardId,@Param("status")  String status);

/** 不用@Param注解 */
List<BuProductCheck> getCheckList(Long hazardId, String type, Long planId, Long id,String status);

/** 不用@Param注解,只有一个参数 */
List<BuProductCheck> getHistChecInfo(String dangerId);

/**参数是对象 */
int updateCheckInfo(BuProductCheck buProductCheck);

}

xml文件

/** 用@Param注解 */
<select id="getEditCheckInfo" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from bu_product_check c
    where c.hazard_id = #{hazardId}
    <if test="status != null and  status != '' ">
        and c.status != '1'
    </if>
    <if test="status == null or status == '' ">
        and c.status = '1'
    </if>

    and c.del_flag='1'
    order by c.update_time desc
    limit 1
</select>


/** 不用@Param注解 */
<select id="getCheckList" resultMap="BaseResultMap">
    select u.user_name, u.nick_name,
    <include refid="Base_Column_List"/>
    from bu_product_check c left join sys_user u on u.user_id=c.update_by
    where c.hazard_id = #{arg0}
    <if test="arg1 != null and arg1 != ''"> and c.`type`=#{arg1} </if>
    <if test="arg2 != null and arg2 != ''"> and c.plan_id=#{arg2} </if>
    <if test="arg3 != null and arg3 != ''"> and c.id=#{arg3} </if>
    <if test="arg4 != null and arg4 != ''"> and c.status != #{arg4} </if>
    and c.del_flag='1'
    order by c.update_time desc
</select>

/** 不用@Param注解,只有一个参数 */
<select id="getHistChecInfo" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List"/>
    from bu_product_check c
    where c.hazard_id = #{dangerId}
    and c.status in ('2','3')
    and c.del_flag='1'
    order by c.update_time desc
</select>	

/**参数是对象 */
<update id="updateCheckInfo" parameterType="com.ruoyi.danger.domain.BuProductCheck">
    update bu_product_check
    <set>
        <if test="hazardIdRepeat != null and hazardIdRepeat != '' ">
            hazard_id_repeat = #{hazardIdRepeat},
        </if>
        <if test="assessDanger != null ">
            assess_danger = #{assessDanger},
        </if>
        <if test="assessOpinion != null and assessOpinion != ''">
            assess_opinion = #{assessOpinion},
        </if>
        <if test="type != null and type != ''">
            type = #{type},
        </if>
        <if test="status != null and status != ''">
            status = #{status},
        </if>
        <if test="reexamineUsers != null and reexamineUsers != ''">
            reexamine_users = #{reexamineUsers},
        </if>
        <if test="planId != null and planId != ''">
            plan_id = #{planId},
        </if>
        <if test="confirmSpec != null and confirmSpec != ''">
            confirm_spec = #{confirmSpec},
        </if>
        <if test="confirmLevel != null and confirmLevel != ''">
            confirm_level = #{confirmLevel},
        </if>
        <if test="delFlag != null and delFlag != ''">
            del_flag = #{delFlag},
        </if>
        <if test="updateBy != null and updateBy != ''">
            update_by = #{updateBy},
        </if>
        update_time = now()
    </set>
    where id = #{id}
</update>