当前位置 : 首页 » 文章分类 :  开发  »  MyBatis

MyBatis

MyBatis笔记

MyBatis 官方中文文档
http://www.mybatis.org/mybatis-3/zh/index.html

MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架,即ORM(Object Relational Mapping 对象关系映射)框架。MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装。MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。

springboot(六):如何优雅的使用mybatis
http://www.ityouknow.com/springboot/2016/11/06/spring-boo-mybatis.html


MyBatis基础

随笔分类 - Mybatis
http://www.cnblogs.com/xiaoxi/category/929946.html

分类 - Mybatis
https://blog.csdn.net/hupanfeng/article/category/1443955

MyBatis的前身叫iBatis,本是apache的一个开源项目, 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis。MyBatis是支持普通SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及结果集的检索。MyBatis使用简单的XML或注解用于配置和原始映射,将接口和Java的POJOs(Plan Old Java Objects,普通的Java对象)映射成数据库中的记录。


mybatis分层架构

Mybatis的功能架构分为三层:

  1. API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
  2. 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
  3. 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

mybatis分层架构

深入浅出MyBatis-快速入门
https://blog.csdn.net/hupanfeng/article/details/9068003


mybatis SQL执行流程

在mybatis中,通过 MapperProxy 动态代理咱们的dao, 也就是说, 当咱们执行自己写的dao里面的方法的时候,其实是对应的mapperProxy在代理。
MapperProxy 中使用的就是 JDK 的动态代理

也就是 sqlSession.getMapper() 获取的其实是 UserDao 的动态代理

UserDao userMapper = sqlSession.getMapper(UserDao.class);
User insertUser = new User();

MapperProxy.invoke()

MapperProxy 的 invoke 方法中是交给 MapperMethod 去执行

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      return method.invoke(this, args);
    } else if (method.isDefault()) {
      if (privateLookupInMethod == null) {
        return invokeDefaultMethodJava8(proxy, method, args);
      } else {
        return invokeDefaultMethodJava9(proxy, method, args);
      }
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  // 交给 mapperMethod 去执行
  return mapperMethod.execute(sqlSession, args);
}

MapperMethod.execute() 源码如下,看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行 sqlSession 中的哪个方法,所以最终是通过 sqlSession 来执行的。

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        // 常用的查多行数据会走到这里
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } 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());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}

以使用较多的 selectList() 多行select语句为例

DefaultSqlSession.selectList()

在 Spring 中会先进入 SqlSessionTemplate , SqlSessionTemplate 是真正 SqlSession 的代理,再跟踪会进入 DefaultSqlSession 的 selectList

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

CachingExecutor.query() 查二级缓存

如果开启了二级缓存,会进入 CachingExecutor 的 query() 方法查二级缓存 Cache cache = ms.getCache();
如果二级缓存中没有,再交给 BaseExecutor 去查询

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
      // 查二级缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 交给 BaseExecutor 去查
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

BaseExecutor.query() 查一级缓存

然后跟踪会进入 BaseExecutorquery 方法, query 方法中会根据缓存 key 尝试去 一级缓存,也就是 SqlSession 缓存中获取,查不到再去查DB
查询完之后,如果一级缓存的类型是 STATEMENT 即语句级,每次查询完删除缓存,即完全不做缓存

@Override
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 {
      // 缓存没有的话从DB查
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }
  if (queryStack == 0) {
    for (DeferredLoad deferredLoad : deferredLoads) {
      deferredLoad.load();
    }
    // issue #601
    deferredLoads.clear();
    // 如果一级缓存的类型是 STATEMENT 即语句级,每次查询完删除缓存,即完全不做缓存
    if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
      // issue #482
      clearLocalCache();
    }
  }
  return list;
}

BaseExecutor.queryFromDatabase() 查DB后放入缓存

queryFromDatabase 中 执行完 doQuery 后,会将查询结果放入缓存 localCache.putObject(key, 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()

可以看到 SqlSession 最终是交给 Excutor 去执行的, 跟进会进入 SimpleExecutor 继承自 BaseExecutor ,最终进入 doQuery 方法

@Override
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();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

PreparedStatementHandler.query()

再跟进会进入 PreparedStatementHandlerquery 方法,里面就是使用 PreparedStatement 进行 SQL 语句执行的,回到了最初的 JDBC 。
查询到结果集后,交给 ResultSetHandler 进行结果的处理,字段的映射。

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  // 执行 SQL
  ps.execute();
  // 结果交给 resultSetHandler 处理,做映射
  return resultSetHandler.handleResultSets(ps);
}

深入浅出Mybatis系列(十)—SQL执行流程分析(源码篇)
https://www.cnblogs.com/dongying/p/4142476.html

MyBatis源码分析-SQL语句执行的完整流程
https://www.cnblogs.com/luoxn28/p/5932648.html


mybatis缓存

Mybatis中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。
一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中进行相同的SQL语句查询时,第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存1024条SQL。二级缓存是指可以跨SqlSession的缓存。

一级缓存(SqlSession缓存/local缓存)

一级缓存,又叫做 localCache 本地缓存, 是 SqlSession 级别的缓存。
在应用运行过程中,我们有可能在一次数据库会话(一个 sqlSession )中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。

每个 SqlSession 中持有了 Executor ,每个 Executor 中有一个 LocalCache 。当用户发起查询时,MyBatis根据当前执行的语句生成 MappedStatement ,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache ,最后返回结果给用户。

BaseExecutor:

protected PerpetualCache localCache;

一级缓存是在SqlSession 层面进行缓存的。即,同一个SqlSession ,多次调用同一个Mapper和同一个方法的同一个参数,只会进行一次数据库查询,然后把数据缓存到缓冲中,以后直接先从缓存中取出数据,不会直接去查数据库。

一级缓存的key是什么?

只要两条SQL的下列五个值相同,即可以认为是相同的SQL
Statement Id + Offset + Limmit + Sql + Params

insert/delete/update会删除缓存

如果是insert/delete/update方法,最终都会统一走 update 方法
update 方法中会调用 clearLocalCache() 来清除缓存

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  clearLocalCache();
  return doUpdate(ms, parameter);
}

一级缓存范围配置

MyBatis 默认开启了一级缓存,即默认 localCacheScope 就是 SESSION,除非要关闭,否则不需要额外设置。

如果需要更改一级缓存的范围,请在 Mybatis 的配置文件中,在 settings 下通过 localCacheScope 指定。
<setting name="localCacheScope" value="SESSION"/>
value 共有两个选项,SESSION 或者 STATEMENT
默认是 SESSION 级别,即在一个 MyBatis 会话中执行的所有语句,都会共享这一个缓存。
一种是 STATEMENT 级别,可以理解为缓存只对当前执行的这一个Statement有效。相当于关闭一级缓存。

或者在 java 代码中 通过 org.apache.ibatis.session.Configuration 配置

Configuration configuration = new Configuration();
configuration.setLocalCacheScope(LocalCacheScope.SESSION);

一级缓存的范围有SESSION和STATEMENT两种,默认是SESSION,如果我们不需要使用一级缓存,那么我们可以把一级缓存的范围指定为STATEMENT,这样每次执行完一个Mapper语句后都会将一级缓存清除。如果需要更改一级缓存的范围,请在Mybatis的配置文件中,在settings下通过localCacheScope指定。

实现原理:
在 BaseExecutor.query 方法执行的最后,会判断一级缓存级别是否是 STATEMENT 级别,如果是的话,就清空缓存,这也就是 STATEMENT 级别的一级缓存无法共享 localCache 的原因。

一级缓存的SqlSession间隔离

1、在修改操作后执行的相同查询,会查询数据库,一级缓存失效。
2、开启 sqlSession1, sqlSession2 两个会话,sqlSession1 查询 id为1的学生信息,然后 sqlSession2 更新id为1的学生的姓名,然后 sqlSession1 继续查询 id为1的学生信息,查到的还是旧值,说明两个 SqlSession 之间是隔离的,且隔离级别是可重复读。


二级缓存(跨SqlSession,Mapper级)

一级缓存的共享范围只在一个SqlSession内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor ,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库

二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
二级缓存与一级缓存区别,二级缓存的范围更大,多个sqlSession可以共享一个Mapper的二级缓存区域。

二级缓存是mapper级别的,也就说不同的sqlsession使用同一个mapper查询是,查询到的数据可能是另一个sqlsession做相同操作留下的缓存。

每一个namespace的mapper都有一个二缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同 的二级缓存区域中。

MyBatis开启二级缓存配置

默认二级缓存是不开启的,需要手动进行配置。

第1步,全局设置 cacheEnabled = true

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

或者在 java 代码中

Configuration configuration = new Configuration();
// 开启二级缓存
configuration.setCacheEnabled(true);

第2步,在 mapper.xml 中开启二缓存,mapper.xml 下的sql执行完成会存储到它的缓存区
在要开启二级缓存的 mapper.xml 中增加
<cache/>
这个配置项有如下参数可自定义:
type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
eviction: 定义回收的策略,常见的有FIFO,LRU。
flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
size: 最多缓存对象的个数。
readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
blocking: 若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

还可以 <cache-ref namespace="mapper.StudentMapper"/> 在 mapper1.xml 中引用 mapper2.xml 的缓存,达到2个 mapper 共用缓存的目的

但我不知道完全用 interface 写的 mapper 接口该如何开启二级缓存。

MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession之间缓存数据的共享,同时粒度更加的细,能够到namespace级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。

聊聊MyBatis缓存机制 - 美团技术团队
https://tech.meituan.com/2018/01/19/mybatis-cache.html
文中的演示代码:
kailuncen / mybatis-cache-demo
https://github.com/kailuncen/mybatis-cache-demo

Mybatis缓存介绍
https://blog.csdn.net/u010643307/article/details/70148723

mybatis一级与二级缓存详解
https://www.cnblogs.com/cuibin/articles/6827116.html

