Mybatis

发布时间:2022-07-01 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了Mybatis脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

MyBatis简介

概念

​ MyBatis 本是apache的一个开项目iBatis, 2010年这个项目由apache Software foundation 迁移到了GOOGLE code,并且改名为MyBatis 。2013年11月迁移到GIThub

用途

简单来说,MyBatis能帮助我们快速开发基于Java + 数据库的程序,能帮助我们快速映射POJO对象和数据库中的数据 同时支持普通 SQL查询,存储过程和高级映射的优秀持久层框架。

安装

  1. 导入mybatis的jar包(包括DTD文档)

  2. 编写配置文件

    <?XMl version="1.0" encoding="UTF-8" standalone="no"?>
    <!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"></transactionManager>
                <dataSource type="POOLED">
                    <PRoperty name="username" value="root"/>
                    <property name="password" value="5863385"/>
                    <property name="url" value="jdbc:MySQL://localhost:3306/mysqltest?useSSL=false"/>
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                </dataSource>
            </environment>
        </environments>
        <;mappers>
            <mapper resource="mybatis02test/mappers/student-mapper.xML"></mapper>
        </mappers>
    </configuration>
    
  3. 编写SQL映射文件

    **注意名字有一一对应关系 **

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="mybatis02test.DAO.StudentDAO">
        <insert id="insertStudent" parameterType="mybatis02test.entity.Student">
            INSERT INTO student VALUES (#{studentNo},#{loginPwd},#{studentName},#{sex},#{gradeiD},#{phone},#{address});
        </insert>
        <delete id="deleteByID" parameterType="java.lang.Integer">
            DELETE From student WHERE studentNo = (#{studentNo});
        </delete>
        <update id="update" parameterType="java.util.HashMap">
            UPDATE student SET LOGinPwd = (#{loginPwd}),studentName = (#{studentName}),sex = (#{sex}),gradeID = (#{gradeID}),phone = (#{phone}),address = (#{address})WHERE studentNo = (#{studentNo});
        </update>
        <update id="updateStudent" parameterType="mybatis02test.entity.Student">
            UPDATE student
            <set>
                <if test="studentNo!=null">
                    studentNo = (#{studentNo}),
                </if>
                <if test="loginPwd!=null">
                    loginPwd = (#{loginPwd})
                </if>
            </set>
            WHERE studentNo = (#{studentNo});
        </update>
        <select id="queryStudents" resultType="mybatis02test.entity.Student">
            SELECT * From student;
        </select>
        <select id="getStudentByNo" parameterType="java.lang.Integer" resultType="mybatis02test.entity.Student">
            SELECT * FROM student WHERE studentNo = (#{studentNo})
        </select>
        <select id="queryStudentByName" parameterType="java.lang.String" resultType="mybatis02test.entity.Student">
            SELECT * FROM  student WHERE studentName LIKE '%' #{name} '%'
        </select>
        <select id="queryStudentByStudentNo" parameterType="java.util.HashMap" resultType="mybatis02test.entity.Student">
            SELECT * FROM  student WHERE studentNo Between #{begin} AND #{end};
        </select>
        <select id="queryStudentByGradeID" parameterType="java.util.List" resultType="mybatis02test.entity.Student">
            SELECT * FROM student
                <if test="list!=null and list.size>0">
                    WHERE gradeID IN
                    <foreach collection="list" open="(" close=")" separator="," item="aa">
                        #{aa}
                    </foreach>
                </if>
        </select>
    </mapper>
    
  4. 代码实现

    //1.加载mybatis数据库的配置文件
    InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
    //2.创建SqlSessionFactoryBuilder 构建者,来创建SqlSessionFactory
    SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
    //3.一个数据库对应一个SqlSessionFactory,是线程安全的对象,创建起来比较耗费资源。
    SqlSessionFactory factory = builder.build(in);
    //4. 获取SqlSession
    SqlSession session = factory.openSession();
    System.out.println(session);
    session.delete("test.deleteEmp");
    session.COMmit();
    session.close();
    

练习:通用的增删改查

batis工具包的编写:(通过将线程和会话绑定,解决了线程问题)

package mybatis02test.dao.impl;

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;

/**
 * @author hzl
 * @Description: 工具类
 * @Date 15:39$ 2021/11/4$
 */
public class MybaseUtil {
    //线程绑定会话
    static ThreadLocal<SqlSession> threadLocale = new ThreadLocal<>();
    //将路径设定默认值
    private static final String CONFIG_FILE = "mybatis02test/mybatis-config.xml";
    static SqlSessionFactory factory;

    static {
        initFactory(CONFIG_FILE);
    }
    //实例化工厂
    public static void initFactory(String path){
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        try {
            InputStream inputStream = Resources.getResourceAsStream(path);
            factory = builder.build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("配置文件加载错误");
        }
    }
    //获取会话
    public static SqlSession getSession(){
        SqlSession session = threadLocale.get();
        if (session == null){
            session=factory.openSession();
            threadLocale.set(session);
        }
        return session;
    }
    //关闭会话
    public static void close(){
        SqlSession session = threadLocale.get();
        if (session!=null){
            session.close();
            threadLocale.set(null);
        }
    }
    //提交
    public static void commit(){
        SqlSession session = threadLocale.get();
        if(session!=null) {
            session.commit();
        }
    }
    //回滚
    public static void rollBack(){
        SqlSession session = threadLocale.get();
        if(session!=null) {
            session.rollback();
        }
    }
}

实现类的编写

package mybatis02test.dao.impl;

import mybatis02test.dao.StudentDAO;
import mybatis02test.entity.Student;
import org.apache.ibatis.session.SqlSession;

import java.util.List;
import java.util.Map;

/**
 * @author hzl
 * @Description: 实现类
 * @Date 15:39$ 2021/11/4$
 */
public class StudentImpl implements StudentDAO {
    //插入学生对象
    @override
    public int insertStudent(Student student) {
        try {
            SqlSession session = MybaseUtil.getSession();
            int a = session.insert("mybatis02test.dao.StudentDAO.insertStudent",student);
            MybaseUtil.commit();
            return a;
        }catch (Exception e){
            MybaseUtil.rollBack();
            e.printStackTrace();
            throw new RuntimeException("插入失败");
        }finally {
            MybaseUtil.close();
        }
    }
    //根据ID删除 表
    @Override
    public int deleteById(int id) {
        try {
            SqlSession session = MybaseUtil.getSession();
            int a = session.delete("mybatis02test.dao.StudentDAO.deleteByID",id);
            MybaseUtil.commit();
            return a;
        }catch (Exception e){
            MybaseUtil.rollBack();
            e.printStackTrace();
            throw new RuntimeException("删除失败");
        }finally {
            MybaseUtil.close();
        }
    }
    //更新一个对象 传入实体类的n个属性
    @Override
    public int update(Map map) {
        try{
            SqlSession session = MybaseUtil.getSession();
            int a = session.update("mybatis02test.dao.StudentDAO.update",map);
            MybaseUtil.commit();
            return a;
        }catch (Exception e){
            MybaseUtil.rollBack();
            e.printStackTrace();
            throw new RuntimeException("更新失败");
        }finally {
            MybaseUtil.close();
        }
    }
    //更新一个对象,传入的对象覆盖
    @Override
    public int updateStudent(Student e) {
        try {
            SqlSession session = MybaseUtil.getSession();
            int a = session.update("mybatis02test.dao.StudentDAO.updateStudent",e);
            MybaseUtil.commit();
            return a;
        }catch (Exception e1){
            MybaseUtil.rollBack();
            e1.printStackTrace();
            throw new RuntimeException("更新失败");
        }finally {
            MybaseUtil.close();
        }
    }
    //查找全部 返回List
    @Override
    public List<Student> queryStudent() {
        try{
            List<Student> list;
            SqlSession session = MybaseUtil.getSession();
            list = session.selectList("mybatis02test.dao.StudentDAO.queryStudents");
            MybaseUtil.commit();
            return list;
        }catch (Exception e){
            MybaseUtil.rollBack();
            e.printStackTrace();
            throw new RuntimeException("查询失败");
        }finally {
            MybaseUtil.close();
        }

    }
    //根据ID指定查找 返回一个实体类
    @Override
    public Student getStudentByNo(Integer i) {
        try {
            SqlSession session = MybaseUtil.getSession();
            Student student= session.selectOne("mybatis02test.dao.StudentDAO.getStudentByNo",i);
            MybaseUtil.commit();
            return student;
        }catch (Exception e){
            MybaseUtil.rollBack();
            e.printStackTrace();
            throw new RuntimeException("查询失败");
        }finally {
            MybaseUtil.close();
        }
    }
    //名字模糊查找 返回List
    @Override
    public List<Student> queryStudentByName(String name) {
        try{
            List<Student> list;
            SqlSession session = MybaseUtil.getSession();
            list = session.selectList("mybatis02test.dao.StudentDAO.queryStudentByName",name);
            MybaseUtil.commit();
            return list;
        }catch (Exception e){
            MybaseUtil.rollBack();
            e.printStackTrace();
            throw new RuntimeException("查询失败");
        }finally {
            MybaseUtil.close();
        }

    }
    //根据集合查找 返回List
    @Override
    public List<Student> queryStudentByGradeID(List<Integer> list) {
        try{
            SqlSession session = MybaseUtil.getSession();
            List<Student> list2;
            list2 = session.selectList("mybatis02test.dao.StudentDAO.queryStudentByGradeID",list);
            MybaseUtil.commit();
            return list2;
        }catch (Exception e){
            MybaseUtil.rollBack();
            e.printStackTrace();
            throw new RuntimeException("查询失败");
        }finally {
            MybaseUtil.close();
        }

    }
    //根据编号区间查找 返回List
    @Override
    public List<Student> queryStudentByStudentNo(Map map) {
        try {
            SqlSession session = MybaseUtil.getSession();
            List<Student> list= session.selectList("mybatis02test.dao.StudentDAO.queryStudentByStudentNo",map);
            MybaseUtil.commit();
            return list;
        }catch (Exception e){
            MybaseUtil.rollBack();
            e.printStackTrace();
            throw new RuntimeException("查询失败");
        }finally {
            MybaseUtil.close();
        }
    }
}

接口类:

import mybatis02test.entity.Student;

import java.util.List;
import java.util.Map;

/**
 * @author hzl
 * @Description: 增删改查接口
 * @Date 15:37$ 2021/11/4$
 */
public interface StudentDAO {
    //插入
    int insertStudent(Student student);
    //删除
    int deleteById(int id);
    //修改
    int update(Map map);
    int updateStudent(Student student);
    //查询
    List<Student> queryStudent();
    Student getStudentByNo(Integer i);
    List<Student> queryStudentByName (String name);
    List<Student> queryStudentByGradeID(List<Integer> list);
    List<Student> queryStudentByStudentNo(Map map);
	}
}

额外

#{},${}的区别

  • #{}是预编译,会在预处理之前把参数部分用一个占位符?替换,可以止SQL注入
  • ${}只是简单的字符串替换,不能防止SQL注入

MyBatis 进阶语法

插入

//保存员工对象
    int saveEmployee(Employee employee);
 <!-- 保存对象 -->
    <!-- Employee 是别名 -->
    <insert id="saveEmployee" parameterType="Employee">
        insert into employees(employee_ID,last_name,First_name,hiredate) values(#{employeeID},#{lastName},#{firstName},#{hiredate})
    </insert>

更新

   //更新员工特定的几个属性
    int update(HashMap map);
  <!--更新特定数据-->
    <!--sQL驼峰映射主要是解决数据库读取问题,读取的值无法赋给实体类,这里改写_还得写 -->
    <update id="update" parameterType="hashMap">
        update employees
        <set>
            <if test="lastName != null">
                last_Name = #{lastName},
            </if>
            <if test="job != null">
                job_ID = #{jobID},
            </if>
            <if test="hiredate != null">
                hiredate = #{hiredate}
            </if>
        </set>
        <where>
            employee_ID = #{employeeID};
        </where>
    </update>

删除

//删除
    public void delete(int id);
<delete id="delete" parameterType="int">
      delete from user where id = #{id}
  </delete>

查询

简单查询

UserDao:
    public List<User> findAll();
UserDao.xml:
  <select id="findAll" resultType="com.domain.User">
    select * from user
  </select>

条件查询

UserDao:
    public List<User> findAll();
UserDao.xml:
  <select id="findAll" resultType="com.domain.User">
    select * from user
  </select>

模糊查询

//根据员工姓名模糊查询
    List<Employee> queryEmployee(HashMap fuzzyName);
<select id="queryEmployee" parameterType="hashMap" resultType="Employee">
        select * from employees where last_name like '%' #{lastName} '%';
    </select>

聚合查询

UserDao:
    public int count();
UserDao.xml:
  <select id="count" resultType="int">
      select count(*) from user
  </select>

区间查询

 //根据薪水区间查询
    List<Employee> queryBySal(HashMap codition);
 <!--根据薪水区间查询-->
    <select id="queryBySal" parameterType="HashMap" resultType="Employee">
        select * from employees where salary <![CDATA[ >= ]]> #{minSal} and salary <![CDATA[ <= ]]> #{maxSal};
    </select>

Map封装查询

方式一:手动指定

//查询所有员工姓名,薪水,工作(给列取别名 封装映射返回结果)
    List<Employee> query();
 <!-- 给列定义别名 ,查询结果使用自定义的resultMap类型来封装数据-->
    <select id="query" resultMap="EmployeeMap">
        select employee_ID,job_ID,hiredate,salary from employees;
    </select>
    <!-- type的值,使用的是别名 -->
    <resultMap type="Employee" id="EmployeeMap">
        <id column="employee_ID" property="employeeID"/>
        <result column="job_ID" property="jobID"/>
    </resultMap>

方式二:开启驼峰自动转型

在config.xml配置

<settings>
        <!--开启mybatis驼峰命名自动映射 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <!--取别名 全局配置 -->
    <typeAliases>
        <typeAlias type="com.pers.mybatistest.entity.Employee" alias="Employee"></typeAlias>
        <!--在mappers里面,参数类型和返回值类型都是实体类,每次都要写这么长长的一串com.pers.mybatistest.entity.Employee,就会非常麻烦。 -->
        <!--这里采用取别名的方式解决 -->
    </typeAliases>

动态查询

//多条件动态查询
List<Employee> queryByCondition(HashMap map);
 <!-- 动态查询 -->
    <select id="queryByCondition" parameterType="HashMap" resultType="Employee">
        select * from employees
        <where>
            <if test="lastName !=null">
            and last_name like '%' #{lastName} '%'
            </if>
            <if test="employeeID != null">
                and employee_ID = #{employeeID}
            </if>
            <if test="begin != null">
                <if test="end!=null">
                    and hiredate between #{begin} and #{end}
                </if>
                <if test="end == null">
                    and hiredate ![CDATA[ >= ]] #{begin}
                </if>
            </if>
            <if test="begin == null">
                <if test="end != null">
                    and hiredate <![CDATA[ <= ]]> #{end}
                </if>
            </if>
        </where>
    </select>

多条件查询(只执行其一)

//多条件choose查询(只有一个条件会查询)
    List<Employee> chooseQuery(HashMap map);
<!-- choose 判断之会选择其中的一个条件 -->
    <select id="chooseQuery" parameterType="hashMap" resultType="Employee">
        select * from employees
        <where>
            <choose>
                <when test="lastName != null">
                    and last_name like '%' #{lastName} '%'
                </when>
                <when test="jobID != null">
                    and job_ID = #{jobID}
                </when>
                <otherwise>
                    and department_ID = 100
                </otherwise>
            </choose>
        </where>
    </select>

分页查询

//分页查询
    List<Employee> selectByPage(HashMap map);
<!-- 分页 -->
    <select id="selectByPage" parameterType="hashMap" resultType="Employee">
        select * from employees limit #{start},#{limit}
    </select>

多表查询(resultMap,association)

一对多

  1. 实体类
  2. xml文件
  3. 实现类
//实体类
package com.pers.mybatistest.entity;

import java.util.Date;

/**
 * @author hzl
 * @Description: 实体类
 * @Date 11:52$ 2021/11/5$
 */
public class Employee {
    private Integer employeeID;
    private Integer managerID;
    private Integer departmentID;


    private String firstName;
    private String lastName;
    private String email;
    private String phoneNumber;
    private String jobID;

    private Double salary;
    private Double commissionPct;

    private Date hiredate;

    private Department department;

    public Employee(Integer employeeID, Integer managerID, Integer departmentID, String firstName, String lastName, String email, String phoneNumber, String jobID, Double salary, Double commissionPct, Date hiredate, Department department) {
        this.employeeID = employeeID;
        this.managerID = managerID;
        this.departmentID = departmentID;
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.phoneNumber = phoneNumber;
        this.jobID = jobID;
        this.salary = salary;
        this.commissionPct = commissionPct;
        this.hiredate = hiredate;
        this.department = department;
    }

    public Employee() {
    }

    public Integer getEmployeeID() {
        return employeeID;
    }

    public void setEmployeeID(Integer employeeID) {
        this.employeeID = employeeID;
    }

    public Integer getManagerID() {
        return managerID;
    }

    public void setManagerID(Integer managerID) {
        this.managerID = managerID;
    }

    public Integer getDepartmentID() {
        return departmentID;
    }

    public void setDepartmentID(Integer departmentID) {
        this.departmentID = departmentID;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public String getJobID() {
        return jobID;
    }

    public void setJobID(String jobID) {
        this.jobID = jobID;
    }

    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    public Double getCommissionPct() {
        return commissionPct;
    }

    public void setCommissionPct(Double commissionPct) {
        this.commissionPct = commissionPct;
    }

    public Date getHiredate() {
        return hiredate;
    }

    public void setHiredate(Date hiredate) {
        this.hiredate = hiredate;
    }

    public Department getDepartment() {
        return department;
    }

    public void setDepartment(Department department) {
        this.department = department;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "employeeID=" + employeeID +
                ", managerID=" + managerID +
                ", departmentID=" + departmentID +
                ", firstName='" + firstName + ''' +
                ", lastName='" + lastName + ''' +
                ", email='" + email + ''' +
                ", phoneNumber='" + phoneNumber + ''' +
                ", jobID='" + jobID + ''' +
                ", salary=" + salary +
                ", commissionPct=" + commissionPct +
                ", hiredate=" + hiredate + (department != null? (", department=" + department.toString()): "") +
                '}';
    }
}
// 多表查询
    List<Employee>selectByJoin();
 <!-- 多表查询 -->
    <select id="selectByJoin" resultMap="EmpDeptMap">
        select a.*,b.department_ID dno,b.department_name   from employees a left join departments b on a.department_ID = b.department_ID;
    </select>

    <resultMap id="EmpDeptMap" type="Employee"  autoMapping="true">
        <!-- id用来定义主键标识列
        在resultMap标签中使用association标签
        标签功能:对一对一映射的返回结果进行封装,即封装从属实体类对象
        标签属性:javaType属性 指定所封装对象的全限定类名-->
        <id column="department_ID" property="departmentID"/>
        <!-- 关联映射 -->
        <!-- property 都是实体类的属性 -->
        <association  column="department_ID" property="department" javaType="com.pers.mybatistest.entity.Department">
            <!--需要查询几个附加的表就添加几列result -->
            <id column="dno" property="departmentID"/>
            <result column="department_name" property="departmentName"/>
        </association>
    </resultMap>

多对一

package com.pers.mybatistest.entity;

import java.util.List;

/**
 * @author hzl
 * @Description:
 * @Date 20:07$ 2021/11/5$
 */
public class Department {
    private Integer departmentID;
    private String departmentName;
    private Integer managerID;
    private Integer locationID;

    private List<Employee> employeeList;

    public Department(Integer departmentID, String departmentName, Integer managerID, Integer locationID, List<Employee> employeeList) {
        this.departmentID = departmentID;
        this.departmentName = departmentName;
        this.managerID = managerID;
        this.locationID = locationID;
        this.employeeList = employeeList;
    }

    public Department() {
    }

    public Integer getDepartmentID() {
        return departmentID;
    }

    public void setDepartmentID(Integer departmentID) {
        this.departmentID = departmentID;
    }

    public String getDepartmentName() {
        return departmentName;
    }

    public void setDepartmentName(String departmentName) {
        this.departmentName = departmentName;
    }

    public Integer getManagerID() {
        return managerID;
    }

    public void setManagerID(Integer managerID) {
        this.managerID = managerID;
    }

    public Integer getLocationID() {
        return locationID;
    }

    public void setLocationID(Integer locationID) {
        this.locationID = locationID;
    }

    public List<Employee> getEmployeeList() {
        return employeeList;
    }

    public void setEmployeeList(List<Employee> employeeList) {
        this.employeeList = employeeList;
    }

    @Override
    public String toString() {
        return "Department{" +
                "departmentID=" + departmentID +
                ", departmentName='" + departmentName + ''' +
                ", managerID=" + managerID +
                ", locationID=" + locationID + (employeeList!=null?( ", employeeList=" + employeeList.toString()):"")+
                '}';
    }
}

 List<Department> queryByJoin();
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pers.mybatistest.dao.DepartmentDAO">
    <select id="queryByJoin" resultMap="DeptMap">
        select a.department_ID,a.department_name, b.department_ID dno,b.first_name,b.last_name from departments a left join employees b on a.department_ID = b.department_ID
    </select>
    <!--采用Map映射的话 需要全部都映射上,不映射都为null-->
    <resultMap id="DeptMap" type="com.pers.mybatistest.entity.Department">
        <id column="department_ID" property="departmentID"></id>
        <result column="department_name" property="departmentName"></result>
        <collection property="employeeList" ofType="com.pers.mybatistest.entity.Employee" resultMap="EmployeeMap"></collection>
    </resultMap>
    <resultMap id="EmployeeMap" type="com.pers.mybatistest.entity.Employee">
        <id column="dno" property="departmentID"></id>
        <result column="first_name" property="firstName"></result>
        <result column="last_name" property="lastName"></result>
    </resultMap>
    
</mapper>

复合语法

添加记录并获取其id值

UserDao:   
    public void add2(User user);
UserDao.xml: 
<insert id="add2" parameterType="com.domain.User">
    <!-- 
        keyColumn:表中的字段名
        keyProperty:类中的属性名
        order:
          BEFORE:在之前执行
          AFTER:在之后执行
     -->
    <selectKey keyColumn="id" keyProperty="id" resultType="int" order="AFTER">
      select last_insert_id()
    </selectKey>
    insert into user values(null, #{name})
</insert>

MyBatis 缓存详解

  缓存是一般的ORM ( 对象关系映射(英语:Object Relational Mapping))框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。跟Hibernate 一样,MyBatis 也有一级缓存和二级缓存,并且预留了集成第三方缓存的接口。

缓存体系结构:

  • cache

    • decorators
      • BlockingCache
      • FifoCache
      • LoggingCache
      • LruCache
      • ScheduledCache
      • SerializedCache
      • SoftCache
      • SynchronizedCache
      • TransactionalCache
      • WeakCache
    • impl
      • PerpetualCache
    • Cache
    • CacheException
    • CacheKey
    • NullCacheKey
    • TransactionalCacheManager

    ​ MyBatis 跟缓存相关的类都在cache 包里面,其中有一个Cache 接口,只有一个默认的实现类PerpetualCache,它是用HashMap 实现的。

      除此之外,还有很多的装饰器,通过这些装饰器可以额外实现很多的功能:回收策略、日志记录、定时刷新等等。但是无论怎么装饰,经过多少层装饰,最后使用的还是基本的实现类(默认PerpetualCache)。可以通过 CachingExecutor 类 Debug 去查看。

    ​ 所有的缓存实现类总体上可分为三类:基本缓存、淘汰算法缓存、装饰器缓存。

缓存实现类 描述 作用 装饰条件
基本缓存 缓存基本实现类 默认是PerpetualCache,也可以自定义比如RedisCache、EhCache 等,具备基本功能的缓存类
LruCache LRU策略的缓存 当缓存到达上限时候,删除最近最少使用的缓存(Least Recently Use) eviction="LRU”(默认)
FifoCache FIFO策略的缓存 当缓存到达上限时候,删除最先入队的缓存 eviction= "FIFO"
SoftCache WeakCache 带清理策略的缓存 通过JVM的软引用和弱引用来实现缓存,当JVM内存不足时,会自动清理掉这些缓存,基于SoftReference和WeakReference eviction= "SOFT' eviction= "WEAK"
LoggingCache 带日志功能的缓存 比如:输出缓存命中率 基本
SynchronizedCache 同步缓存 基于synchronized关键字实现,解决并发问题 基本
BlockingCache 阻塞缓存 通过在get/put方式中加锁,保证只有一个线程操作缓存,基于Java重入锁实现 blocking=true
SerializedCache 支持序列化的缓存 将对象序列化以后存到缓存中,取出时反序列化 readOnly=false(默认)
ScheduledCache 时调度的缓存 在进行get/put/remove/getSize等操作前,判断缓存时间是否超过了设置的最长缓存时间(默认是一小时) .如果是则清空缓存- -即每隔一-段时间清空一次缓存 flushInterval不为空
TransactionalCache 事务缓存 在二级缓存中使用,可一次存入多个缓存,移除多个缓存 TransactionalCacheManager中用Map维护对应关系

一级缓存

一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话(SqlSession)层面进行缓存的。MyBatis 的一级缓存是默认开启的,不需要任何的配置。首先我们必须去弄清楚一个问题,在MyBatis 执行的流程里面,涉及到这么多的对象,那么缓存PerpetualCache 应该放在哪个对象里面去维护?如果要在同一个会话里面共享一级缓存,这个对象肯定是在SqlSession 里面创建的,作为SqlSession 的一个属性。

  defaultsqlSession 里面只有两个属性,Configuration 是全局的,所以缓存只可能放在Executor 里面维护——SimpleExecutor/ReuseExecutor/BatchExecutor 的父类BaseExecutor 的构造函数中持有PerpetualCache。在同一个会话里面,多次执行相同的SQL 语句,会直接从内存取到缓存的结果,不会再发送SQL 到数据库。但是不同的会话里面,即使执行的SQL 一模一样(通过一个Mapper 的同一个方法的相同参数调用),也不能使用到一级缓存。

每当我们使用MyBatis开启一次和数据库的会话,MyBatis会创建出一个SqlSession对象表示一次数据库会话。

  在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,如果不采取一些措施的话,每一次查询都会查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。

  为了解决这一问题,减少资源的浪费,MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

  如下图所示,MyBatis会在一次会话的表示----一个SqlSession对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。

一级缓存的生命周期有多长?

  1. MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象;当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
  2. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
  3. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据;
  4. SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空PerpetualCache对象的数据;

SqlSession 一级缓存的工作流程:

  1. 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果

  2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;

  3. 如果命中,则直接将缓存结果返回;

  4. 如果没命中:

    1. 去数据库中查询数据,得到查询结果;
    2. 将key和查询到的结果分别作为key,value对存储到Cache中;
    3. 将查询结果返回;

注意点

  • 在同一个session 中共享(不同session 不能共享)
  • 同一个会话中,update(包括delete)会导致一级缓存被清空
  • 其他会话更新了数据,导致读取到脏数据(一级缓存不能跨会话共享)

一级缓存的不足:

  使用一级缓存的时候,因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题。如果要解决这个问题,就要用到二级缓存。MyBatis 一级缓存(MyBaits 称其为 Local Cache)无法关闭,但是有两种级别可选:

  1. session 级别的缓存,在同一个 sqlSession 内,对同样的查询将不再查询数据库,直接从缓存中。
  2. 事务级别的缓存 statement 级别的缓存,避坑: 为了避免这个问题,可以将一级缓存的级别设为 statement 级别的,这样每次查询结束都会清掉一级缓存。

二级缓存

  二级缓存是用来解决一级缓存不能跨会话共享的问题的,范围是namespace 级别的,可以被多个SqlSession 共享(只要是同一个接口里面的相同方法,都可以共享),生命周期和应用同步。如果你的MyBatis使用了二级缓存,并且你的Mapper和select语句也配置使用了二级缓存,那么在执行select查询的时候,MyBatis会先从二级缓存中取输入,其次才是一级缓存,即MyBatis查询数据的顺序是:二级缓存 —> 一级缓存 —> 数据库。

  作为一个作用范围更广的缓存,它肯定是在SqlSession 的外层,否则不可能被多个SqlSession 共享。而一级缓存是在SqlSession 内部的,所以第一个问题,肯定是工作在一级缓存之前,也就是只有取不到二级缓存的情况下才到一个会话中去取一级缓存。第二个问题,二级缓存放在哪个对象中维护呢? 要跨会话共享的话,SqlSession 本身和它里面的BaseExecutor 已经满足不了需求了,那我们应该在BaseExecutor 之外创建一个对象。

  实际上MyBatis 用了一个装饰器的类来维护,就是CachingExecutor。如果启用了二级缓存,MyBatis 在创建Executor 对象的时候会对Executor 进行装饰。CachingExecutor 对于查询请求,会判断二级缓存是否有缓存结果,如果有就直接返回,如果没有委派交给真正的查询器Executor 实现类,比如SimpleExecutor 来执行查询,再走到一级缓存的流程。最后会把结果缓存起来,并且返回给用户。

 开启二级缓存的方法

第一步:配置 mybatis.configuration.cache-enabled=true,只要没有显式地设置cacheEnabled=false,都会用CachingExecutor 装饰基本的执行器。

  <setting name="cacheEnabled" value="true"/>

第二步:在Mapper.xml 中配置< cache/>标签:

<!--缓存类型可以不指定 默认是这个 -->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
    size="1024"<!--二级缓存中最多可以存放对象的个数,默认是1024 -->
eviction="LRU"<!-- 当二级缓存中的对象达到最大值时,就需要通过逐出策略将缓存中的对象移出缓存-->
<!-- FIFO: First In First Out 先进先出 -->
<!--LRU: Least Recently Used 未被使用时间最长的,默认-->
flushInterval="120000"<!--刷新时间 刷新缓存及清空缓存,一般不指定,即当执行增删改时刷新缓存 -->
readOnly="false"/><!-- 设置缓存中数据是否只读的,true表示缓存中的对象不能被修改,提升性能-->

基本上就是这样。这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

  注:二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

  Mapper.xml 配置了< cache>之后,select()会被缓存。update()、delete()、insert()会刷新缓存。如果cacheEnabled=true,Mapper.xml 没有配置标签,还有二级缓存吗?(没有)还会出现CachingExecutor 包装对象吗?(会,创建Session对象的时候创建)

  只要cacheEnabled=true 基本执行器就会被装饰。有没有配置< cache>,决定了在启动的时候会不会创建这个mapper 的Cache 对象,只是最终会影响到CachingExecutorquery 方法里面的判断。如果某些查询方法对数据的实时性要求很高,不需要二级缓存,怎么办?我们可以在单个查询语句 ID 上显式关闭二级缓存(默认是true):

<!-- useCache使用二级缓存 -->
<select id="select" resultMap="EmpMap" useCache="false">
    select * from emp
    </select>

注意点:

  • 事务不提交,二级缓存 不存在
  • 使用不同的session 和mapper,验证二级缓存可以跨session 存在取消以上commit()的注释
  • 在其他的session 中执行增删改操作,验证缓存会被刷新

 为什么增删改操作会清空缓存?在CachingExecutor 的update()方法里面会调用flushCacheIfRequired(ms),isFlushCacheRequired 就是从标签里面渠道的flushCache 的值。而增删改操作的flushCache 属性默认为true。

什么时候开启二级缓存?

一级缓存默认是打开的,二级缓存需要配置才可以开启。那么我们必须思考一个问题,在什么情况下才有必要去开启二级缓存?

  1. 因为所有的增删改都会刷新二级缓存,导致二级缓存失效,所以适合在查询为主的应用中使用,比如历史交易、历史订单的查询。否则缓存就失去了意义。
  2. 如果多个namespace 中有针对于同一个表的操作,比如blog 表,如果在一个namespace 中刷新了缓存,另一个namespace 中没有刷新,就会出现读到脏数据的情况。所以,推荐在一个Mapper 里面只操作单表的情况使用。

  如果要让多个namespace 共享一个二级缓存,应该怎么做?跨namespace 的缓存共享的问题,可以使用< cache-ref>来解决:

<cache-ref namespace="com.wuzz.crud.dao.DepartmentMapper" />

  cache-ref 代表引用别的命名空间的Cache 配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。

  注意:在这种情况下,多个Mapper 的操作都会引起缓存刷新,缓存的意义已经不大了.

自定义缓存 (空)

第三方缓存做二级缓存

  除了MyBatis 自带的二级缓存之外,我们也可以通过实现Cache 接口来自定义二级缓存。MyBatis 官方提供了一些第三方缓存集成方式,比如ehcache 和redis:https://github.com/mybatis/redis-cache ,这里就不过多介绍了。当然,我们也可以使用独立的缓存服务,不使用MyBatis 自带的二级缓存。

自定义缓存:

  除了上述自定义缓存的方式,你也可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。

<cache type="com.domain.something.MyCustomCache"/>

  这个示例展示了如何使用一个自定义的缓存实现。type 属性指定的类必须实现 org.mybatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器。 这个接口是 MyBatis 框架中许多复杂的接口之一,但是行为却非常简单。

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}

  为对你的缓存进行配置,只需要简单地在你的缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值,例如,下面的例子将在你的缓存实现上调用一个名为 setCacheFile(String file) 的方法:

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>

  你可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。 你也可以使用占位符(如 ${cache.file}),以便替换成在配置文件属性中定义的值。从版本 3.4.2 开始,MyBatis 已经支持在所有属性设置完毕之后,调用一个初始化方法。 如果想要使用这个特性,请在你的自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject 接口。

public interface InitializingObject {
  void initialize() throws Exception;
}

  请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。 每条语句可以自定义与缓存交互的方式,或将它们完全排除于缓存之外,这可以通过在每条语句上使用两个简单属性来达成。 默认情况下,语句会这样来配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

  鉴于这是默认行为,显然你永远不应该以这样的方式显式配置一条语句。但如果你想改变默认的行为,只需要设置 flushCache 和 useCache 属性。比如,某些情况下你可能希望特定 select 语句的结果排除于缓存之外,或希望一条 select 语句清空缓存。类似地,你可能希望某些 update 语句执行时不要刷新缓存。

脚本宝典总结

以上是脚本宝典为你收集整理的Mybatis全部内容,希望文章能够帮你解决Mybatis所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。