mybatis
v20190523
目录
- 一、框架概述
- 二、Mybatis概述
- 三、Mybatis开发
- 四、mybatis-config.xml
- 五、Mybatis源码专题
- 六、关联查询/延迟加载
- 七、动态SQL
- 八、缓存
- 九、Mybatis相关工具和插件
- 十、扩展知识
- 十一、参考
一、框架概述
1 什么是框架?
- 一个框架是一个
可复用
的设计构件 - 整体设计、依赖关系、责任分配、流程控制
- 上下文 Context
2 为什么要使用框架?
- 软件系统十分复杂
- 现代系统,不仅是模块很多,系统也很多
- 例如互联网、电商、分布式等项目
- 帮助完成基础工作
- 开发人员可以集中精力在业务逻辑设计
- 消除业务无关的重复代码
- 解决细节问题
- 成熟稳健
- 事务处理、安全性、数据流控制等
- 持续升级
- 很多人使用和维护
- 不需要自己升级代码
3 软件分层分层
- 表现层
- servlet
- spring mvc
- struts2
- 业务层
- java bean
- spring
- 持久层
- jdbc
- mybatis
- hibernate
- spring data jpa
二、Mybatis概述
1 官网
http://www.mybatis.org/mybatis-3/zh/index.html
2 mybatis是什么?
- 持久层框架
- java本身使用jdbc驱动做持久层开发
- JDBC Java数据库连接,Java Database Connectivity,简称JDBC
- 半自动化ORM框架
- ORM 对象关系映射,Object Relational Mapping,简称ORM,或O/RM,或O/R mapping
- 封装通用方法
- 开发人员只需要关注 SQL 本身
- 封装了 注册驱动、获取连接、创建statement、设置参数、对结果处理等
- xml/注解方式处理SQL
3 mybatis由来
- iBatis
- iBATIS一词来源于“internet”和“abatis”的组合
- mybatis 前身
- mybaits
- 官方简介 https://blog.mybatis.org/p/about.html
- MyBatis项目继承自iBATIS 3.0,其维护团队也包含iBATIS的初创成员。
- 2010年5月19日项目创建。当时Apache iBATIS 3.0发布,其开发团队宣布会在新的名字、新的站点中继续开发。
- 2013年11月10日,项目迁移到了GitHub。
4 JDBC使用步骤
- JDBC代码示例
- 1 加载驱动
- 2 获取数据库连接
- 3 sql预处理
- 4 执行sql
- 5 获取执行结果
5 简单使用JDBC存在的问题
- 数据库连接操作存在硬编码
- Mybatis全局配置文件,支持配置连接信息
- statement操作存在硬编码
- Mybatis通过Mapper文件,提供sql、参数映射
- 频繁开启数据库连接会降低数据库性能
- Mybatis全局配置文件,支持配置连接池
6 mybatis架构原理
- XML配置文件解析
- SqlSessionFactory
- SqlSession
- Executor
- MappedStatement
三、Mybatis开发
1 开发示例
2 需要关注的文件
- POJO类(DTO、VO、PO等)
- Mapper(dao)
- Mapper 映射文件
- 全局配置文件
3 mybatis使用常见问题
#{}
相当于jdbc里的占位符 ?- PrepareStatement
- 可以做SQL预编译
- 进行输入映射时,会对参数进行类型解析(例如 string 类型,解析后SQL语句会加上 '')
- 如果进行
简单类型
(String、Date、8种基本类型的包装类)的输入映射时,#{}中参数名称可以任意
${}
相当于jdbc里的连接符 +- Statement
- 每一次都是一个新的SQL语句,就是字符串拼SQL
- 存在SQL注入问题,使用 OR 关键字(OR 1=1),将查询条件忽略
- 如果进行
简单类型
(String、Date、8种基本类型的包装类)的输入映射时,#{}中参数名称必须用value
- OGNL
- 对象图导航语言 Object-Graph Navigation Language
- 多个入参时,使用对象
- 返回值是单一/多个对象,resultType都只描述单一对象的类型
- mapper xml 加载注意事项
- 当遇到 resource not found 或者 parsing error 时,注意检查mapper是否有内容写错误(参数、返回类型等)
4 mybatis开发dao层的三种方式
- 原始开发实现 dao接口及实现
- 代码示例
- 该种方式常见于ibatis迁移过来的项目
- 实现层注入 SqlSessionFactory 创建SqlSession
- 作用域
- SqlSessionFactoryBuilder
- 方法级别
- SqlSessionFactory
- 全局范围(应用级别)
- SqlSession
- 方法级别
- SqlSessionFactoryBuilder
- Mapper 代理开发实现(基于JDK的动态代理)
- 代码示例
- 动态代理(代理分为静态代理、动态代理)
- 基于JDK
- 针对有
接口
的类,进行动态代理
- 针对有
- 基于CGLIB
- 针对
子类继承父类
的方式,进行动态代理 - 目标类是父类,代理类是子类
- 针对
- 基于JDK
- XML方式
- 只需要 Mapper 接口(dao 接口)和 Mapper 约束文件,不需要实现类
- 规范
- 接口类的路径与 XML 文件中的 namespace 相同
- 接口方法名与 XML 文件中每个 statement 的 id 相同
- 接口方法入参类型与 XML 文件中每个 statement 的 parameterType 相同
- 接口方法返回值类型与 XML 文件中每个 statement 的 resultType 相同
- 与 spring 整合后,需要在 ioc 容器中管理一个 SqlSessionFactory 对象
- 注解方式
- 后续讲解
5 POJO输入映射和输出映射
5.1 parameterType 输入映射
- 输入参数类型
- 简单类型
- POJO类型
#{}
通过反射获取数据- StaticSqlSource (RawSqlSource)
${}
通过OGNL表达式获取数据- DynamicSqlSource
- Map类型
- List类型
- 动态SQL会讲解
5.2 resultType/resultMap 结果映射
- resultType
- 查询结果列名和属性名一致
- 不一致的会得不到结果
- resultMap
- 关联查询 一对多查询可以进行对象嵌套
- 延时加载(懒加载)
四、mybatis-config.xml
- properties(属性)
- 引入外部的 Java 配置文件(properties)
- 直接设置子标签
- 优先级 环境变量 > 配置文件 > 子标签
- 建议加前缀,避免受到环境影响
- settings(全局配置参数)
- typeAliases(类型别名)
- 用于POJO类
- 简化映射文件中的 parameterType resultType
- typeHandlers(类型处理器)
- 转换:Java 类型 -> JDBC 类型(mybatis) -> 数据库类型(数据库驱动)
- objectFactory(对象工厂)
- plugins(插件)
- 增强 Mybatis 执行 SQL 语句过程中的功能
- 例如:PageHelper分页插件
- environments(环境集合属性对象)
- 一般不使用这个,例如连接池
- environment(环境子属性对象)
- transactionManager(事务管理)
- dataSource(数据源)
- mappers(映射器)
- 批量加载 package,批量加载同一包下的 Mapper XML 文件,该包下接口和 XML 要配对才起效
五、Mybatis源码专题
1 设计模式
Java 常见三类23种设计模式
- 创建型 5
- 工厂模式 同属性的同一对象
- 构建者模式 不同属性的同一对象
- 结构型 7
- 行为性 11
2 初始化过程解析
2.1 核心流程
全局 XML 配置文件 -> Configuration 配置对象 -> SqlSessionFactory对象
// 1. 顶层 创建SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 1.1 new SqlSessionFactoryBuilder().build()底层 // 1.1.1 创建XMLConfigBuilder XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); // 1.1.2 XMLConfigBuilder进行解析,返回Configuration Configuration config = parser.parse(); // 1.1.3 根据配置构造,返回DefaultSqlSessionFactory build(config); public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }
2.2 XMLxxxBuilder XML文件解析构造器
父类 BaseBuilder
(Configuration、TypeAliasRegistry、TypeHandlerRegistry)
- XMLConfigBuilder 用来解析 MyBatis 的全局配置文件
mybatis-config.xml
- XMLMapperBuilder 用来解析 MyBatis 中的映射文件
xxxMapper.xml
- XMLStatementBuilder 用来解析映射文件中的 statement 语句
<select>xxx</select>
- MapperBuilderAssistant 用来辅助解析映射文件并生成
MappedStatement
对象
XML 解析使用了 XPath(javax.xml.xpath.XPath),将 XML 解析为七种类型节点(XNode)
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { // 1 初始化Configuration super(new Configuration()); ... this.configuration.setVariables(props); this.parsed = false; this.environment = environment; this.parser = parser; } // 1.1 new Configuration() // 1.1.1 注册常用别名 typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class); ...
2.3 parser.parse() 解析成 Configuration 流程
// 1 只解析一次,从configuration根节点开始 if (parsed) { ... } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; // 1.1 parseConfiguration() 流程 XML -> Configuration // 按顺序解析XML标签 // 解析properties propertiesElement(root.evalNode("properties")); ... // 解析Mappers mapperElement(root.evalNode("mappers"));
2.4 mapperElement() Mapper 解析流程
XMLMapperBuilder
// 1 全局配置文件获取到resource资源路径,加载xxxMapper.xml映射文件 String resource = child.getStringAttribute("resource"); ... InputStream inputStream = Resources.getResourceAsStream(resource); XMLMapperBuilder mapperParser = new XMLMapperBuilder(...); // 2 执行Mapper解析 mapperParser.parse(); // 2.1 执行Mapper解析流程 ... configurationElement(parser.evalNode("/mapper")); ... parsePendingResultMaps(); ... parsePendingStatements(); // 2.1.1 configurationElement() ... parameterMapElement(context.evalNodes("/mapper/parameterMap")); resultMapElements(context.evalNodes("/mapper/resultMap")); sqlElement(context.evalNodes("/mapper/sql")); buildStatementFromContext(context.evalNodes("select|insert|update|delete")); // 2.1.1.1 buildStatementFromContext() // 使用 XMLStatementBuilder 解析 select|insert|update|delete 标签为 statement statementParser.parseStatementNode(); // 2.1.1.1.1 statementParser.parseStatementNode() 解析核心部分 ... // 2.1.1.1.2 转换为真正的SQL语言 SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); ... // 2.1.1.1.3 使用 MapperBuilderAssistant 转换为 statement builderAssistant.addMappedStatement(...);
3 SQL执行流程
3.1 核心流程
openSession ->
SqlSession sqlSession = sqlSessionFactory.openSession(); User user = sqlSession.selectOne("findUserById", 1);
3.2 openSessionFromDataSource() 从数据源打开session
- 执行器类型 ExecutorType
- SIMPLE
- REUSE
- BATCH
- 设置事务级别 TransactionIsolationLevel
- NONE
- READ_COMMITTED
- READ_UNCOMMITTED
- REPEATABLE_READ
- SERIALIZABLE
- 是否自动提交 autoCommit
// 创建 DefaultSqlSession 流程 ... final Environment environment = configuration.getEnvironment(); ... tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); final Executor executor = configuration.newExecutor(tx, execType); return new DefaultSqlSession(configuration, executor, autoCommit); ...
3.3 查询执行流程解析 sqlSession.selectList()
- 参数1 statement
- "com.aizain.jhome.user.findUserById"
- 参数2 唯一入参
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { // 根据 statement(id)获取 MappedStatement MappedStatement ms = configuration.getMappedStatement(statement); // 委托者模式,委托给 executor 去执行 // 默认使用 CachingExecutor // CachingExecutor 主要用于处理二级缓存 // 如果无二级缓存 CachingExecutor 默认委托给 SimpleExecutor/BaseExecutor 真正执行 return executor.query( ms, // 特殊处理入参为集合类型的 涉及到动态SQL wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER ); ... }
3.4 executor.query() 执行细节
CachingExecutor sqlSource绑定parameter -> CachingExecutor 查二级缓存 -> BaseExecutor 查一级缓存 -> BaseExecutor 查数据库 -> SimpleExecutor 真正执行查询
// 1 executor.query() // 绑定入参 通过sqlSource,之前初始化流程时,已装载过sqlSource BoundSql boundSql = ms.getBoundSql(parameterObject); CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql); // query 会先走二级缓存,之后走一级缓存,最后走数据库查询 BaseExecutor.queryFromDatabase() query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); // 1.1 BaseExecutor.queryFromDatabase() 部分 // queryFromDatabase会调用SimpleExecutor.doQuery()真正执行查询,然后保存一级缓存 ... // 为了防止缓存穿透 后续讲解 localCache.putObject(key, EXECUTION_PLACEHOLDER); ... // SimpleExecutor.doQuery() list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); ... localCache.removeObject(key); ... localCache.putObject(key, list);
3.5 SimpleExecutor.doQuery() 真正执行查询
- 真正开始与 jdbc 打交道
- 根据 Statement 类型路由到不同 statement 处理
- 本质上是 jdbc statement 类型
- STATEMENT
- 用于对数据库进行通用访问
- 在运行时使用静态SQL语句时很有用
- Statement 接口不能接受参数
- PREPARED
- 当计划要多次使用SQL语句时使用
- PreparedStatement 接口在运行时接受输入参数
- CALLABLE
- 当想要访问数据库存储过程时使用
- CallableStatement 接口也可以接受运行时输入参数
// 1 SimpleExecutor.doQuery() Configuration configuration = ms.getConfiguration(); // 获取不同类型的Statement处理器 StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt = prepareStatement(handler, ms.getStatementLog()); handler.query(stmt, resultHandler); ... closeStatement(stmt); // 1.1 prepareStatement() Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); // 内部执行了 DefaultParameterHandler.setParameters() // 底层根据参数类型,调用不同的 prepareStatement.setXXX() handler.parameterize(stmt); // 1.1.1 handler.prepare() ... // 内部执行了 connection.prepareStatement() 生成 prepareStatement statement = instantiateStatement(connection); setStatementTimeout(statement, transactionTimeout); setFetchSize(statement); ...
4 Mapper代理解析
// 解析xml时,注册mapper接口 XMLConfigBuilder.mapperElement() // 注册到 MapperRegistry对象中 configuration.addMappers(mapperPackage); ... configuration.addMapper(mapperInterface); // MapperRegistry对象 // 用于存储注册的 接口类:代理工厂 Map<Class<?>, MapperProxyFactory<?>> knownMappers // MapperProxyFactory 中使用 jdk 生成动态代理 protected T newInstance(MapperProxy<T> mapperProxy) { // jdk 方法 java.lang.reflect.Proxy return (T) Proxy.newProxyInstance( mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy ); } // addMapper() 解析 if (hasMapper(type)) { throw new BindingException("Type " + type + " is already known to the MapperRegistry."); } knownMappers.put(type, new MapperProxyFactory<>(type)); // It's important that the type is added before the parser is run // otherwise the binding may automatically be attempted by the // mapper parser. If the type is already known, it won't try. // MapperAnnotationBuilder 用于处理Mapper的注解 MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse();
六、关联查询/延迟加载
1 关联查询
- 关联查询 一对多查询可以进行对象嵌套 嵌套结果
- 对象嵌套需要使用association/collection子标签
- 对象嵌套示例
- 对象嵌套可以将一对多的单维数据转换为二维数据,见下方例子
1.1 对象嵌套前
列 | 列2 |
---|---|
a | 1 |
a | 2 |
a | 3 |
b | 1 |
b | 2 |
1.2 对象嵌套后
列 | 列2 |
---|---|
a | [ 1 2 3 ] |
b | [ 1 2 ] |
2 延迟加载
- 懒加载,推迟关联对象的select查询,可以减小数据库压力
- 在resultMap中依赖association/collection子标签实现
- 延迟加载也被称为嵌套查询
- 分类 三类
- 直接加载
- 主/关联对象select同时执行
- 侵入式延迟
- 用到了对象的任何属性,就执行select
- 深度延迟
- 只有用到了关联对象,才执行select
- 直接加载
- 为了防止逆向工程覆盖特殊业务字段或处理,可以使用继承关系避免该问题(父类/子类)
- 对修改关闭,对扩展开放
2.1 延迟加载 映射文件配置
- select 指定延迟执行的语句
- column 以哪一列为基准进行延迟查询
- property 放入当前java对象的那个属性
- javaType 延迟查询结果的java类型
<association column="user_id" property="user" javaType="User" select="xxx.findUserById"/>
2.2 延迟加载 全局文件配置
- lazyLoadingEnabled
- 是否启用延迟加载
- 默认 false
- false 所有延迟加载配置都不会生效
- aggressiveLazyLoading
- 任何方法的调用都会触发延迟加载
- true 侵入式延迟模式 false 深度延迟
- 默认 true
- lazyLoadTriggerMethods
- 指定哪些方法触发延迟加载
- 默认 equals,clone,hashCode,toString
<settings> <setting name="lazyLoadingEnabled" value="false"/> <setting name="aggressiveLazyLoading" value="true"/> </settings>
for (OrderWithUser orderWithUser : orderWithUsers) { // debug可能看不出来区别,需要日志观察 // 开启侵入式延迟加载时 aggressiveLazyLoading true, 这步之前就会查询sql log.debug("Order get id {}", orderWithUser.getId()); // 开启深度延迟加载时 aggressiveLazyLoading false, 这步之前才会查询sql log.debug("Order get user {}", orderWithUser.getUser()); }
2.3 延迟加载 N+1问题
- 主表查询一次,关联表查询 N 次
七、动态SQL
- 目的:处理SQL字符串动态拼接
- 源码 BoundSql 处理该动态SQL参数绑定
- if 根据入参动态拼接SQL
- sql 去除重复部分的语句
- foreach 遍历拼接集合SQL
1 foreach collection属性 处理源码
- DefaultSqlSessionFactory.wrapCollection()
if (object instanceof Collection) { StrictMap<Object> map = new StrictMap<>(); map.put("collection", object); if (object instanceof List) { map.put("list", object); } return map; } else if (object != null && object.getClass().isArray()) { StrictMap<Object> map = new StrictMap<>(); map.put("array", object); return map; } return object;
八、缓存
- 一级缓存
- 默认开启
- 执行增删改会使缓存失效
- SqlSession 内共享
- SqlSession 使用 HashMap 存储一级缓存
- 取缓存源码位置 BaseExecutor.query()
- 存缓存源码位置 BaseExecutor.queryFromDatabase()
- 二级缓存
- 默认不开启
- 缓存对象需要实现序列化
- 同一 Mapper(namespace) 内共享
- 源码位置 CacheExecutor.query()
1 二级缓存
- 需要开启两处
- 缓存对象需要实现 Serializable
1.1 全局配置
<setting name="cacheEnabled" value="true"/>
1.2 Mapper配置
- 指定 mapper 文件命名空间下的二级缓存
<cache/> <select useCache="true" >xxx</select>
1.3 整合ehcache
1.3.1 简介
- 开源的 Java 分布式缓存
- 主要面向通用缓存
- 具有内存和磁盘存储、缓存加载器、缓存扩展、缓存异常处理程序
- 支持 REST、SOAP 等
- 分布式缓存常用 Redis
- Mybatis 的定位是做持久层框架,缓存数据管理不是 Mybatis 特长
1.3.2 特点
- 快速、简单
- 多种缓存策略
- 支持磁盘和内存存储
- 缓存数据会在虚拟机重启的过程中写入磁盘
- 可通过 RMI 可插入 API 等方式进行分布式缓存
- 具有缓存和缓存管理器的侦听接口
- 支持多缓存管理实例,以及一个实例的多个缓存区域
- 提供 Hibernate 的缓存实现
1.3.3 分布式缓存
- Mybatis 自身无法支持分布式缓存,需要整合其他框架
- 分布式缓存场景:集群部署方式
- 可以解决缓存不一致问题
1.3.4 整合思路
- Mybatis Cache 接口
- 默认实现 PerpetualCache
- 实现该接口,就可以实现二级缓存
- 引入 jar 包
- ehcache
- mybatis-ehcache
- 指定 type 类型
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
九、Mybatis相关工具和插件
1 逆向工程
- 自动生成文件
- po 类
- mapper 接口
- mapper 映射文件
- 相关扩展框架
- mybatis-plus
- 注意事项
- mapper.xml文件的内容不是被覆盖而是进行内容追加
- po类及mapper.java文件的内容是直接覆盖
2 PafeHelper分页插件
3 注解开发
- 代码示例
- 不需要使用 xml 映射文件
- 增删改查 静态SQL
- @Select
- @Insert
- @Delete
- @Update
- @SelectKey
- 增删改查 动态SQL
- @SelectProvider
- @InsertProvider
- @DeleteProvider
- @UpdateProvider
- @Results 注解
- 代替的是标签
<resultMap>
- 该注解中可以使用单个@Result 注解,也可以使用@Result 集合
- @Results({@Result(),@Result()})或@Results(@Result())
- 代替的是标签
- @Resutl 注解
- 代替了
<id>
标签和<result>
标签 - @Result 中 属性介绍:
- column 数据库的列名
- Property 需要装配的属性名
- one 需要使用的@One 注解(@Result(one=@One)()))
- many 需要使用的@Many 注解(@Result(many=@many)()))
- 代替了
- @One 注解(一对一)
- 代替了
<assocation>
标签,是多表查询的关键,在注解中用来指定子查询返回单一对象。 - @One 注解属性介绍:
- select 指定用来多表查询的 sqlmapper
- fetchType 会覆盖全局的配置参数 lazyLoadingEnabled。。
- 使用格式:
- @Result(column=" ",property="",one=@One(select=""))
- 代替了
- @Many 注解(多对一)
- 代替了标签,是是多表查询的关键,在注解中用来指定子查询返回对象集合。
- 注意:聚集元素用来处理“一对多”的关系。需要指定映射的 Java 实体类的属性,属性的 javaType(一般为 ArrayList)但是注解中可以不定义; 使用格式:
- @Result(property="",column="",many=@Many(select=""))
十、扩展知识
JDBC
- 参考 https://zh.wikipedia.org/wiki/Java%E6%95%B0%E6%8D%AE%E5%BA%93%E8%BF%9E%E6%8E%A5
- JDBC Java数据库连接,Java Database Connectivity,简称JDBC
- JDBC API主要位于JDK中的java.sql包中(之后扩展的内容位于javax.sql包中)
- DriverManager:负责加载各种不同驱动程序(Driver),并根据不同的请求,向调用者返回相应的数据库连接(Connection)。
- Driver:驱动程序,会将自身加载到DriverManager中去,并处理相应的请求并返回相应的数据库连接(Connection)。
- Connection:数据库连接,负责进行与数据库间的通讯,SQL执行以及事务处理都是在某个特定Connection环境中进行的。可以产生用以执行SQL的Statement。
- Statement:用以执行SQL查询和更新(针对静态SQL语句和单次执行)。
- PreparedStatement:用以执行包含动态参数的SQL查询和更新(在服务器端编译,允许重复执行以提高效率)。
- CallableStatement:用以调用数据库中的存储过程。
- SQLException:代表在数据库连接的创建和关闭和SQL语句的执行过程中发生了例外情况(即错误)。