为什么要使用MyBatis
没有MyBatis的时候连接数据库
JDBC连接
//注册JDBC驱动
Class.forName("com.mysql.jdbc.Driver");
// 打开连接
conn DriverManager.getConnection(DB_URL,USER,PASSWORD);
//执行查询
stmt conn.createStatement();
String sql = "select id,name from book";
ResultSet rs = stmt.executeQuery(sql);
while(rs.next()){
...
}
从代码上可以看出,JDBC连接将数据库操作功能会与业务功能耦合在一起,如果调整表结构、或者修改业务逻辑,工作量难以估计,且容易对正常功能产生影响。
同时对结果集的处理也必须根据字段一个一个匹配,增加很多重复的工作量。
Book book = new Book();
book.id = res.getInt("id");
book.name = rs.getString("name");
DbUtils
通过QueryRunner简单的封装将数据库的驱动、连接、执行与业务隔离,同时提供了ResultSetHandler进行结果集到POJO的转换。
缺点:DbUtils要求数据库字段跟对象的属性名称完全一致,否则转换失败。
Spring JDBC
Spring也对原生JDBC进行了封装,并提供了JdbcTemplate,来简化我们对数据库的操作。并提供了RowMapper接口将结果集转换成对象。
缺点:每个对象都需要定义一个Mapper并实现RowMapper接口,手动编写转换代码。
Hibernate
Hibernate通过xml方式配置了实体类和数据库的映射关系,提供session的增删查改方法。
//获取加载配置管理类
Configuration configuration = new Configuration();
//加载默认的hibernate.cfg.xml
configuration.configure();
//创建session工厂
SessionFactory factory = configuration.buildSessionFactory();
Session session = factory.openSession();
//开启事务
Transaction transaction = session.getTransaction();
transaction.begin();
doSomeThing();
transaction.commit();
session.close();
通过Hibernate我们操作数据库就跟操作对象一样,Hibernate帮我们自动生成SQL语句(屏蔽数据库的差异),自动映射实体类和数据表结构,代码更加简洁。
缺点:
- 调用get、save、update等方法时基于对象,操作所有字段,无法制定部分字段,不够灵活。
- 对于自动生成的SQL,想去优化非常困难。
- 不支持动态SQL,如:分表中表名变化等。
MyBatis
MyBatis跟Hibernate相比去掉了SQL的自动生成,同时通过xml文件将SQL语句与代码分离。
特性:
- 使用连接池对连接进行管理
- SQL和代码分离,集中管理
- 结果集映射
- 参数映射和动态SQL
- 重复SQL提取
- 缓存管理
- 插件机制
String resource = "mybatis-config.xml";
InputStream is = Resource.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = sqlSessionFactory.openSession();
BookMapper bookMapper = session.getMapper(BookMapper.class);
Book book = bookMapper.selectBookById(1);
session.close();
MyBatis
整体架构
接口层
SqlSession是上层应用和MyBatis打交道的桥梁,定义了对数据库的操作方法。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作。
核心处理层
核心处理层主要负责:
- 解析xml文件中的SQL语句,包括插入参数,动态生成SQL语句
- 把接口出入的参数解析并映射成JDBC类型
- 执行SQL语句
- 处理结果集并映射成Java对象
- 提供插件功能
基础支持层
基础支持层主要是抽取通用的功能,用来支撑核心处理层的功能。
核心对象
- Configuration
包含MyBatis所有的配置信息, - SqlSessionFactoryBuilder
用于构建SqlSessionFactory生命周期存在于方法。 - SqlSessionFactory
创建SqlSession用于应用程序访问数据库,生命周期为应用的生命周期,单例实现 - SqlSession
访问数据库的会话,线程不安全,不能在线程间共享。因此在操作完成后需要关闭,生命周期为请求或者操作中。 - Executor
执行器,是MyBatis调度的核心,负责SQL语句的生成和缓存的查询维护等工作。 - StatementHandler
封装具体的JDBC操作,设置参数,转换结果集等 - ParameterHandle
将用户的参数转换为JDBC Statement需要的参数 - ResultSetHandler
将JDBC返回的结果集对象转换为List类型的集合 - MapperProxy
代理Mapper接口 - MappedStatement
维护增删查改等SQL语句的封装
MyBatis缓存
MyBatis提供了一级缓存和二级缓存,并且预留了集成第三方缓存的接口。MyBatis的缓存都在cache包里面,其中有一个Cache接口,同时提供了一个默认实现类PerpetualCache。此外通过装饰器,提供了很多额外的功能,如:回收策略、日志记录、定时刷新等。
缓存淘汰策略
缓存淘汰基本分为三类:基本缓存、淘汰缓存、装饰器缓存。
- PerpetualCache
-
- 基本实现
- LruCache
-
- LRU策略,当缓存到达上限的时候,删除最少使用的缓存
- FifoCache
-
- FIFO策略,当缓存到达上限的时候,删除最先入队的缓存
- SoftCache、WeakCache
-
- 利用JVM的软引用、弱引用实现缓存,当JVM内存不足时会自动清理掉
- LoggingCache
-
- 带日志功能,可以用于缓存命中率计算
- SynchronizedCache
-
- 同步缓存,解决并发问题
- BlockingCache
-
- 阻塞缓存,
- SerializedCache
- ScheduledCache
- TransactionalCache
一级缓存
一级缓存也叫本地缓存,保存在SqlSession层面。一级缓存默认开启,且不需要任何配置。
一级缓存在BaseExecutor的query
(获取)和queryFromDatabase
(保存)中,且在同一个会话的update
调用clearLocalCache
方法使缓存失效。
一级缓存可以通过xml、注解的方式关闭。
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>
@Options(flushCache = Options.FlushCachePolicy.TRUE)
@Select("select * from book where id = #{id,jdbcType=BIGINT}")
@ResultMap("resultMap")
Book find(Long id);
由于一级缓存是在会话中,在多会话或者分布式环境下,会存在脏数据的情况。为了解决这个问题,MyBatis提供了二级缓存。
二级缓存
二级缓存的作用范围是namespace,可以被多个SqlSession共享。开启二级缓存MyBatis则使用CachingExecutor
对Executor
进行了装饰。CachingExecutor
对于查询的请求会先判断是否有缓存结果如果有则直接返回,否则交给真正的查询器执行,如SimpleExecutor
。开启方式:
- 在mybatis-config.xml中配置cacheEnabled(默认是开启的,可以不配置)
<setting name="cacheEnabled" value="true"/>
- 在Mapper.xml中配置cache标签
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" size="1024" eviction="LRU" flushInterval="12000" readOnly="false"/>
对于部分实时性不高的方法,不需要二级缓存可以在具体StatementId上显示关闭二级缓存useCache="false"
默认是true。
二级缓存使用TCM来管理,因此在存在事务的情况下事务提交后才会生效。
二级缓存也存在脏读的情况,如表Book在两个namespace中存在,则一个namespace刷新,另一个则会出现脏读。
cache-ref
MyBatis用cache-ref避免二级缓存脏读问题,cache-ref代表引用别的命名控件的Cache配置,即两个空间使用同一个Cache。
第三方缓存
MyBatis可以集成第三方缓存方式。如redis、ehcahe等。
<cache type="org.mybatis.caches.redis.RedisCache" eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
配置文件
configuration
配置文件根目录,对应着MyBatis最重要的配置类Configuration。
properties
配置参数,如数据库连接信息,避免写死在xml中可以将参数单独放在properties文件中,通过properties标签引入${}.
settings
typeAliases
类型别名,通过typeAliases可以为我们自己的Bean创建别名。MyBatis里面有系统预先定义好的类型别名,在TypeAliasRegistry中。
typeHandlers
Java类型与数据库的JDBC类型不完全一致,我们可以通过MyBatis提供的typeHandlers进行转换。MyBatis内置了很多TypeHandler(type包下),已经在TypeHandlerRegistry中注册了。
如果我们需要自定义一套类型转换规则,或者在处理时做一些特殊动作,那么可以继承BaseTypeHander来编写自己的TypeHandler。
public class MyTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
throws SQLException {
// 设置 String 类型的参数的时候调用,Java类型到JDBC类型
ps.setString(i, parameter);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
// 根据列名获取 String 类型的参数的时候调用,JDBC类型到java类型
return rs.getString(columnName);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
// 根据下标获取 String 类型的参数的时候调用
System.out.println("---------------getNullableResult2:"+columnIndex);
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
System.out.println("---------------getNullableResult3:");
return cs.getString(columnIndex);
}
}
在config文件中注册:
<typeHandlers> <typeHandler handler="com.Test.MyTypeHandler"></typeHandler> </typeHandlers>
在需要使用的字段上指定Handler:
<insert id="insertBook" parameterType="com.Test.domain.book"> insert into book(name)values(#{name,jdbcType=VARCHAR,typeHandler=com.Test.MyTypeHandler}) </insert>
最后在resultMap上指定typeHandler:
<result column="name" property="name" jdbcType="VARCHAR" typeHandler="com.Test.MyTypeHandler"/>
objectFactory
MyBatis提供了一个工厂类接口(ObjectFactory)专门用来创建对象实例,用来处理数据库返回的结果集转换为实体类。
ObjectFactory有一个默认实现DefaultObjectFactory,通过调用instantiateClass(),利用反射实现。同样的我们可以自己定义一个类,实现ObjectFactory,来控制生成对象时的行为。
public class MyObjectFactory extends DefaultObjectFactory {
@Override
public Object create(Class type) {
System.out.println("创建对象方法:" + type);
if (type.equals(Book.class)) {
Book book = (Book) super.create(type);
blog.setName("myBook");
return book;
}
Object result = super.create(type);
return result;
}
public static void main(String[] args){
MyObjectFactory myObjectFactory = new MyObjectFactory();
Book book = myObjectFactory.create(Book.class);
}
}
同时也可以在config文件里注册,这样在创建对象的时候会被自动调用:
<objectFactory type="com.Test.MyObjectFactory">
<property name="name" value="myBook"/>
</objectFactory>
objectFactory.create()什么时候会调用?
创建对象和创建DefaultResultSetHandler的时候。
plugins
MyBatis预留了插件的接口让MyBatis更容易扩展,MyBatis提供了四个对象插件,Executor、ParameterHandler、ResultSetHandler、StatementHandler。
environment、environments
管理数据库环境,比如开发环境、测试环境、生产环境等。可以在不通给环境中使用不通的数据库地址或者类型。
<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>
transactionManager
- 配置JDBC则会使用Connection对象的commmit()、rollback()、close()管理事务。
- 配置MANAGED则会交给容器管理,如Weblogic等。
- 如果是Spring+MyBatis则无需配置,applicationContext.xml里的数据源配置会覆盖MyBatis的。
mappers
配置Mapper.xml的路径,让MyBatis启动的时候去扫描这些映射器,创建映射关系。
- 使用相对于类路径的资源引用
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
- 使用完全限定资源定位符(URL)
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
- 使用映射器接口实现类的完全限定类名
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
- 将包内的映射器接口实现全部注册为映射器
<mappers> <package name="org.mybatis.builder"/> </mappers>
MyBatis在哪个步骤拿到SQL语句?
SQL语句在MappedStatement中的SqlSource中,运行时通过getBoundSql()方法获取,MappedStatement由configuration的getMappedStatement()方法获取。
dataSource
settings
属性名 | 简介 | 有效值 | 默认值 |
cacheEnabled | 所有映射器配置缓存的开关,全局缓存开关 | true|false | true |
lazyLoadingEnabled | 控制是否延迟加载。特殊关系单独配置可以使用fetchType属性来覆盖 | true|false | false |
aggressiveLazyLoading | 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载 | true|false | false |
multipleResultSetsEnabled | 是否允许单个语句返回多结果集(需要数据库驱动支持)。 | true|false | true |
useColumnLabel | 使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 | true|false | true |
useGeneratedKeys | 允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 | true|false | False |
autoMappingBehavior | 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 | NONE, PARTIAL, FULL | PARTIAL |
autoMapping UnknownColumnBehavior | 指定发现自动映射目标未知列(或未知属性类型)的行为。 | NONE, WARNING, FAILING | NONE |
defaultExecutorType | 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 | SIMPLE REUSE BATCH | SIMPLE |
defaultStatementTimeout | 设置超时时间,它决定数据库驱动等待数据库响应的秒数。 | 任意正整数 | 未设置 (null) |
defaultFetchSize | 为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。 | 任意正整数 | 未设置 (null) |
defaultResultSetType | 指定语句默认的滚动策略。(新增于 3.5.2) | FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置) | 未设置 (null) |
safeRowBoundsEnabled | 是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。 | true | false | False |
safeResultHandlerEnabled | 是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 | true | false | True |
mapUnderscoreToCamelCase | 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 | true | false | False |
localCacheScope | MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。 若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 | SESSION | STATEMENT | SESSION |
jdbcTypeForNull | 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 | JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。 | OTHER |
lazyLoadTriggerMethods | 指定对象的哪些方法触发一次延迟加载。 | 用逗号分隔的方法列表。 | equals,clone,hashCode,toString |
defaultScriptingLanguage | 指定动态 SQL 生成使用的默认脚本语言。 | 一个类型别名或全限定类名。 | org.apache.ibatis.scripting.xmltags.XMLLanguageDriver |
defaultEnumTypeHandler | 指定 Enum 使用的默认 。(新增于 3.4.5) | 一个类型别名或全限定类名。 | org.apache.ibatis.type.EnumTypeHandler |
callSettersOnNulls | 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。 | true | false | false |
returnInstanceForEmptyRow | 当返回行的所有列都是空时,MyBatis默认返回 。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2) | true | false | false |
logPrefix | 指定 MyBatis 增加到日志名称的前缀。 | 任何字符串 | 未设置 |
logImpl | 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 | SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING | 未设置 |
proxyFactory | 指定 Mybatis 创建可延迟加载对象所用到的代理工具。 | CGLIB | JAVASSIST | JAVASSIST (MyBatis 3.3 以上) |
vfsImpl | 指定 VFS 的实现 | 自定义 VFS 的实现的类全限定名,以逗号分隔。 | 未设置 |
useActualParamName | 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 选项。(新增于 3.4.1) | true | false | true |
configurationFactory | 指定一个提供 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为 的方法。(新增于 3.2.3) | 一个类型别名或完全限定类名。 | 未设置 |
shrinkWhitespacesInSql | 从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5) | true | false | false |
| Specifies an sql provider class that holds provider method (Since 3.5.6). This class apply to the (or ) attribute on sql provider annotation(e.g. ), when these attribute was omitted. | A type alias or fully qualified class name | Not set |
Mapper.xml配置文件
cache
二级缓存配置
cache-ref
resultMap
数据集加载转换对象
sql
可重用语句块
<sql id="Base_Column_List"> id,name </sql>
操作标签
insert、update、delete、select、resultType、resultMap
关联查询通常配置resultMap或者修改dto,增加字段,或者引用关联对象(association等)
其他标签
if、choose(when、otherwise)、trim(where、set)、foreach
嵌套查询
- 嵌套结果
<resultMap id="BookResultMap" type="com.Test.domain.book">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<!-- 联合查询,将author的属性映射到ResultMap -->
<association property="author" javaType="com.Test.domain.Author">
<id column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
</association>
</resultMap>
- 嵌套查询
<resultMap id="BlogWithAuthorQueryMap" type="com.Test.domain.book">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.Test.domain.Author" column="author_id" select="selectAuthor"/>
</resultMap>
<!-- 嵌套查询 -->
<select id="selectAuthor" parameterType="int" resultType="com.Test.domain.Author">
select author_id authorId, author_name authorName
from author where author_id = #{authorId}
</select>
该方式存在N+1问题,即查询了一次员工,返回了多个员工信息时会再次查询N次部门信息。在MyBatis中可以通过延迟加载的方式解决。
<setting name="lazyLoadingEnabled" value="true"/><!--启用延迟加载-->
<setting name="aggressiveLazyLoading" value="false"/><!--是否对象的所有方法都会触发-->
翻页
逻辑分页
MyBatis提供了RowBounds逻辑分页对象,里面有offset和limit两个属性。只需要在Mapper接口的方法上加上这个参数,不需要修改xml里面的SQL语句。通过skipRows(resultSet,rowBounds)会舍弃掉offset条数据,然后对剩下的数据取limit条。
如果数据量大,则存在效率问题(改方式等同于内存分页)。
物理分页
方法一、将limit和pageSize作为参数传给sql语句
<select id="selectBookPage" parameterType="map" resultMap="BaseResultMap">
select * from book limit #{limit} , #{pageSize}
</select>
该方法需要在代码里计算limit数据,需要在每个Statement里编写代码造成Mapper映射器代码冗余。
方法二、利用MyBatis拦截器实现,如PageHeler
PageHelper.startPage(0,10);
List<Book> books = bookService.list();
PageInfo page = new Page(books);
通用Mapper
通过编写泛型通用接口,如BaseMapper,把实体类作为参数传入。接口里定义大量的增删查改接口,方法支持泛型。自定义的Mapper接口继承通用接口,则可以避免大量类似的代码。改方式只适用于单表。
插件
MyBatis使用代理模式和责任链模式来实现插件机制。允许Executor、StatementHandler、ParameterHandler、ResultSetHandler。
实现步骤:
- 实现Interceptor接口
- 添加@Interceptors({@Signature()}),指定拦截的方法和对象
- 实现接口方法
intercept
、plugin
、setProperties
- 在config文件中注册
<plugins>
<plugin intercept="com.github.pagehelper.PageInterceptor">
<property name="offsetAsPageNum" value="true"/>
</plugin>
</plugins>