MyBatis 一、二级缓存和自定义缓存
https://www.cnblogs.com/moongeek/p/7689683.html

深入理解MyBatis——缓存
https://blog.csdn.net/qq_37169817/article/details/78985527


mybatis拦截器(插件)

拦截器有什么用?

拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法。Mybatis拦截器设计的一个初衷就是为了供用户在某些时候可以实现自己的逻辑而不必去动Mybatis固有的逻辑。打个比方,对于Executor,Mybatis中有几种实现:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。这个时候如果你觉得这几种实现对于Executor接口的query方法都不能满足你的要求,那怎么办呢?是要去改源码吗?当然不。我们可以建立一个Mybatis拦截器用于拦截Executor接口的query方法,在拦截之后实现自己的query方法逻辑,之后可以选择是否继续执行原来的query方法。

【myBatis】Mybatis中的拦截器
https://blog.csdn.net/moshenglv/article/details/52699976

拦截器的种类

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
1、Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
2、ParameterHandler (getParameterObject, setParameters)
3、ResultSetHandler (handleResultSets, handleOutputParameters)
4、StatementHandler (prepare, parameterize, batch, update, query)

我们看到了可以拦截Executor接口的部分方法,比如update,query,commit,rollback等方法,还有其他接口的一些方法等。

总体概括为:
拦截执行器的方法
拦截参数的处理
拦截结果集的处理
拦截Sql语法构建的处理

MyBatis拦截器原理探究
http://www.importnew.com/24143.html

拦截器的实现原理(JDK动态代理)

Mybatis的拦截器实现机制,使用的是JDK的InvocationHandler.
当我们调用ParameterHandler,ResultSetHandler,StatementHandler,Executor的对象的时候,
实际上使用的是Plugin这个代理类的对象,这个类实现了InvocationHandler接口.
接下来我们就知道了,在调用上述被代理类的方法的时候,就会执行Plugin的invoke方法.
Plugin在invoke方法中根据@Intercepts的配置信息(方法名,参数等)动态判断是否需要拦截该方法.
再然后使用需要拦截的方法Method封装成Invocation,并调用Interceptor的proceed方法.
这样我们就达到了拦截目标方法的结果.


mybatis拦截器原理

Mybatis那些事-拦截器(Plugin+Interceptor)
https://blog.csdn.net/yhjyumi/article/details/49188051


Interceptor接口

Mybatis为我们提供了一个Interceptor接口,通过实现该接口就可以定义我们自己的拦截器。我们先来看一下这个接口的定义:

package org.apache.ibatis.plugin;

import java.util.Properties;

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}

我们可以看到在该接口中一共定义有三个方法,intercept、plugin和setProperties。plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法,当然也可以调用其他方法,这点将在后文讲解。setProperties方法是用于在Mybatis配置文件中指定一些属性的。

定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法

【myBatis】Mybatis中的拦截器
https://blog.csdn.net/moshenglv/article/details/52699976


自定义拦截器

1、实现Interceptor接口,使用@Intercepts注解配置拦截信息,重写其中的方法,实现自己的逻辑
2、在配置文件中添加plugin元素,注册自定义的拦截器,及配置参数

【myBatis】Mybatis中的拦截器
https://blog.csdn.net/moshenglv/article/details/52699976

自定义拦截器示例:

package com.tiantian.mybatis.interceptor;

import java.sql.Connection;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

@Intercepts( {
      @Signature(method = "query", type = Executor.class, args = {
              MappedStatement.class, Object.class, RowBounds.class,
              ResultHandler.class }),
      @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) })
public class MyInterceptor implements Interceptor {

    public Object intercept(Invocation invocation) throws Throwable {
      Object result = invocation.proceed();
      System.out.println("Invocation.proceed()");
      return result;
    }

    public Object plugin(Object target) {
      return Plugin.wrap(target, this);
    }

    public void setProperties(Properties properties) {
      String prop1 = properties.getProperty("prop1");
      String prop2 = properties.getProperty("prop2");
      System.out.println(prop1 + "------" + prop2);
    }

}

@Intercepts和@Signature注解

对于实现自己的Interceptor而言有两个很重要的注解,一个是@Intercepts,其值是一个@Signature数组。@Intercepts用于表明当前的对象是一个Interceptor,而@Signature则表明要拦截的接口、方法以及对应的参数类型。

MyInterceptor类上我们用@Intercepts标记了这是一个Interceptor,然后在@Intercepts中定义了两个@Signature,即两个拦截点。
第一个@Signature我们定义了该Interceptor将拦截Executor接口中参数类型为MappedStatement、Object、RowBounds和ResultHandler的query方法;
第二个@Signature我们定义了该Interceptor将拦截StatementHandler中参数类型为Connection的prepare方法。

setProperties()方法

这个方法在Configuration初始化当前的Interceptor时就会执行,用于获取相关配置参数。

plugin()方法

plugin方法是拦截器用于封装目标对象的,通过该方法我们可以返回目标对象本身,也可以返回一个它的代理。当返回的是代理的时候我们可以对其中的方法进行拦截来调用intercept方法

intercept()方法

定义自己的Interceptor最重要的是要实现plugin方法和intercept方法,在plugin方法中我们可以决定是否要进行拦截进而决定要返回一个什么样的目标对象。而intercept方法就是要进行拦截的时候要执行的方法。

在每个拦截器的intercept方法内,最后一个语句一定是returninvocation.proceed()(不这么做的话拦截器链就断了,你的mybatis基本上就不能正常工作了)。invocation.proceed()只是简单的调用了下target的对应方法,如果target还是个代理,就又回到了上面的Plugin.invoke方法了。这样就形成了拦截器的调用链推进。

深入浅出Mybatis-插件原理
https://blog.csdn.net/hupanfeng/article/details/9247379

Plugin类及wrap(),invoke()方法

MyBatis提供了一个Plugin类的实现,里面有一个静态方法wrap(Object target,Interceptor interceptor),通过该方法可以决定要返回的对象是目标对象还是对应的代理。
源码如下:

package org.apache.ibatis.plugin;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.ibatis.reflection.ExceptionUtil;

public class Plugin implements InvocationHandler {

  private Object target;
  private Interceptor interceptor;
  private Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

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

  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
  ...
  }

  private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
  ...
  }
}

我们先看一下Plugin的wrap方法,它根据当前的Interceptor上面的注解定义哪些接口需要拦截,然后判断当前目标对象是否有实现对应需要拦截的接口,如果没有则返回目标对象本身,如果有则返回一个代理对象。而这个代理对象的InvocationHandler正是一个Plugin。所以当目标对象在执行接口方法时,如果是通过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。

所以接着我们来看一下该invoke方法的内容。这里invoke方法的逻辑是:如果当前执行的方法是定义好的需要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数传递给当前拦截器的intercept方法。如果不需要拦截,则直接调用当前的方法。Invocation中定义了定义了一个proceed方法,其逻辑就是调用当前方法,所以如果在intercept中需要继续调用当前方法的话可以调用invocation的procced方法。

配置文件中注册自定义拦截器

注册拦截器是通过在Mybatis配置文件中plugins元素下的plugin元素来进行的。一个plugin对应着一个拦截器,在plugin元素下面我们可以指定若干个property子元素。Mybatis在注册定义的拦截器时会先把对应拦截器下面的所有property通过Interceptor的setProperties方法注入给对应的拦截器。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="config/jdbc.properties"></properties>
    <typeAliases>
      <package name="com.tiantian.mybatis.model"/>
    </typeAliases>
    <plugins>
      <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor">
          <property name="prop1" value="prop1"/>
          <property name="prop2" value="prop2"/>
      </plugin>
    </plugins>
    <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>
    <mappers>
      <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/>
    </mappers>
</configuration>

拦截器工作工作流程

1、XMLConfigBuilder解析mybatis配置文件,通过反射实例化plugin节点中的interceptor属性表示的类。
2、然后调用全局配置类Configuration的addInterceptor方法,将此拦截器添加到拦截器链中。

public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

这个interceptorChain是Configuration的内部属性,类型为InterceptorChain,也就是一个拦截器链,里面维护这一个ArrayList()

3、Configuration先后创建Executor,ParameterHandler,ResultSetHandler,StatementHandler这四个对象的实例,例化了对应的对象之后,都会调用interceptorChain的pluginAll方法,InterceptorChain的pluginAll刚才已经介绍过了,就是遍历所有的拦截器,然后调用各个拦截器的plugin方法。

4、Plugin的wrap(Object target,Interceptor interceptor)中,判断当前目标对象是否有实现对应需要拦截的接口,如果没有则返回目标对象本身,如果有则返回一个代理对象。如果是通过代理对象执行的,则会调用对应InvocationHandler的invoke方法,也就是Plugin的invoke方法。

5、Plugin的invoke方法中,如果当前执行的方法是定义好的需要拦截的方法,则把目标对象、要执行的方法以及方法参数封装成一个Invocation对象,再把封装好的Invocation作为参数传递给当前拦截器的intercept方法。

6、所以,需要拦截的方法最终被交给Interceptor接口实现类的intercept(Invocation invocation)方法去拦截执行。

MyBatis拦截器原理探究
http://www.importnew.com/24143.html


mybatis分页

逻辑分页(内存分页)和物理分页

物理分页
物理分页指的是在SQL查询过程中实现分页,依托与不同的数据库厂商,实现也会不同。比如MySQL数据库提供了limit关键字,程序员只需要编写带有limit关键字的SQL语句,数据库返回的就是分页结果。

逻辑分页
逻辑分页依赖的是程序员编写的代码。数据库返回的不是分页结果,而是全部数据,然后再由程序员通过代码获取分页数据,常用的操作是一次性从数据库中查询出全部数据并存储到List集合中,因为List集合有序,再根据索引获取指定范围的数据。

不同数据库的物理分页语句

不同数据库对物理分页语句的支持
mysql : limit
SQL service: top
oracle: rownum
PostgreSQL: limit

hibernate和MyBatis对分页的支持

