MyBatis 终极版

第一部分:自定义持久层框架

1.1 分析JDBC操作问题

public static void main(String[] args) {
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    try {
        // 加载数据库驱动
        Class.forName("com.mysql.jdbc.Driver");
        // 通过驱动管理类获取数据库链接
        connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8", "root", "root");
        // 定义sql语句?表示占位符
        String sql = "select * from user where username = ?";
        // 获取预处理statement
        preparedStatement = connection.prepareStatement(sql);
        // 设置参数,第一个参数为sql语句中参数的序号(从 1 开始),第二个参数为设置的参数值
        preparedStatement.setString(1, "tom");
        // 向数据库发出sql执行查询,查询出结果集
        resultSet = preparedStatement.executeQuery();
        // 遍历查询结果集
        while (resultSet.next()) {
            int id = resultSet.getInt("id");
            String username = resultSet.getString("username");
            // 封装User
            user.setId(id);
            user.setUsername(username);
        }
       	System.out.println(user);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // 释放资源
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (preparedStatement != null) {
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
	}
}

JDBC问题总结:

原始jdbc开发存在的问题如下:

1 、 数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能。

2 、 Sql语句在代码中硬编码,造成代码不易维护,实际应用中sql变化的可能较大,sql变动需要改变 java代码。

3 、 使用preparedStatement向占有位符号传参数存在硬编码,因为sql语句的where条件不一定,可能 多也可能
少,修改sql还要修改代码,系统不易维护。

4 、 对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据 库记录封装成
pojo对象解析比较方便

1.2 问题解决思路

①使用数据库连接池初始化连接资源

②将sql语句抽取到xml配置文件中

③使用反射、内省等底层技术,自动将实体与表进行属性与字段的自动映射

1.3 自定义框架设计

使用端

提供核心配置文件:

sqlMapConfig.xml : 存放数据源信息,引入mapper.xml

Mapper.xml : sql语句的配置文件信息

框架端:

1.读取配置文件

读取完成以后以流的形式存在,我们不能将读取到的配置信息以流的形式存放在内存中,不好操作,可以创建

javaBean来存储

( 1 )Configuration : 存放数据库基本信息、Map<唯一标识,Mapper> 唯一标识:namespace + "." + id

( 2 )MappedStatement:sql语句、statement类型、输入参数java类型、输出参数java类型

2.解析配置文件

创建sqlSessionFactoryBuilder类:

方法:sqlSessionFactory build():

第一:使用dom4j解析配置文件,将解析出来的内容封装到Configuration和MappedStatement中

第二:创建SqlSessionFactory的实现类DefaultSqlSession

3.创建SqlSessionFactory:

方法:openSession() : 获取sqlSession接口的实现类实例对象

4.创建sqlSession接口及实现类:主要封装crud方法

方法:selectList(String statementId,Object param):查询所有

selectOne(String statementId,Object param):查询单个

具体实现:封装JDBC完成对数据库表的查询操作

涉及到的设计模式:

Builder构建者设计模式、工厂模式、代理模式

1.4 自定义框架实现

在使用端项目中创建配置配置文件

创建 sqlMapConfig.xml

<configuration>
    <!--数据库连接信息-->
    <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    <property name="jdbcUrl" value="jdbc:mysql:///zdy_mybatis"></property>
    <property name="username" value="root"></property>
    <property name="password" value="root"></property>
    <! --引入sql配置信息-->
    <mapper resource="mapper.xml"></mapper>
</configuration>

mapper.xml

<mapper namespace="User">
 <select id="selectOne" paramterType="com.lagou.pojo.User" resultType="com.lagou.pojo.User">
 	select * from user where id = #{id} and username =#{username}
 </select>

 <select id="selectList" resultType="com.lagou.pojo.User">
 	select * from user
 </select>
</mapper>

User实体

public class User {
    //主键标识
    private Integer id;
    //⽤户名
    private String username;

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    @Override
    public String toString() {
        return "User{" +
            "id=" + id +
            ", username='" + username + '\'' + '}';
    }
}

再创建一个Maven子工程并且导入需要用到的依赖坐标

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.17</version>
    </dependency>

    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.1.2</version>
    </dependency>

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

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.10</version>
    </dependency>

    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>

    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.1.6</version>
    </dependency>
</dependencies>

Configuration

public class Configuration {
    //数据源
    private DataSource dataSource;
    //map集合: key:statementId value:MappedStatement
    private Map<String,MappedStatement> mappedStatementMap = new HashMap<String, MappedStatement>();
    public DataSource getDataSource() {
        return dataSource;
    }
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    public Map<String, MappedStatement> getMappedStatementMap() {
        return mappedStatementMap;
    }
    public void setMappedStatementMap(Map<String, MappedStatement> mappedStatementMap) {
        this.mappedStatementMap = mappedStatementMap;
    }
}

MappedStatement

public class MappedStatement {
    //id
    private Integer id;
    //sql语句
    private String sql;
    //输入参数
    private Class<?> paramterType;
    //输出参数
    private Class<?> resultType;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getSql() {
        return sql;
    }
    public void setSql(String sql) {
        this.sql = sql;
    }
    public Class<?> getParamterType() {
        return paramterType;
    }
    public void setParamterType(Class<?> paramterType) {
        this.paramterType = paramterType;
    }
    public Class<?> getResultType() {
        return resultType;
    }
    public void setResultType(Class<?> resultType) {
        this.resultType = resultType;
    }
}

Resources

public class Resources {
    public static InputStream getResourceAsSteam(String path){ 
        InputStream resourceAsStream = Resources.class.getClassLoader.getResourceAsStream(path);
        return resourceAsStream;
    }
}

SqlSessionFactoryBuilder

public class SqlSessionFactoryBuilder {
    private Configuration configuration;
    public SqlSessionFactoryBuilder() {
        this.configuration = new Configuration();
    }
    public SqlSessionFactory build(InputStream inputStream) throws DocumentException, PropertyVetoException, ClassNotFoundException {
        //1.解析配置⽂件,封装Configuration 
        XMLConfigerBuilder xmlConfigerBuilder = new XMLConfigerBuilder(configuration);
        Configuration configuration = xmlConfigerBuilder.parseConfiguration(inputStream);
        //2.创建 sqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
        return sqlSessionFactory;
    }
}

XMLConfigerBuilder

public class XMLConfigerBuilder {
    private Configuration configuration;
    public XMLConfigerBuilder(Configuration configuration) {
        this.configuration = new Configuration();
    }
    public Configuration parseConfiguration(InputStream inputStream) throws DocumentException, PropertyVetoException, ClassNotFoundException {
        Document document = new SAXReader().read(inputStream); //<configuation>
        Element rootElement = document.getRootElement();
        List<Element> propertyElements = rootElement.selectNodes("//property");
        Properties properties = new Properties();
        for (Element propertyElement : propertyElements) {
            String name = propertyElement.attributeValue("name");
            String value = propertyElement.attributeValue("value");
            properties.setProperty(name,value);
        }
        //连接池
        ComboPooledDataSource comboPooledDataSource = new ComboPooledDataSource();
        comboPooledDataSource.setDriverClass(properties.getProperty("driverClass"));
        comboPooledDataSource.setJdbcUrl(properties.getProperty("jdbcUrl"));
        comboPooledDataSource.setUser(properties.getProperty("username"));
        comboPooledDataSource.setPassword(properties.getProperty("password"));
        //填充 configuration
        configuration.setDataSource(comboPooledDataSource);
        //mapper 部分
        List<Element> mapperElements = rootElement.selectNodes("//mapper");
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
        for (Element mapperElement : mapperElements) {
            String mapperPath = mapperElement.attributeValue("resource");
            InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
            xmlMapperBuilder.parse(resourceAsSteam);
        }
        return configuration;
    }
}

XMLMapperBuilder

public class XMLMapperBuilder {
    private Configuration configuration;
    public XMLMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }
    public void parse(InputStream inputStream) throws DocumentException,ClassNotFoundException {
        Document document = new SAXReader().read(inputStream);
        Element rootElement = document.getRootElement();
        String namespace = rootElement.attributeValue("namespace");
        List<Element> select = rootElement.selectNodes("select");
        for (Element element : select) { //id的值
            String id = element.attributeValue("id");
            String paramterType = element.attributeValue("paramterType");
            String resultType = element.attributeValue("resultType"); //输⼊参数class
            Class<?> paramterTypeClass = getClassType(paramterType);
            //返回结果class
            Class<?> resultTypeClass = getClassType(resultType);
            //statementId
            String key = namespace + "." + id;
            //sql语句
            String textTrim = element.getTextTrim();
            //封装 mappedStatement
            MappedStatement mappedStatement = new MappedStatement();
            mappedStatement.setId(id);
            mappedStatement.setParamterType(paramterTypeClass);
            mappedStatement.setResultType(resultTypeClass);
            mappedStatement.setSql(textTrim);
            //填充 configuration
            configuration.getMappedStatementMap().put(key, mappedStatement);
            private Class<?> getClassType (String paramterType) throws ClassNotFoundException {
                Class<?> aClass = Class.forName(paramterType);
                return aClass;
            }
        }
    }
}

sqlSessionFactory 接⼝及D efaultSqlSessionFactory 实现类

public interface SqlSessionFactory {
    public SqlSession openSession();
}

public class DefaultSqlSessionFactory implements SqlSessionFactory {
    private Configuration configuration;
    public DefaultSqlSessionFactory(Configuration configuration) { 
        this.configuration = configuration;
    }
    public SqlSession openSession(){
        return new DefaultSqlSession(configuration);
    }
}

sqlSession 接⼝及 DefaultSqlSession 实现类

public interface SqlSession {
 public <E> List<E> selectList(String statementId, Object... param) Exception;
 public <T> T selectOne(String statementId,Object... params) throws Exception;
 public void close() throws SQLException;
}
public class DefaultSqlSession implements SqlSession {
    private Configuration configuration;
    public DefaultSqlSession(Configuration configuration) {
        this.configuration = configuration;
        //处理器对象
        private Executor simpleExcutor = new SimpleExecutor();
        public <E > List < E > selectList(String statementId, Object...param) throws Exception {
            MappedStatement mappedStatement = configuration.getMappedStatementMap().get(statementId);
            List<E> query = simpleExcutor.query(configuration, mappedStatement, param);
            return query;
        }
        //selectOne 中调⽤ selectList
        public <T > T selectOne(String statementId, Object...params) throws Exception {
            List<Object> objects = selectList(statementId, params);
            if (objects.size() == 1) {
                return (T) objects.get(0);
            } else {
                throw new RuntimeException("返回结果过多");
            }
        }
        public void close () throws SQLException {
            simpleExcutor.close();
        }
    }
}

Executor

public interface Executor {
    <E> List<E> query(Configuration configuration, MappedStatement mappedStatement,Object[] param) throws Exception;
    void close() throws SQLException;
}

SimpleExecutor

public class SimpleExecutor implements Executor {
    private Connection connection = null;
    public <E> List<E> query(Configuration configuration, MappedStatement mappedStatement, Object[] param) throws SQLException, NoSuchFieldException,IllegalAccessException, InstantiationException, IntrospectionException,InvocationTargetException {
        //获取连接
        connection = configuration.getDataSource().getConnection();
        // select * from user where id = #{id} and username = #{username} String sql =
        mappedStatement.getSql();
        //对sql进⾏处理
        BoundSql boundsql = getBoundSql(sql);
        // select * from where id = ? and username = ?
        String finalSql = boundsql.getSqlText();
        //获取传⼊参数类型
        Class<?> paramterType = mappedStatement.getParamterType();
        //获取预编译preparedStatement对象
        PreparedStatement preparedStatement = connection.prepareStatement(finalSql);
        List<ParameterMapping> parameterMappingList = boundsql.getParameterMappingList();
        for (int i = 0; i < parameterMappingList.size(); i++) {
            ParameterMapping parameterMapping = parameterMappingList.get(i);
            String name = parameterMapping.getName();
            //反射
            Field declaredField = paramterType.getDeclaredField(name);
            declaredField.setAccessible(true);
            //参数的值
            Object o = declaredField.get(param[0]);
            //给占位符赋值
            preparedStatement.setObject(i + 1, o);
        }
        ResultSet resultSet = preparedStatement.executeQuery();
        Class<?> resultType = mappedStatement.getResultType();
        ArrayList<E> results = new ArrayList<E>();
        while (resultSet.next()) {
            ResultSetMetaData metaData = resultSet.getMetaData();
            (E) resultType.newInstance();
            int columnCount = metaData.getColumnCount();
            for (int i = 1; i <= columnCount; i++) {
                //属性名
                String columnName = metaData.getColumnName(i);
                //属性值
                Object value = resultSet.getObject(columnName);
                //创建属性描述器,为属性⽣成读写⽅法
                PropertyDescriptor propertyDescriptor = new PropertyDescriptor(columnName, resultType);
                //获取写⽅法
                Method writeMethod = propertyDescriptor.getWriteMethod();
                //向类中写⼊值
                writeMethod.invoke(o, value);
            }
            results.add(o);
        }
        return results;
    }
    @Override
    public void close() throws SQLException {
        connection.close();
    }
    private BoundSql getBoundSql(String sql) {
        //标记处理类:主要是配合通⽤标记解析器GenericTokenParser类完成对配置⽂件等的解 析⼯作,其中TokenHandler主要完成处理
        ParameterMappingTokenHandler parameterMappingTokenHandler = new ParameterMappingTokenHandler();
        //GenericTokenParser :通⽤的标记解析器,完成了代码⽚段中的占位符的解析,然后再根 据给定的标记处理器(TokenHandler)来进⾏表达式的处理
        //三个参数:分别为openToken (开始标记)、closeToken (结束标记)、handler (标记处 理器)
        GenericTokenParser genericTokenParser = new GenericTokenParser("# {", "}", parameterMappingTokenHandler);
        String parse = genericTokenParser.parse(sql);
        List<ParameterMapping> parameterMappings = parameterMappingTokenHandler.getParameterMappings();
        BoundSql boundSql = new BoundSql(parse, parameterMappings);
        return boundSql;
    }
}

BoundSql

