简介
Mybatis官方文档: www.mybatis.org/mybatis-3/z…
MyBatis是一个支持普通SQL查询,存储过程和高级映射的优秀持久层框架。MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装。MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。
配置(configuration)
- properties 属性
- settings 设置
- typeAliases 类型别名
- typeHandlers 类型处理器
- objectFactory 对象工厂
- objectWrapperFactory
- plugins 插件
- environments 环境z
- databaseIdProvider 数据库厂商标识
- mappers 映射器
10种配置: properties、typeAliases、plugins、objectFactory、objectWrapperFactory、settings、environments、databaseIdProvider、typeHandlers、mappers
properties
可外部配置且可动态替换的属性
settings
<?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>
<!-- 配置mybatis的缓存,延迟加载等等一系列属性 -->
<settings>
<!--启用log4j日志-->
<setting name="logImpl" value="LOG4J"/>
<!-- 全局映射器启用缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指 定),不会加载关联表的所有字段,以提高性能 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 对于未知的SQL查询,允许返回不同的结果集以达到通用的效果 -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 允许使用列标签代替列名 -->
<setting name="useColumnLabel" value="true"/>
<!-- 给予被嵌套的resultMap以字段-属性的映射支持 -->
<setting name="autoMappingBehavior" value="FULL"/>
<!-- 对于批量更新操作缓存SQL以提高性能 -->
<setting name="defaultExecutorType" value="REUSE"/>
<!-- 数据库超过25000秒仍未响应则超时 -->
<setting name="defaultStatementTimeout" value="25000"/>
<!-- 自动转换驼峰命名 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
复制代码
mybatis:
mapper-locations: classpath:mapper/*.xml # 实体放置目录
type-aliases-package: com.mlh.model
type-handlers-package: org.mybatis.example
configuration:
cache-enabled: true # 这个是开启一级缓存
default-enum-type-handler: org.apache.ibatis.type.IntegerTypeHandler # 数据库的枚举类型怎么映射,可以自定义
return-instance-for-empty-row: true
use-column-label: true
lazy-loading-enabled: true
multiple-result-sets-enabled: true
use-generated-keys: false
auto-mapping-behavior: PARTIAL
auto-mapping-unknown-column-behavior: WARNING
default-executor-type: simple
default-statement-timeout: 25
default-fetch-size: 100
safe-row-bounds-enabled: false
local-cache-scope: session
jdbc-type-for-null: OTHER
lazy-load-trigger-methods: equals,clone,hashCode,toString
map-underscore-to-camel-case: false
spring:
datasource:
name: mysql_test
master:
jdbcUrl: jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8
username: root
password: asdf1234
driver-class-name: com.mysql.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/mytestdb2?useUnicode=true&characterEncoding=utf8
username: root
password: asdf1234
driver-class-name: com.mysql.jdbc.Driver
复制代码
typeAliases
类型别名是为 Java 类型设置一个短的名字。它只和 XML 配置有关,存在的意义仅在于用来减少类完全限定名的冗余
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
<typeAlias alias="Comment" type="domain.blog.Comment"/>
<typeAlias alias="Post" type="domain.blog.Post"/>
<typeAlias alias="Section" type="domain.blog.Section"/>
<typeAlias alias="Tag" type="domain.blog.Tag"/>
<typeAlias alias="byte" type="_byte"/><!--sql type类型别名-->
</typeAliases>
复制代码
typeHandlers
用类型处理器将获取的不同sql字段类型的值以合适的方式转换成 Java 类型,可自定义,比如枚举、Json类型
自定义typeHandler
//GenericTypeHandler.java
public class GenericTypeHandler<E extends MyObject> extends BaseTypeHandler<E> {
private Class<E> type;
public GenericTypeHandler(Class<E> type) {
if (type == null) throw new IllegalArgumentException("Type argument cannot be null");
this.type = type;
}
...
复制代码
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import org.apache.ibatis.type.TypeHandler;
/**
* @author
* java中的boolean和jdbc中的char之间转换;true-Y;false-N
*/
//方式一:
@MappedJdbcTypes(JdbcType.CHAR)
@MappedTypes(Boolean.class)
public class BooleanTypeHandler implements TypeHandler {
/* (non-Javadoc)
* @see org.apache.ibatis.type.TypeHandler#getResult(java.sql.ResultSet, java.lang.String)
*/
@Override
public Object getResult(ResultSet resultSet, String columnLabel) throws SQLException {
String str = resultSet.getString(columnLabel);
Boolean rt = Boolean.FALSE;
if (str.equalsIgnoreCase("Y")){
rt = Boolean.TRUE;
}
return rt;
}
@Override
public Object getResult(ResultSet resultSet, int columnIndex) throws SQLException {
String str = resultSet.getString(columnIndex);
Boolean rt = Boolean.FALSE;
if (str.equalsIgnoreCase("Y")){
rt = Boolean.TRUE;
}
return rt;
}
/* (non-Javadoc)
* @see org.apache.ibatis.type.TypeHandler#getResult(java.sql.CallableStatement, int)
*/
@Override
public Object getResult(CallableStatement arg0, int arg1)
throws SQLException {
Boolean b = arg0.getBoolean(arg1);
return b == true ? "Y" : "N";
}
/* (non-Javadoc)
* @see org.apache.ibatis.type.TypeHandler#setParameter(java.sql.PreparedStatement, int, java.lang.Object, org.apache.ibatis.type.JdbcType)
*/
@Override
public void setParameter(PreparedStatement arg0, int arg1, Object arg2,
JdbcType arg3) throws SQLException {
Boolean b = (Boolean) arg2;
String value = (Boolean) b == true ? "Y" : "N";
arg0.setString(arg1, value);
}
}
复制代码
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
复制代码
objectFactory
MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成。
//自定义的ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
public Object create(Class type) {
return super.create(type);
}
public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
return super.create(type, constructorArgTypes, constructorArgs);
}
public void setProperties(Properties properties) {
super.setProperties(properties);
}
public <T> boolean isCollection(Class<T> type) {
return Collection.class.isAssignableFrom(type);
}}
复制代码
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
<property name="someProperty" value="100"/>
</objectFactory>
复制代码
plugins
MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。以下方法都可拦截
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) 执行器
- ParameterHandler (getParameterObject, setParameters) 参数映射
- ResultSetHandler (handleResultSets, handleOutputParameters) 结果集
- StatementHandler (prepare, parameterize, batch, update, query) sql编译状态
//ExamplePlugin.java
@Intercepts({@Signature(
type= Executor.class,
method = "update",
args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
//
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
//
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
//
public void setProperties(Properties properties) {
}
}
复制代码
<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="org.mybatis.example.ExamplePlugin">
<property name="someProperty" value="100"/>
</plugin>
</plugins>
复制代码
environments,databaseIdProvider,mappers
- environments 事务管理、数据源配置
- databaseIdProvider 不同类型的数据库设置
- mappers mapper.xml位置
使用
动态SQL
- if 单条件判断
- choose (when, otherwise) 多条件判断
- trim (where, set prefix prefixOverrides)
- foreach
- choose, when, otherwise
- bind 从 OGNL 表达式中创建一个变量并将其绑定到上下文
<!--条件查询 prefix前缀、prefixoverride 去掉第一个and或者是or-->
<trim prefix="WHERE" prefixoverride="AND |OR">
    <if test="name != null and name.length()>0"> AND name=#{name}</if>
    <if test="gender != null and gender.length()>0"> AND gender=#{gender}</if>
  </trim>
复制代码
<!--动态更新 suffixoverride:去掉最后一个逗号(也可以是其他的标记,就像是上面前缀中的and一样)suffix:后缀-->
<trim prefix="set" suffixoverride="," suffix=" where id = #{id} ">
    <if test="name != null and name.length()>0"> name=#{name},</if>
    <if test="gender != null and gender.length()>0"> gender=#{gender},</if>
  </trim>
复制代码
<select id="selectBlogsLike" resultType="Blog">
<bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
SELECT * FROM BLOG
WHERE title LIKE #{pattern}
</select>
复制代码
Mapper.xml
- cache – 给定命名空间的缓存配置。
- cacheref – 其他命名空间缓存配置的引用。
- resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
- parameterMap – 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
- sql – 可被其他语句引用的可重用语句块。
- insert – 映射插入语句
- update – 映射更新语句
- delete – 映射删除语句
- select – 映射查询语句
<select
id="selectPerson"
parameterType="int"
parameterMap="deprecated"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"<!--将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。-->
useCache="true"<!--将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true。-->
timeout="10000"<!--这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。-->
fetchSize="256"<!--这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)-->
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
复制代码
<insert
id="insertAuthor"
useGeneratedKeys="true"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
keyProperty=""<!--唯一标记一个对象属性-->
keyColumn=""<!--通过生成的键值设置表中的列名,这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。-->
useGeneratedKeys=""
timeout="20">
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" keyColumn='id' order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>
<update
id="updateAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
<delete
id="deleteAuthor"
parameterType="domain.blog.Author"
flushCache="true"
statementType="PREPARED"
timeout="20">
复制代码
- #{} 与 ${}
默认情况下,使用 #{} 格式的语法会导致 MyBatis 创建 PreparedStatement 参数并安全地设置参数(就像使用 ? 一样)。这样做更安全,更迅速,通常也是首选做法,不过有时你就是想直接在 SQL 语句中插入一个不转义的字符串。比如,像 ORDER BY,你可以这样来使用
ORDER BY ${columnName}
cache
这篇文章介绍比较详细: 聊聊MyBatis缓存机制
Mybatis 使用到了两种缓存:本地缓存(local cache)session级别
和二级缓存(second level cache)namespace级别
。
一级缓存 每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询语句本身都会被保存在本地缓存中,那么,相同的查询语句和相同的参数所产生的更改就不会二度影响数据库了。本地缓存会被增删改、提交事务、关闭事务以及关闭 session 所清空。
默认情况下,本地缓存数据可在整个 session的周期内使用,这一缓存需要被用来解决循环引用错误和加快重复嵌套查询的速度,所以它可以不被禁用掉,但是你可以设置 localCacheScope=STATEMENT 表示缓存仅在语句执行时有效。
二级缓存开启的效果:
- 映射语句文件中的所有 select 语句将会被缓存。
- 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
- 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
- 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序 来刷新。
- 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
- 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改
<!--开启二级缓存-->
<cache
eviction="FIFO"<!--LRU、FIFO、SOFT、WEAK-->
flushInterval="60000" <!--刷新间隔,默认不刷新-->
size="512"<!--缓存的对象数目 默认1024-->
readOnly="true"/><!--只读缓存会快一点,可读写,返回对象的拷贝-->
复制代码
<update id="updateById" parameterType="User" flushCache="false" /><!--二级缓存不刷新-->
<select id="selectAll" resultMap="BaseResultMap" useCache="false"><!--不走二级缓存-->
复制代码
package org.apache.ibatis.cache.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;
/**
* @author Clinton Begin
*/
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
复制代码
日志(logging)
- SLF4J
- Apache Commons Logging
- Log4j 2
- Log4j
- JDK logging
# namespace:mapper.StudentMapper
# MyBatis logging configuration...
log4j.logger.mapper.StudentMapper=TRACE
复制代码
实现分析
- SqlSession
作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
- Executor
MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
- StatementHandler
封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
- ParameterHandler
负责对用户传递的参数转换成JDBC Statement 所需要的参数,
- ResultSetHandler
负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
- TypeHandle
负责java数据类型和jdbc数据类型之间的映射和转换
- MappedStatement
MappedStatement维护了一条<select|update|delete|insert>节点的封装,
- SqlSource
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
- BoundSql
表示动态生成的SQL语句以及相应的参数信息
- Configuration
MyBatis所有的配置信息都维持在Configuration对象之中,datasource、environment、transactionManager。
初始化过程(构造sqlSession)
- (1)首先,SqlSessionFactoryBuilder去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory。
public class SqlSessionFactoryBuilder {
//Reader读取mybatis配置文件,传入构造方法
//除了Reader外,其实还有对应的inputStream作为参数的构造方法,
//这也体现了mybatis配置的灵活性
public SqlSessionFactory build(Reader reader) {
return build(reader, null, null);
}
public SqlSessionFactory build(Reader reader, String environment) {
return build(reader, environment, null);
}
//mybatis配置文件 + properties, 此时mybatis配置文件中可以不配置properties,也能使用${}形式
public SqlSessionFactory build(Reader reader, Properties properties) {
return build(reader, null, properties);
}
//通过XMLConfigBuilder解析mybatis配置,然后创建SqlSessionFactory对象
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//下面看看这个方法的源码
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
}
复制代码
/**
* mybatis 配置文件解析
*/
public class XMLConfigBuilder extends BaseBuilder {
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
//外部调用此方法对mybatis配置文件进行解析
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
//从根节点configuration
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
//此方法就是解析configuration节点下的子节点
//由此也可看出,我们在configuration下面能配置的节点为以下10个节点
private void parseConfiguration(XNode root) {
try {
propertiesElement(root.evalNode("properties")); //issue #117 read properties first
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
settingsElement(root.evalNode("settings"));
environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
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);
}
}
}
复制代码
- (2)当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。
/**
* 通常一系列openSession方法最终都会调用本方法
* @param execType
* @param level
* @param autoCommit
* @return
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
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()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
复制代码
实例化Dao、Mapper的过程(代理模式)
通过MapperProxy动态代理咱们的dao(或称为mapper), 也就是说,当咱们执行自己写的dao里面的方法的时候,其实是对应的mapperProxy在代理。
- (1)SqlSession.getMapper() -> Configuration.getMapper()
@Override
public <T> T getMapper(Class<T> type) {
return configuration.<T>getMapper(type, this);
}
复制代码
- (2) Configuration.getMapper() -> mapperRegistry.getMapper()
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
复制代码
- (3) MapperRegistry.getMapper() -> mapperProxyFactory.newInstance()
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
//能偷懒的就偷懒,俺把粗活交给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);
}
}
复制代码
- (4) MapperProxyFactory.newInstance() -> userMapper
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//动态代理我们写的dao接口
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
复制代码
通过实例化dao(mapper)执行SQL的过程
- (1)userMapper.method() -> MapperProxy.invoke -> mapperMethod.ececute(sqlSession,args)
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
//二话不说,主要交给MapperMethod自己去管
return mapperMethod.execute(sqlSession, args);
}
复制代码
- (2) MapperedMethod.executor() -> defaultSqlSession.executor()
/**
* 看着代码不少,不过其实就是先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法,绕了一圈,又转回sqlSession了
* @param sqlSession
* @param args
* @return
*/
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
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 {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
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;
}
复制代码
- (3) sqlSession.selectList() -> Excutor.query(StatementHandler,parameter,ResultHandler)
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
//Excutor.query
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();
}
}
复制代码
PageHelper
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
复制代码
tk.mybatis
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
复制代码
Mybatis自动生成代码插件
<!-- Mybatis Generator -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
复制代码
mybatis-generator:generate -e maven插件
多数据源动态切换
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface TargetDataSource {
//master、slave
String name();
}
复制代码
public enum DatabaseType {
master("master"), slave("slave");
DatabaseType(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "DatabaseType{" +
"name='" + name + '\'' +
'}';
}
}
复制代码
public class DynamicDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
String type = DynamicDataSourceContextHolder.getDatabaseType();
if(StringUtils.isEmpty(type)){
type = DatabaseType.master.getName();
}
logger.info("====================dataSource ==========" + type);
return type;
}
}
复制代码
/**
* 作用:
* 1、保存一个线程安全的DatabaseType容器
*/
public class DynamicDataSourceContextHolder {
/*
* 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
* 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setDatabaseType(String type){
contextHolder.set(type);
}
public static String getDatabaseType(){
return contextHolder.get();
}
/**
* @Title: clearDateSoureType
* @Description: 清空所有的数据源变量
* @return void
* @throws
*/
public static void clearDataSoureType(){
contextHolder.remove();
}
}
复制代码
package com.mlh.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* @author: linghan.ma
* @DATE: 2018/10/11
* @description:
*/
@Configuration
@EnableTransactionManagement
@MapperScan("com.mlh.dao")
public class DataSourceConfig {
private static Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);
@Autowired
private Environment env; // (1)
@Autowired
private DataSourceProperties properties; // (2)
@Value("${spring.datasource.druid.filters}") // (3)
private String filters;
@Value("${spring.datasource.druid.initial-size}")
private Integer initialSize;
@Value("${spring.datasource.druid.min-idle}")
private Integer minIdle;
@Value("${spring.datasource.druid.max-active}")
private Integer maxActive;
@Value("${spring.datasource.druid.max-wait}")
private Integer maxWait;
@Value("${spring.datasource.druid.time-between-eviction-runs-millis}")
private Long timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.min-evictable-idle-time-millis}")
private Long minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.validation-query}")
private String validationQuery;
@Value("${spring.datasource.druid.test-while-idle}")
private Boolean testWhileIdle;
@Value("${spring.datasource.druid.test-on-borrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.druid.test-on-return}")
private boolean testOnReturn;
@Value("${spring.datasource.druid.pool-prepared-statements}")
private boolean poolPreparedStatements;
@Value("${spring.datasource.druid.max-pool-prepared-statement-per-connection-size}")
private Integer maxPoolPreparedStatementPerConnectionSize;
/**
* 通过Spring JDBC 快速创建 DataSource
* @return
*/
@Bean(name = "masterDataSource")
@Qualifier("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master") // (4)
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 手动创建DruidDataSource,通过DataSourceProperties 读取配置
* @return
* @throws SQLException
*/
@Bean(name = "slaveDataSource")
@Qualifier("slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() throws SQLException {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setFilters(filters);
dataSource.setUrl(properties.getUrl());
dataSource.setDriverClassName(properties.getDriverClassName());
dataSource.setUsername(properties.getUsername());
dataSource.setPassword(properties.getPassword());
dataSource.setInitialSize(initialSize);
dataSource.setMinIdle(minIdle);
dataSource.setMaxActive(maxActive);
dataSource.setMaxWait(maxWait);
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
dataSource.setValidationQuery(validationQuery);
dataSource.setTestWhileIdle(testWhileIdle);
dataSource.setTestOnBorrow(testOnBorrow);
dataSource.setTestOnReturn(testOnReturn);
dataSource.setPoolPreparedStatements(poolPreparedStatements);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
return dataSource;
}
/**
* 构造多数据源连接池
* Master 数据源连接池采用 HikariDataSource
* Slave 数据源连接池采用 DruidDataSource
* @param master
* @param slave
* @return
*/
@Bean
@Primary
public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master,
@Qualifier("slaveDataSource") DataSource slave) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DatabaseType.master, master);
targetDataSources.put(DatabaseType.slave, slave);
DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);
dataSource.setDefaultTargetDataSource(master);
dataSource.afterPropertiesSet();
// String read = env.getProperty("spring.datasource.read");
// dataSource.setMethodType(DatabaseType.slave, read);
// String write = env.getProperty("spring.datasource.write");
// dataSource.setMethodType(DatabaseType.master, write);
return dataSource;
}
@Bean
public PageInterceptor pageHelper(){
//分页插件设置
PageInterceptor pageHelper = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("reasonable", "true");
properties.setProperty("supportMethodsArguments", "true");
properties.setProperty("returnPageInfo", "check");
properties.setProperty("params", "count=countSql");
pageHelper.setProperties(properties);
return pageHelper;
}
/**
* mapper路径
* @param masterDataSource
* @param slaveDataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
fb.setDataSource(this.dataSource(masterDataSource, slaveDataSource));
fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package"));
fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations")));
//添加分页插件
// fb.setPlugins(new Interceptor[]{pageHelper()});
return fb.getObject();
}
/**
* 事务管理
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
}
}
复制代码
/**
* @author: linghan.ma
* @DATE: 2018/9/26
* @description: 动态数据源通知
* @see: https://www.cnblogs.com/foreveravalon/p/8653832.html
*/
@Component
@Order(-1) // 保证在 @Transactional之前执行
@Aspect
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DynamicDataSourceAspect {
private static final Logger logger = LogManager.getLogger(DynamicDataSourceAspect.class);
//改变数据源
@Before("@annotation(targetDataSource)")
public void changeDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
String dbid = targetDataSource.name();
logger.debug("使用数据源:" + dbid+ joinPoint.getSignature());
DynamicDataSourceContextHolder.setDatabaseType(dbid);
}
/**
* 销毁数据源 在所有的方法执行执行完毕后
* @param joinPoint
* @param targetDataSource
*/
@After("@annotation(targetDataSource)")
public void clearDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
logger.debug("清除数据源 " + targetDataSource.name() + " !",joinPoint.getSignature());
DynamicDataSourceContextHolder.clearDataSoureType();
}
}
复制代码
@TargetDataSource(name = "master")
User findUserById (int userId);
复制代码
spring:
application:
name: SpringBoot
# master(jdbcurl)数据源使用 Hikari连接池,slave(url)使用的是druid作为数据库连接池,数据库的连接不一样
datasource:
name: mysql_test
#-------------- start ----------------# (1)
master:
#基本属性--注意,这里的为【jdbcurl】-- 默认使用HikariPool作为数据库连接池
jdbcUrl: jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=utf8
username: root
password: asdf1234
driver-class-name: com.mysql.jdbc.Driver
#基本属性--注意,这里的为【jdbcurl】-- 默认使用HikariPool作为数据库连接池
slave:
#基本属性--注意,这里为 【url】-- 使用 druid 作为数据库连接池
url: jdbc:mysql://localhost:3306/mytestdb2?useUnicode=true&characterEncoding=utf8
username: root
password: asdf1234
driver-class-name: com.mysql.jdbc.Driver
# read: get,select,count,list,query,find
# write: add,create,update,delete,remove,insert
#-------------- end ----------------#
#druid相关配置
druid:
#监控统计拦截的filters
filters: stat,wall
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#获取连接等待超时时间
max-wait: 60000
#间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
复制代码
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
复制代码
分析
/**
* Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
* calls to one of various target DataSources based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.
*
* @author Juergen Hoeller
* @since 2.0.1
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
/**
* Specify the map of target DataSources, with the lookup key as key.
* The mapped value can either be a corresponding {@link javax.sql.DataSource}
* instance or a data source name String (to be resolved via a
* {@link #setDataSourceLookup DataSourceLookup}).
* <p>The key can be of arbitrary type; this class implements the
* generic lookup process only. The concrete key representation will
* be handled by {@link #resolveSpecifiedLookupKey(Object)} and
* {@link #determineCurrentLookupKey()}.
*
* 设置一个map<key,datasource>,装要切换的数据源
**/
public void setTargetDataSources(Map<Object, Object> targetDataSources) {
this.targetDataSources = targetDataSources;
}
......
/**
* Determine the current lookup key. This will typically be
* implemented to check a thread-bound transaction context.
* <p>Allows for arbitrary keys. The returned key needs
* to match the stored lookup key type, as resolved by the
* {@link #resolveSpecifiedLookupKey} method。
*
* 设置一个获取key的匹配规则,自己实现,比如注解。。。
**/
protected abstract Object determineCurrentLookupKey();
}
复制代码