常用orm框架采用的分页技术:
1、hibernate采用的是物理分页;
2、MyBatis使用RowBounds实现的分页是逻辑分页,也就是先把数据记录全部查询出来,然在再根据offset和limit截断记录返回(数据量大的时候会造成内存溢出),不过可以用插件或其他方式能达到物理分页效果。
为了在数据库层面上实现物理分页,又不改变原来MyBatis的函数逻辑,可以编写plugin截获MyBatis Executor的statementhandler,重写SQL来执行查询

对比和使用场景

对比
1、物理分页数据库压力大,逻辑分页服务器内存压力大
物理分页每次都访问数据库,逻辑分页只访问一次数据库,物理分页对数据库造成的负担大。
逻辑分页一次性将数据读取到内存,占用了较大的内容空间,物理分页每次只读取一部分数据,占用内存空间较小。

2、实时性
逻辑分页一次性将数据读取到内存,数据发生改变,数据库的最新状态不能实时反映到操作中,实时性差。物理分页每次需要数据时都访问数据库,能够获取数据库的最新状态,实时性强。

适用场合
逻辑分页主要用于数据量不大、数据稳定的场合,物理分页主要用于数据量较大、更新频繁的场合。

物理分页与逻辑分页
https://www.cnblogs.com/tonghun/p/7122801.html


利用MyBatis的RowBounds分页(逻辑分页)

Mybatis 如何分页查询?
Mysql 中可以使用 limit 语句,但 limit 并不是标准 SQL 中的,如果是其它的数据库,则需要使用其它语句。
MyBatis 提供了 RowBounds 类,用于实现分页查询。RowBounds 中有两个数字,offset 和 limit。

Mybatis 的逻辑分页比较简单,简单来说就是取出所有满足条件的数据,然后舍弃掉前面 offset 条数据,然后再取剩下的数据的 limit 条

RowBounds使用方法

1、Mapper 接口中的sql语句添加RowBounds参数

List<Student> queryStudentsBySql(RowBounds rowBounds);

2、Mapper.xml文件中不需要任何改动,还是原来的sql

3、代码中调用DAO的sql时传入RowBounds参数

public List<Student> queryStudentsBySql(int currPage, int pageSize) {
    return studentMapper.queryStudentsBySql(new RowBounds(currPage,pageSize));
}

RowBounds实现原理

逻辑分页的实现原理:
在DefaultResultSetHandler中,逻辑分页会将所有的结果都查询到,然后根据RowBounds中提供的offset和limit值来获取最后的结果。

DefaultResultSetHandler实现如下:

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
    //跳过RowBounds设置的offset值
    skipRows(rsw.getResultSet(), rowBounds);
    //判断数据是否小于limit,如果小于limit的话就不断的循环取值
    while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
      Object rowValue = getRowValue(rsw, discriminatedResultMap);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
    }
  }

  private boolean shouldProcessMoreRows(ResultContext<?> context, RowBounds rowBounds) throws SQLException {
    //判断数据是否小于limit,小于返回true
    return !context.isStopped() && context.getResultCount() < rowBounds.getLimit();
  }

  //跳过不需要的行,应该就是rowbounds设置的limit和offset
  private void skipRows(ResultSet rs, RowBounds rowBounds) throws SQLException {
    if (rs.getType() != ResultSet.TYPE_FORWARD_ONLY) {
      if (rowBounds.getOffset() != RowBounds.NO_ROW_OFFSET) {
        rs.absolute(rowBounds.getOffset());
      }
    } else {
      //跳过RowBounds中设置的offset条数据
      for (int i = 0; i < rowBounds.getOffset(); i++) {
        rs.next();
      }
    }
  }

Mybatis逻辑分页原理解析RowBounds
https://blog.csdn.net/qq924862077/article/details/52611848


分页插件PageHelper(别人写好的过滤器)(物理分页)

官网
https://pagehelper.github.io/

使用方法:
1、添加jar包依赖
使用 PageHelper 你只需要在 classpath 中包含 pagehelper-x.x.x.jar 和 jsqlparser-0.9.5.jar。
如果你使用 Maven,你只需要在 pom.xml 中添加下面的依赖:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>最新版本</version>
</dependency>

2、注册Plugin和配置参数
在MyBatis配置文件中:

<plugins>
    <plugin interceptor="com.github.pagehelper.PageHelper">
        <property name="dialect" value="mysql"/>
        <property name="offsetAsPageNum" value="false"/>
        <property name="rowBoundsWithCount" value="false"/>
        <property name="pageSizeZero" value="true"/>
        <property name="reasonable" value="false"/>
        <property name="supportMethodsArguments" value="false"/>
        <property name="returnPageInfo" value="none"/>
    </plugin>
</plugins>

3、Mapper接口中的sql语句无需任何改动
List queryStudentsBySql();

4、Mapper.xml文件中不需要任何改动,还是原来的sql

5、代码中调用DAO的sql时添加PageHelper:

public List<Student> queryStudentsBySql(int currPage, int pageSize) {
    PageHelper.startPage(currentPage, pageSize);
    return studentMapper.queryStudentsBySql(new RowBounds(currPage,pageSize));
}

优点:PageHelper的优点是,分页和Mapper.xml完全解耦,不侵入Mapper代码。实现原理是MyBatis拦截器,属于物理分页。

Mybatis分页插件PageHelper的配置和使用方法
https://www.cnblogs.com/kangoroo/p/7998433.html

分页的线程安全性(内部使用ThreadLocal保存分页参数)

什么时候会导致不安全的分页?

PageHelper 方法使用了静态的 ThreadLocal 参数,分页参数和线程是绑定的。

只要你可以保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,这就是安全的。因为 PageHelper 在 finally 代码段中自动清除了 ThreadLocal 存储的对象。

但是如果你写出下面这样的代码,就是不安全的用法:

PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
    list = countryMapper.selectIf(param1);
} else {
    list = new ArrayList<Country>();
}

这种情况下由于 param1 存在 null 的情况,就会导致 PageHelper 生产了一个分页参数,但是没有被消费,这个参数就会一直保留在这个线程上。当这个线程再次被使用时,就可能导致不该分页的方法去消费这个分页参数,这就产生了莫名其妙的分页。

Mybatis分页插件PageHelper简单使用
https://www.cnblogs.com/lxl57610/p/7766146.html


使用拦截器重写sql添加limit子句(物理分页)

MyBatis精通之路之分页功能的实现(数组分页、sql分页、拦截器,RowBounds分页)
https://blog.csdn.net/chenbaige/article/details/70846902

深入浅出Mybatis-分页
https://blog.csdn.net/hupanfeng/article/details/9265341


查出所有结果在代码中分页(逻辑分页)

进行数据库查询操作时,获取到数据库中所有满足条件的记录,保存在应用的临时数组中,再通过List的subList方法,获取到满足条件的所有记录。

缺点:数据库查询并返回所有的数据,而我们需要的只是极少数符合要求的数据。当数据量少时,还可以接受。当数据库数据量过大时,每次查询对数据库和程序的性能都会产生极大的影响。

手动写limit子句(物理分页)

直接在数据库语言中只检索符合条件的记录,不需要在通过程序对其作处理。
limit关键字的用法:
LIMIT [offset,] rows
offset指定要返回的第一行的偏移量(默认为0),rows第二个指定返回行的最大数目。初始行的偏移量是0(不是1)。

首先还是在StudentMapper接口中添加sql语句查询的方法,如下:
List queryStudentsBySql(Map<String,Object> data);

然后在StudentMapper.xml文件中编写sql语句通过limiy关键字进行分页:

<select id="queryStudentsBySql" parameterType="map" resultMap="studentmapper">
        select * from student limit #{currIndex} , #{pageSize}
</select>

代码中调用DAO层接口,传入两个参数:

public List<Student> queryStudentsBySql(int currPage, int pageSize) {
    Map<String, Object> data = new HashedMap();
    data.put("currIndex", (currPage-1)*pageSize);
    data.put("pageSize", pageSize);
    return studentMapper.queryStudentsBySql(data);
}

缺点:虽然这里实现了按需查找,每次检索得到的是指定的数据。但是每次在分页的时候都需要去编写limit语句,很冗余。而且不方便统一管理,维护性较差。所以我们希望能够有一种更方便的分页实现。


mybatis动态sql

#与$的区别

#{}: 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,一个 #{ } 被解析为一个参数占位符 。可以防止sql注入等等问题,所以在大多数情况下还是经常使用#{}
${}: 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。

#{}会在变量的值外面加引号
${}不对变量值进行任何更改直接拼接

sql注入举例

SELECT fieldlist FROM table WHERE field = '$EMAIL';

EMAIL参数传入 anything' OR 1=1

SELECT fieldlist FROM table WHERE field = 'anything' OR 1=1;

而使用


BaseTypeHandler枚举自动转换

如何在MyBatis中优雅的使用枚举
https://segmentfault.com/a/1190000010755321


Java API

通用Mapper

轻松搞定增删改查 - Mybatis通用Mapper介绍与使用
http://www.ciphermagic.cn/mybatis-mapper.html

问题

text类型为空

mybatis selectByExample() 方法返回的text类型字段为空,但数据库里有值,后来发现还有个 selectByExampleWithBLOBs() , 只有后缀带WithBLOBs的select方法才能查到text类型内容。

Invalid bound statement (not found)

一开始以为是没有配 maven 的 <build> <resources> <resource>,导致mapper.xml没有打包进去,从而找不到xml,后来仔细检查发现.xml在包中,但还是提示这个错误。
https://blog.csdn.net/hello_world_qwp/article/details/79030823
https://blog.csdn.net/k469785635/article/details/77532512

后来发现是application.properties中没有配置 mybatis.mapperLocations 属性