public class BoundSql {
    //解析过后的sql语句
    private String sqlText;
    //解析出来的参数
    private List<ParameterMapping> parameterMappingList = new ArrayList<ParameterMapping>();
    public BoundSql(String sqlText, List<ParameterMapping> parameterMappingList) {
        this.sqlText = sqlText;
        this.parameterMappingList = parameterMappingList;
    }
    public String getSqlText() {
        return sqlText;
    }
    public void setSqlText(String sqlText) {
        this.sqlText = sqlText;
    }
    public List<ParameterMapping> getParameterMappingList() {
        return parameterMappingList;
    }
    public void setParameterMappingList(List<ParameterMapping> parameterMappingList) {
        this.parameterMappingList = parameterMappingList;
    }
}

1.5 自定义框架优化

通过上述我们的自定义框架,我们解决了JDBC操作数据库带来的一些问题:例如频繁创建释放数据库连 接,硬编

码,手动封装返回结果集等问题,但是现在我们继续来分析刚刚完成的自定义框架代码,有没 有什么问题?

问题如下:

  • dao的实现类中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调⽤sqlsession⽅ 法,关闭 sqlsession)
  • dao的实现类中存在硬编码,调⽤sqlsession的⽅法时,参数statement的id硬编码

解决:使⽤代理模式来创建接⼝的代理对象

@Test
public void test2() throws Exception {
    InputStream resourceAsSteam = Resources.getResourceAsSteam("sqlMapConfig.xml")
    SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsSteam);
    SqlSession sqlSession = build.openSession();
    User user = new User();
    user.setld(l);
    user.setUsername("tom");
    //代理对象
    UserMapper userMapper = sqlSession.getMappper(UserMapper.class);
    User userl = userMapper.selectOne(user);
    System・out.println(userl);
}

在sqlSession中添加⽅法

public interface SqlSession {
 	public <T> T getMappper(Class<?> mapperClass);
}

实现类

@Override
public <T> T getMappper(Class<?> mapperClass) {
    T o = (T) Proxy.newProxyInstance(mapperClass.getClassLoader(), new Class[] {mapperClass}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // selectOne
            String methodName = method.getName();
            // className:namespace
            String className = method.getDeclaringClass().getName();
            //statementid
            String key = className+"."+methodName;
            MappedStatement mappedStatement = configuration.getMappedStatementMap().get(key);
            Type genericReturnType = method.getGenericReturnType();
            ArrayList arrayList = new ArrayList<> ();
            //判断是否实现泛型类型参数化
            if(genericReturnType instanceof ParameterizedType){
                return selectList(key,args);
                return selectOne(key,args);
            }
        });
        return o;
    }
}

第二部分:Mybatis相关概念

2.1 对象/关系数据库映射(ORM)

ORM全称Object/Relation Mapping:表示对象-关系映射的缩写

ORM完成面向对象的编程语言到关系数据库的映射。当ORM框架完成映射后,程序员既可以利用面向 对象程序设
计语言的简单易用性,又可以利用关系数据库的技术优势。ORM把关系数据库包装成面向对 象的模型。ORM框架
是面向对象设计语言与关系数据库发展不同步时的中间解决方案。采用ORM框架 后,应用程序不再直接访问底层
数据库,而是以面向对象的方式来操作持久化对象,而ORM框架则将这 些面向对象的操作转换成底层SQL操作。
ORM框架实现的效果:把对持久化对象的保存、修改、删除 等操作,转换为对数据库的操作

2.2 Mybatis简介

MyBatis是一款优秀的基于ORM的半自动轻量级持久层框架,它支持定制化SQL、存储过程以及高级映 射。
MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以使用简单的 XML或注解来配置
和映射原生类型、接口和Java的POJO (Plain Old Java Objects,普通老式Java对 象)为数据库中的记录。

2.3 Mybatis历史

原是apache的一个开源项目iBatis, 2010年 6 月这个项目由apache software foundation 迁移到了google code,随
着开发团队转投Google Code旗下,ibatis3.x正式更名为Mybatis ,代码于 2013 年 11 月迁移到Github。

iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框架包括SQL
Maps和Data Access Objects(DAO)

2.4 Mybatis优势

Mybatis是一个半自动化的持久层框架,对开发人员开说,核心sql还是需要自己进行优化,sql和java编码进行分
离,功能边界清晰,一个专注业务,一个专注数据。

分析图示如下:

image-20220526093241429

第三部分:Mybatis基本应用

3.1 快速入⻔

MyBatis官网地址:http://www.mybatis.org/mybatis-3/

3.1.1 开发步骤:

①添加MyBatis的坐标

②创建user数据表

③编写User实体类

④编写映射文件UserMapper.xml

⑤编写核心文件SqlMapConfig.xml

⑥编写测试类

3.1.2环境搭建:

1)导入MyBatis的坐标和其他相关坐标

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
<!--mybatis坐标-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.5</version>
</dependency>
<!--mysql驱动坐标-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.6</version>
    <scope>runtime</scope>
</dependency>
<!--单元测试坐标-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!--日志坐标-->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.12</version>
</dependency>

2)创建user数据表

image-20220530150628220

3)编写User实体

public class User {
    private int id;
    private String username;
    private String password;
    //省略get个set⽅法
}

4)编写UserMapper映射文件

<?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="userMapper">
    <select id="findAll" resultType="com.lagou.domain.User">
        select * from User
    </select>
</mapper>
  1. 编写MyBatis核心文件
<!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.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql:///test"/>
                <property name="username" value="root"/>
                <property name="password" alue="root"/>
            </dataSource> 
        </environment>
    </environments>

    <mappers>
        <mapper resource="com/lagou/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

6) 编写测试代码

//加载核⼼配置⽂件
InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
//获得sqlSession⼯⼚对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
//获得sqlSession对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//执⾏sql语句
List<User> userList = sqlSession.selectList("userMapper.findAll");
//打印结果
System.out.println(userList);
//释放资源
sqlSession.close();

3.1.3 MyBatis的增删改查操作

MyBatis的插入数据操作

1)编写UserMapper映射文件

<mapper namespace="userMapper">
    <insert id="add" parameterType="com.lagou.domain.User">
        insert into user values(#{id},#{username},#{password})
    </insert>
</mapper>

2)编写插入实体User的代码

InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
int insert = sqlSession.insert("userMapper.add", user);
System.out.println(insert);
//提交事务
sqlSession.commit();
sqlSession.close();

3)插⼊操作注意问题

  • 插⼊语句使⽤insert标签
  • 在映射⽂件中使⽤parameterType属性指定要插⼊的数据类型
  • Sql语句中使⽤#{实体属性名}⽅式引⽤实体中的属性值
  • 插⼊操作使⽤的API是sqlSession.insert(“命名空间.id”,实体对象);
  • 插⼊操作涉及数据库数据变化,所以要使⽤sqlSession对象显示的提交事务,即sqlSession.commit()

3.1.4 MyBatis的修改数据操作

1)编写UserMapper映射文件

<mapper namespace="userMapper">
    <update id="update" parameterType="com.lagou.domain.User">
        update user set username=#{username},password=#{password} where id=#{id}
    </update>
</mapper>

2)编写修改实体User的代码

InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
int update = sqlSession.update("userMapper.update", user);
System.out.println(update);
sqlSession.commit();
sqlSession.close();

3)修改操作注意问题

  • 修改语句使用update标签
  • 修改操作使用的API是sqlSession.update(“命名空间.id”,实体对象)

3.1.5 MyBatis的删除数据操作

1)编写UserMapper映射文件

<mapper namespace="userMapper">
    <delete id="delete" parameterType="java.lang.Integer">
        delete from user where id=#{id}
    </delete>
</mapper>

2)编写删除数据的代码

InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
int delete = sqlSession.delete("userMapper.delete",3);
System.out.println(delete);
sqlSession.commit();
sqlSession.close();

3)删除操作注意问题

  • 删除语句使用delete标签

  • Sql语句中使用#{任意字符串}方式引用传递的单个参数

  • 删除操作使用的API是sqlSession.delete(“命名空间.id”,Object);

3.1.5 MyBatis的映射文件概述

image-20220530150540587

3.1.6 入⻔核心配置文件分析:

MyBatis核心配置文件层级关系

image-20220530150714724

MyBatis常用配置解析

1)environments标签

数据库环境的配置,支持多环境配置

image-20220530150752223

其中,事务管理器(transactionManager)类型有两种:

  • JDBC:这个配置就是直接使用了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。
  • MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期
    (比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将
    closeConnection 属性设置为 false 来阻止它默认的关闭行为。

其中,数据源(dataSource)类型有三种:

  • UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
  • POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
  • JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,
    然后放置一个 JNDI 上下文的引用。

2)mapper标签

该标签的作用是加载映射的,加载方式有如下几种:

•使⽤相对于类路径的资源引⽤,例如:
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
•使⽤完全限定资源定位符(URL),例如:
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
•使⽤映射器接⼝实现类的完全限定类名,例如:
<mapper class="org.mybatis.builder.AuthorMapper"/>
•将包内的映射器接⼝实现全部注册为映射器,例如:
<package name="org.mybatis.builder"/>

3.1.7 Mybatis相应API介绍

SqlSession工厂构建器SqlSessionFactoryBuilder

常用API:SqlSessionFactory build(InputStream inputStream)

通过加载mybatis的核心文件的输入流的形式构建一个SqlSessionFactory对象

String resource = "org/mybatis/builder/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(inputStream);

其中, Resources 工具类,这个类在 org.apache.ibatis.io 包中。Resources 类帮助你从类路径下、文件系统或一
个 web URL 中加载资源文件。

SqlSession工厂对象SqlSessionFactory

SqlSessionFactory 有多个个方法创建SqlSession 实例。常用的有如下两个:

image-20220530150938765

SqlSession会话对象

SqlSession 实例在 MyBatis 中是⾮常强⼤的⼀个类。在这⾥你会看到所有执⾏语句、提交或回滚事务和获取映射 器实例的⽅法。

执⾏语句的⽅法主要有:

<T> T selectOne(String statement, Object parameter)
<E> List<E> selectList(String statement, Object parameter)
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)

操作事务的⽅法主要有:

void commit()
void rollback()

3.2 Mybatis的Dao层实现

3.2.1 传统开发方式

编写UserDao接口

public interface UserDao {
    List<User> findAll() throws IOException;
}

编写UserDaoImpl实现

public class UserDaoImpl implements UserDao {
    public List<User> findAll() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        List<User> userList = sqlSession.selectList("userMapper.findAll");
        sqlSession.close();
        return userList;
    }
}

测试传统⽅式

@Test
public void testTraditionDao() throws IOException {
    UserDao userDao = new UserDaoImpl();
    List<User> all = userDao.findAll();
    System.out.println(all);
}

3.2.2 代理开发方式

代理开发方式介绍

采用 Mybatis 的代理开发方式实现 DAO 层的开发,这种方式是我们后面进入企业的主流。

Mapper 接口开发方法只需要程序员编写Mapper 接口(相当于Dao 接口),由Mybatis 框架根据接口定义创建接
口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。

Mapper 接口开发需要遵循以下规范:

1) Mapper.xml文件中的namespace与mapper接口的全限定名相同

2) Mapper接口方法名和Mapper.xml中定义的每个statement的id相同

3) Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同

4) Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同

编写UserMapper接口

image-20220530151257220

测试代理⽅式

@Test
public void testProxyDao() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //获得MyBatis框架⽣成的UserMapper接⼝的实现类
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    User user = userMapper.findById(1);
    System.out.println(user);
    sqlSession.close();
}

第四部分:Mybatis配置文件深入

4.1 核心配置文件SqlMapConfig.xml

4.1.1 MyBatis核心配置文件层级关系

image-20220530151351537

4.2 MyBatis常用配置解析

1)environments标签

数据库环境的配置,支持多环境配置

image-20220530151446078

其中,事务管理器(transactionManager)类型有两种:

  • JDBC:这个配置就是直接使用了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作用域。
  • MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生命周期
    (比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因此需要将
    closeConnection 属性设置为 false 来阻止它默认的关闭行为。

其中,数据源(dataSource)类型有三种:

  • UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
  • POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
  • JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,
    然后放置一个 JNDI 上下文的引用。

2)mapper标签

该标签的作用是加载映射的,加载方式有如下几种:

•使⽤相对于类路径的资源引⽤,例如:
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
•使⽤完全限定资源定位符(URL),例如:
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
•使⽤映射器接⼝实现类的完全限定类名,例如:
<mapper class="org.mybatis.builder.AuthorMapper"/>
•将包内的映射器接⼝实现全部注册为映射器,例如:
<package name="org.mybatis.builder"/>

3)Properties标签

实际开发中,习惯将数据源的配置信息单独抽取成一个properties文件,该标签可以加载额外配置的properties文

image-20220530151554386

4)typeAliases标签

类型别名是为Java 类型设置⼀个短的名字。原来的类型名称配置如下

image-20220530151615554字。原来的类型名

配置typeAliases,为com.lagou.domain.User定义别名为user

image-20220530151657907

上面我们是自定义的别名,mybatis框架已经为我们设置好的一些常用的类型的别名

image-20220530151716011

4.3 映射配置文件mapper.xml

动态sql语句

动态sql语句概述

Mybatis 的映射文件中,前面我们的 SQL 都是比较简单的,有些时候业务逻辑复杂时,我们的 SQL是动态变化
的,此时在前面的学习中我们的 SQL 就不能满足要求了。

参考的官方文档,描述如下:

image-20220530151743261

4.4 动态 SQL 之 if

我们根据实体类的不同取值,使用不同的 SQL语句来进行查询。比如在 id如果不为空时可以根据id查询,如果
username 不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。

<select id="findByCondition" parameterType="user" resultType="user">
    select * from User
    <where>
        <if test="id!=0">
            and id=#{id}
        </if>
        <if test="username!=null">
            and username=#{username}
        </if>
    </where>
</select>

当查询条件id和username都存在时,控制台打印的sql语句如下:

//获得MyBatis框架⽣成的UserMapper接⼝的实现类
 UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
 User condition = new User();
 condition.setId(1);
 condition.setUsername("lucy");
 User user = userMapper.findByCondition(condition);
image-20220530151924628

当查询条件只有id存在时,控制台打印的sql语句如下:

//获得MyBatis框架生成的UserMapper接口的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User condition = new User();
condition.setId(1);
User user = userMapper.findByCondition(condition);
image-20220530152010945

4.5 动态 SQL 之where

循环执行sql的拼接操作,例如:SELECT * FROM USER WHERE id IN (1,2,5)。

<select id="findByIds" parameterType="list" resultType="user">
    select * from User
    <where>
        <foreach collection="list" open="id in(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

