Skip to content

Mybatis

MyBatis生命周期

image-20230207185239827

  • 创建SqlSessionFactory

    java
    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  • 通过SqlSessionFactory创建SqlSession

    java
    SqlSession session = sqlSessionFactory.openSession();
    SqlSession session = sqlSessionFactory.openSession();
  • 通过sqlsession执行数据库操作

    • 通过 SqlSession 实例来直接执行已映射的 SQL 语句

      java
      Blog blog = (Blog)session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
      Blog blog = (Blog)session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
    • 获取Mapper(映射),然后再执行SQL语句

      java
      BlogMapper mapper = session.getMapper(BlogMapper.class);
      Blog blog = mapper.selectBlog(101);
      BlogMapper mapper = session.getMapper(BlogMapper.class);
      Blog blog = mapper.selectBlog(101);
  • 调用session.commit()提交事务

  • 调用session.close()关闭会话

Mybatis工作流程

image-20230207190105360

Mybatis延迟加载

  • Mybatis支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
  • 原理是使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。

Mybatis缓存

一级缓存

json
在同一个SqlSession中发起的多次请求,会将相同的数据缓存起来,后面相同的查询不进行SQL操作数据库
MyBatis默认支持一级缓存,不需要任何配置
在同一个SqlSession中发起的多次请求,会将相同的数据缓存起来,后面相同的查询不进行SQL操作数据库
MyBatis默认支持一级缓存,不需要任何配置

一级缓存失效

json
1.sqlSession不同(每个sqlSession中的缓存相互独立)
2.sqlSession相同,查询条件不同
3.sqlSession相同,两次查询之间执行了增删改操作(增删改操作可能会对当前数据产生影响)
4.sqlSession相同,一级缓存被手动清除[session.clearCache()]
1.sqlSession不同(每个sqlSession中的缓存相互独立)
2.sqlSession相同,查询条件不同
3.sqlSession相同,两次查询之间执行了增删改操作(增删改操作可能会对当前数据产生影响)
4.sqlSession相同,一级缓存被手动清除[session.clearCache()]

二级缓存

只要是由同一个SqlSessionFactory构造的SqlSession对象,可以缓存数据
MyBatis默认是关闭二级缓存的
只要是由同一个SqlSessionFactory构造的SqlSession对象,可以缓存数据
MyBatis默认是关闭二级缓存的

操作二级缓存

1.主配置文件中设置开启功能

xml
<!--Mybatis的配置-->
<configuration>
<properties resource="jdbc.properties"/>
<!--放在该位置-->
<settings>
  <!--开启缓存-->
  <setting name="cacheEnabled" value="true"/>
</settings>
<!--Mybatis的配置-->
<configuration>
<properties resource="jdbc.properties"/>
<!--放在该位置-->
<settings>
  <!--开启缓存-->
  <setting name="cacheEnabled" value="true"/>
</settings>

2.映射文件中使用缓存

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

3.将相应的对象数据序列化

java
public class 类名 implements Serializable {}
public class 类名 implements Serializable {}

Mybatis插件原理

Mybatis会话的运行需要ParameterHandler、ResultSetHandler、StatementHandler、Executor这四大对象的配合,插件的原理就是在这四大对象调度的时候,插入一些我我们自己的代码。

image-20230207190721444

Mybatis使用JDK的动态代理,为目标对象生成代理对象。它提供了一个工具类Plugin,实现了InvocationHandler接口。

java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        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) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        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) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

使用Plugin生成代理对象,代理对象在调用方法的时候,就会进入invoke方法,在invoke方法中,如果存在签名的拦截方法,插件的intercept方法就会在这里被我们调用,然后就返回结果。如果不存在签名方法,那么将直接反射调用我们要执行的方法

MyBatis分页插件

MyBatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的原理是什么?

  • 分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,拦截Executor的query方法
  • 在执行查询的时候,拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
  • 举例:select * from student,拦截sql后重写为:select t.* from (select * from student) t limit 0, 10

如何编写一个插件?