mybatis.mapperLocations=classpath:mapper/*.xml

mybatis.mapperLocations 属性

当mybatis的xml文件和mapper接口不在相同包下时,需要用mapperLocations属性指定xml文件的路径。*是个通配符,代表所有的文件,**代表所有目录下

如果Mapper.xml与Mapper.class在同一个包下且同名,spring扫描Mapper.class的同时会自动扫描同名的Mapper.xml并装配到Mapper.class。
如果Mapper.xml与Mapper.class不在同一个包下或者不同名,就必须使用配置mapperLocations指定mapper.xml的位置。
此时spring是通过识别mapper.xml中的 <mapper namespace="com.fan.mapper.UserDao"> namespace的值来确定对应的Mapper.class的。

官方文档:
如果 MyBatis 映射器 XML 文件在和映射器类相同的路径下不存在,那么另外一个需要 配置文件的原因就是它了。使用这个配置,有两种选择。第一是手动在 MyBatis 的 XML 配 置文件中使用<mappers>部分来指定类路径。第二是使用工厂 bean 的 mapperLocations 属 性。
mapperLocations 属性使用一个资源位置的 list。 这个属性可以用来指定 MyBatis 的 XML 映射器文件的位置。 它的值可以包含 Ant 样式来加载一个目录中所有文件, 或者从基路径下 递归搜索所有路径。比如:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations" value="classpath*:sample/config/mappers/**/*.xml" />
</bean>

这会从类路径下加载在 sample.config.mappers 包和它的子包中所有的 MyBatis 映射器 XML 文件。

或者设置 SqlSessionFactoryBean 的对应属性:

ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml"));

或者在spring配置文件中:

mybatis.mapperLocations=classpath:mapper/*.xml

Springboot mybatis集成 Invalid bound statement (not found)
https://blog.csdn.net/qq_35981283/article/details/78590090

SqlSessionFactoryBean
http://www.mybatis.org/spring/zh/factorybean.html


@Mapper 和 @MapperScan

自定义mapper接口后,为了让mybatis将他扫描成bean,需要在每个接口上加 @Mapper 注解,如果嫌在每个接口上加太麻烦可以使用 @MapperScan 注解指定要扫描的包

使用@Mapper注解

为了让DemoMapper能够让别的类进行引用,我们可以在DemMapper类上添加@Mapper注解:

@Mapper
public interface DemoMapper {
    @Insert("insert into Demo(name) values(#{name})")
    @Options(keyProperty="id",keyColumn="id",useGeneratedKeys=true)
    public void save(Demo demo);
}

直接在Mapper类上面添加注解@Mapper,这种方式要求每一个mapper类都需要添加此注解,麻烦。

使用@MapperScan注解

通过使用@MapperScan可以指定要扫描的Mapper类的包的路径,可以用*通配符,也可以同时指定多个包路径

@SpringBootApplication
@MapperScan({
  "com.kfit.*.mapper",
  "com.kfit.user"
})
public class App {
    public static void main(String[] args) {
       SpringApplication.run(App.class, args);
    }
}

@MapperScan 扫描不同包中的同名Mapper导致冲突

例如我们应用有两个数据源,mysql和tidb,要把这两个库的mapper都扫描上:

@MapperScan({"com.mysql.mapper", "com.tidb.mapper"})
@SpringBootApplication
public class Application implements ApplicationRunner {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

但如果这两个包中都有 UserInfoMapper, 就会导致冲突而无法启动,如下:

@2019-01-08 19:33:28,602 [ERROR] RID= UID= AID= MOBILE= -- [main] org.springframework.boot.SpringApplication [771]: Application startup failed
org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'userInfoMapper' for bean class [com.masikkk.tidb.mapper.UserMapper] conflicts with existing, non-compatible bean definition of same name and class [org.mybatis.spring.mapper.MapperFactoryBean]
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.checkCandidate(ClassPathBeanDefinitionScanner.java:345) ~[spring-context-4.3.16.RELEASE.jar:4.3.16.RELEASE]
    at org.mybatis.spring.mapper.ClassPathMapperScanner.checkCandidate(ClassPathMapperScanner.java:237) ~[mybatis-spring-1.3.0.jar:1.3.0]
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:283) ~[spring-context-4.3.16.RELEASE.jar:4.3.16.RELEASE]
    at org.mybatis.spring.mapper.ClassPathMapperScanner.doScan(ClassPathMapperScanner.java:164) ~[mybatis-spring-1.3.0.jar:1.3.0]
    at org.mybatis.spring.annotation.MapperScannerRegistrar.registerBeanDefinitions(MapperScannerRegistrar.java:105) ~[mybatis-spring-1.3.0.jar:1.3.0]
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:359) ~[spring-context-4.3.16.RELEASE.jar:4.3.16.RELEASE]

网上有人问过这个问题,问 @MapperScan 是否可以增加一个 排除列表,像 Spring的 @ComponentScanexcludeFilters 参数一样,结果是支持不了。解决方法只能是改表名,两个数据源的mapper包中不能有重名的mapper

could @MapperScan annotaion support excludeFilter?
I write a base abstract mapper and put it into my mapper package, but i don’t want it to be scanned as a bean.
I can only figure out three solutions:
1.move the abstract mapper to another package, which seems stupid
2.similar to 1,put the abstract mapper to mapper.base and others to mapper.whateverlelse
2.add@Component or @Repository to every mapper rather than using @MapperScan automatically

MapperScan excludeFilters support
http://mybatis-user.963551.n3.nabble.com/MapperScan-excludeFilters-support-td4029831.html


SqlSession

MyBatis常用对象SqlSessionFactory和SqlSession介绍和运用
https://blog.csdn.net/u013412772/article/details/73648537

SqlSession创建过程

(1)定义一个Configuration对象,其中包含数据源、事务、mapper文件资源以及影响数据库行为属性设置settings
(2)通过配置对象,则可以创建一个SqlSessionFactoryBuilder对象 或者 SqlSessionFactoryBean
(3)通过 SqlSessionFactoryBuilder 或 SqlSessionFactoryBean 获得SqlSessionFactory 的实例。
(4)SqlSessionFactory 的实例可以获得操作数据的SqlSession实例,通过这个实例对数据库进行操作

构建SqlSession并操作数据库实例

package com.cn.testIUserService;

import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import com.cn.entity.User;

public class MyBatisTest {

    public static void main(String[] args) {
        try {
            //读取mybatis-config.xml文件
            InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml");
            //初始化mybatis,创建SqlSessionFactory类的实例
            SqlSessionFactory sqlSessionFactory =  new SqlSessionFactoryBuilder().build(resourceAsStream);
            //创建session实例
            SqlSession session = sqlSessionFactory.openSession();
            /*
             * 接下来在这里做很多事情,到目前为止,目的已经达到得到了SqlSession对象.通过调用SqlSession里面的方法,
             * 可以测试MyBatis和Dao层接口方法之间的正确性,当然也可以做别的很多事情,在这里就不列举了
             */
            //插入数据
            User user = new User();
            user.setC_password("123");
            user.setC_username("123");
            user.setC_salt("123");
            //第一个参数为方法的完全限定名:位置信息+映射文件当中的id
            session.insert("com.cn.dao.UserMapping.insertUserInformation", user);
            //提交事务
            session.commit();
            //关闭session
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

SqlSessionFactoryBean

在基础的 MyBatis 用法中,是通过 SqlSessionFactoryBuilder 来创建 SqlSessionFactory 的。 而在 MyBatis-Spring 中,则使用 SqlSessionFactoryBean 来创建。

方式一、XML中通过 SqlSessionFactoryBean 配置 sqlSessionFactory bean

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="mapperLocations" value="classpath*:sample/config/mappers/**/*.xml" />
</bean>

方式二、java 代码中通过 SqlSessionFactoryBean 配置 sqlSessionFactory bean

@Bean
public SqlSessionFactory sqlSessionFactory() {
  SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  sqlSessionFactoryBean.setDataSource(dataSource());
  sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
  return factoryBean.getObject();
}

通常,在 MyBatis-Spring 中,你不需要直接使用 SqlSessionFactoryBean 或对应的 SqlSessionFactory。相反,session 的工厂 bean 将会被注入到 MapperFactoryBean 或其它继承于 SqlSessionDaoSupport 的 DAO(Data Access Object,数据访问对象)中。

SqlSessionFactoryBean
http://www.mybatis.org/spring/zh/factorybean.html


sql日志

所有通过 mybatis 执行的sql,都会打出相关 debug 日志,能看到预编译sql和传入的动态参数
必须开启debug级别日志才能看到,例如:

2019-06-03 12:04:40,412 [DEBUG] [XNIO-1 task-1] com.masikkk.mapper.UserMapper.countUserByUserUuids [145]: ==>  Preparing: SELECT ur.user_uuid, COUNT(*) AS amount FROM user_register ur INNER JOIN user_type ut ON ur.user_type_id = ut.id WHERE ur.user_uuid IN ( ? ) AND ut.type IN ( ? ) GROUP BY ur.user_uuid
2019-06-03 12:04:40,413 [DEBUG] [XNIO-1 task-1] com.masikkk.mapper.UserMapper.countUserByUserUuids [145]: ==> Parameters: 6824186360112128(Long), 4(Byte)
2019-06-03 12:04:40,428 [DEBUG] [XNIO-1 task-1] com.masikkk.mapper.UserMapper.countUserByUserUuids [145]: <==      Total: 1

SQL语句

注意事项

内部类作为返回结果必须是静态类

假如有如下sql

// 统计user的地址数
@Select("SELECT user_id, COUNT(*) FROM user_address GROUP BY user_uuid")
List<UserAddressCount> countAddressNum();

其中返回结果是个内部类

class ResultBean {
    private String name;
    ...

    class UserAddressCount {
        private Long userId;
        private Integer count;
    }

    getter...
    setter...
}

执行时会报错:

No constructor found in com.masikkk.ResultBean$UserAddressCount matching [java.lang.Long, java.lang.Integer]