测试代码片段如下:

//获得MyBatis框架⽣成的UserMapper接⼝的实现类
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int[] ids = new int[]{2,5};
List<User> userList = userMapper.findByIds(ids);
System.out.println(userList);
image-20220530152223972

foreach标签的属性含义如下:

标签用于遍历集合,它的属性:

  • collection:代表要遍历的集合元素,注意编写时不要写#{}
  • open:代表语句的开始部分
  • close:代表结束部分
  • item:代表遍历集合的每个元素,生成的变量名
  • sperator:代表分隔符

SQL片段抽取

Sql 中可将重复的 sql 提取出来,使用时用 include 引用即可,最终达到 sql 重用的目的

<!--抽取sql⽚段简化编写-->
<sql id="selectUser"> select * from User </sql>

<select id="findById" parameterType="int" resultType="user">
    <include refid="selectUser"></include> where id=#{id}
</select>
<select id="findByIds" parameterType="list" resultType="user">
    <include refid="selectUser"></include>
    <where>
        <foreach collection="array" open="id in(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </where>
</select>

第五部分:Mybatis复杂映射开发

5.1 一对一查询

5.1.1 一对一查询的模型

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户

image-20220530152422265

5.1.2一对一查询的语句

对应的sql语句:select * from orders o,user u where o.uid=u.id;

查询的结果如下:

image-20220530152735839

5.1.3 创建Order和User实体

public class Order {
    private int id;
    private Date ordertime;
    private double total;
    //代表当前订单从属于哪⼀个客户
    private User user;
}
public class User {

    private int id;
    private String username;
    private String password;
    private Date birthday;
}

5.1.4 创建OrderMapper接口

public interface OrderMapper {
    List<Order> findAll();
}

5.1.5 配置OrderMapper.xml

<mapper namespace="com.lagou.mapper.OrderMapper">
    <resultMap id="orderMap" type="com.lagou.domain.Order">
        <result column="uid" property="user.id"></result>
        <result column="username" property="user.username"></result>
        <result column="password" property="user.password"></result>
        <result column="birthday" property="user.birthday"></result>
    </resultMap>
    <select id="findAll" resultMap="orderMap">
        select * from orders o,user u where o.uid=u.id
    </select>
</mapper>

其中还可以配置如下:

<resultMap id="orderMap" type="com.lagou.domain.Order">
    <result property="id" column="id"></result>
    <result property="ordertime" column="ordertime"></result>
    <result property="total" column="total"></result>
    <association property="user" javaType="com.lagou.domain.User">
        <result column="uid" property="id"></result>
        <result column="username" property="username"></result>
        <result column="password" property="password"></result>
        <result column="birthday" property="birthday"></result>
    </association>
</resultMap>

5.1.6 测试结果

OrderMapper mapper = sqlSession.getMapper(OrderMapper.class);
List<Order> all = mapper.findAll();
for(Order order : all){
    System.out.println(order);
}
image-20220530152958576

5.2 一对多查询

5.2.1 一对多查询的模型

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单

image-20220530153034464

5.2.2 一对多查询的语句

对应的sql语句:select *,o.id oid from user u left join orders o on u.id=o.uid;

查询的结果如下:

image-20220530153112184

5.2.3 修改User实体

public class Order {
    private int id;
    private Date ordertime;
    private double total;
    //代表当前订单从属于哪⼀个客户
    private User user;
}
public class User {

    private int id;
    private String username;
    private String password;
    private Date birthday;
    //代表当前⽤户具备哪些订单
    private List<Order> orderList;
}

5.2.4 创建UserMapper接口

public interface UserMapper {
 List<User> findAll();
}

5.2.5 配置UserMapper.xml

<mapper namespace="com.lagou.mapper.UserMapper">
    <resultMap id="userMap" type="com.lagou.domain.User">
        <result column="id" property="id"></result>
        <result column="username" property="username"></result>
        <result column="password" property="password"></result>
        <result column="birthday" property="birthday"></result>
        <collection property="orderList" ofType="com.lagou.domain.Order">
            <result column="oid" property="id"></result>
            <result column="ordertime" property="ordertime"></result>
            <result column="total" property="total"></result>
        </collection>
    </resultMap>
    <select id="findAll" resultMap="userMap">
        select *,o.id oid from user u left join orders o on u.id=o.uid
    </select>
</mapper>

5.2.6 测试结果

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAll();
for(User user : all){
    System.out.println(user.getUsername());
    List<Order> orderList = user.getOrderList();
    for(Order order : orderList){
        System.out.println(order);
    }
    System.out.println("----------------------------------");
}
image-20220530153328527

5.3 多对多查询

5.3.1 多对多查询的模型

用户表和⻆色表的关系为,一个用户有多个⻆色,一个⻆色被多个用户使用

多对多查询的需求:查询用户同时查询出该用户的所有⻆色

image-20220530153401076

5.3.2 多对多查询的语句

对应的sql语句:select u. ,r. ,r.id rid from user u left join user_role ur on u.id=ur.user_id

inner join role r on ur.role_id=r.id;

查询的结果如下:

image-20220530153418775

5.3.3 创建Role实体,修改User实体

public class User {
    private int id;
    private String username;
    private String password;
    private Date birthday;
    //代表当前⽤户具备哪些订单
    private List<Order> orderList;
    //代表当前⽤户具备哪些⻆⾊
    private List<Role> roleList;
}
public class Role {
    private int id;
    private String rolename;
}

5.3.4 添加UserMapper接口方法

List<User> findAllUserAndRole();

5.3.5 配置UserMapper.xml

<resultMap id="userRoleMap" type="com.lagou.domain.User">
    <result column="id" property="id"></result>
    <result column="username" property="username"></result>
    <result column="password" property="password"></result>
    <result column="birthday" property="birthday"></result>
    <collection property="roleList" ofType="com.lagou.domain.Role">
        <result column="rid" property="id"></result>
        <result column="rolename" property="rolename"></result>
    </collection>
</resultMap>
<select id="findAllUserAndRole" resultMap="userRoleMap">
    select u.*,r.*,r.id rid from user u left join user_role ur on u.id=ur.user_id
    inner join role r on ur.role_id=r.id
</select>

5.3.6 测试结果

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAllUserAndRole();
for(User user : all){
    System.out.println(user.getUsername());
    List<Role> roleList = user.getRoleList();
    for(Role role : roleList){
        System.out.println(role);
    }
    System.out.println("----------------------------------");
}
image-20220530153601449

5.4 知识小结

MyBatis多表配置方式:

一对一配置:使用做配置

一对多配置:使用+做配置

多对多配置:使用+做配置

第六部分:Mybatis注解开发

6.1 MyBatis的常用注解

这几年来注解开发越来越流行,Mybatis也可以使用注解开发方式,这样我们就可以减少编写Mapper

映射文件了。我们先围绕一些基本的CRUD来学习,再学习复杂映射多表操作。

@Insert:实现新增

@Update:实现更新

@Delete:实现删除

@Select:实现查询

@Result:实现结果集封装

@Results:可以与@Result 一起使用,封装多个结果集

@One:实现一对一结果集封装

@Many:实现一对多结果集封装

6.2 MyBatis的增删改查

我们完成简单的user表的增删改查的操作

private UserMapper userMapper;
@Before
public void before() throws IOException {
    InputStream resourceAsStream = Resources.getResourceAsStream("SqlMapConfig.xml");
    SqlSessionFactory sqlSessionFactory = new
        SqlSessionFactoryBuilder().build(resourceAsStream);
    SqlSession sqlSession = sqlSessionFactory.openSession(true);
    userMapper = sqlSession.getMapper(UserMapper.class);
}
@Test
public void testAdd() {
    User user = new User();
    user.setUsername("测试数据");
    user.setPassword("123");
    user.setBirthday(new Date());
    userMapper.add(user);
}
@Test
public void testUpdate() throws IOException {
    User user = new User();
    user.setId(16);
    user.setUsername("测试数据修改");
    user.setPassword("abc");
    user.setBirthday(new Date());
    userMapper.update(user);
}
@Test
public void testDelete() throws IOException {
    userMapper.delete(16);
}
@Test
public void testFindById() throws IOException {
    User user = userMapper.findById(1);
    System.out.println(user);
}
@Test
public void testFindAll() throws IOException {
    List<User> all = userMapper.findAll();
    for(User user : all){
        System.out.println(user);
    }
}

修改MyBatis的核⼼配置⽂件,我们使⽤了注解替代的映射⽂件,所以我们只需要加载使⽤了注解的Mapper接⼝ 即可

<mappers>
    <!--扫描使⽤注解的类-->
    <mapper class="com.lagou.mapper.UserMapper"></mapper>
</mappers>

或者指定扫描包含映射关系的接口所在的包也可以

<mappers>
    <!--扫描使⽤注解的类所在的包-->
    <package name="com.lagou.mapper"></package>
</mappers>

6.3 MyBatis的注解实现复杂映射开发

实现复杂关系映射之前我们可以在映射文件中通过配置来实现,使用注解开发后,我们可以使用@Results注解,
@Result注解,@One注解,@Many注解组合完成复杂关系的配置

image-20220530153746592

6.4 一对一查询

6.4.1 一对一查询的模型

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对一查询的需求:查询一个订单,与此同时查询出该订单所属的用户

image-20220530153808291

6.4.2 一对一查询的语句

对应的sql语句:

select * from orders;
select * from user where id=查询出订单的uid;

查询的结果如下:

image-20220530153915687

6.4.3 创建Order和User实体

public class Order {
    private int id;
    private Date ordertime;
    private double total;
    //代表当前订单从属于哪⼀个客户
    private User user;
}
public class User {

    private int id;
    private String username;
    private String password;
    private Date birthday;
}

6.4.4 创建OrderMapper接口

public interface OrderMapper {
    List<Order> findAll();
}

6.4.5 使用注解配置Mapper

public interface OrderMapper {
    @Select("select * from orders")
    @Results({
        @Result(id=true,property = "id",column = "id"),
        @Result(property = "ordertime",column = "ordertime"),
        @Result(property = "total",column = "total"),
        @Result(property = "user",column = "uid",
                javaType = User.class,
                one = @One(select = "com.lagou.mapper.UserMapper.findById"))
    })
    List<Order> findAll();
}
public interface UserMapper {
    @Select("select * from user where id=#{id}")
    User findById(int id);

}

6.4.6 测试结果

@Test
public void testSelectOrderAndUser() {
    List<Order> all = orderMapper.findAll();
    for(Order order : all){
        System.out.println(order);
    }
}
image-20220530154351955

6.5 一对多查询

6.5.1 一对多查询的模型

用户表和订单表的关系为,一个用户有多个订单,一个订单只从属于一个用户

一对多查询的需求:查询一个用户,与此同时查询出该用户具有的订单

image-20220530154420771

6.5.2 一对多查询的语句

对应的sql语句:

select * from user;
select * from orders where uid=查询出⽤户的id;

查询的结果如下:

image-20220530154505641

6.5.3 修改User实体

public class Order {
    private int id;
    private Date ordertime;
    private double total;
    //代表当前订单从属于哪⼀个客户
    private User user;
}
public class User {

    private int id;
    private String username;
    private String password;
    private Date birthday;
    //代表当前⽤户具备哪些订单
    private List<Order> orderList;
}

6.5.4 创建UserMapper接口

List<User> findAllUserAndOrder();

6.5.5 使用注解配置Mapper

public interface UserMapper {
    @Select("select * from user")
    @Results({
        @Result(id = true,property = "id",column = "id"),
        @Result(property = "username",column = "username"),
        @Result(property = "password",column = "password"),
        @Result(property = "birthday",column = "birthday"),
        @Result(property = "orderList",column = "id",
                javaType = List.class,
                many = @Many(select = "com.lagou.mapper.OrderMapper.findByUid"))
    })
    List<User> findAllUserAndOrder();
}
public interface OrderMapper {
    @Select("select * from orders where uid=#{uid}")
    List<Order> findByUid(int uid);
}

6.5.6 测试结果

List<User> all = userMapper.findAllUserAndOrder();
for(User user : all){
    System.out.println(user.getUsername());
    List<Order> orderList = user.getOrderList();
    for(Order order : orderList){
        System.out.println(order);
    }
    System.out.println("-----------------------------");
}
image-20220530154640261

6.6 多对多查询

6.6.1 多对多查询的模型

用户表和⻆色表的关系为,一个用户有多个⻆色,一个⻆色被多个用户使用

多对多查询的需求:查询用户同时查询出该用户的所有⻆色

image-20220530154715480

6.6.2 多对多查询的语句

对应的sql语句:

select * from user;
select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=⽤户的id

查询的结果如下:

image-20220530154807584

6.6.3 创建Role实体,修改User实体

public class User {
    private int id;
    private String username;
    private String password;
    private Date birthday;
    //代表当前⽤户具备哪些订单
    private List<Order> orderList;
    //代表当前⽤户具备哪些⻆⾊
    private List<Role> roleList;
}
public class Role {
    private int id;
    private String rolename;
}

6.6.4 添加UserMapper接口方法

List<User> findAllUserAndRole();

6.6.5 使用注解配置Mapper

public interface UserMapper {
    @Select("select * from user")
    @Results({
        @Result(id = true,property = "id",column = "id"),
        @Result(property = "username",column = "username"),
        @Result(property = "password",column = "password"),
        @Result(property = "birthday",column = "birthday"),
        @Result(property = "roleList",column = "id",
                javaType = List.class,
                many = @Many(select = "com.lagou.mapper.RoleMapper.findByUid"))
    })
    List<User> findAllUserAndRole();
}
public interface RoleMapper {
    @Select("select * from role r,user_role ur where r.id=ur.role_id and ur.user_id=#{uid}")
    List<Role> findByUid(int uid);
}

6.6.6 测试结果

UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> all = mapper.findAllUserAndRole();
for(User user : all){
    System.out.println(user.getUsername());
    List<Role> roleList = user.getRoleList();
    for(Role role : roleList){
        System.out.println(role);
    }
    System.out.println("----------------------------------");
}
image-20220530155021845

第七部分:Mybatis缓存

7.1 一级缓存

①、在一个sqlSession中,对User表根据id进行两次查询,查看他们发出sql语句的情况

