Mybatis

Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注sql语句本身, 不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。 开发人员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。

MyBatis可以使用xml或注解来配置和映射原生信息,将POJO映射成数据库中的记录,避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。

通过xml文件或注解的方式将要执行的各种statement配置起来,并通过java对象和statementsql的动态参数进行映射生成最终执行的sql语句, 最后由MyBatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。

Mybatis的优缺点

优点:

  • 基于sql语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,sql写在xml 里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态sql语句,并可重用。
  • 代码少,与JDBC相比,减少了一半以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
  • 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
  • 能够与Spring很好的集成;
  • 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

缺点:

  • sql语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写sql语句能力有一定要求。
  • sql语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

Mybatis和Hibernate的区别

相同点

都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。

不同点

  • MyBatis是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简单
  • Hibernate是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂

sql优化和移植性

  • Hibernatesql语句封装,提供了日志、缓存、级联(级联比MyBatis强大)等特性,</br> 此外还提供 hqlHibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。</br> 如果项目需要支持多种数据库,代码开发量少,但sql语句优化困难。

  • MyBatis需要手动编写sql,支持动态sql、处理列表、动态生成表名、支持存储过程。</br> 开发工作量相对大些。直接使用sql语句操作数据库,不支持数据库无关性,但sql语句优化容易。

Mybatis 常用标签

最常见的无非就是crud(增删改查)此类标签:

  • insert:新增
  • update:修改
  • delete:删除
  • select:查询

除了以上还有很多:

  • resultMap:结果映射
  • parameterMap:参数映射
  • resultType:结果类型
  • parameterType:参数类型
  • sql:sql片段
  • include:引用sql片段
  • selectKey:主键生成策略,获取自增主键id的值并进行设置
  • association:一对一关联
  • collection:一对多关联
  • discriminator:多表继承
  • setwhereifforeachtrimchoosewhenotherwisebind:一般写动态sql涉及到的标签
  • 等等

Mybatis $()#()的区别

  • ${}是字符串替换,是Properties文件中的变量占位符,它可以用于标签属性值和sql内部,属于静态文本替换
    • Mybatis在处理${}时,就是把${}直接替换成变量的值,这种会出现sql注入的风险。
  • #{}是预编译处理,是sql的参数占位符,
    • Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatementset方法来赋值

Mybatis 模糊查询

Mybatis的模糊查询一般存在两种写法,一种是使用${},另一种是使用#{}

使用${}的存在sql注入的风险,一般不推荐使用

一般的写法:

select * from user where name like CONCAT('%', #{name}, '%')

like也可以根据使用情况替换成likeLeftlikeRight

  • likeLeft:使用%作为通配符,只能用在字符串的开头。如:name likeLeft '%T',表示查询name字段以T结尾的记录。
  • likeRight:使用%作为通配符,只能用在字符串的末尾。如:name likeRight 'T%',表示查询name字段以T开头的记录

Mybatis 嵌套查询

MyBatis嵌套查询通常指的是在一个查询中嵌套另一个查询的结果。

这可以通过使用<select>标签嵌套来实现,也可以通过在映射文件中使用<collection><association>等复杂类型的属性来实现。

以下是一个使用<collection>进行嵌套查询的例子:

假设我们有两个表:学生表student、班级表clazz

现在,我们查询一个班级和它的所有学生的信息。

在班级类中定义一个集合类型的allStudents属性来放所有学生:

public class Clazz {
    private Long id;
    private String name;
    // 其他字段...
    private List<Student> allStudents;
    // getters、setters...
}

然后,在映射文件中定义查询并使用<collection>进行嵌套查询:

<mapper namespace="com.xxx.mapper.ClazzMapper">
  <!-- 结果映射 -->
  <resultMap id="ClazzMap" type="Clazz">
    <id property="id" column="id"/>
    <result property="title" column="title"/>
    <!-- 其他字段映射... -->
    <collection property="allStudents"
                ofType="Student"
                select="selectStudentByClazzId"
                column="id"/>
  </resultMap>

  <!-- 查询班级 -->
  <select id="selectById" resultType="Clazz">
      SELECT * FROM clazz WHERE id = #{id}
  </select>

  <!-- 查询学生,并嵌套在帖子查询中 -->
  <select id="selectStudentByClazzId" resultType="Student">
      SELECT * FROM student WHERE clazz_id = #{id}
  </select>

</mapper>

<collection>标签中,property属性指定了嵌套查询的属性名,ofType属性指定了集合中元素的类型,select属性指定了用于查询集合的嵌套查询的IDcolumn属性指定了嵌套查询使用的外键列。

最后调用selectById方法来查询班级信息,同时会自动执行嵌套查询selectStudentByClazzId,并将结果映射到Clazz对象的allStudents属性中。

Mybatis 缓存

Mybatis中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。

一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中进行相同的sql语句查询时, 第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存1024sql

二级缓存是指可以跨SqlSession的缓存。是mapper级别的缓存,对于mapper级别的缓存不同的SqlSession是可以共享的。

一级缓存原理(SqlSession级别)

第一次发出一个查询sqlsql查询结果写入SqlSession的一级缓存中,缓存使用的数据结构是一个map

  • keyMapperID+offset+limit+sql+所有的入参
  • value:用户信息

同一个SqlSession再次发出相同的sql,就从缓存中取出数据。</br> 如果两次中间出现commit操作(修改、添加、删除),本SqlSession中的一级缓存区域全部清空,</br> 下次再去缓存中查询不到,就要从数据库查询,从数据库查询到再写入缓存。

二级缓存原理(mapper基本)

二级缓存的范围是mapper级别(mapper同一个命名空间),mapper以命名空间为单位创建缓存数据结构,结构是map

  • keyMapperID+offset+limit+sql+所有的入参

mybatis的二级缓存是通过CacheExecutor实现的。

CacheExecutor其实是Executor的代理对象。

所有的查询操作,在CacheExecutor中都会先匹配缓存中是否存在,不存在则查询数据库。

具体使用需要配置:

  • Mybatis全局配置中启用二级缓存配置
  • 在对应的Mapper.xml中配置cache节点
  • 在对应的select查询节点中添加useCache=true
  • 属性类需要实现Serializable序列化接口

Mybatis 工作原理

  • 读取MyBatis配置文件:mybatis-config.xmlMyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。
  • 加载sql映射文件。文件中包含了操作数据库的sql语句,需要在MyBatis配置文件mybatis-config.xml中加载。
    • mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。
  • 构造会话工厂:通过MyBatis的环境等配置信息构建会话工厂SqlSessionFactory
  • 创建会话对象:由会话工厂创建SqlSession对象,该对象中包含了执行sql语句的所有方法。
  • Executor执行器:MyBatis底层定义了一个Executor接口来操作数据库, 根据SqlSession传递的参数动态地生成需要执行的sql语句,同时负责查询缓存的维护。
  • MappedStatement对象:在Executor接口的执行方法中有一个MappedStatement类型的参数, 是对映射信息的封装,用于存储要映射的sql语句的id参数等信息。
  • 输入参数映射:输入参数类型可以是MapList等集合类型,也可以是基本数据类型和POJO类型。 输入参数映射过程类似于JDBCpreparedStatement对象设置参数的过程。
  • 输出结果映射:输出结果类型可以是MapList等集合类型,也可以是基本数据类型和POJO类型。 输出结果映射过程类似于JDBC对结果集的解析过程。

Mybatis 插件

MyBatis可以拦截以下四大核心组件的方法调用:

  • Executor:执行器,负责sql语句的执行和事务管理(updatequerycommitrollback等方法)。
  • StatementHandler:语句处理器,处理具体的sql语句,包括预编译和参数设置等(prepareparameterizebatchupdates query等方法)。
  • ParameterHandler:参数处理器,负责将用户传递的参数转换成JDBC可识别的参数(getParameterObjectsetParameters等方法)。
  • ResultSetHandler:结果集处理器,负责将JDBC返回的结果集转换成用户所需的对象或集合(handlerResultSethandleOutputParameters等方法)。

通过拦截这些方法调用,MyBatis插件可以实现:sql重写、日志记录、性能监控、事务管理增强等等功能。

MyBatis插件的原理

MyBatis插件的实现原理基于Java的动态代理机制。

当配置了MyBatis插件后,初始化时MyBatis会使用JDK动态代理, 为目标对象(SqlSessionExecutorStatementHandler等)生成一个代理对象。 这个代理对象会包装原始对象,并在方法调用时执行自定义的拦截逻辑。

当调用这些代理对象的方法时,实际上会触发MyBatis提供的Invocation对象的proceed方法, 这个方法会在执行原始方法逻辑前后执行插件中定义的逻辑。

拦截过程如下:

  • 当目标对象的方法被调用时,代理对象会先检查是否存在对应的插件拦截器。
  • 如果存在拦截器,且该方法的签名与拦截器配置的方法签名匹配,则调用拦截器的intercept方法。
  • intercept方法中,开发者可以实现自定义的拦截逻辑。通常,这里会包含对原始方法调用的修改或增强。
  • 执行完拦截逻辑后,可以选择是否继续执行原始方法。如果继续执行,则通过反射调用原始对象的方法;否则,直接返回自定义的结果。
  • 需要注意的是,由于MyBatis插件是基于方法签名进行拦截的,因此开发者在编写插件时需要谨慎选择需要拦截的方法签名,以避免不必要的性能开销和潜在问题。

MyBatis插件注解

@Intercepts注解的作用是,标记需要拦截的方法列表。

  • Mybatis通过该注解去判断当前方法是否需要被拦截。
  • @Intercepts其实就是一个数组,用来添加复数个@Signature
  • 每个@Signature都指定了一个需要拦截的方法。

@Signature注解参数说明:

  • type:就是指定拦截器类型(ExecutorStatementHandlerParameterHandlerResultSetHandler
  • method:是拦截器类型中的方法,不是自己写的方法
  • args:是method中方法的入参

MyBatis插件示例:

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;

import java.util.Properties;

@Intercepts({
    @Signature(
        type = Executor.class,
        method = "update",
        args = {MappedStatement.class, Object.class}
    )
})
public class ExamplePlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 在原始方法执行前执行的逻辑
        System.out.println("Before method execution");

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);

        // 分离代理对象链
        // (由于目标类可能被多个拦截器拦截,从而形成多次代理,通过下面的两次操作可以分离出最原始的的目标类)
        while (metaStatementHandler.hasGetter("h")) {
          Object object = metaStatementHandler.getValue("h");
          metaStatementHandler = SystemMetaObject.forObject(object);
        }

        // 获取到当前的映射语句对象(MappedStatement)
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");

        // 只对需要拦截的语句进行处理
        if (mappedStatement.getId().endsWith("ById")) {

        }

        // 执行原始方法
        Object result = invocation.proceed();

        // 在原始方法执行后执行的逻辑
        System.out.println("After method execution");

        // 返回原始方法的执行结果
        return result;
    }

    @Override
    public Object plugin(Object target) {
        // 生成代理对象
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 设置插件属性,可以通过配置文件配置
    }
}

MyBatis配置文件中注册这个插件:

<configuration>
  <!-- 其他配置... -->
  <plugins>
    <plugin interceptor="com.xxx.ExamplePlugin">
      <!-- 如果插件需要配置属性,可以在这里添加 -->
      <!-- <property name="someProperty" value="someValue"/> -->
      <!-- 插件属性配置 -->
    </plugin>
  </plugins>
  <!-- 其他配置... -->
</configuration>

当执行Executorupdate方法时,插件中定义的intercept方法会被调用, 并且会在原始方法的执行前后打印出相应的日志信息。


results matching ""

    No results matching ""