原因是 UserAddressCount 非静态类,构造函数中的第一个参数是 父类的this指针,所以构造函数的签名是 (ResultBean, Long, Integer) 自然和mybatis期望的 (Long, Integer) 匹配不上,所以报找不到构造函数的错误。
解决方法可以将 UserAddressCount 拿出来单独写成一个类,或者写成静态内部类: public static class UserAddressCount

mybatis支持返回内部类吗?如果是,该如何实现?
https://segmentfault.com/q/1010000010436451/

集合判空中必须用小写and

比如根据user_ids列表批量查询user信息,在mybatis动态sql中进行集合判空

// 根据user_ids查询user信息
@Select("<script>" +
        "SELECT * FROM user WHERE status = 'enable'" +
        "<if test = 'user_ids != null and user_ids.size() > 0'>" +
        "AND id IN " +
        "  <foreach item='user_id' collection='user_ids' open='(' separator=',' close=')'>" +
        "      #{user_id}" +
        "  </foreach>" +
        "</if>" +
        "</script>")
List<Long> queryUsersByIds(@Param("user_ids") List<Long> userIds);

注意 test 表达式中的 and 必须小写,如果改成大写mybatis会报如下错误,提示 AND 是非法表达式

org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.builder.BuilderException: Error evaluating expression 'user_ids != null AND user_ids.size() > 0'. Cause: org.apache.ibatis.ognl.ExpressionSyntaxException: Malformed OGNL expression: user_ids != null AND user_ids.size() > 0 [org.apache.ibatis.ognl.ParseException: Encountered " <IDENT> "AND "" at line 1, column 23.
Was expecting one of:
    <EOF>
    "," ...
    "=" ...
    "?" ...
    "||" ...
    "or" ...
    "&&" ...
    "and" ...
    "|" ...
    "bor" ...
    "^" ...
    "xor" ...
    "&" ...
    "band" ...
    "==" ...
    "eq" ...
    "!=" ...
    "neq" ...
    "<" ...
    "lt" ...
    ">" ...
    "gt" ...
    "<=" ...
    "lte" ...
    ">=" ...
    "gte" ...
    "in" ...
    "not" ...
    "<<" ...
    "shl" ...
    ">>" ...
    "shr" ...
    ">>>" ...
    "ushr" ...
    "+" ...
    "-" ...
    "*" ...
    "/" ...
    "%" ...
    "instanceof" ...
    "." ...
    "(" ...
    "[" ...
    <DYNAMIC_SUBSCRIPT> ...
    ]
    at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:79) ~[mybatis-spring-1.3.0.jar:1.3.0]
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:447) ~[mybatis-spring-1.3.0.jar:1.3.0]
    at com.sun.proxy.$Proxy121.selectList(Unknown Source) ~[?:?]
    at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:231) ~[mybatis-spring-1.3.0.jar:1.3.0]
    at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:128) ~[mybatis-3.4.0.jar:3.4.0]
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:68) ~[mybatis-3.4.0.jar:3.4.0]
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:53) ~[mybatis-3.4.0.jar:3.4.0]
    at com.sun.proxy.$Proxy125.queryCarOrderRelationsByCarOrderIds(Unknown Source) ~[?:?]

大于小于号

普通字符串sql中直接写小于号<

无动态sql语句的字符串中直接写小于号

@Select("SELECT car_order_id FROM car_order_relation WHERE user_id = #{user_id} AND relation = #{relation} AND create_time <= #{create_time} ")
List<CarOrder> queryCarOrderIdNoStatusByUserIdAndTime(@Param("user_id") long userId, @Param("relation") byte relation, @Param("create_time") Date createTime);

如果写成 &lt; 反而报错:
bad SQL grammar []; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ‘;= ‘2018-11-24 14:53:47’ )’ at line 1

动态sql标签中小于号必须转义为&lt;

动态sql中则必须使用 &lt; 代替小于号,否则报错
大于号不用动

org.xml.sax.SAXParseException: 元素内容必须由格式正确的字符数据或标记组成。
// 根据用户id,关系,起止时间查询用户历史
@Select({ "<script>"
        + "SELECT * FROM user_transaction"
        + "  <trim prefix = 'WHERE' prefixOverrides = 'AND |OR'>"
        + "    <if test = 'user_id != null'> AND user_id = #{user_id} </if>"
        + "    <if test = 'start_time != null'> AND create_time &gt;= #{start_time} </if>"
        + "    <if test = 'end_time != null'> AND create_time &lt;= #{end_time} </if>"
        + "    <if test = 'relations != null and relations.size() > 0'>"
        + "       AND relation IN "
        + "       <foreach item='item' collection='relations' open='(' separator=',' close=')'>"
        + "           #{item}"
        + "       </foreach>"
        + "    </if>"
        + "  </trim>"
        + "ORDER BY create_time DESC LIMIT #{offset},#{count}"
        + "</script>"
})
List<FellowUserRelationshipTransaction> queryFellowUserRelationshipTransaction(@Param("user_id") Long userId,
        @Param("relations") List<Short> relations, @Param("start_time") Date startTime, @Param("end_time") Date endTime,
        @Param("offset") long offset, @Param("count") int count);

insert和insertSelective区别

看MyBatis自动生成的mapper.xml很容易看出区别:
insertSelective对应的sql语句加入了NULL校验,即只会插入数据不为null的字段值。
insert则会插入所有字段,会插入null。

updateByPrimaryKey和updateByPrimaryKeySelective区别

updateByPrimaryKey对你注入的字段全部更新(不判断是否为Null)
updateByPrimaryKeySelective会对字段进行判断再更新(如果为Null就忽略更新)

select

<!-- 查询学生,根据id -->
<select id="getStudent" parameterType="String" resultMap="studentResultMap">
    SELECT ST.STUDENT_ID,
           ST.STUDENT_NAME,
           ST.STUDENT_SEX,
           ST.STUDENT_BIRTHDAY,
           ST.CLASS_ID
    FROM STUDENT_TBL ST
    WHERE ST.STUDENT_ID = #{studentID}
</select>

这条语句就叫做getStudent,有一个String参数,并返回一个StudentEntity类型的对象。

select语句的参数

属性 是否必须 描述
id 必须 在这个模式下唯一的标识符,可被其它语句引用。这个id也应该对应dao里面的某个方法(相当于方法的实现),因此id应该与方法名一致。
parameterType 可选 传给此语句的参数的完整类名或别名。如果不配置,mybatis会通过ParameterHandler根据参数类型默认选择合适的typeHandler进行处理。parameterType指定参数类型,可以是int,string等简单类型,也可以是复杂类型(如对象)
resultType 与resultMap二选一 语句返回值类型的整类名或别名。指定的类型可以是基本类型,可以是java容器,也可以是javabean(resultType与resultMap不能并用)
resultMap 与resultType二选一 resultMap用于引用我们通过resultMap标签定义的映射类型,这也是mybatis组件高级复杂映射的关键(resultType与resultMap不能并用)
flushCache 可选 将其设置为true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,select语句默认设为false
useCache 可选 将其设置为true,将会导致本条语句的结果被二级缓存,默认值:对select元素为true
timeout 可选 正整数值,这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数,默认为不设值,由驱动器自己决定
fetchSize 可选 正整数值,设置一个值后,驱动器会在结果集数目达到此数值后,激发返回,默认为不设值,由驱动器自己决定
statementType 可选 取值为STATEMENT,PREPARED或CALLABLE中的一个。这会让MyBatis分别使用 Statement,PreparedStatement或CallableStatement,默认值为PREPARED
resultSetType 可选 取值为FORWARD_ONLY,SCROLL_SENSITIVE或SCROLL_INSENSITIVE中的一个,默认值为unset(依赖驱动)

sql

sql元素用来定义一个可以复用的SQL 语句段,供其它语句调用。比如:

<!-- 复用sql语句  查询student表所有字段 -->
<sql id="selectStudentAll">
        SELECT ST.STUDENT_ID,
               ST.STUDENT_NAME,
               ST.STUDENT_SEX,
               ST.STUDENT_BIRTHDAY,
               ST.CLASS_ID
        FROM STUDENT_TBL ST
</sql>

这样,在select的语句中就可以直接引用使用了,将上面select语句改成:

<!-- 查询学生,根据id -->
<select id="getStudent" parameterType="String" resultMap="studentResultMap">
    <include refid="selectStudentAll"/>
    WHERE ST.STUDENT_ID = #{studentID}
</select>

参数类型

MyBatis可以使用的参数包括基本数据类型和Java的复杂数据类型。
基本数据类型,如String,int,Date等,但是使用基本数据类型,只能提供一个参数
使用Java实体类,或Map类型做参数类型,通过#{}可以直接得到其属性。

基本类型参数

根据入学时间,检索学生列表:
SQL:

<!-- 查询学生list,根据入学时间  -->
<select id="getStudentListByDate"  parameterType="Date" resultMap="studentResultMap">
    SELECT *
    FROM STUDENT_TBL ST LEFT JOIN CLASS_TBL CT ON ST.CLASS_ID = CT.CLASS_ID
    WHERE CT.CLASS_YEAR = #{classYear};
</select>

java调用

List<StudentEntity> studentList = studentMapper.getStudentListByClassYear(StringUtil.parse("2007-9-1"));
for (StudentEntity entityTemp : studentList) {
    System.out.println(entityTemp.toString());
}

Java实体类型参数

根据姓名和性别,检索学生列表,使用实体类做参数,通过#{}可以直接得到实体中的字段
SQL:

<!-- 查询学生list,like姓名、=性别,参数entity类型 -->
<select id="getStudentListWhereEntity" parameterType="StudentEntity" resultMap="studentResultMap">
    SELECT * from STUDENT_TBL ST
    WHERE ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{studentName}),'%')
          AND ST.STUDENT_SEX = #{studentSex}
</select>

java调用:

StudentEntity entity = new StudentEntity();
entity.setStudentName("李");
entity.setStudentSex("男");
List<StudentEntity> studentList = studentMapper.getStudentListWhereEntity(entity);
for (StudentEntity entityTemp : studentList) {
    System.out.println(entityTemp.toString());
}