@Test
public void test1(){
    //根据 sqlSessionFactory 产生 session
    SqlSession sqlSession = sessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    //第一次查询,发出sql语句,并将查询出来的结果放进缓存中
    User u1 = userMapper.selectUserByUserId( 1 );
    System.out.println(u1);
    //第二次查询,由于是同一个sqlSession,会在缓存中查询结果
    //如果有,则直接从缓存中取出来,不和数据库进行交互
    User u2 = userMapper.selectUserByUserId( 1 );
    System.out.println(u2);
    sqlSession.close();
}

查看控制台打印情况:

image-20220530155111116

② 、同样是对user表进行两次查询,只不过两次查询之间进行了一次update操作。

@Test
public void test2(){
    //根据 sqlSessionFactory 产⽣ session
    SqlSession sqlSession = sessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
    User u1 = userMapper.selectUserByUserId( 1 );
    System.out.println(u1);
    //第⼆步进⾏了⼀次更新操作,sqlSession.commit()
    u1.setSex("⼥");
    userMapper.updateUserByUserId(u1);
    sqlSession.commit();
    //第⼆次查询,由于是同⼀个sqlSession.commit(),会清空缓存信息
    //则此次查询也会发出sql语句
    User u2 = userMapper.selectUserByUserId(1);
    System.out.println(u2);
    sqlSession.close();
}

查看控制台打印情况:

image-20220530155147108

③、 总结

1 、第一次发起查询用户id为 1 的用户信息,先去找缓存中是否有id为 1 的用户信息,如果没有,从 数据库查询用户
信息。得到用户信息,将用户信息存储到一级缓存中。

2 、 如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的 一级缓存,这
样做的目的为了让缓存中存储的是最新的信息,避免脏读。

3 、 第二次发起查询用户id为 1 的用户信息,先去找缓存中是否有id为 1 的用户信息,缓存中有,直 接从缓存中获取
用户信息

image-20220530155208004

一级缓存原理探究与源码分析

一级缓存到底是什么?一级缓存什么时候被创建、一级缓存的工作流程是怎样的?相信你现在应该会有 这几个疑

问,那么我们本节就来研究一下一级缓存的本质

大家可以这样想,上面我们一直提到一级缓存,那么提到一级缓存就绕不开SqlSession,所以索性我们 就直接从
SqlSession,看看有没有创建缓存或者与缓存有关的属性或者方法

image-20220530155241460

调研了一圈,发现上述所有方法中,好像只有clearCache()和缓存沾点关系,那么就直接从这个方 法入手吧,分析
源码时 ,我们要看它(此类)是谁,它的父类和子类分别又是谁 ,对如上关系了解了,你才 会对这个类有更深的认
识,分析了一圈,你可能会得到如下这个流程图

image-20220530155257273

再深入分析,流程走到 Perpetualcache 中的clear()方法之后,会调用其 cache.clear() 方法,那 么这个cache是什
么东⻄呢?点进去发现,cache其实就是private Map cache = new

HashMap();也就是一个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是 本地存放的
一个map对象,每一个SqISession都会存放一个map对象的引用,那么这个cache是何 时创建的呢?

你觉得最有可能创建缓存的地方是哪里呢?我觉得是 Executor ,为什么这么认为?因为Executor是 执行器,用来
执行SQL请求,而且清除缓存的方法也在Executor中执行,所以很可能缓存的创建也很 有可能在Executor中,看了
一圈发现Executor中有一个createCacheKey方法,这个方法很像是创 建缓存的方法啊,跟进去看看,你发现
createCacheKey方法是由BaseExecutor执行的,代码如下

CacheKey cacheKey = new CacheKey();
//MappedStatement 的 id
// id就是Sql语句的所在位置包名+类名+ SQL名称
cacheKey.update(ms.getId());
// offset 就是 0
cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
cacheKey.update(rowBounds.getLimit());
//具体的SQL语句
cacheKey.update(boundSql.getSql());
//后面是update 了 sql中带的参数
...
cacheKey.update(value);
if (configuration.getEnvironment() != null) {
    // issue #176
    cacheKey.update(configuration.getEnvironment().getId());
}

创建缓存key会经过一系列的update方法,udate方法由一个CacheKey这个对象来执行的,这个 update方法最终
由updateList的list来把五个值存进去,对照上面的代码和下面的图示,你应该能 理解这五个值都是什么了

image-20220530155347399

这里需要注意一下最后一个值,configuration.getEnvironment().getId()这是什么,这其实就是 定义在mybatis-
config.xml中的标签,⻅如下。

<environments default="development">
    <environment id="development">
        <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>

那么我们回归正题,那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会 的,经过我们对一级缓存的探究之后,我们发现一级缓存更多是用 于查询操作,毕竟一级缓存也叫做查 询缓存吧,为什么叫查询缓存我们一会儿说 。我们先来看一下这个缓存到底用在哪了,我们跟踪到 query方法如下:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    //创建缓存
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@SuppressWarnings("unchecked")
Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    	...
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list
                                                                                       != null) {
        //这个主要是处理存储过程⽤的。
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
    ...
}
// queryFromDatabase ⽅法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。 localcache对象的put方
法最终交给Map进行存放

private Map<Object, Object> cache = new HashMap<Object, Object>();
@Override
public void putObject(Object key, Object value) { 
    cache.put(key, value);
}

7.2 二级缓存

二级缓存的原理和一级缓存原理一样,第一次查询,会将数据放入缓存中,然后第二次查询则会直接去 缓存中取。但是一级缓存是基于sqlSession的,而二级缓存是基于mapper文件的namespace的,也 就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域 中

image-20220530161725002

如何使用二级缓存

① 、 开启二级缓存

和一级缓存默认开启不一样,二级缓存需要我们手动开启

首先在全局配置文件sqlMapConfig.xml文件中加入如下代码:

<!--开启⼆级缓存-->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

其次在UserMapper.xml文件中开启缓存

<!--开启⼆级缓存-->
<cache></cache>

我们可以看到mapper.xml文件中就这么一个空标签,其实这里可以配置,PerpetualCache这个类是 mybatis默认实
现缓存功能的类。我们不写type就使用mybatis默认的缓存,也可以去实现Cache接口 来自定义缓存。

image-20220530161850366
public class PerpetualCache implements Cache {
    private final String id;
    private MapcObject, Object> cache = new HashMapC);
    public PerpetualCache(St ring id) { this.id = id;
}

我们可以看到二级缓存底层还是HashMap结构

public class User implements Serializable(
    //⽤户ID
    private int id;
    //⽤户姓名
    private String username;
    //⽤户性别
    private String sex;
}

开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操 作,因为
二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取 这个缓存的话,就需
要反序列化了。所以mybatis中的pojo都去实现Serializable接口

③、测试

一、测试二级缓存和sqlSession无关

@Test
public void testTwoCache(){
    //根据 sqlSessionFactory 产⽣ session
    SqlSession sqlSession1 = sessionFactory.openSession();
    SqlSession sqlSession2 = sessionFactory.openSession();

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );
    //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
    User u1 = userMapper1.selectUserByUserId(1);
    System.out.println(u1);
    sqlSession1.close(); //第⼀次查询完后关闭 sqlSession

    //第⼆次查询,即使sqlSession1已经关闭了,这次查询依然不发出sql语句
    User u2 = userMapper2.selectUserByUserId(1);
    System.out.println(u2);
    sqlSession2.close();
}

可以看出上面两个不同的sqlSession,第一个关闭了,第二次查询依然不发出sql查询语句

二、测试执行commit()操作,二级缓存数据清空

@Test
public void testTwoCache(){
    //根据 sqlSessionFactory 产⽣ session
    SqlSession sqlSession1 = sessionFactory.openSession();
    SqlSession sqlSession2 = sessionFactory.openSession();
    SqlSession sqlSession3 = sessionFactory.openSession();
    String statement = "com.lagou.pojo.UserMapper.selectUserByUserld" ;
    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper. class );
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper. class );
    UserMapper userMapper3 = sqlSession2.getMapper(UserMapper. class );
    //第⼀次查询,发出sql语句,并将查询的结果放⼊缓存中
    User u1 = userMapperl.selectUserByUserId( 1 );
    System.out.println(u1);
    sqlSessionl .close(); //第⼀次查询完后关闭sqlSession

    //执⾏更新操作,commit()
    u1.setUsername( "aaa" );
    userMapper3.updateUserByUserId(u1);
    sqlSession3.commit();

    //第⼆次查询,由于上次更新操作,缓存数据已经清空(防⽌数据脏读),这⾥必须再次发出sql语
    User u2 = userMapper2.selectUserByUserId( 1 );
    System.out.println(u2);
    sqlSession2.close();
}

查看控制台情况:

image-20220530162121114

④、useCache和flushCache

mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓 存的,在
statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出 sql去查询,默认情况
是true,即该sql使用二级缓存

<select id="selectUserByUserId" useCache="false" resultType="com.lagou.pojo.User" parameterType="int">
    select * from user where id=#{id}
</select>

这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数 据库中获取。

在mapper的同一个namespace中,如果有其它insert、update, delete操作数据后需要刷新缓 存,如果不执行刷
新缓存会出现脏读。

设置statement配置中的flushCache="true”属性,默认情况下为true,即刷新缓存,如果改成false则 不会刷新。使
用缓存时如果手动修改数据库表中的查询数据会出现脏读。

<select id="selectUserByUserId" flushCache="true" useCache="false" resultType="com.lagou.pojo.User" parameterType="int">
    select * from user where id=#{id}
</select>

一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可

7.3 二级缓存整合redis

上面我们介绍了 mybatis自带的二级缓存,但是这个缓存是单服务器工作,无法实现分布式缓存。 那么什么是分布
式缓存呢?假设现在有两个服务器 1 和2,用户访问的时候访问了 1 服务器,查询后的缓 存就会放在 1 服务器上,假设
现在有个用户访问的是 2 服务器,那么他在 2 服务器上就无法获取刚刚那个 缓存,如下图所示:

image-20220530162251238

为了解决这个问题,就得找一个分布式的缓存,专⻔用来存储缓存数据的,这样不同的服务器要缓存数 据都往它那里存,取缓存数据也从它那里取,如下图所示:

image-20220530162328857

如上图所示,在几个不同的服务器之间,我们使用第三方缓存框架,将缓存都放在这个第三方框架中, 然后无论有多少台服务器,我们都能从缓存中获取数据。

这里我们介绍 mybatisredis 的整合。

刚刚提到过,mybatis提供了一个eache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。

mybati s本身默认实现了一个,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。

redis分布式缓存就可以,mybatis提供了一个针对cache接口的redis实现类,该类存在mybatis-redis包 中

实现:

  1. pom文件

    <dependency>
        <groupId>org.mybatis.caches</groupId>
        <artifactId>mybatis-redis</artifactId>
        <version>1.0.0-beta2</version>
    </dependency>
    
  2. 配置文件:Mapper.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.lagou.mapper.IUserMapper">
        <cache type="org.mybatis.caches.redis.RedisCache" />
        <select id="findAll" resultType="com.lagou.pojo.User" useCache="true">
            select * from user
        </select>
    
  3. redis.properties

    redis.host=localhost
    redis.port=6379
    redis.connectionTimeout=5000
    redis.password=
    redis.database=0
    
  4. 测试

    @Test
    public void SecondLevelCache(){
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        SqlSession sqlSession3 = sqlSessionFactory.openSession();
        IUserMapper mapper1 = sqlSession1.getMapper(IUserMapper.class);
        lUserMapper mapper2 = sqlSession2.getMapper(lUserMapper.class);
        lUserMapper mapper3 = sqlSession3.getMapper(IUserMapper.class);
        User user1 = mapper1.findUserById(1);
        sqlSession1.close(); //清空⼀级缓存
    
        User user = new User();
        user.setId(1);
        user.setUsername("lisi");
        mapper3.updateUser(user);
        sqlSession3.commit();
        User user2 = mapper2.findUserById(1);
        System.out.println(user1==user2);
    }
    

源码分析:

RedisCache和大家普遍实现Mybatis的缓存方案大同小异,无非是实现Cache接口,并使用jedis操作缓存;不过该项目在设计细节上有一些区别;

public final class RedisCache implements Cache {
    public RedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require anID");
        }
        this.id = id;
        RedisConfig redisConfig = RedisConfigurationBuilder.getInstance().parseConfiguration();
        pool = new JedisPool(redisConfig, redisConfig.getHost(),
                             redisConfig.getPort(),
                             redisConfig.getConnectionTimeout(),
                             redisConfig.getSoTimeout(), redisConfig.getPassword(), redisConfig.getDatabase(),
                             redisConfig.getClientName());
    }
}

RedisCache在mybatis启动的时候,由MyBatis的CacheBuilder创建,创建的方式很简单,就是调用 RedisCache的带有String参数的构造方法,即RedisCache(String id);而在RedisCache的构造方法中, 调用了 RedisConfigurationBuilder 来创建 RedisConfig 对象,并使用 RedisConfig 来创建JedisPool。

RedisConfig类继承了 JedisPoolConfig,并提供了 host,port等属性的包装,简单看一下RedisConfig的 属性:

public class RedisConfig extends JedisPoolConfig {
private String host = Protocol.DEFAULT_HOST;
private int port = Protocol.DEFAULT_PORT;
private int connectionTimeout = Protocol.DEFAULT_TIMEOUT;
private int soTimeout = Protocol.DEFAULT_TIMEOUT;
private String password;
private int database = Protocol.DEFAULT_DATABASE;
private String clientName;

RedisConfig对象是由RedisConfigurationBuilder创建的,简单看下这个类的主要方法:

public RedisConfig parseConfiguration(ClassLoader classLoader) {
    Properties config = new Properties();
    InputStream input =classLoader.getResourceAsStream(redisPropertiesFilename);
    if (input != null) {
        try {
            config.load(input);
        } catch (IOException e) {
            throw new RuntimeException(
                "An error occurred while reading classpath property '"
                + redisPropertiesFilename
                + "', see nested exceptions", e);
        } finally {
            try {
                input.close();
            } catch (IOException e) {
                // close quietly
            }
        }
    }
    RedisConfig jedisConfig = new RedisConfig();
    setConfigProperties(config, jedisConfig);
    return jedisConfig;
}

核心的方法就是parseConfiguration方法,该方法从classpath中读取一个redis.properties文件:

host=localhost
port=6379
connectionTimeout=5000
soTimeout=5000
password= 
database=0 
clientName=

并将该配置文件中的内容设置到RedisConfig对象中,并返回;接下来,就是RedisCache使用 RedisConfig类创建完成edisPool;在RedisCache中实现了一个简单的模板方法,用来操作Redis:

private Object execute(RedisCallback callback) {
    Jedis jedis = pool.getResource();
    try {
        return callback.doWithRedis(jedis);
    } finally {
        jedis.close();
    }
}

模板接口为RedisCallback,这个接口中就只需要实现了一个doWithRedis方法而已:

public interface RedisCallback {
    Object doWithRedis(Jedis jedis);
}

接下来看看Cache中最重要的两个方法:putObject和getObject,通过这两个方法来查看mybatis-redis 储存数据的格式:

@Override
public void putObject(final Object key, final Object value) {
    execute(new RedisCallback() {
        @Override
        public Object doWithRedis(Jedis jedis) {
            jedis.hset(id.toString().getBytes(), key.toString().getBytes(),
                       SerializeUtil.serialize(value));
            return null;
        }
    });
}
@Override
public Object getObject(final Object key) {
    return execute(new RedisCallback() {

        @Override
        public Object doWithRedis(Jedis jedis) {
            return SerializeUtil.unserialize(jedis.hget(id.toString().getBytes(),
                                                        key.toString().getBytes()));
        }
    });
}

可以很清楚的看到,mybatis-redis在存储数据的时候,是使用的hash结构,把cache的id作为这个hash 的key(cache的id在mybatis中就是mapper的namespace);这个mapper中的查询缓存数据作为 hash的field,需要缓存的内容直接使用SerializeUtil存储,SerializeUtil和其他的序列化类差不多,负责 对象的序列化和反序列化;

第八部分:Mybatis插件

8.1 插件简介

一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展。这样的好处是显而易⻅ 的,一是增加了框架的灵活性。二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工 作。以MyBatis为例,我
们可基于MyBati s插件机制实现分⻚、分表,监控等功能。由于插件和业务 无关,业务也无法感知插件的存在。因
此可以无感植入插件,在无形中增强功能

8.2 Mybatis插件介绍

Mybati s作为一个应用广泛的优秀的ORM开源框架,这个框架具有强大的灵活性,在四大组件(Executor、StatementHandler、ParameterHandler、ResultSetHandler)处提供了简单易用的插 件扩展机制。Mybatis对持久层的操作就是借助于四大核心对象。MyBatis支持用插件对四大核心对象进 行拦截,对mybatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的 动态代理实现的,换句话说,MyBatis中的四大对象都是代理对象

image-20220530170440957

MyBatis所允许拦截的方法如下:

  • 执⾏器Executor (update、query、commit、rollback等⽅法);
  • SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等⽅ 法);
  • 参数处理器ParameterHandler (getParameterObject、setParameters⽅法);
  • 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等⽅法);

8.3 Mybatis插件原理

在四大对象创建的时候

1、每个创建出来的对象不是直接返回的,⽽是interceptorChain.pluginAll(parameterHandler);

2、获取到所有的Interceptor (拦截器)(插件需要实现的接⼝);调⽤ interceptor.plugin(target);返回 target 包 装后的对象

3、插件机制,我们可以使⽤插件为⽬标对象创建⼀个代理对象;AOP (⾯向切⾯)我们的插件可 以为四⼤对象 创建出代理对象,代理对象就可以拦截到四⼤对象的每⼀个执⾏;

拦截

插件具体是如何拦截并附加额外的功能的呢?以ParameterHandler来说

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object object, BoundSql sql, InterceptorChain interceptorChain){
    ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
    parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    return parameterHandler;
}
public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
        target = interceptor.plugin(target);
    }
    return target;
}

interceptorChain保存了所有的拦截器(interceptors),是mybatis初始化的时候创建的。调用拦截器链 中的拦截器依次的对目标进行拦截或增强。interceptor.plugin(target)中的target就可以理解为mybatis 中的四大对象。返回的target是被重重代理后的对象

如果我们想要拦截Executor的query方法,那么可以这样定义插件:

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "query",
        args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class}
    )
})
public class ExeunplePlugin implements Interceptor {
    //省略逻辑
}

除此之外,我们还需将插件配置到sqlMapConfig.xm l中。

<plugins>
    <plugin interceptor="com.lagou.plugin.ExamplePlugin">
    </plugin>
</plugins>

这样MyBatis在启动时可以加载插件,并保存插件实例到相关对象(InterceptorChain,拦截器链) 中。待准备工作做完后,MyBatis处于就绪状态。我们在执行SQL时,需要先通过DefaultSqlSessionFactory 创建 SqlSession。Executor 实例在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在 Executor相关方法被调用前执行。

以上就是MyBatis插件机制的基本原理

8.4 自定义插件

8.4.1 插件接口

Mybatis 插件接口-Interceptor

  • Intercept方法,插件的核心方法

  • plugin方法,生成target的代理对象

  • setProperties方法,传递插件所需参数

8.4.2自定义插件

设计实现一个自定义插件

@Intercepts ({//注意看这个⼤花括号,也就这说这⾥可以定义多个@Signature对多个地⽅拦截,都⽤这个拦截器
    @Signature (type = StatementHandler .class , //这是指拦截哪个接⼝
                method = "prepare",//这个接⼝内的哪个⽅法名,不要拼错了
                args = { Connection.class, Integer .class}),//// 这是拦截的⽅法的⼊参,按顺序写到这,不要多也不要少,如果⽅法重载,可是要通过⽅法名和⼊参来确定唯⼀的
})
public class MyPlugin implements Interceptor {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    // 这⾥是每次执⾏操作的时候,都会进⾏这个拦截器的⽅法内
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //增强逻辑
        System.out.println("对⽅法进⾏了增强....");
        return invocation.proceed(); //执⾏原⽅法
    }

    /**
     * //主要是为了把这个拦截器⽣成⼀个代理放到拦截器链中
     * ^Description包装⽬标对象 为⽬标对象创建代理对象
     * @Param target为要拦截的对象
     * @Return代理对象
     */
    @Override
    public Object plugin(Object target) {
        System.out.println("将要包装的⽬标对象:"+target);
        return Plugin.wrap(target,this);
    }

    /**获取配置⽂件的属性**/
    //插件初始化的时候调⽤,也只调⽤⼀次,插件配置的属性从这⾥设置进来
    @Override
    public void setProperties(Properties properties) {
     System.out.println("插件配置的初始化参数:"+properties );
    }
}

sqlMapConfig.xml

<plugins>
    <plugin interceptor="com.lagou.plugin.MySqlPagingPlugin">
        <!--配置参数-->
        <property name="name" value="Bob"/>
    </plugin>
</plugins>

mapper接⼝

public interface UserMapper {
    List<User> selectUser();
}

mapper.xml

<mapper namespace="com.lagou.mapper.UserMapper">
    <select id="selectUser" resultType="com.lagou.pojo.User">
        SELECT
        id,username
        FROM
        user
    </select>
</mapper>

测试类

public class PluginTest {
    @Test
    public void test() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> byPaging = userMapper.selectUser();
        for (User user : byPaging) {
            System.out.println(user);
        }
    }
}

8.5 源码分析

执行插件逻辑

Plugin实现了 InvocationHandler接口,因此它的invoke方法会拦截所有的方法调用。invoke方法会 对所拦截的方法进行检测,以决定是否执行插件逻辑。该方法的逻辑如下:

// -Plugin
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        /*
         *获取被拦截⽅法列表,⽐如:
         * signatureMap.get(Executor.class), 可能返回 [query, update, commit]
         */
        Set<Method> methods = signatureMap.get(method.getDeclaringClass());
        //检测⽅法列表是否包含被拦截的⽅法
        if (methods != null && methods.contains(method)) {
            //执⾏插件逻辑
            return interceptor.intercept(new Invocation(target, method, args));
            //执⾏被拦截的⽅法
            return method.invoke(target, args);
        } catch(Exception e){
        }
    }
}

invoke方法的代码比较少,逻辑不难理解。首先,invoke方法会检测被拦截方法是否配置在插件的 @Signature注解中,若是,则执行插件逻辑,否则执行被拦截方法。插件逻辑封装在intercept中,该 方法的参数类型为Invocationo Invocation主要用于存储目标类,方法以及方法参数列表。下面简单看 一下该类的定义

public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;
    public Invocation(Object targetf Method method, Object[] args) {
        this.target = target;
        this.method = method;
        //省略部分代码
        public Object proceed() throws InvocationTargetException, IllegalAccessException { //调⽤被拦截的⽅法
        }
    }
}

8.6 pageHelper分⻚插件

MyBati s可以使用第三方的插件来对功能进行扩展,分⻚助手PageHelper是将分⻚的复杂操作进行封 装,使用简单的方式即可获得分⻚的相关数据

开发步骤:

① 导入通用PageHelper的坐标

② 在mybatis核心配置文件中配置PageHelper插件

③ 测试分⻚数据获取

①导入通用PageHelper坐标

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>3.7.5</version>
</dependency>
<dependency>
    <groupId>com.github.jsqlparser</groupId>
    <artifactId>jsqlparser</artifactId>
    <version>0.9.1</version>
</dependency>

② 在mybatis核心配置文件中配置PageHelper插件

<!--注意:分⻚助⼿的插件 配置在通⽤馆mapper之前*-->*
<plugin interceptor="com.github.pagehelper.PageHelper">
    <!—指定⽅⾔ —>
    <property name="dialect" value="mysql"/>
</plugin>

③ 测试分⻚代码实现

@Test
public void testPageHelper() {
    //设置分⻚参数
    PageHelper.startPage(1, 2);
    List<User> select = userMapper2.select(null);
    for (User user : select) {
        System.out.println(user);
    }
}

获得分⻚相关的其他参数

//其他分⻚的数据
PageInfo<User> pageInfo = new PageInfo<User>(select);
System.out.println("总条数:"+pageInfo.getTotal());
System.out.println("总⻚数:"+pageInfo. getPages ());
System.out.println("当前⻚:"+pageInfo. getPageNum());
System.out.println("每⻚显万⻓度:"+pageInfo.getPageSize());
System.out.println("是否第⼀⻚:"+pageInfo.isIsFirstPage());
System.out.println("是否最后⼀⻚:"+pageInfo.isIsLastPage());

8.7 通用 mapper

什么是通用Mapper

通用Mapper就是为了解决单表增删改查,基于Mybatis的插件机制。开发人员不需要编写SQL,不需要 在DAO中增加方法,只要写好实体类,就能支持相应的增删改查方法

如何使用

  1. 首先在maven项目,在pom.xml中引入mapper的依赖

    <dependency>
        <groupId>tk.mybatis</groupId>
        <artifactId>mapper</artifactId>
        <version>3.1.2</version>
    </dependency>
    
  2. Mybatis配置文件中完成配置

    <plugins>
        <!--分⻚插件:如果有分⻚插件,要排在通⽤mapper之前-->
        <plugin interceptor="com.github.pagehelper.PageHelper">
            <property name="dialect" value="mysql"/>
        </plugin>
        <plugin interceptor="tk.mybatis.mapper.mapperhelper.MapperInterceptor">
            <!-- 通⽤Mapper接⼝,多个通⽤接⼝⽤逗号隔开 -->
            <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
        </plugin>
    </plugins>
    
  3. 实体类设置主键

    @Table(name = "t_user")
    public class User {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        private String username;
    }
    
  4. 定义通用mapper

    import com.lagou.domain.User;
    import tk.mybatis.mapper.common.Mapper;
    public interface UserMapper extends Mapper<User> {
    }
    
  5. 测试

    public class UserTest {
        @Test
        public void test1() throws IOException {
            Inputstream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
            SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
            SqlSession sqlSession = build.openSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            User user = new User();
            user.setId(4);
            //(1)mapper基础接⼝
            //select 接⼝
            User user1 = userMapper.selectOne(user); //根据实体中的属性进⾏查询,只能有 —个返回值
            List<User> users = userMapper.select(null); //查询全部结果
            userMapper.selectByPrimaryKey(1); //根据主键字段进⾏查询,⽅法参数必须包含完 整的主键属性,查询条件使⽤等号
            userMapper.selectCount(user); //根据实体中的属性查询总数,查询条件使⽤等号
            // insert 接⼝
            int insert = userMapper.insert(user); //保存⼀个实体,null值也会保存,不会使 ⽤数据库默认值
            int i = userMapper.insertSelective(user); //保存实体,null的属性不会保存, 会使⽤数据库默认值
            // update 接⼝
            int i1 = userMapper.updateByPrimaryKey(user);//根据主键更新实体全部字段, null值会被更新
            // delete 接⼝
            int delete = userMapper.delete(user); //根据实体属性作为条件进⾏删除,查询条件 使⽤等号
            userMapper.deleteByPrimaryKey(1); //根据主键字段进⾏删除,⽅法参数必须包含完 整的主键属性
            //(2)example⽅法
            Example example = new Example(User.class);
            example.createCriteria().andEqualTo("id", 1);
            example.createCriteria().andLike("val", "1");
            //⾃定义查询
            List<User> users1 = userMapper.selectByExample(example);
        }
    }
    
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String username;

第九部分:Mybatis架构原理

9.1架构设计

image-20220530175203250

我们把Mybatis的功能架构分为三层:

(1) API接口层:提供给外部使用的接口 API,开发人员通过这些本地API来操纵数据库。接口层一接收到 调用请求
就会调用数据处理层来完成具体的数据处理。

MyBatis和数据库的交互有两种方式:

a. 使用传统的MyBati s提供的API ;

b. 使用Mapper代理的方式

(2) 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根 据调用的
请求完成一次数据库操作。

(3) 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共 用的东
⻄,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑

9.2主要构件及其相互关系

构建 描述
SqlSession 作为MyBatis⼯作的主要顶层API,表示和数据库交互的会话,完成必要数 据库增删改查功能
Executor MyBatis执⾏器,是MyBatis调度的核⼼,负责SQL语句的⽣成和查询缓 存的维护
StatementHandler 封装了JDBC Statement操作,负责对JDBC statement的操作,如设置参 数、将Statement结果集转换成List集合。
ParameterHandler 负责对⽤户传递的参数转换成JDBC Statement所需要的参数,
ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
MappedStatement MappedStatement维护了⼀条<select | update | delete | insert> 节点封装
SqlSource 负责根据⽤户传递的parameterObject,动态地⽣成SQL语句,将信息封 装到BoundSql对象中,并返回
BoundSql 表示动态⽣成的SQL语句以及相应的参数信息
image-20220530184418699

