前言
MyBatis 本是apache的一个开源项目iBatis,而我们对于mybatis上的使用包括最基本的使用 CRUD上的使用,而本篇文章会除了会介绍CRUD的使用、以及集成redis,以及 如何开启多级缓存, 多数据源,以及Mybatis插件实现原理 分页插件等等 这都是在根据项目的大小以及根据业务功能上的支撑 是否需要达到的情况,对于mybatis 在不同方向上的扩展。
Mybatis简介
官方文档:mybatis – MyBatis 3 | Introduction
github:https://github.com/mybatis/
在使用上,突出一点是对sql更加灵活 可以写复杂的sql,当然也有非常多的灵活控件,这也是mybatis为什么这么火的原因把。
Mybatis整体框架
mybatis为什么能将我们写的sql交给驱动发送到 mysql 、oracle等等数据库上进行执行。通过 sqlsessionfactory 创建 sqlsession 交给执行器执行 具体的sql 映射到 mappedstatement
整个框架支撑着。
SpringBoot项目如何使用
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
public class User {
private String id;
private String userName;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
@Override
public String toString() {
return "User [id=" + id + ", userName=" + userName + "]";
}
}
动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。
可以直接复制即可。
<mapper namespace="com.mybatis.dao.UserMapper">
<select id="query" resultType="com.mybatis.domain.User">
select id ,user_name from user where 1=1
<if test="userName != null">
and user_name like CONCAT('%',#{userName},'%')
</if>
</select>
<!-- <select id="findById" resultType="User">
select id ,user_name
from user
where 1=1
<if test="userName != null">
and id = #{id}
</if>
</select>
<insert id="insert" parameterType="User">
insert user(id, user_name) values(#{id}, #{userName})
</insert> -->
</mapper>
- 数据源配置
<?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 系统配置 会改变 MyBatis 的运行时行为-->
<settings>
<!-- 使全局的映射器启用或禁用缓存。 -->
<setting name="cacheEnabled" value="true" />
<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 是否允许单条sql 返回多个数据集 (取决于驱动的兼容性) default:true -->
<setting name="multipleResultSetsEnabled" value="true" />
<!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
<setting name="useColumnLabel" value="true" />
<!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。 default:false -->
<setting name="useGeneratedKeys" value="false" />
<!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分 FULL:全部 -->
<setting name="autoMappingBehavior" value="PARTIAL" />
<!-- 这是默认的执行类型 (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新) -->
<setting name="defaultExecutorType" value="SIMPLE" />
<setting name="defaultStatementTimeout" value="25" />
<setting name="defaultFetchSize" value="100" />
<setting name="safeRowBoundsEnabled" value="false" />
<!-- 使用驼峰命名法转换字段。 -->
<setting name="mapUnderscoreToCamelCase" value="true" />
<!-- 设置本地缓存范围 session:就会有数据的共享 statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
<setting name="localCacheScope" value="SESSION" />
<!-- 默认为OTHER,为了解决oracle插入null报错的问题要设置为NULL -->
<setting name="jdbcTypeForNull" value="NULL" />
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
</settings>
<!--
<plugins>
插件配置
<plugin interceptor=""></plugin>
</plugins>
<mappers>
mapper.xml的映射文件配置
<package name=""/>
<mapper url=""/>
</mappers>
<databaseIdProvider type=""></databaseIdProvider>
<typeAliases>
为 Java 类型设置一个缩写名字
<typeAlias alias="Author" type="domain.blog.Author"/>
<package name="domain.blog"/>
</typeAliases>
<typeHandlers>
查询结果类型转换器
<typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>
<databaseIdProvider type="DB_VENDOR">
根据不同的数据库厂商执行不同的语句->
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle" />
</databaseIdProvider>
-->
</configuration>
- 扫描包配置
- 插件配置
mybatis的配置 在 application.yml 可以 指定
mybatis:
mapperLocations: classpath:/com/mybatis/dao/*.xml
typeAliasesPackage: com.mybatis.dao
mapperScanPackage: com.mybatis.dao
configLocation: classpath:/mybatis-config.xml
mysql:
datasource:
url 以及 username password 等
![](https://img-blog.csdnimg.cn/97d21a386cb746aeb43253244c920d84.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6Lip6Lip6Lip5LuO6Lip,size_19,color_FFFFFF,t_70,g_se,x_16)
//注:方法名和要UserMapper.xml中的id一致
@Update({"<script>", "update t_user " +
"<set> " +
"<if test='userName!=null'> user_name = #{userNaem}</if>" +
"<if test='userName!=null'> user_name = #{userNaem}</if>" +
"<if test='userName!=null'> user_name = #{userNaem}</if>" +
"<if test='userName!=null'> user_name = #{userNaem}</if>" +
"<if test='userName!=null'> user_name = #{userNaem}</if>" +
"</set>" +
"where id =#{id}",
"</script>"})
List<User> query(@Param("userName")String userName);
@Select("select id,user_name from user where id=#{id} ")
User findById(@Param("id")String id);
在SpringBoot使用mybatis是非常简单的。
Mybatis集成Redis缓存
mybatis-redis – MyBatis Redis | Reference Documentation
官方给我们提供的是,它支持缓存
MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:
<cache/>
这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
可用的清除策略有:
LRU
– 最近最少使用:移除最长时间不被使用的对象。FIFO
– 先进先出:按对象进入缓存的顺序来移除它们。SOFT
– 软引用:基于垃圾回收器状态和软引用规则移除对象。WEAK
– 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
默认的清除策略是 LRU。
<!-- -->
<cache />
<!--
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
-->
<!-- 开启基于redis的二级缓存 -->
<cache type="com.mybatis.cache.RedisCache"/>
其实对于 mybatis来说 ,它提供的cache接口 给我们, 具体的的实现由我们选择。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在springboot中添加 redis的配置
spring:
redis:
# redis服务器地址(默认为localhost)
host: localhost
# redis端口(默认为6379)
port: 6379
database: 10
cache 实现 必须要 有一个 构造参数 将 cache instance id
*/
public class RedisCache implements Cache {
private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final String id; // cache instance id
private RedisTemplate redisTemplate;
private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间
public RedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return id;
}
/**
* Put query result to redis
*
* @param key
* @param value
*/
@Override
@SuppressWarnings("unchecked")
public void putObject(Object key, Object value) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
opsForValue.set(key, value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
logger.debug("Put query result to redis");
}
catch (Throwable t) {
logger.error("Redis put failed", t);
}
}
/**
* Get cached query result from redis
*
* @param key
* @return
*/
@Override
public Object getObject(Object key) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
ValueOperations opsForValue = redisTemplate.opsForValue();
logger.debug("Get cached query result from redis");
return opsForValue.get(key);
}
catch (Throwable t) {
logger.error("Redis get failed, fail over to db", t);
return null;
}
}
/**
* Remove cached query result from redis
*
* @param key
* @return
*/
@Override
@SuppressWarnings("unchecked")
public Object removeObject(Object key) {
try {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(key);
logger.debug("Remove cached query result from redis");
}
catch (Throwable t) {
logger.error("Redis remove failed", t);
}
return null;
}
/**
* Clears this cache instance
*/
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.execute((RedisCallback) connection -> {
connection.flushDb();
return null;
});
logger.debug("Clear all the cached query result from redis");
}
/**
* This method is not used
*
* @return
*/
@Override
public int getSize() {
return 0;
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = (RedisTemplate) SpringContextUtil.getBean("redisTemplate");
}
return redisTemplate;
}
}
其实这个也可以抽象出来 成为 starter 成为 一个 公共的组件,更方便我们使用的。
这里 是 mybatis去加载的 所以 没把这个类注册到 ioc容器中 ,而是采用 SpringContextUtil 方式去获取的。
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = (RedisTemplate) SpringContextUtil.getBean("redisTemplate");
}
return redisTemplate;
}
如果没有实现cache的情况 则采用默认的二级缓存。
<cache type="com.mybatis.cache.RedisCache"/>
在 application.yml中配置的
mysql:
datasource:
readSize: 1 # 读库个数
type: com.alibaba.druid.pool.DruidDataSource
mapperLocations: classpath:/com/mybatis/dao/*.xml
configLocation: classpath:/mybatis-config.xml
write:
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 98.7%root
driver-class-name: com.mysql.cj.jdbc.Driver
minIdle: 5
maxActive: 100
initialSize: 10
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 50
removeAbandoned: true
filters: stat
read01:
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 98.7%root
driver-class-name: com.mysql.cj.jdbc.Driver
minIdle: 5
maxActive: 100
initialSize: 10
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 50
removeAbandoned: true
filters: stat
怎么实现多数据源 读写分离 的进行路由的。
sqlsessionfactory 只能修改 mybatis中创建。
根据数据类型进行判断 DataSourceConfiguration 去判断
/**
* 数据库源配置
*
*
*/
@Configuration
public class DataSourceConfiguration {
private static Logger log = LoggerFactory.getLogger(DataSourceConfiguration.class);
@Value("${mysql.datasource.type}")
private Class<? extends DataSource> dataSourceType;
/**
* 写库 数据源配置
* @return
*/
@Bean(name = "writeDataSource")
@Primary
@ConfigurationProperties(prefix = "mysql.datasource.write")
public DataSource writeDataSource() {
log.info("-------------------- writeDataSource init ---------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
/**
* 有多少个从库就要配置多少个
* @return
*/
@Bean(name = "readDataSource01")
@ConfigurationProperties(prefix = "mysql.datasource.read01")
public DataSource readDataSourceOne() {
log.info("-------------------- read01 DataSourceOne init ---------------------");
return DataSourceBuilder.create().type(dataSourceType).build();
}
}
这个datasourcetype 则需要根据 选择 到那个 数据源。
添加MybatisConfiguration 并且 添加注解扫描。
@Configuration
@AutoConfigureAfter(DataSourceConfiguration.class)
@MapperScan(basePackages="com.mybatis.dao")
针对sqlsessionfactory进行代理 开创建sqlSessionFactorys
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(roundRobinDataSouceProxy());
- 读取配置
- 设置mapper.xml文件所在位置
- 添加分页插件、打印sql插件
其实就是 创建 sqlsessionfacotry时创建动态代理对象,使用时,在判断使用那个 代理对象。
@Configuration
@AutoConfigureAfter(DataSourceConfiguration.class)
@MapperScan(basePackages="com.dongnaoedu.mybatis.dao")
public class MybatisConfiguration {
private static Logger log = LoggerFactory.getLogger(MybatisConfiguration.class);
@Value("${mysql.datasource.readSize}")
private String readDataSourceSize;
//XxxMapper.xml文件所在路径
@Value("${mysql.datasource.mapperLocations}")
private String mapperLocations;
// 加载全局的配置文件
@Value("${mysql.datasource.configLocation}")
private String configLocation;
@Autowired
@Qualifier("writeDataSource")
private DataSource writeDataSource;
@Autowired
@Qualifier("readDataSource01")
private DataSource readDataSource01;
@Bean(name="sqlSessionFactory")
public SqlSessionFactory sqlSessionFactorys() throws Exception {
log.info("-------------------- sqlSessionFactory init ---------------------");
try {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(roundRobinDataSouceProxy());
// 读取配置
sessionFactoryBean.setTypeAliasesPackage("com.mybatis.domain");
//设置mapper.xml文件所在位置
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(mapperLocations);
sessionFactoryBean.setMapperLocations(resources);
//设置mybatis-config.xml配置文件位置
sessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
//添加分页插件、打印sql插件
Interceptor[] plugins = new Interceptor[]{pageHelper(), new SqlPrintInterceptor()};
sessionFactoryBean.setPlugins(plugins);
return sessionFactoryBean.getObject();
} catch (IOException e) {
log.error("mybatis resolver mapper*xml is error",e);
return null;
} catch (Exception e) {
log.error("mybatis sqlSessionFactoryBean create error",e);
return null;
}
}
@Bean("roundRobinDataSouceProxy")
public AbstractRoutingDataSource roundRobinDataSouceProxy() {
AbstractRoutingDataSource proxy = new AbstractRoutingDataSource() {
private AtomicInteger count = new AtomicInteger();
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getReadOrWrite();
if(typeKey == null) {
throw new NullPointerException("数据库路由是,决定使用哪个数据源。");
}
if(DataSourceType.write.getType().contentEquals(typeKey)) {
return DataSourceType.write.getType();
}
int num = count.getAndAdd(1);
int lookupKey = num % Integer.valueOf(readDataSourceSize);
System.out.println("使用数据库Read-"+(lookupKey+1));
return DataSourceType.read.getType()+(lookupKey+1);
}
};
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
//把所有数据库都放在targetDataSources中,注意key值要和determineCurrentLookupKey()中代码写的一至,
//否则切换数据源时找不到正确的数据源
targetDataSources.put(DataSourceType.write.getType(), writeDataSource);
targetDataSources.put(DataSourceType.read.getType()+"1", readDataSource01);
proxy.setDefaultTargetDataSource(writeDataSource);
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
/**
* 分页插件
* @return
*/
@Bean
public PageHelper pageHelper() {
PageHelper pageHelper = new PageHelper();
Properties p = new Properties();
p.setProperty("offsetAsPageNum", "true");
p.setProperty("rowBoundsWithCount", "true");
p.setProperty("reasonable", "true");
p.setProperty("returnPageInfo", "check");
p.setProperty("params", "count=countSql");
pageHelper.setProperties(p);
return pageHelper;
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
//事务管理
@Bean
public PlatformTransactionManager annotationDrivenTransactionManager() {
return new DataSourceTransactionManager((DataSource)SpringContextUtil.getBean("roundRobinDataSouceProxy"));
}
}
选择那个 读写的数据源。
@Bean("roundRobinDataSouceProxy")
public AbstractRoutingDataSource roundRobinDataSouceProxy() {
AbstractRoutingDataSource proxy = new AbstractRoutingDataSource() {
private AtomicInteger count = new AtomicInteger();
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getReadOrWrite();
if(typeKey == null) {
throw new NullPointerException("数据库路由是,决定使用哪个数据源。");
}
if(DataSourceType.write.getType().contentEquals(typeKey)) {
return DataSourceType.write.getType();
}
int num = count.getAndAdd(1);
int lookupKey = num % Integer.valueOf(readDataSourceSize);
System.out.println("使用数据库Read-"+(lookupKey+1));
return DataSourceType.read.getType()+(lookupKey+1);
}
};
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
//把所有数据库都放在targetDataSources中,注意key值要和determineCurrentLookupKey()中代码写的一至,
//否则切换数据源时找不到正确的数据源
targetDataSources.put(DataSourceType.write.getType(), writeDataSource);
targetDataSources.put(DataSourceType.read.getType()+"1", readDataSource01);
proxy.setDefaultTargetDataSource(writeDataSource);
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
数据源的选择这里,
这里 可以 选择 判断 走那个数据源。
可以获取到MappedStatement 可以获取到 sql等并且可以打印出来。
MybatisPlus
官网:MyBatis-Plus (baomidou.com)
MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
特性
- 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
- 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
- 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
- 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
- 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
- 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
- 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
- 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
- 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
- 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
- 内置性能分析插件:可输出 SQL 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
- 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作
具体的实现就可以看看 对应官网 就可以了。
分页插件
利用 责任链的链条 对 于具体的sql 进行增加 并 获得创建。
对于实现来说是很简单 。