mybatis详解(原理、常用配置、源码、案例、相关工具)

    xiaoxiao2022-07-15  164

    mybatis

    v20190523

    目录

    一、框架概述二、Mybatis概述三、Mybatis开发四、mybatis-config.xml五、Mybatis源码专题六、关联查询/延迟加载七、动态SQL八、缓存九、Mybatis相关工具和插件十、扩展知识十一、参考

    一、框架概述

    1 什么是框架?

    一个框架是一个可复用的设计构件整体设计、依赖关系、责任分配、流程控制上下文 Context

    2 为什么要使用框架?

    软件系统十分复杂 现代系统,不仅是模块很多,系统也很多例如互联网、电商、分布式等项目帮助完成基础工作 开发人员可以集中精力在业务逻辑设计消除业务无关的重复代码解决细节问题 成熟稳健事务处理、安全性、数据流控制等持续升级 很多人使用和维护不需要自己升级代码

    3 软件分层分层

    表现层 servletspring mvcstruts2业务层 java beanspring持久层 jdbcmybatishibernatespring 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.htmlMyBatis项目继承自iBATIS 3.0,其维护团队也包含iBATIS的初创成员。2010年5月19日项目创建。当时Apache iBATIS 3.0发布,其开发团队宣布会在新的名字、新的站点中继续开发。2013年11月10日,项目迁移到了GitHub。

    4 JDBC使用步骤

    JDBC代码示例1 加载驱动2 获取数据库连接3 sql预处理4 执行sql5 获取执行结果

    5 简单使用JDBC存在的问题

    数据库连接操作存在硬编码 Mybatis全局配置文件,支持配置连接信息statement操作存在硬编码 Mybatis通过Mapper文件,提供sql、参数映射频繁开启数据库连接会降低数据库性能 Mybatis全局配置文件,支持配置连接池

    6 mybatis架构原理

    XML配置文件解析SqlSessionFactorySqlSessionExecutorMappedStatement

    架构图

    三、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种基本类型的包装类)的输入映射时,#{}中参数名称必须用valueOGNL 对象图导航语言 Object-Graph Navigation Language多个入参时,使用对象返回值是单一/多个对象,resultType都只描述单一对象的类型mapper xml 加载注意事项 当遇到 resource not found 或者 parsing error 时,注意检查mapper是否有内容写错误(参数、返回类型等)

    4 mybatis开发dao层的三种方式

    原始开发实现 dao接口及实现 代码示例该种方式常见于ibatis迁移过来的项目实现层注入 SqlSessionFactory 创建SqlSession作用域 SqlSessionFactoryBuilder 方法级别SqlSessionFactory 全局范围(应用级别)SqlSession 方法级别Mapper 代理开发实现(基于JDK的动态代理) 代码示例动态代理(代理分为静态代理、动态代理) 基于JDK 针对有接口的类,进行动态代理基于CGLIB 针对子类继承父类的方式,进行动态代理目标类是父类,代理类是子类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表达式获取数据 DynamicSqlSourceMap类型List类型 动态SQL会讲解

    5.2 resultType/resultMap 结果映射

    resultType 查询结果列名和属性名一致不一致的会得不到结果resultMap 关联查询 一对多查询可以进行对象嵌套延时加载(懒加载)

    四、mybatis-config.xml

    properties(属性) 引入外部的 Java 配置文件(properties)直接设置子标签优先级 环境变量 > 配置文件 > 子标签建议加前缀,避免受到环境影响settings(全局配置参数)typeAliases(类型别名) 用于POJO类简化映射文件中的 parameterType resultTypetypeHandlers(类型处理器) 转换: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.xmlXMLMapperBuilder 用来解析 MyBatis 中的映射文件 xxxMapper.xmlXMLStatementBuilder 用来解析映射文件中的 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 SIMPLEREUSEBATCH设置事务级别 TransactionIsolationLevel NONEREAD_COMMITTEDREAD_UNCOMMITTEDREPEATABLE_READSERIALIZABLE是否自动提交 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 对象嵌套前

    列列2a1a2a3b1b2

    1.2 对象嵌套后

    列列2a[ 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 是否启用延迟加载默认 falsefalse 所有延迟加载配置都不会生效aggressiveLazyLoading 任何方法的调用都会触发延迟加载true 侵入式延迟模式 false 深度延迟默认 truelazyLoadTriggerMethods 指定哪些方法触发延迟加载默认 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 根据入参动态拼接SQLsql 去除重复部分的语句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 等分布式缓存常用 RedisMybatis 的定位是做持久层框架,缓存数据管理不是 Mybatis 特长

    1.3.2 特点

    快速、简单多种缓存策略支持磁盘和内存存储缓存数据会在虚拟机重启的过程中写入磁盘可通过 RMI 可插入 API 等方式进行分布式缓存具有缓存和缓存管理器的侦听接口支持多缓存管理实例,以及一个实例的多个缓存区域提供 Hibernate 的缓存实现

    1.3.3 分布式缓存

    Mybatis 自身无法支持分布式缓存,需要整合其他框架分布式缓存场景:集群部署方式可以解决缓存不一致问题

    1.3.4 整合思路

    Mybatis Cache 接口 默认实现 PerpetualCache实现该接口,就可以实现二级缓存引入 jar 包 ehcachemybatis-ehcache指定 type 类型 <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

    九、Mybatis相关工具和插件

    1 逆向工程

    自动生成文件 po 类mapper 接口mapper 映射文件相关扩展框架 mybatis-plus注意事项 mapper.xml文件的内容不是被覆盖而是进行内容追加po类及mapper.java文件的内容是直接覆盖

    2 PafeHelper分页插件

    Github地址

    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 指定用来多表查询的 sqlmapperfetchType 会覆盖全局的配置参数 lazyLoadingEnabled。。使用格式: @Result(column=" ",property="",one=@One(select=""))@Many 注解(多对一) 代替了标签,是是多表查询的关键,在注解中用来指定子查询返回对象集合。注意:聚集元素用来处理“一对多”的关系。需要指定映射的 Java 实体类的属性,属性的 javaType(一般为 ArrayList)但是注解中可以不定义; 使用格式:@Result(property="",column="",many=@Many(select=""))

    十、扩展知识

    JDBC

    参考 https://zh.wikipedia.org/wiki/Java数据库连接JDBC Java数据库连接,Java Database Connectivity,简称JDBCJDBC 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语句的执行过程中发生了例外情况(即错误)。

    十一、参考

    https://www.kaikeba.com/vipcourse/javahttps://zh.wikipedia.org/wiki/Wikipedia:首页http://www.mybatis.org/mybatis-3/zh/index.htmlhttps://blog.mybatis.org/https://book.douban.com/subject/26858114/https://github.com/guangyuzhihun/JHome
    最新回复(0)