9.3总体流程

(1) 加载配置并初始化

触发条件: 加载配置文件

配置来源于两个地方,一个是配置文件(主配置文件conf.xml,mapper文件*.xml),—个是java代码中的 注解,将主配置文件内容解析封装到Configuration,将sql的配置信息加载成为一个mappedstatement 对象,存储在内存之中

(2) 接收调用请求

触发条件 :调用Mybatis提供的API

传入参数:为 SQL的ID和传入参数对象

处理过程: 将请求传递给下层的请求处理层进行处理。

(3) 处理操作请求

触发条件: API接口层传递请求过来

传入参数:为 SQL的ID和传入参数对象

处理过程:

(A) 根据SQL的ID查找对应的MappedStatement对象。

(B) 根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。

(C) 获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。

(D) 根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理 结果。

(E) 释放连接资源。

(4) 返回处理结果

将最终的处理结果返回。

第十部分:Mybatis源码剖析

10.1传统方式源码剖析:

源码剖析-初始化

Inputstream inputstream = Resources.getResourceAsStream("mybatis-config.xml");
//这一行代码正是初始化工作的开始。
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

进入源码分析:

// 1.我们最初调用的build
public SqlSessionFactory build (InputStream inputStream){
    //调⽤了重载⽅法
    return build(inputStream, null, null);
}
// 2.调⽤的重载⽅法
public SqlSessionFactory build (InputStream inputStream, String environment, Properties properties){
    try {
        // XMLConfigBuilder是专⻔解析mybatis的配置⽂件的类
        XMLConfigBuilder parser = new XMLConfigBuilder(inputstream, environment, properties);
        //这⾥⼜调⽤了⼀个重载⽅法。parser.parse()的返回值是Configuration对象
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e)
    }
}

MyBatis在初始化的时候,会将MyBatis的配置信息全部加载到内存中,使用 org.apache.ibatis.session.Configuratio n 实例来维护

下面进入对配置文件解析部分:

首先对Configuration对象进行介绍:

Configuration对象的结构和xml配置⽂件的对象⼏乎相同。

回顾⼀下xml中的配置标签有哪些:properties (属性),settings (设置),typeAliases (类型别名),typeHandlers (类型处理 器),objectFactory (对象⼯⼚),mappers (映射器)等 Configuration也有对应的对象属性来封装它们

也就是说,初始化配置⽂件信息的本质就是创建Configuration对象,将解析的xml数据封装到 Configuration内
部属性中

/**
* 解析 XML 成 Configuration 对象。
*/
public Configuration parse () {
    //若已解析,抛出BuilderException异常
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
	}
    //标记已解析
    parsed = true;
    // 解析 XML configuration 节点
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}
/**
*解析XML
*/
private void parseConfiguration (XNode root){
    try {
        //issue #117 read properties first
        // 解析 <properties /> 标签
        propertiesElement(root.evalNode("properties"));
        // 解析〈settings /> 标签
        Properties settings =
            settingsAsProperties(root.evalNode("settings"));
        //加载⾃定义的VFS实现类
        loadCustomVfs(settings);
        // 解析 <typeAliases /> 标签
        typeAliasesElement(root.evalNode("typeAliases"));
        //解析<plugins />标签
        pluginElement(root.evalNode("plugins"));
        // 解析 <objectFactory /> 标签
        objectFactoryElement(root.evalNode("objectFactory"));
        // 解析 <objectWrapperFactory /> 标签
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        // 解析 <reflectorFactory /> 标签
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        // 赋值 <settings /> ⾄ Configuration 属性
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        // 解析〈environments /> 标签
        environmentsElement(root.evalNode("environments"));
        // 解析 <databaseIdProvider /> 标签
        databaseldProviderElement(root.evalNode("databaseldProvider"));
        // 解析 <typeHandlers /> 标签
        typeHandlerElement(root.evalNode("typeHandlers"));
        //解析<mappers />标签
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration.Cause:" + e, e);
    }
}

介绍一下 MappedStatement :

作用:MappedStatement与Mapper配置文件中的一个select/update/insert/delete节点相对应。

mapper中配置的标签都被封装到了此对象中,主要用途是描述一条SQL语句。

初始化过程: 回顾刚开 始介绍的加载配置文件的过程中,会对mybatis-config.xm l中的各个标签都进行解析,其
中有mappers 标签用来引入mapper.xml文件或者配置mapper接口的目录。

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

样的一个select标签会在初始化配置文件时被解析封装成一个MappedStatement对象,然后存储在 Configuration
对象的mappedStatements属性中,mappedStatements 是一个HashMap,存储时key =全限定类名+方法名,
value =对应的MappedStatement对象。

  • 在configuration中对应的属性为
Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement> ("MappedStatements collection")

在 XMLConfigBuilder 中的处理:

private void parseConfiguration(XNode root) {
    try {
        //省略其他标签的处理
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration.Cause:" + e, e);
    }
}

到此对xml配置文件的解析就结束了,回到步骤2.中调用的重载build方法

// 5.调⽤的重载⽅法
public SqlSessionFactory build(Configuration config) {
    //创建了 DefaultSqlSessionFactory 对象,传⼊ Configuration 对象。
    return new DefaultSqlSessionFactory(config);
}

源码剖析-执行SQL流程

先简单介绍 SqlSession :

SqlSession是一个接口,它有两个实现类:DefaultSqlSession (默认)和 SqlSessionManager (弃用,不做介绍)

SqlSession是MyBatis中用于和数据库交互的顶层类,通常将它与ThreadLocal绑定,一个会话使用一 个 SqlSession,并且在使用完毕后需要close

public class DefaultSqlSession implements SqlSession {
    private final Configuration configuration;
    private final Executor executor;
}

SqlSession中的两个最重要的参数,configuration与初始化时的相同,Executor为执行器

Executor:

Executor也是一个接口,他有三个常用的实现类:

BatchExecutor (重用语句并执行批量更新)

ReuseExecutor (重用预处理语句 prepared statements)

SimpleExecutor (普通的执行器,默认)

继续分析,初始化完毕后,我们就要执行SQL 了

SqlSession sqlSession = factory.openSession();
List<User> list = sqlSession.selectList("com.lagou.mapper.UserMapper.getUserByName");

获得 sqlSession

//6. 进⼊ o penSession ⽅法。
public SqlSession openSession() {
    //getDefaultExecutorType()传递的是SimpleExecutor
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
//7. 进⼊penSessionFromDataSource。
//ExecutorType 为Executor的类型,TransactionIsolationLevel为事务隔离级别, autoCommit是否开启事务
//openSession的多个重载⽅法可以指定获得的SeqSession的Executor类型和事务的处理 
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try{
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        //根据参数创建指定类型的Executor
        final Executor executor = configuration.newExecutor(tx, execType);
        //返回的是 DefaultSqlSession
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch(Exception e){
        closeTransaction(tx); // may have fetched a connection so lets call close()
    }
}

执⾏ sqlsession 中的 api

//8.进⼊selectList⽅法,多个重载⽅法。
public <E > List < E > selectList(String statement) {
    return this.selectList(statement, null);
}
public <E > List < E > selectList(String statement, Object parameter) {
    return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E > List < E > selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        //根据传⼊的全限定名+⽅法名从映射的Map中取出MappedStatement对象
        MappedStatement ms = configuration.getMappedStatement(statement);
        //调⽤Executor中的⽅法处理
        //RowBounds是⽤来逻辑分⻚
        // wrapCollection(parameter)是⽤来装饰集合或者数组参数
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.Cause: + e, e);
    } finally {
          rrorContext.instance().reset();
    }
}

源码剖析-executor

继续源码中的步骤,进入executor.query()

//此⽅法在SimpleExecutor的⽗类BaseExecutor中实现
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    //根据传⼊的参数动态获得SQL语句,最后返回⽤BoundSql对象表示
    BoundSql boundSql = ms.getBoundSql(parameter);
    //为本次查询创建缓存的Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
//进⼊query的重载⽅法中
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            //如果缓存中没有本次查找的值,那么从数据库中查询
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        // issue #601
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { //
            issue #482 clearLocalCache();
        }
    }
    return list;
}
//从数据库查询
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter,
                                      RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        //查询的⽅法
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        localCache.removeObject(key);
    }
    //将查询结果放⼊缓存
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}
// SimpleExecutor中实现⽗类的doQuery抽象⽅法
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds
                           rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
        Configuration configuration = ms.getConfiguration();
        //传⼊参数创建StatementHanlder对象来执⾏查询
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms,
                                                                     parameter, rowBounds, resultHandler, boundSql);
        //创建jdbc中的statement对象
        stmt = prepareStatement(handler, ms.getStatementLog());
        // StatementHandler 进⾏处理
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}
//创建Statement的⽅法
private Statement prepareStatement(StatementHandler handler, Log statementLog)
    throws SQLException {
    Statement stmt;
    //条代码中的getConnection⽅法经过重重调⽤最后会调⽤openConnection⽅法,从连接池中获 得连
    接。
        Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}
//从连接池获得连接的⽅法
protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
        log.debug("Opening JDBC Connection");
    }
    //从连接池获得连接
    connection = dataSource.getConnection();
    if (level != null) {
        connection.setTransactionIsolation(level.getLevel());
    }
}

上述的Executor.query()⽅法⼏经转折,最后会创建⼀个StatementHandler对象,然后将必要的参数传 递给
StatementHandler,使⽤StatementHandler来完成对数据库的查询,最终返回List结果集。

从上⾯的代码中我们可以看出,Executor的功能和作⽤是:

(1、根据传递的参数,完成SQL语句的动态解析,⽣成BoundSql对象,供StatementHandler使⽤;
(2、为查询创建缓存,以提⾼性能
(3、创建JDBC的Statement连接对象,传递给StatementHandler对象,返回List查询结果。

源码剖析-StatementHandler

StatementHandler对象主要完成两个工作:

  • 对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使⽤的是SQL语句字符串会包含若⼲个?占位符,我们其后再对占位符进⾏设值。StatementHandler通过 parameterize(statement)⽅法对 S tatement 进⾏设值;
  • StatementHandler 通过 List query(Statement statement, ResultHandler resultHandler)⽅法来 完成执⾏Statement,和将Statement对象返回的resultSet封装成List;

进⼊到 StatementHandler 的 parameterize(statement)⽅法的实现:

public void parameterize(Statement statement) throws SQLException {
    //使⽤ParameterHandler对象来完成对Statement的设值
    parameterHandler.setParameters((PreparedStatement) statement);
}
/** ParameterHandler 类的 setParameters(PreparedStatement ps) 实现
* 对某⼀个Statement进⾏设置参数
* */
public void setParameters(PreparedStatement ps) throws SQLException {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) { 
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i); 
            if (parameterMapping.getMode() != ParameterMode.OUT) { 
                Object value;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                        value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) { 
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { 
                    value = parameterObject;
                } else {
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName); }
                // 每⼀个 Mapping都有⼀个 TypeHandler,根据 TypeHandler 来对 preparedStatement 进⾏设置参数
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                if (value == null && jdbcType == null) jdbcType = configuration.getJdbcTypeForNull();
                //设置参数
                typeHandler.setParameter(ps, i + 1, value, jdbcType);
            }
        }
    }
}

从上述的代码可以看到,StatementHandler的parameterize(Statement)方法调用了 ParameterHandler的setParameters(statement)方法,

ParameterHandler的setParameters(Statement )方法负责根据我们输入的参数,对statement对象的 ?占位符处
进行赋值。

进入到StatementHandler 的 List query(Statement statement, ResultHandler resultHandler)方法的 实现:

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws
    SQLException {
    // 1.调⽤preparedStatemnt。execute()⽅法,然后将resultSet交给ResultSetHandler处理
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    //2.使⽤ ResultHandler 来处理 ResultSet
    return resultSetHandler.<E> handleResultSets(ps);
}

从上述代码我们可以看出,StatementHandler 的List query(Statement statement, ResultHandler resultHandler)方法的实现,是调用了 ResultSetHandler 的 handleResultSets(Statement)方法。

ResultSetHandler 的 handleResultSets(Statement)方法会将 Statement 语句执行后生成的 resultSet 结 果集转换
成List结果集

public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    //多ResultSet的结果集合,每个ResultSet对应⼀个Object对象。⽽实际上,每 个 Object 是 List<Object> 对象。
    //在不考虑存储过程的多ResultSet的情况,普通的查询,实际就⼀个ResultSet,也 就是说,multipleResults最多就⼀个元素。
    final List<Object> multipleResults = new ArrayList<>();
    int resultSetCount = 0;
    //获得⾸个ResultSet对象,并封装成ResultSetWrapper对象
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    //获得ResultMap数组
    //在不考虑存储过程的多ResultSet的情况,普通的查询,实际就⼀个ResultSet,也 就是说,resultMaps就⼀个元素。
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount); // 校验
    while (rsw != null && resultMapCount > resultSetCount) {
        //获得ResultMap对象
        ResultMap resultMap = resultMaps.get(resultSetCount);
        //处理ResultSet,将结果添加到multipleResults中
        handleResultSet(rsw, resultMap, multipleResults, null);
        //获得下⼀个ResultSet对象,并封装成ResultSetWrapper对象
        rsw = getNextResultSet(stmt);
        //清理
        cleanUpAfterHandlingResultSet();
        // resultSetCount ++
        resultSetCount++;
    }
}
//因为'mappedStatement.resultSets'只在存储过程中使⽤,本系列暂时不考虑,忽略即可
String[] resultSets = mappedStatement.getResultSets();
if(resultSets!=null) {
    while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
            String nestedResultMapId = parentMapping.getNestedResultMapId();
            ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
            handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
    }
}
//如果是multipleResults单元素,则取⾸元素返回
return collapseSingleResultList(multipleResults);
}

10.2 Mapper代理方式:

回顾下写法:

public static void main(String[] args) {
    //前三步都相同
    InputStream inputStream = Resources.getResourceAsStream("sqlMapConfig.xml");
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = factory.openSession();
    //这⾥不再调⽤SqlSession的api,⽽是获得了接⼝对象,调⽤接⼝中的⽅法。
    UserMapper mapper = sqlSession.getMapper(UserMapper.class);
    List<User> list = mapper.getUserByName("tom");
}