只需要实现拦截器接口 Interceptor (org.apache.ibatis. plugin Interceptor )在实现类中对拦截对象和方法进行处理。

  • 实现Mybatis的Interceptor接口并重写intercept()方法

    java
    @Intercepts({@Signature(
            type = Executor.class,  //确定要拦截的对象
            method = "update",        //确定要拦截的方法
            args = {MappedStatement.class,Object.class}   //拦截方法的参数
    )})
    public class MyInterceptor implements Interceptor {
        
        Properties props=null;
        
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("before……");
            // 如果当前代理的是一个非代理对象,那么就会调用真实拦截对象的方法
            // 如果不是它就会调用下个插件代理对象的invoke方法
            Object obj=invocation.proceed();
            System.out.println("after……");
            return obj;
        }
    }
    @Intercepts({@Signature(
            type = Executor.class,  //确定要拦截的对象
            method = "update",        //确定要拦截的方法
            args = {MappedStatement.class,Object.class}   //拦截方法的参数
    )})
    public class MyInterceptor implements Interceptor {
        
        Properties props=null;
        
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            System.out.println("before……");
            // 如果当前代理的是一个非代理对象,那么就会调用真实拦截对象的方法
            // 如果不是它就会调用下个插件代理对象的invoke方法
            Object obj=invocation.proceed();
            System.out.println("after……");
            return obj;
        }
    }
  • MyBatis配置文件里面配置插件

    xml
    <plugins>
        <plugin interceptor="xxx.MyPlugin">
           <property name="dbType",value="mysql"/>
        </plugin>
    </plugins>
    <plugins>
        <plugin interceptor="xxx.MyPlugin">
           <property name="dbType",value="mysql"/>
        </plugin>
    </plugins>

Mapper动态代理

image-20230207190212500

  • getMapper(...)

    • 获取Mapper的过程,需要先获取MapperProxyFactory——Mapper代理工厂。

      java
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
          MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
          if (mapperProxyFactory == null) {
              throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
          } else {
              try {
                  return mapperProxyFactory.newInstance(sqlSession);
              } catch (Exception var5) {
                  throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
              }
          }
      }
      public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
          MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
          if (mapperProxyFactory == null) {
              throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
          } else {
              try {
                  return mapperProxyFactory.newInstance(sqlSession);
              } catch (Exception var5) {
                  throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
              }
          }
      }
      java
      public class MapperProxyFactory<T> {
          private final Class<T> mapperInterface;
          ……
          protected T newInstance(MapperProxy<T> mapperProxy) {
              return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
          }
      
          public T newInstance(SqlSession sqlSession) {
              MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
              return this.newInstance(mapperProxy);
          }
      }
      public class MapperProxyFactory<T> {
          private final Class<T> mapperInterface;
          ……
          protected T newInstance(MapperProxy<T> mapperProxy) {
              return Proxy.newProxyInstance(this.mapperInterface.getClassLoader(), new Class[]{this.mapperInterface}, mapperProxy);
          }
      
          public T newInstance(SqlSession sqlSession) {
              MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
              return this.newInstance(mapperProxy);
          }
      }

      动态代理对接口的绑定,它的作用就是生成动态代理对象(占位),而代理的方法被放到了MapperProxy中。MapperProxy里,通常会生成一个MapperMethod对象,它是通过cachedMapperMethod方法对其进行初始化的,然后执行excute方法。

      java
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          try {
              return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
          } catch (Throwable var5) {
              throw ExceptionUtil.unwrapThrowable(var5);
          }
      }
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          try {
              return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
          } catch (Throwable var5) {
              throw ExceptionUtil.unwrapThrowable(var5);
          }
      }
  • MapperMethod.#execute

    java
    // 这里用到了命令模式,最终还是通过SqlSession的实例去运行对象的sql。
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        Object param;
        ……
            case SELECT:
        if (this.method.returnsVoid() && this.method.hasResultHandler()) {
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (this.method.returnsMany()) {
            result = this.executeForMany(sqlSession, args);
        } else if (this.method.returnsMap()) {
            result = this.executeForMap(sqlSession, args);
        } else if (this.method.returnsCursor()) {
            result = this.executeForCursor(sqlSession, args);
        } else {
            param = this.method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(this.command.getName(), param);
            if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
                result = Optional.ofNullable(result);
            }
        }
        break;
        ……
    }
    // 这里用到了命令模式,最终还是通过SqlSession的实例去运行对象的sql。
    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        Object param;
        ……
            case SELECT:
        if (this.method.returnsVoid() && this.method.hasResultHandler()) {
            this.executeWithResultHandler(sqlSession, args);
            result = null;
        } else if (this.method.returnsMany()) {
            result = this.executeForMany(sqlSession, args);
        } else if (this.method.returnsMap()) {
            result = this.executeForMap(sqlSession, args);
        } else if (this.method.returnsCursor()) {
            result = this.executeForCursor(sqlSession, args);
        } else {
            param = this.method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(this.command.getName(), param);
            if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
                result = Optional.ofNullable(result);
            }
        }
        break;
        ……
    }