Map参数

根据姓名和性别,检索学生列表,使用Map做参数:
SQL:

<!-- 查询学生list,=性别,参数map类型 -->
<select id="getStudentListWhereMap" parameterType="Map" resultMap="studentResultMap">
    SELECT * from STUDENT_TBL ST
    WHERE ST.STUDENT_SEX = #{sex}
          AND ST.STUDENT_NAME = #{name}
</select>

java调用:

Map<String, String> map = new HashMap<String, String>();
map.put("sex", "女");
map.put("name", "李");
List<StudentEntity> studentList = studentMapper.getStudentListWhereMap(map);
for (StudentEntity entityTemp : studentList) {
    System.out.println(entityTemp.toString());
}

多参数

如果想传入多个参数,则需要在接口的参数上添加@Param注解,给出一个实例:
接口写法:

public List<StudentEntity> getStudentListWhereParam(@Param(value = "name") String name, @Param(value = "sex") String sex, @Param(value = "birthday") Date birthday, @Param(value = "classEntity") ClassEntity classEntity);

SQL写法:

<!-- 查询学生list,like姓名、=性别、=生日、=班级,多参数方式 -->
<select id="getStudentListWhereParam" resultMap="studentResultMap">
    SELECT * from STUDENT_TBL ST
    <where>
        <if test="name!=null and name!='' ">
            ST.STUDENT_NAME LIKE CONCAT(CONCAT('%', #{name}),'%')
        </if>
        <if test="sex!= null and sex!= '' ">
            AND ST.STUDENT_SEX = #{sex}
        </if>
        <if test="birthday!=null">
            AND ST.STUDENT_BIRTHDAY = #{birthday}
        </if>
        <if test="classEntity!=null and classEntity.classID !=null and classEntity.classID!='' ">
            AND ST.CLASS_ID = #{classEntity.classID}
        </if>
    </where>
</select>

进行查询:

List<StudentEntity> studentList = studentMapper.getStudentListWhereParam("", "",StringUtil.parse("1985-05-28"), classMapper.getClassByID("20000002"));
for (StudentEntity entityTemp : studentList) {
    System.out.println(entityTemp.toString());
}

Mybatis中#$的区别

  • #{} 是预编译处理, MyBatis在处理#{}时,它会将sql中的#{}替换为?,然后调用PreparedStatementset方法来赋值,传入字符串后,会在值两边加上单引号,比如值”4,44,514”就会变成 “‘4,44,514’”;
    ${} 是字符串替换, MyBatis在处理${}时,它会将sql中的${}替换为变量的值,传入的数据不会加两边加上单引号。

  • #{}: 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,一个 #{ } 被解析为一个参数占位符 。
    ${}: 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。

  • #{}会在变量的值外面加引号
    ${}不对变量值进行任何更改直接拼接

  • #{}是将传入的值当做字符串的形式,例如select id,name,age from student where id =#{id},当前端把id值1,传入到后台的时候,就相当于select id,name,age from student where id ='1'
    ${}是将传入的数据直接显示生成sql语句,例如select id,name,age from student where id =${id},当前端把id值1,传入到后台的时候,就相当于select id,name,age from student where id = 1

  • #{}方式可以很大程度上防止sql注入(语句的拼接)。
    ${}方式无法防止sql注入

  • 使用#{}就等于使用了PrepareStatement这种占位符的形式。可以防止sql注入等等问题,所以在大多数情况下还是经常使用#{}
    类似group by 字段 ,order by 字段,表名,字段名等没法使用占位符的就需要使用${}

比如拼凑了一个逗号分隔的id字符串,想要直接用IN在这个字符串中查询,就得用${},例如:

@Select("SELECT * FROM user WHERE id IN (${ids_str})")
List<User> queryUsersByIds(@Param("ids_str") String idsStr);

这时如果用 #{} 就会出错,比如传入拼接好的逗号分隔id字符串为 "1,12,33" , 如果写成 id IN (#{ids_str}), 就会变成 id IN ("1,12,33"), 即在变量外加了一层引号。

其实IN语句更推荐用 foreach 动态sql来拼装:

@Select("<script>"
        + "SELECT * FROM user WHERE id IN "
        + " <foreach collection='ids' item='item' open='(' separator=',' close=')'>"
        + "   #{item}"
        + " </foreach>"
        + "</script>")
List<User> queryUsersByIds(@Param("ids") Collection<Long> ids);

使用PrepareStatement的好处

数据库有个功能叫绑定变量,就是针对一条sql预编译生成多个执行计划,如果只是参数改变的重复sql,绑定变量则会提高很大的性能。PrepareStatement就会使用数据库的绑定变量的功能。

${}引起sql注入示例

加入有sql语句:

SELECT * FROM ${tableName}

如果用户给tableName参数传入值user WHERE username ='xx',则组成sql为:

SELECT * FROM user WHERE username ='xx'

即实现了sql注入


注解形式的动态SQL

group by count查询

根据cityIds集合查询people表中各个city id的人数:

public interface MyMapper {
  // 统计people表中各个city id的人数
  @Select({
          "<script>",
          "   SELECT city_id, COUNT(*) AS amount FROM people WHERE city_id IN",
          "   <foreach item='item' collection='city_ids' open='(' separator=',' close=')'>#{item}</foreach>",
          "   GROUP BY city_id",
          "</script>"
  })
  List<CountResult> countByCityId(@Param("city_ids") Collection<Long> cityIds);
}

其中结果类 CountResult 如下:

public class CountResult {
    private Long cityId;
    private Integer amount;
}

@Select 查询语句

可以给 @Select 传入可变参数,比如:

import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

public interface MyMapper {
  // 根据id批量查询user信息
  @Select({
          "<script>",
          "   SELECT *",
          "   FROM user",
          "   WHERE",
          "   <if test='user_ids == null or user_ids.size() == 0'> 1 = 2 </if>",
          "   <if test='user_ids != null and user_ids.size() > 0'>",
          "    id IN",
          "       <foreach item='each_id' collection='user_ids' open='(' separator=',' close=')'>",
          "           #{each_id}",
          "       </foreach>",
          "   </if>",
          "</script>"
  })
  List<User> queryUserByIds(@Param("user_ids") List<Long> userIds);
}

这个sql做了userIds为空时的处理,即使传入null也不会报错。传入null或空集合时,where条件是1=2,恒为false

或者可以直接用加号做字符串拼接:

// 根据orderNos批量查询Order
@Select("<script>" +
        "SELECT * FROM order " +
        "<if test='order_nos != null and order_nos.size() > 0' >" +
        "  WHERE order_no IN" +
        "     <foreach item='order_no' collection='order_nos' open='(' separator=',' close=')'>" +
        "         #{order_no}" +
        "     </foreach>" +
        "</if>" +
        "ORDER BY update_time DESC" +
        "</script>")
List<Order> queryOrdersByOrderNos(@Param("order_nos") List<String> orderNos);

这个sql就需要在调用时加判断了,orderNos参数不为空时再调用。

表字段串接匹配

// 根据 country_code-mobile list查询员工信息(匹配个人手机和NIO手机)
@Select({
        "<script>",
        "SELECT * FROM user_employee_info WHERE status = 'employee' AND ( CONCAT(country_code,'-', mobile) IN ",
        "<foreach collection = 'mobiles' item = 'item' open = '(' separator = ',' close = ')' >",
        "   #{item}",
        "</foreach>",
        "OR CONCAT(nio_country_code, '-', nio_mobile) IN ",
        "<foreach collection = 'mobiles' item = 'item' open = '(' separator = ',' close = ')' >",
        "   #{item}",
        "</foreach>",
        ") ",
        "</script>"
})
List<UserEmployeeInfo> queryEmployeeInfosByCountryCodeAndMobiles(@Param("mobiles") Collection<String> mobiles);

调用端:

// 国家码-手机号 86-13612341234
String mobiles = "86-13612341234, 81-13613661366";
List<String> mobileList = Splitter.on(",").omitEmptyStrings().trimResults().splitToList(mobiles);
employeeInfos = userBiz.queryEmployeeInfosByCountryCodeAndMobiles(mobileList);

@Update 更新语句

//根据userId更新user手机号
@Update("UPDATE user SET country_code = #{country_code}, mobile = #{mobile} WHERE id = #{user_id}")
void updateUserCountryCodeAndMobile(@Param("user_id") long userId, @Param("country_code") String countryCode, @Param("mobile") String mobile);

//更新user表UpdateTime
@Update("UPDATE user SET update_time = NOW() WHERE id = #{user_id}")
void updateUserUpdateTime(@Param("user_id") long userId);

TRUNCATE清空表

// 清空user表
@Update("TRUNCATE TABLE user")
void truncateUserTable();

@Insert 插入语句

//新建user, 避免插入重复记录报错
@Insert("INSERT INTO user (mobile, country_code, name, create_time) VALUES (#{user.mobile}, #{user.countryCode}, #{user.name}, #{user.createTime}) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), update_time = CURRENT_TIMESTAMP")
@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "user.id", before = false, resultType = Long.class)
int insertUserIgnoreDuplicateKey(@Param("user") User user);

@Delete 删除语句

//根据userId删除用户
@Delete("DELETE FROM user WHERE user_id = #{user_id}")
void deleteUserByUserId(@Param("user_id") long userId);

@NotEmpty 非空校验不起作用

比如写一个mapper,不希望入参集合 userIds 为空,可以加上 @NotEmpty 注解,但一定要在类上加 @Validated 注解,否则不起作用。

import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.validation.annotation.Validated;

@Validated
public interface BaseUserMapper {
  // 根据User ID列表查询用户列表
  @Select("<script>"
          + "SELECT * FROM user WHERE user_id IN "
          + " <foreach collection = 'user_ids' item = 'user_id' open = '(' separator = ',' close = ')'>"
          + "     #{user_id}"
          + " </foreach>"
          + "</script>")
  List<User> queryUsersByUserIds(@Param("user_ids") @NotEmpty(message = "Param 'userIds' can not be empty.") List<Long> userIds);
}

原因:没有使用 @Validated 或者 @Valid 注解,原因如下:对JavaBean的属性值进行校验前会首先判断是否存在@Validated或者@Valid注解,只有存在才会执行具体的校验逻辑;

通用语句抽取为公共字符串

public interface MyCampaignMapper {
    String CAMPAIGNS_FROM_CLAUSE = "FROM campaign AS c"
            + " <if test = 'city_ids != null and city_ids.size() > 0'> LEFT JOIN campaign_city_mapping AS ccm ON c.id = ccm.campaign_id </if>"
            + " <if test = 'channel_ids != null and channel_ids.size() > 0'> LEFT JOIN campaign_channel_mapping AS ccm2 ON c.id = ccm2.campaign_id </if>";

    String CAMPAIGNS_WHERE_CLAUSE = "WHERE c.enabled = TRUE "
            + " <if test = \"key_word != null and key_word != ''\"> AND ( c.name LIKE CONCAT('%',#{key_word},'%') OR c.code LIKE CONCAT('%',#{key_word},'%') ) </if>"
            + " <if test = 'status != null'> AND c.status = #{status} </if>"
            + " <if test = 'from_time != null'> AND ( c.end_time &gt;= #{from_time} OR c.time_limit = FALSE ) </if>"
            + " <if test = 'to_time != null'> AND ( c.begin_time &lt;= #{to_time} OR c.time_limit = FALSE ) </if>"
            + " <if test = 'area_limit != null'>"
            + "    <if test = 'area_limit == false'>"
            + "        AND c.area_limit = FALSE "
            + "    </if>"
            + "    <if test = 'area_limit == true'>"
            + "        <if test = 'city_ids != null and city_ids.size() > 0'>"
            + "           AND ( ccm.city_id IN "
            + "           <foreach item='item' collection='city_ids' open='(' separator=',' close=')'>"
            + "               #{item}"
            + "           </foreach>"
            + "               OR c.area_limit = FALSE ) "
            + "        </if>"
            + "    </if>"
            + " </if>"
            + " <if test = 'channel_ids != null and channel_ids.size() > 0'>"
            + "    AND ccm2.leads_channel_id IN "
            + "    <foreach item='item' collection='channel_ids' open='(' separator=',' close=')'>"
            + "        #{item}"
            + "    </foreach>"
            + " </if>";

    @Select("<script>"
            + "SELECT DISTINCT c.* "
            + CAMPAIGNS_FROM_CLAUSE
            + CAMPAIGNS_WHERE_CLAUSE
            + "ORDER BY FIELD(c.status, 2, 1, 3), c.id DESC"
            + "<if test = 'offset != null and count != null'> LIMIT #{offset}, #{count} </if>"
            + "</script>"
    )
    List<Campaign> queryCampaigns(
            @Param("key_word") String keyWord,
            @Param("status") Byte status,
            @Param("channel_ids") List<Long> channel_ids,
            @Param("area_limit") Boolean areaLimit,
            @Param("city_ids") List<Long> city_ids,
            @Param("from_time") Date fromTime,
            @Param("to_time") Date toTime,
            @Param("offset") Long offset,
            @Param("count") Integer count
    );
}

动态SQL

if

if就是简单的条件判断,利用if语句我们可以实现某些简单的条件选择。先来看如下一个例子:

<select id="dynamicIfTest" parameterType="Blog" resultType="Blog">
    select * from t_blog where 1 = 1
    <if test="title != null">
        and title = #{title}
    </if>
    <if test="content != null">
        and content = #{content}
    </if>
    <if test="owner != null">
        and owner = #{owner}
    </if>
</select>

这条语句的意思非常简单,如果你提供了title参数,那么就要满足title=#{title},同样如果你提供了Content和Owner的时候,它们也需要满足相应的条件,之后就是返回满足这些条件的所有Blog,这是非常有用的一个功能,以往我们使用其他类型框架或者直接使用JDBC的时候, 如果我们要达到同样的选择效果的时候,我们就需要拼SQL语句,这是极其麻烦的,比起来,上述的动态SQL就要简单多了。


choose

choose元素的作用就相当于JAVA中的switch语句,基本上跟JSTL中的choose的作用和用法是一样的,通常都是与when和otherwise搭配的。看如下一个例子:

<select id="dynamicChooseTest" parameterType="Blog" resultType="Blog">
    select * from t_blog where 1 = 1
    <choose>
        <when test="title != null">
            and title = #{title}
        </when>
        <when test="content != null">
            and content = #{content}
        </when>
        <otherwise>
            and owner = "owner1"
        </otherwise>
    </choose>
</select>

when元素表示当when中的条件满足的时候就输出其中的内容,跟JAVA中的switch效果差不多的是按照条件的顺序,当when中有条件满足的时候,就会跳出choose,即所有的when和otherwise条件中,只有一个会输出,当所有的when条件都不满足的时候就输出otherwise中的内容。所以上述语句的意思非常简单, 当title!=null的时候就输出and titlte = #{title},不再往下判断条件,当title为空且content!=null的时候就输出and content = #{content},当所有条件都不满足的时候就输出otherwise中的内容。


where

where语句的作用主要是简化SQL语句中where中的条件判断的,先看一个例子,再解释一下where的好处。

<select id="dynamicWhereTest" parameterType="Blog" resultType="Blog">
    select * from t_blog
    <where>
        <if test="title != null">
            title = #{title}
        </if>
        <if test="content != null">
            and content = #{content}
        </if>
        <if test="owner != null">
            and owner = #{owner}
        </if>
    </where>
</select>

where元素的作用是会在写入where元素的地方输出一个where,另外一个好处是你不需要考虑where元素里面的条件输出是什么样子的,MyBatis会智能的帮你处理,如果所有的条件都不满足那么MyBatis就会查出所有的记录,如果输出后是and 开头的,MyBatis会把第一个and忽略,当然如果是or开头的,MyBatis也会把它忽略;此外,在where元素中你不需要考虑空格的问题,MyBatis会智能的帮你加上。像上述例子中,如果title=null, 而content != null,那么输出的整个语句会是select * from t_blog where content = #{content},而不是select * from t_blog where and content = #{content},因为MyBatis会智能的把首个and 或 or 给忽略


trim

trim元素的主要功能是可以 在自己包含的内容前加上某些前缀或后缀,与之对应的属性是prefix(加前缀)和suffix(加后缀) ;也可以 把包含内容的首部或尾部某些内容覆盖,即忽略,对应的属性是prefixOverrides(覆盖首部内容)和suffixOverrides(覆盖尾部内容) ;正因为trim有这样的功能,所以我们也可以非常简单的利用trim来代替where元素的功能,示例代码如下:

<select id="dynamicTrimTest" parameterType="Blog" resultType="Blog">
    select * from t_blog
    <trim prefix="where" prefixOverrides="and |or">
        <if test="title != null">
            title = #{title}
        </if>
        <if test="content != null">
            and content = #{content}
        </if>
        <if test="owner != null">
            or owner = #{owner}
        </if>
    </trim>
</select>

意思是在包含内容前加上”where”,并将首部的and或or忽略。

trim示例

// 根据id、角色role、状态status批量查询User
@Select("<script>" +
        "SELECT * FROM user" +
        "<trim prefix = 'WHERE' prefixOverrides = 'AND |OR'>" +
            "<if test = 'status != null'> AND status = #{status} </if>" +
            "<if test = 'role != null'> AND role = #{role} </if>" +
            "<if test = 'user_ids == null or user_ids.size() == 0'> AND 1 = 2 </if>" +
            "<if test = 'user_ids != null and user_ids.size() > 0'>" +
            "AND id IN " +
            "  <foreach item='id' collection='user_ids' open='(' separator=',' close=')'>" +
            "      #{id}" +
            "  </foreach>" +
            "</if>" +
        "</trim>" +
        "ORDER BY update_time DESC" +
        "</script>")
List<User> queryUserByIds(@Param("user_ids") List<Long> userIds, @Param("role") Byte role, @Param("status") Byte status);

set

set元素主要是用在更新操作的时候,它的主要功能和where元素其实是差不多的,主要是在包含的语句前输出一个set,然后如果包含的语句是以逗号结束的话将会把该逗号忽略,如果set包含的内容为空的话则会出错。有了set元素我们就可以动态的更新那些修改了的字段。下面是一段示例代码:

<update id="dynamicSetTest" parameterType="Blog">
    update t_blog
    <set>
        <if test="title != null">
            title = #{title},
        </if>
        <if test="content != null">
            content = #{content},
        </if>
        <if test="owner != null">
            owner = #{owner}
        </if>
    </set>
    where id = #{id}
</update>

上述示例代码中,如果set中一个条件都不满足,即set中包含的内容为空的时候就会报错。


foreach

foreach的主要用在构建in条件中,它可以在SQL语句中进行迭代一个集合。
foreach元素的属性主要有item,index,collection,open,separator,close。

  • collection 是迭代的集合
  • item 表示集合中每一个元素进行迭代时的别名,支持属性的点路径访问,如item.age,item.info.details。 具体说明:在list和数组中是其中的对象,在map中是value。 该参数为必选。
  • index 指定一个名字,用于表示在迭代过程中,每次迭代到的位置。在list和数组中,index是元素的序号,在map中,index是元素的key,该参数可选。
  • separator 元素之间的分隔符,例如在in()的时候,separator=”,”会自动在元素中间用“,“隔开,避免手动输入逗号导致sql错误,如in(1,2,)这样。该参数可选。
  • open foreach代码的开始符号,一般是(和close=”)”合用。常用在in(),values()时。该参数可选。,
  • close foreach代码的关闭符号,一般是)和open=”(“合用。常用在in(),values()时。该参数可选。

在使用foreach的时候最关键的也是最容易出错的就是collection属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有一下3种情况:

  • 如果传入的是单参数且参数类型是一个List的时候,collection属性值为list
  • 如果传入的是单参数且参数类型是一个array数组的时候,collection的属性值为array
  • 如果传入的参数是多个的时候,我们就需要把它们封装成一个Map了,当然单参数也可以封装成map,实际上如果你在传入参数的时候,在MyBatis里面也是会把它封装成一个Map的,map的key就是参数名,所以这个时候collection属性值就是传入的List或array对象在自己封装的map里面的key

单List类型参数

<select id="dynamicForeachTest" resultType="Blog">
    select * from t_blog where id in
    <foreach collection="list" index="index" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

上述collection的值为list,对应的Mapper是这样的
public List<Blog> dynamicForeachTest(List<Integer> ids);
测试代码:

public void dynamicForeachTest() {
    SqlSession session = Util.getSqlSessionFactory().openSession();
    BlogMapper blogMapper = session.getMapper(BlogMapper.class);
    List<Integer> ids = new ArrayList<Integer>();
    ids.add(1);
    ids.add(3);
    ids.add(6);
    List<Blog> blogs = blogMapper.dynamicForeachTest(ids);
    for (Blog blog : blogs)
        System.out.println(blog);
    session.close();
}

单array类型参数

<select id="dynamicForeach2Test" resultType="Blog">
    select * from t_blog where id in
    <foreach collection="array" index="index" item="item" open="(" separator="," close=")">
        #{item}
    </foreach>
</select>

上述collection为array,对应的Mapper代码:
public List<Blog> dynamicForeach2Test(int[] ids);
对应的测试代码:

public void dynamicForeach2Test() {
    SqlSession session = Util.getSqlSessionFactory().openSession();
    BlogMapper blogMapper = session.getMapper(BlogMapper.class);
    int[] ids = new int[] {1,3,6,9};
    List<Blog> blogs = blogMapper.dynamicForeach2Test(ids);
    for (Blog blog : blogs)
        System.out.println(blog);
    session.close();
}

Map类型参数

当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是 key ,item 是 value 。

user 表有两个字段 country_code 和 mobile,如果想批量查询的话,可以使用 OR

// 根据 Map<country_code, mobile> 批量查询 User
@Select({"<script>",
        "   SELECT * ",
        "   FROM user u ",
        "   WHERE  <foreach collection='map' index='key' item='item' open='(' separator='OR' close=')'>",
        "           ( u.country_code = #{key} AND u.mobile = #{item} )",
        "       </foreach>",
        "</script>"
})
List<User> queryUserByMobiles(@Param("map") Map<String, String> countryCodeMobileMap);

java 调用代码
其实这么写不对,后面的 key 把前面的都覆盖了,如果确定key不重复,可以这么用。

Map<String, String> params = Maps.newHashMap();
// countryCodeMobileStr: 86-13916425833, 49-11100002581
ids.forEach(countryCodeMobileStr -> {
    List<String> mobileList = Splitter.on("-").trimResults().omitEmptyStrings().splitToList(countryCodeMobileStr);
    if (mobileList.size() != 2) {
        return;
    }
    params.put(mobileList.get(0), mobileList.get(1));
});
List<User> users = userMapper.queryUserByMobiles(params);
Map<String, User> userBeanMap = Maps.uniqueIndex(users, user -> Joiner.on("-").join(user.getCountryCode(), user.getMobile()));

pass list of Pair to myBatis
https://stackoverflow.com/questions/53290994/pass-list-of-pair-to-mybatis

List<Object>类型参数

// 根据 List<UserIdBean(countryCode,mobile)> 批量查询UserIdBean
@Select({"<script>",
        "   SELECT u.id, u.uuid, uai.account_id, u.mobile, u.country_code ",
        "   FROM user u LEFT JOIN user_account_info uai on u.id = uai.user_id",
        "   WHERE (uai.status = 'enable' OR uai.status IS NULL)",
        "   AND  <foreach collection='user_id_bean_list' item='item' open='(' separator='OR' close=')'>",
        "           ( u.country_code = #{item.countryCode} AND u.mobile = #{item.mobile} )",
        "       </foreach>",
        "</script>"
})
List<UserIdBean> queryUserIdBeanByMobiles(@Param("user_id_bean_list") List<UserIdBean> userIdBeanList);

java 调用代码

List params = Lists.newArrayList();
// countryCodeMobileStr: 86-13916425833, 49-11100002581
ids.forEach(countryCodeMobileStr -> {
    List<String> mobileList = Splitter.on("-").trimResults().omitEmptyStrings().splitToList(countryCodeMobileStr);
    if (mobileList.size() != 2) {
        return;
    }
    params.add(UserIdBean.builder().countryCode(mobileList.get(0)).mobile(mobileList.get(1)).build());
});
List<UserIdBean> userIdBeans = userBiz.queryUserIdBeanByMobiles(params);

自己遇到的例子

orgcabin格式为”C/Y/W”,分割后生成orgcabin的list,放入map,供mybatis中使用,
变更前舱位:增加多舱位的解析

if (StringUtils.isNotBlank(cons.getOrgCabin())) { //变更前舱位
    String orgCabinArray[] = cons.getOrgCabin().split(Symbol_Split_SLASH);//'/'分割多舱位
    List<String> orgCabinList = Arrays.asList(orgCabinArray);
    map.put("orgcabinList", orgCabinList);
}

变更前舱位list的解析:

<sql id="where">
    <where>
        <if test="@com.masikkk.common.OgnlUtil@isNotEmpty(orgcabinList)">
            and orgcabin in
            <foreach collection="orgcabinList" item="orgcabinItem" index="index" open="(" close=")" separator=",">
                #{orgcabinItem}
            </foreach>
        </if>
    </where>
</sql>

参考


上一篇 GitHub

下一篇 SoapUI使用笔记

阅读
评论
19k
阅读预计86分钟
创建日期 2015-11-16
修改日期 2020-04-07
类别
目录
  1. MyBatis基础
    1. mybatis分层架构
    2. mybatis SQL执行流程
      1. MapperProxy.invoke()
      2. DefaultSqlSession.selectList()
      3. CachingExecutor.query() 查二级缓存
      4. BaseExecutor.query() 查一级缓存
      5. BaseExecutor.queryFromDatabase() 查DB后放入缓存
      6. SimpleExecutor.doQuery()
      7. PreparedStatementHandler.query()
    3. mybatis缓存
      1. 一级缓存(SqlSession缓存/local缓存)
        1. 一级缓存的key是什么?
        2. insert/delete/update会删除缓存
        3. 一级缓存范围配置
        4. 一级缓存的SqlSession间隔离
      2. 二级缓存(跨SqlSession,Mapper级)
        1. MyBatis开启二级缓存配置
    4. mybatis拦截器(插件)
      1. 拦截器有什么用?
      2. 拦截器的种类
      3. 拦截器的实现原理(JDK动态代理)
      4. Interceptor接口
      5. 自定义拦截器
        1. @Intercepts和@Signature注解
        2. setProperties()方法
        3. plugin()方法
        4. intercept()方法
        5. Plugin类及wrap(),invoke()方法
        6. 配置文件中注册自定义拦截器
        7. 拦截器工作工作流程
    5. mybatis分页
      1. 逻辑分页(内存分页)和物理分页
        1. 不同数据库的物理分页语句
        2. hibernate和MyBatis对分页的支持
        3. 对比和使用场景
      2. 利用MyBatis的RowBounds分页(逻辑分页)
        1. RowBounds使用方法
        2. RowBounds实现原理
      3. 分页插件PageHelper(别人写好的过滤器)(物理分页)
        1. 分页的线程安全性(内部使用ThreadLocal保存分页参数)
      4. 使用拦截器重写sql添加limit子句(物理分页)
      5. 查出所有结果在代码中分页(逻辑分页)
      6. 手动写limit子句(物理分页)
    6. mybatis动态sql
      1. #与$的区别
      2. sql注入举例
  2. BaseTypeHandler枚举自动转换
  3. Java API
    1. 通用Mapper
    2. 问题
      1. text类型为空
      2. Invalid bound statement (not found)
      3. mybatis.mapperLocations 属性
    3. @Mapper 和 @MapperScan
      1. 使用@Mapper注解
      2. 使用@MapperScan注解
      3. @MapperScan 扫描不同包中的同名Mapper导致冲突
    4. SqlSession
      1. SqlSession创建过程
      2. 构建SqlSession并操作数据库实例
      3. SqlSessionFactoryBean
    5. sql日志
  4. SQL语句
    1. 注意事项
      1. 内部类作为返回结果必须是静态类
      2. 集合判空中必须用小写and
      3. 大于小于号
        1. 普通字符串sql中直接写小于号<
        2. 动态sql标签中小于号必须转义为<
      4. insert和insertSelective区别
      5. updateByPrimaryKey和updateByPrimaryKeySelective区别
    2. select
    3. sql
    4. 参数类型
      1. 基本类型参数
      2. Java实体类型参数
      3. Map参数
      4. 多参数
    5. Mybatis中#与$的区别
      1. 使用PrepareStatement的好处
      2. ${}引起sql注入示例
  5. 注解形式的动态SQL
    1. group by count查询
    2. @Select 查询语句
      1. 表字段串接匹配
    3. @Update 更新语句
    4. TRUNCATE清空表
    5. @Insert 插入语句
    6. @Delete 删除语句
    7. @NotEmpty 非空校验不起作用
    8. 通用语句抽取为公共字符串
  6. 动态SQL
    1. if
    2. choose
    3. where
    4. trim
    5. set
    6. foreach
      1. 单List类型参数
      2. 单array类型参数
      3. Map类型参数
      4. List<Object>类型参数
      5. 自己遇到的例子
  7. 参考

页面信息

location:
protocol:
host:
hostname:
origin:
pathname:
href:
document:
referrer:
navigator:
platform:
userAgent:

评论