思考一个问题,通常的Mapper接口我们都没有实现的方法却可以使用,是为什么呢?答案很简单动态 代理开始之前介绍一下MyBatis初始化时对接口的处理:MapperRegistry是Configuration中的一个属性,它内部维护一个HashMap用于存放mapper接口的工厂类,每个接口对应一个工厂类。mappers中可以 配置接口的包路径,或者某个具体的接口类。

<mappers>
    <mapper class="com.lagou.mapper.UserMapper"/>
    <package name="com.lagou.mapper"/>
</mappers>

当解析mappers标签时,它会判断解析到的是mapper配置文件时,会再将对应配置文件中的增删 改查标签 封装成MappedStatement对象,存入mappedStatements中。(上文介绍了)

当判断解析到接口时,会建此接口对应的MapperProxyFactory对象,存入HashMap中,key =接口的字节码对象,value =此接口对应的 MapperProxyFactory对象。

源码剖析-getmapper()

进入 sqlSession.getMapper(UserMapper.class )中

//DefaultSqlSession 中的 getMapper
public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
}
//configuration 中的给 g etMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
}
//MapperRegistry 中的 g etMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    //从 MapperRegistry 中的 HashMap 中拿 MapperProxyFactory
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>)
        knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        //通过动态代理⼯⼚⽣成示例。
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e,
                                   e);
    }
}
//MapperProxyFactory 类中的 newInstance ⽅法
public T newInstance(SqlSession sqlSession) {
    //创建了 JDK动态代理的Handler类
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession,
                                                         mapperInterface, methodCache);
    //调⽤了重载⽅法
    return newInstance(mapperProxy);
}
//MapperProxy 类,实现了 InvocationHandler 接⼝
public class MapperProxy<T> implements InvocationHandler, Serializable {
    //省略部分源码
    private final SqlSession sqlSession;
    private final Class<T> mapperInterface;
    private final Map<Method, MapperMethod> methodCache;
    //构造,传⼊了 SqlSession,说明每个session中的代理对象的不同的!
    public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method,
                       MapperMethod> methodCache) {
        this.sqlSession = sqlSession;
        this.mapperInterface = mapperInterface;
        this.methodCache = methodCache;
    }
    //省略部分源码
}

源码剖析-invoke()

在动态代理返回了示例后,我们就可以直接调用mapper类中的方法了,但代理对象调用方法,执行是在MapperProxy中的invoke方法中

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        //如果是Object定义的⽅法,直接调⽤
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    // 获得 MapperMethod 对象
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    //重点在这:MapperMethod最终调⽤了执⾏的⽅法
    return mapperMethod.execute(sqlSession, args);
}

进入execute方法:

public Object execute(SqlSession sqlSession, Object[] args) { 
    Object result;
    //判断mapper中的⽅法类型,最终调⽤的还是SqlSession中的⽅法 
    switch (command.getType()) {
        case INSERT: {
            //转换参数
            Object param = method.convertArgsToSqlCommandParam(args);
            //执⾏INSERT操作
            // 转换 rowCount
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
        case UPDATE: {
            //转换参数
            Object param = method.convertArgsToSqlCommandParam(args);
            // 转换 rowCount
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
        }
        case DELETE: {
            //转换参数
            Object param = method.convertArgsToSqlCommandParam(args);
            // 转换 rowCount
            result = rowCountResult(sqlSession.delete(command.getName(),
                                                      param));
            break;
        }
        case SELECT:
            //⽆返回,并且有ResultHandler⽅法参数,则将查询的结果,提交给 ResultHandler 进⾏处理
            if (method.returnsVoid() && method.hasResultHandler()) {
                executeWithResultHandler(sqlSession, args);
                result = null;
                //执⾏查询,返回列表
            } else if (method.returnsMany()) {
                result = executeForMany(sqlSession, args);
                //执⾏查询,返回Map
            } else if (method.returnsMap()) {
                result = executeForMap(sqlSession, args);
                //执⾏查询,返回Cursor
            } else if (method.returnsCursor()) {
                result = executeForCursor(sqlSession, args);
                //执⾏查询,返回单个对象
            } else {
                //转换参数
                Object param = method.convertArgsToSqlCommandParam(args);
                //查询单条
                result = sqlSession.selectOne(command.getName(), param);
                if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
                    result = Optional.ofNullable(result);
                }
            }
            break;
        case FLUSH:
            result = sqlSession.flushStatements();
            break;
        default:
        	throw new BindingException("Unknown execution method for: " + command.getName());
    }
    //返回结果为null,并且返回类型为基本类型,则抛出BindingException异常
    if(result ==null&&method.getReturnType().isPrimitive() && !method.returnsVoid()) {
        throw new BindingException("Mapper method '" + command.getName() + " attempted to return null from a method with a primitivereturn type(" + method.getReturnType() + "). ");
     }
    //返回结果
    return result;
}

10.3 二级缓存源码剖析:

二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis 首先会查询二级缓存,若二级缓存未命中,再去查询一级缓存,一级缓存没有,再查询数据库。

二级缓存------》 一级缓存------》数据库与一级缓存不同,二级缓存和具体的命名空间绑定,一个Mapper中有一个Cache,相同Mapper中的MappedStatement共用一个Cache,一级缓存则是和 SqlSession 绑定。

启用二级缓存

分为三步走:

1 )开启全局二级缓存配置:

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

2)在需要使用二级缓存的Mapper配置文件中配置标签

<cache></cache>

3 )在具体CURD标签上配置 useCache=true

<select id="findById" resultType="com.lagou.pojo.User" useCache="true">
	select * from user where id = #{id}
</select>

标签 < cache/> 的解析

根据之前的mybatis源码剖析,xml的解析工作主要交给XMLConfigBuilder.parse()方法来实现

// XMLConfigBuilder.parse()
public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));// 在这⾥
    return configuration;
}
// parseConfiguration()
// 既然是在xml中添加的,那么我们就直接看关于mappers标签的解析
private void parseConfiguration(XNode root) {
    try {
        Properties settings = settingsAsPropertiess(root.evalNode("settings"));
        propertiesElement(root.evalNode("properties"));
        loadCustomVfs(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectionFactoryElement(root.evalNode("reflectionFactory"));
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        // 就是这⾥
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " +
                                   e, e);
    }
}
// mapperElement()
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                // 按照我们本例的配置,则直接⾛该if判断
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // ⽣成XMLMapperBuilder,并执⾏其parse⽅法
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

我们来看看解析Mapper.xml

// XMLMapperBuilder.parse()
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        // 解析mapper属性
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }
    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
}
// configurationElement()
private void configurationElement(XNode context) {
    try {
        String namespace = context.getStringAttribute("namespace");
        if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);
        cacheRefElement(context.evalNode("cache-ref"));
        // 最终在这⾥看到了关于cache属性的处理
        cacheElement(context.evalNode("cache"));
        parameterMapElement(context.evalNodes("/mapper/parameterMap"));
        resultMapElements(context.evalNodes("/mapper/resultMap"));
        sqlElement(context.evalNodes("/mapper/sql"));
        // 这⾥会将⽣成的Cache包装到对应的MappedStatement
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
}
// cacheElement()
private void cacheElement(XNode context) throws Exception {
    if (context != null) {
        //解析<cache/>标签的type属性,这⾥我们可以⾃定义cache的实现类,⽐如redisCache,如果没有⾃
        定义,这⾥使⽤和⼀级缓存相同的PERPETUAL
            String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass =
            typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        // 构建Cache对象
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size,
                                     readWrite, blocking, props);
    }
}

先来看看是如何构建Cache对象的

MapperBuilderAssistant.useNewCache()

public Cache useNewCache(Class<? extends Cache> typeClass,
                         Class<? extends Cache> evictionClass,
                         Long flushInterval,
                         Integer size,
                         boolean readWrite,
                         boolean blocking,
                         Properties props) {
    // 1.⽣成Cache对象
    Cache cache = new CacheBuilder(currentNamespace)
        //这⾥如果我们定义了<cache/>中的type,就使⽤⾃定义的Cache,否则使⽤和⼀级缓存相同的
        PerpetualCache
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    // 2.添加到Configuration中
    configuration.addCache(cache);
    // 3.并将cache赋值给MapperBuilderAssistant.currentCache
    currentCache = cache;
    return cache;
}

我们看到一个Mapper.xml只会解析一次标签,也就是只创建一次Cache对象,放进configuration中,并将cache赋值给MapperBuilderAssistant.currentCache

buildStatementFromContext(context.evalNodes("select|insert|update|delete"));将Cache包装MappedStatement

// buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}
//buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new
            XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // 每⼀条执⾏语句转换成⼀个MappedStatement
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}
// XMLStatementBuilder.parseStatementNode();
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    ...
        Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
    ...
        // 创建MappedStatement对象
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                            fetchSize, timeout, parameterMap,
                                            parameterTypeClass, resultMap, resultTypeClass,
                                            resultSetTypeEnum, flushCache, useCache,
                                            resultOrdered,
                                            keyGenerator, keyProperty, keyColumn,
                                            databaseId, langDriver, resultSets);
}
// builderAssistant.addMappedStatement()
public MappedStatement addMappedStatement(String id, ...) {
    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }
    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    //创建MappedStatement对象
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        ...
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);// 在这⾥将之前⽣成的Cache封装到MappedStatement
    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

我们看到将Mapper中创建的Cache对象,加入到了每个MappedStatement对象中,也就是同一个Mapper中所有的 2

有关于标签的解析就到这了。

查询源码分析

CachingExecutor

// CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创建 CacheKey
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    // 从 MappedStatement 中获取 Cache,注意这⾥的 Cache 是从MappedStatement中获取的
    // 也就是我们上⾯解析Mapper中<cache/>标签中创建的,它保存在Configration中
    // 我们在上⾯解析blog.xml时分析过每⼀个MappedStatement都有⼀个Cache对象,就是这⾥
    Cache cache = ms.getCache();
    // 如果配置⽂件中没有配置 <cache>,则 cache 为空
    if (cache != null) {
        //如果需要刷新缓存的话就刷新:flushCache="true"
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            // 访问⼆级缓存
            List<E> list = (List<E>) tcm.getObject(cache, key);
            // 缓存未命中
            if (list == null) {
                // 如果没有值,则执⾏查询,这个查询实际也是先⾛⼀级缓存查询,⼀级缓存也没有的话,则进⾏DB查询
                    list = delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 缓存查询结果
                tcm.putObject(cache, key, list);
            }
            return list;
        }
    }
    return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key,  boundSql);
}

如果设置了 flushCache="true",则每次查询都会刷新缓存

<!-- 执⾏此语句清空缓存 -->
<select id="findbyId" resultType="com.lagou.pojo.user" useCache="true"  flushCache="true" >
    select * from t_demo
</select>

如上,注意二级缓存是从 MappedStatement 中获取的。由于 MappedStatement 存在于全局配置中,可以多个CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个事务共用一个缓存实例,会导致脏读问题。至于脏读问题,需要借助其他类来处理,也就是上面代码中 tcm 变量对应的类型。下面分析一下。

TransactionalCacheManager

/** 事务缓存管理器 */
public class TransactionalCacheManager {
    // Cache 与 TransactionalCache 的映射关系表
    private final Map<Cache, TransactionalCache> transactionalCaches = new  HashMap<Cache, TransactionalCache>();
    public void clear(Cache cache) {
        // 获取 TransactionalCache 对象,并调⽤该对象的 clear ⽅法,下同
        getTransactionalCache(cache).clear();
    }
    public Object getObject(Cache cache, CacheKey key) {
        // 直接从TransactionalCache中获取缓存
        return getTransactionalCache(cache).getObject(key);
    }
    public void putObject(Cache cache, CacheKey key, Object value) {
        // 直接存⼊TransactionalCache的缓存中
        getTransactionalCache(cache).putObject(key, value);
    }
    public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.commit();
        }
    }
    public void rollback() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.rollback();
        }
    }
    private TransactionalCache getTransactionalCache(Cache cache) {
        // 从映射表中获取 TransactionalCache
        TransactionalCache txCache = transactionalCaches.get(cache);
        if (txCache == null) {
            // TransactionalCache 也是⼀种装饰类,为 Cache 增加事务功能
            // 创建⼀个新的TransactionalCache,并将真正的Cache对象存进去
            txCache = new TransactionalCache(cache);
            transactionalCaches.put(cache, txCache);
        }
        return txCache;
    }
}

TransactionalCacheManager 内部维护了 Cache 实例与 TransactionalCache 实例间的映射关系,该类也仅负责维护两者的映射关系,真正做事的还是 TransactionalCache。TransactionalCache 是一种缓存装饰器,可以为Cache 实例增加事务功能。我在之前提到的脏读问题正是由该类进行处理的。下面分析一下该类的逻辑。

TransactionalCache

public class TransactionalCache implements Cache {
    //真正的缓存对象,和上⾯的Map<Cache, TransactionalCache>中的Cache是同⼀个
    private final Cache delegate;
    private boolean clearOnCommit;
    // 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
    private final Map<Object, Object> entriesToAddOnCommit;
    // 在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
    private final Set<Object> entriesMissedInCache;
    @Override
    public Object getObject(Object key) {
        // 查询的时候是直接从delegate中去查询的,也就是从真正的缓存对象中查询
        Object object = delegate.getObject(key);
        if (object == null) {
            // 缓存未命中,则将 key 存⼊到 entriesMissedInCache 中
            entriesMissedInCache.add(key);
        }
        if (clearOnCommit) {
            return null;
        } else {
            return object;
        }
    }
    @Override
    public void putObject(Object key, Object object) {
        // 将键值对存⼊到 entriesToAddOnCommit 这个Map中中,⽽⾮真实的缓存对象 delegate 中
        entriesToAddOnCommit.put(key, object);
    }
    @Override
    public Object removeObject(Object key) {
        return null;
    }
    @Override
    public void clear() {
        clearOnCommit = true;
        // 清空 entriesToAddOnCommit,但不清空 delegate 缓存
        entriesToAddOnCommit.clear();
    }
    public void commit() {
        // 根据 clearOnCommit 的值决定是否清空 delegate
        if (clearOnCommit) {
            delegate.clear();
        }
        // 刷新未缓存的结果到 delegate 缓存中
        flushPendingEntries();
        // 重置 entriesToAddOnCommit 和 entriesMissedInCache
        reset();
    }
    public void rollback() {
        unlockMissedEntries();
        reset();
    }
    private void reset() {
        clearOnCommit = false;
        // 清空集合
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
    }
    private void flushPendingEntries() {
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            // 将 entriesToAddOnCommit 中的内容转存到 delegate 中
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                // 存⼊空值
                delegate.putObject(entry, null);
            }
        }
    }
    private void unlockMissedEntries() {
        for (Object entry : entriesMissedInCache) {
            try {
                // 调⽤ removeObject 进⾏解锁
                delegate.removeObject(entry);
            } catch (Exception e) {
                log.warn("...");
            }
        }
    }
}

存储二级缓存对象的时候是放到了TransactionalCache.entriesToAddOnCommit这个map中,但是每次查询的时候是直接从TransactionalCache.delegate中去查询的,所以这个二级缓存查询数据库后,设置缓存值是没有立刻生效的,主要是因为直接存到 delegate 会导致脏数据问题

为何只有SqlSession提交或关闭之后?

那我们来看下SqlSession.commit()方法做了什么

SqlSession

@Override
public void commit(boolean force) {
    try {
        // 主要是这句
        executor.commit(isCommitOrRollbackRequired(force));
        dirty = false;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error committing transaction. Cause: " +
                                             e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}
// CachingExecutor.commit()
@Override
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();// 在这⾥
}
// TransactionalCacheManager.commit()
public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
        txCache.commit();// 在这⾥
    }
}
// TransactionalCache.commit()
public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    flushPendingEntries();//这⼀句
    reset();
}
// TransactionalCache.flushPendingEntries()
private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        // 在这⾥真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,⼆级缓存才真正
        的⽣效
            delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}

二级缓存的刷新

我们来看看SqlSession的更新操作

public int update(String statement, Object parameter) {
    int var4;
    try {
        this.dirty = true;
        MappedStatement ms = this.configuration.getMappedStatement(statement);
        var4 = this.executor.update(ms, this.wrapCollection(parameter));
    } catch (Exception var8) {
        throw ExceptionFactory.wrapException("Error updating database. Cause: " +
                                             var8, var8);
    } finally {
        ErrorContext.instance().reset();
    }
    return var4;
}
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    this.flushCacheIfRequired(ms);
    return this.delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
    //获取MappedStatement对应的Cache,进⾏清空
    Cache cache = ms.getCache();
    //SQL需设置flushCache="true" 才会执⾏清空
    if (cache != null && ms.isFlushCacheRequired()) {
        this.tcm.clear(cache);
    }
}

MyBatis⼆级缓存只适⽤于不常进⾏增、删、改的数据,⽐如国家⾏政区省市区街道数据。⼀但数据变更,MyBatis会清空缓存。因此⼆级缓存不适⽤于经常进⾏更新的数据。

总结

在二级缓存的设计上,MyBatis大量地运用了装饰者模式,如CachingExecutor, 以及各种Cache接口的装饰器。

  • ⼆级缓存实现了Sqlsession之间的缓存数据共享,属于namespace级别
  • ⼆级缓存具有丰富的缓存策略。
  • ⼆级缓存可由多个装饰器,与基础缓存组合⽽成
  • ⼆级缓存⼯作由 ⼀个缓存装饰执⾏器CachingExecutor和 ⼀个事务型预缓存TransactionalCache 完成。

10.4 延迟加载源码剖析:

什么是延迟加载?
问题
在开发过程中很多时候我们并不需要总是在加载⽤户信息时就⼀定要加载他的订单信息。此时就是我们所说的延迟加载。
举个栗⼦

  • 在⼀对多中,当我们有⼀个⽤户,它有个100个订单在查询⽤户的时候,要不要把关联的订单查出来?在查询订单的时候,要不要把关联的⽤户查出来?
  • 回答:在查询⽤户时,⽤户下的订单应该是,什么时候⽤,什么时候查询。在查询订单时,订单所属的⽤户信息应该是随着订单⼀起查询出来。

延迟加载:就是在需要⽤到数据时才进⾏加载,不需要⽤到数据时就不加载数据。延迟加载也称懒加载。

  • 优点:先从单表查询,需要时再从关联表去关联查询,⼤⼤提⾼数据库性能,因为查询单表要⽐关联查询多张表速度快。
  • 缺点:因为只有当需要⽤到数据时,才会进⾏数据库查询,这样在⼤批量数据查询时,因为查询⼯作也要消耗时间,所可能造成⽤户等待时间变⻓,造成⽤户体验下降。
  • 在多表中:⼀对多,多对多:通常情况下采⽤延迟加载⼀对⼀(多对⼀):通常情况下采⽤⽴即加载
  • 注意:延迟加载是基于嵌套查询来实现的

实现
局部延迟加载
在association和collection标签中都有⼀个fetchType属性,通过修改它的值,可以修改局部的加载策略。

<!-- 开启⼀对多 延迟加载 -->
<resultMap id="userMap" type="user">
    <id column="id" property="id"></id>
    <result column="username" property="username"></result>
    <result column="password" property="password"></result>
    <result column="birthday" property="birthday"></result>
    <!--
fetchType="lazy" 懒加载策略
fetchType="eager" ⽴即加载策略
-->
    <collection property="orderList" ofType="order" column="id"
                select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy">
    </collection>
</resultMap>
<select id="findAll" resultMap="userMap">
    SELECT * FROM `user`
</select>

全局延迟加载
在Mybatis的核⼼配置⽂件中可以使⽤setting标签修改全局的加载策略。

<settings>
    <!--开启全局延迟加载功能-->
    <setting name="lazyLoadingEnabled" value="true"/>
</settings>

注意:

<!-- 关闭⼀对⼀ 延迟加载 -->
<resultMap id="orderMap" type="order">
    <id column="id" property="id"></id>
    <result column="ordertime" property="ordertime"></result>
    <result column="total" property="total"></result>
    <!--
fetchType="lazy" 懒加载策略
fetchType="eager" ⽴即加载策略
-->
    <association property="user" column="uid" javaType="user"
                 select="com.lagou.dao.UserMapper.findById" fetchType="eager">
    </association>
</resultMap>
<select id="findAll" resultMap="orderMap">
    SELECT * from orders
</select>

延迟加载原理实现
它的原理是,使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属性的getting ⽅法时,进⼊拦截器⽅法。⽐如调⽤ a.getB().getName() ⽅法,进⼊拦截器的 invoke(...) ⽅法,发现 a.getB() 需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调⽤a.setB(b) ⽅法,于是 a 对象 b 属性就有值了,接着完成a.getB().getName() ⽅法的调⽤。这就是延迟加载的基本原理.

总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。

延迟加载原理(源码剖析)

MyBatis延迟加载主要使⽤:Javassist,Cglib实现,类图展示:

image-20220530193605682

Setting 配置加载:

public class Configuration {
    /** aggressiveLazyLoading:
* 当开启时,任何⽅法的调⽤都会加载该对象的所有属性。否则,每个属性会按需加载(参考
lazyLoadTriggerMethods).
* 默认为true
* */
    protected boolean aggressiveLazyLoading;
    /**
* 延迟加载触发⽅法
*/
    protected Set<String> lazyLoadTriggerMethods = new HashSet<String>(Arrays.asList(new
                                                                                     String[] { "equals", "clone", "hashCode", "toString" }));
    /** 是否开启延迟加载 */
    protected boolean lazyLoadingEnabled = false;
    /**
* 默认使⽤Javassist代理⼯⼚
* @param proxyFactory
*/
    public void setProxyFactory(ProxyFactory proxyFactory) {
        if (proxyFactory == null) {
            proxyFactory = new JavassistProxyFactory();
        }
        this.proxyFactory = proxyFactory;
    }
    //省略...
}

延迟加载代理对象创建
Mybatis的查询结果是由ResultSetHandler接⼝的handleResultSets()⽅法处理的。ResultSetHandler接⼝只有⼀个实现,DefaultResultSetHandler,接下来看下延迟加载相关的⼀个核⼼的⽅法

<code class="language-Java">//#mark 创建结果对象
    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
    this.useConstructorMappings = false; // reset previous mapping result
    final List&lt;Class&lt;?&gt;&gt; constructorArgTypes = new ArrayList&lt;Class&lt;? &gt;&gt;();
    final List&lt;Object&gt; constructorArgs = new ArrayList&lt;Object&gt;();
    //#mark 创建返回的结果映射的真实对象
    Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes,
                                             constructorArgs, columnPrefix);
    if (resultObject != null &amp;&amp; !hasTypeHandlerForResultObject(rsw,
                                                                       resultMap.getType())) {
        final List&lt;ResultMapping&gt; propertyMappings =
            resultMap.getPropertyResultMappings();
        for (ResultMapping propertyMapping : propertyMappings) {
            // 判断属性有没配置嵌套查询,如果有就创建代理对象
            if (propertyMapping.getNestedQueryId() != null &amp;&amp;
                propertyMapping.isLazy()) {
                //#mark 创建延迟加载代理对象
                resultObject = configuration.getProxyFactory().createProxy(resultObject,
                                                                           lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                break;
            }
        }
    }
    this.useConstructorMappings = resultObject != null &amp;&amp;
    !constructorArgTypes.isEmpty(); // set current mapping result
    return resultObject;
}

默认采⽤javassistProxy进⾏代理对象的创建

JavasisstProxyFactory实现

public class JavassistProxyFactory implements org.apache.ibatis.executor.loader.ProxyFactory {
    /**
    * 接⼝实现
    * @param target ⽬标结果对象
    * @param lazyLoader 延迟加载对象
    * @param configuration 配置
    * @param objectFactory 对象⼯⼚
    * @param constructorArgTypes 构造参数类型
    * @param constructorArgs 构造参数值
    * @return
    */
    @Override
    public Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration  configuration, ObjectFactory objectFactory, List&lt;Class&lt;?&gt;&gt; constructorArgTypes, List&lt;Object&gt; constructorArgs) {
        return EnhancedResultObjectProxyImpl.createProxy(target, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
    }
    //省略...
    /**
    * 代理对象实现,核⼼逻辑执⾏
    */
        private static class EnhancedResultObjectProxyImpl implements MethodHandler {
            /**
    * 创建代理对象
    * @param type
    * @param callback
    * @param constructorArgTypes
    * @param constructorArgs
    * @return
    */
            static Object crateProxy(Class&lt;?&gt; type, MethodHandler callback,
                                     List&lt;Class&lt;?&gt;&gt; constructorArgTypes, List&lt;Object&gt; constructorArgs) {
                ProxyFactory enhancer = new ProxyFactory();
                enhancer.setSuperclass(type);
                try {
                    //通过获取对象⽅法,判断是否存在该⽅法
                    type.getDeclaredMethod(WRITE_REPLACE_METHOD);
                    // ObjectOutputStream will call writeReplace of objects returned by writeReplace
                    if (log.isDebugEnabled()) {
                        log.debug(WRITE_REPLACE_METHOD + &quot; method was found on bean &quot; + type
                                  + &quot;, make sure it returns this&quot;);
                    }
                } catch (NoSuchMethodException e) {
                    //没找到该⽅法,实现接⼝
                    enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
                } catch (SecurityException e) {
                    // nothing to do here
                }
                Object enhanced;
            Class&lt;?&gt;[] typesArray = constructorArgTypes.toArray(new
                                                                      Class[constructorArgTypes.size()]);
            Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
            try {
                //创建新的代理对象
                enhanced = enhancer.create(typesArray, valuesArray);
            } catch (Exception e) {
                throw new ExecutorException(&quot;Error creating lazy proxy. Cause: &quot; + e,
                                            e);
            }
            //设置代理执⾏器
            ((Proxy) enhanced).setHandler(callback);
            return enhanced;
        }
        /**
* 代理对象执⾏
* @param enhanced 原对象
* @param method 原对象⽅法
* @param methodProxy 代理⽅法
* @param args ⽅法参数
* @return
* @throws Throwable
*/
        @Override
        public Object invoke(Object enhanced, Method method, Method methodProxy, Object[]
                             args) throws Throwable {
            final String methodName = method.getName();
            try {
                synchronized (lazyLoader) {
                    if (WRITE_REPLACE_METHOD.equals(methodName)) {
                        //忽略暂未找到具体作⽤
                        Object original;
                        if (constructorArgTypes.isEmpty()) {
                            original = objectFactory.create(type);
                        } else {
                            original = objectFactory.create(type, constructorArgTypes,
                                                            constructorArgs);
                        }
                        PropertyCopier.copyBeanProperties(type, enhanced, original);
                        if (lazyLoader.size() &gt; 0) {
                            return new JavassistSerialStateHolder(original,
                                                                  lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
                        } else {
                            return original;
                        }
                    } else {
                        //延迟加载数量⼤于0
                        if (lazyLoader.size() &gt; 0 &amp;&amp;
                            !FINALIZE_METHOD.equals(methodName)) {
                            //aggressive ⼀次加载性所有需要要延迟加载属性或者包含触发延迟加载⽅法
                            if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                                log.debug(&quot;==&gt; laze lod trigger method:&quot; + methodName +
                                          &quot;,proxy method:&quot; + methodProxy.getName() + &quot; class:&quot; +
                                          enhanced.getClass());
                                //⼀次全部加载
                                lazyLoader.loadAll();
                            } else if (PropertyNamer.isSetter(methodName)) {
                                //判断是否为set⽅法,set⽅法不需要延迟加载
                                final String property = PropertyNamer.methodToProperty(methodName);
                                lazyLoader.remove(property);
                            } else if (PropertyNamer.isGetter(methodName)) {
                                final String property = PropertyNamer.methodToProperty(methodName);
                                if (lazyLoader.hasLoader(property)) {
                                    //延迟加载单个属性
                                    lazyLoader.load(property);
                                    log.debug(&quot;load one :&quot; + methodName);
                                }
                            }
                        }
                    }
                }
                return methodProxy.invoke(enhanced, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
    }

注意事项

IDEA调试问题 当配置aggressiveLazyLoading=true,在使⽤IDEA进⾏调试的时候,如果断点打到代理执⾏逻辑当中,你会发现延迟加载的代码永远都不能进⼊,总是会被提前执⾏。 主要产⽣的原因在 aggressiveLazyLoading,因为在调试的时候,IDEA的Debuger窗体中已经触发了延迟加载对象的⽅法。

posted @ 2022-06-07 19:52  Maple~  阅读(52)  评论(0编辑  收藏  举报