Mybatis篇
ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。
为什么使用代理对象来执行 SQL 语句
在 MyBatis 中,数据操作通常由 Mapper 接口和 XML 配置文件来实现。当 MyBatis 执行一个 Mapper 接口方法时,它首先会根据 Mapper 接口方法名、方法参数等信息,查找对应的 SQL 语句,并执行这条 SQL 语句。而这个 SQL 语句是通过 MyBatis 在后台动态创建的,生成过程包括了 SQL 语句的拼装、参数的设置等步骤。
MyBatis 为了记录和跟踪 SQL 执行过程,需要在调用 Mapper 接口方法时动态生成这个代理对象,这个代理对象可以拦截 Mapper 接口方法的调用,并记录 SQL 执行的相关信息。同时,代理对象还可以通过调用底层 SQL 执行 API,将生成的 SQL 语句和参数传递给底层数据库操作来实现数据访问。
另外,使用代理对象还可以提供更好的灵活性。如果直接使用普通对象进行操作,一个对象只能对应一条 SQL 语句,而代理对象可以根据配置动态生成多条 SQL 语句,并对传入的参数进行动态调整,因此提供了更强的灵活性和扩展性。
MyBatis执行流程
- 读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
- 根据读取到的配置信息,MyBatis 构建一个
SqlSessionFactory
对象 - 会话工厂创建
SqlSession
对象(包含了执行SQL语句的所有方法)它负责管理数据库连接、执行SQL语句、提交或回滚事务等。 - 操作数据库的接口
Executor
,SqlSession
中的方法实际上是由Executor
执行器来执行的。Executor
负责管理数据库连接、执行 SQL 语句,并维护查询缓存。 - Executor接口的执行方法中有一个
MappedStatement
类型的参数,它封装了 SQL 语句的映射信息。MappedStatement
包含了 SQL 语句、参数映射、结果映射等相关配置。 - 输入参数映射,当执行 SQL 语句时,MyBatis 可以将传入的 Java 对象与 SQL 语句中的参数进行映射,从而方便地传递参数给 SQL 语句。
- 输出结果映射,在执行查询操作时,MyBatis 将查询结果映射到指定的 Java 对象中。这可以通过配置或注解来指定结果集resultMap与 Java 对象之间的映射关系。
MyBatis 延迟加载
Mybatis支持延迟记载,但默认没有开启
什么叫做延迟加载?
MyBatis 的延迟加载是指在使用一条SQL查询语句获取对象时,并不立即加载该对象关联的其他对象或属性,而是在真正需要使用这些关联对象或属性时,才发起相应的 SQL 查询加载。这种方式可以有效地减少不必要的数据库查询,提高系统性能。
<resultMap id="userResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<collection property="orderList" ofType="order"
select="com.itheima.mapper.OrderMapper.findByUid" // 关联查询的mapper函数
column="id">
</collection>
</resultMap>
<select id="getUser" resultMap="userResultMap">
select
*
from
t_user
where
id = #{id}
</select>
对于以上这个SQL,由于其未设置延迟加载操作,所以当调用 getUser()
函数的时候,就会夹带 OrderMapper.findByUid
函数一起查询用来获取当前封装类的关联对象列表数据,所以说会执行两端SQL,但是是实际上,有时候我们只需要拿到用户相关的数据就行,不必要将其的订单数据也查询出来返回,如果我们采用了延迟加载,当我们在用到最终封装类中的订单的数据板块的时候,才会去执行获取订单数据的这么段SQL,反之是不会执行的,这样子就避免了额外的资源消耗。
延迟加载的原理
- 使用CGLIB创建目标对象的代理对象
- 当调用目标方法user.getOrderList()时,被拦截器拦截,进入拦截器invoke方法,发现user.getOrderList()是null值,执行sql查询order列表
- 把order查询上来,然后调用user.setOrderList(List orderList) ,接着完成user.getOrderList()方法的调用
延迟加载实现
全局延迟
mybatis.configuration:
lazy-loading-enabled: true # 开启全局延迟加载
aggressive-lazy-loading: true # 延迟加载的层次深度
proxy-factory: JAVASSIST # 延迟加载时用的代理类型
局部延迟
<resultMap id="userResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<collection property="orderList" ofType="order"
select="com.itheima.mapper.OrderMapper.findByUid" // 关联查询的mapper函数
column="id" fetchType="lazy"> // 针对该对象关联的其他对象列表进行延迟加载
</collection>
</resultMap>
<select id="getUser" resultMap="userResultMap">
select
*
from
t_user
where
id = #{id}
</select>
延迟加载的弊端
- 增加额外的查询次数。 当使用延迟加载时,需要在要使用关联对象时对该对象进行查询,这样会增加额外的查询次数,从而影响了系统的响应速度。
- 增加内存消耗。 在延迟加载的情况下,每加载一个对象就会增加一个对象的内存消耗,如果同时加载多个对象,就会增加大量的内存消耗,从而影响系统的性能。
- 可能导致 N+1 查询问题。 当使用延迟加载时,会出现 N+1 查询问题,即在查询一个对象时,需要先查询主对象,然后再查询关联对象,这样就会导致 N+1 个查询,从而影响系统的性能
Mabatis 缓存
Mabatis缓存主要包含一级缓存和二级缓存两种,二者都是基于 PerpetualCache
的 HashMap 本地缓存
在 MyBatis 中,缓存的实现是以
CacheKey
对象为 key,查询结果为 value 保存在缓存中的。CacheKey
对象是一个复合键,它包含了执行语句的 id、查询参数等信息,以保证相同的语句执行以及相同参数的查询能返回同样的结果。
一级缓存
一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存 (存储在内存中) ,其存储作用域为 Session
,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认一级缓存是打开的(这里的Session是SqlSession,不是前后端交互的Session)
二级缓存
二级缓存(全局缓存)是基于 namespace
和 mapper
的作用域起作用的,所有的 SqlSession
对象共享同一个缓存,默认也是采用 PerpetualCache,HashMap 存储 (存储在本地外存持久文件中)
二级缓存默认是关闭的
开启方式
- 全局配置文件
- 映射文件Mapper.xml上加上标签让当前mapper生效二级缓存
实现原理
一级缓存的实现原理
在 SqlSession 里面持有一个 Executor,每个 Executor 中有一个 LocalCache 对象。当用户发起查询的时候,Mybatis 会根据执行语句在 Local Cache 里面查询,如果没命中,再去查询数据库并写入到 LocalCache,否则直接返回。
二级缓存的实现原理
使用 CachingExecutor 装饰了 Executor,所以在进入一级缓存的查询流程前,会先通过 CachingExecutor 进行二级缓存的查询。开启二级缓存以后,会被多个 SqlSession 共享,所以它是个全局缓存。因此它的查询流程是先查二级缓存,再查一级缓存,最后再查数据库。
注意点
- 对于缓存数据更新机制,当某一个作用域进行了事务操作后,默认该作用域下所有 select 中的缓存将被 clear
- 只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中
- 使用了二级缓存后,先查二级缓存,再查一级缓存,最后再查数据库
- 二级缓存从缓存中移除某个对象的淘汰算法。默认采用LRU策略。可以设置定期清除
- MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时缓存粒度也能够到 namespace 级别,并且还可以通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强
- MyBatis 的一级缓存中,缓存的数据是存储在内存中的,并不需要进行序列化和反序列化操作。因此,针对一级缓存的封装类不需要实现
Serializable
接口;对于 MyBatis 的二级缓存,缓存的数据是存储在持久化介质(如文件、数据库等)上的,所以需要进行序列化和反序列化操作。 - 一级缓存,在多个 Sqlsession 或者分布式环境下,可能会导致数据库写操作出现脏数据。 (联系主存和工作内存的知识)
Mybatis 中#{}和${}的区别
Mybatis 提供到的#号占位符和$号占位符,都是实现动态 SQL 的一种方式,通过这两种方式把参数传递到 XML 之后
#
号占位符它相当于向 PreparedStatement 中的预处理语句中设置参数,而 PreparedStatement 中的 sql 语句是预编译的,SQL 语句中使用了占位符,规定了sql 语句的结构,并且在设置参数的时候,如果有特殊字符,会自动进行转义。所以#号占位符可以防止 SQL 注入- 使用
$
的方式传参,相当于直接把参数拼接到了原始的 SQL 里面,Mybatis不会对它进行特殊处理。有SQL 注入的风险
所以$和#最大的区别在于,前者是动态参数,后者是占位符, 动态参数无法防止 SQL注入的问题,所以在实际应用中,应该尽可能的使用#号占位符。
另外,$符号的动态传参,可以适合应用在一些动态 SQL 场景中,比如动态传递表名、动态设置排序字段等。
SQL注入是攻击者通过插入恶意的SQL代码欺骗应用程序,可能导致数据泄露、数据库破坏和应用程序完全被控制。预防方法包括过滤和转义输入参数、使用参数化查询、实施权限和访问控制,并加强安全意识和防范措施。
Mybatis 分页操作
- Interceptor 拦截器实现,通过拦截需要分页的 select 语句,然后在这个 sql 语句里面动态拼接分页关键字,从而实现分页查询。
分页插件的基本原理是什么?
当 MyBatis 创建一个代理对象来执行 SQL 语句时,会使用 JDK动态代理机制。通过动态代理,它会创建一个代理对象,这个代理对象可以拦截目标类的方法调用,并在方法执行前后做一些额外的处理。
对于分页插件来说,它会拦截 Executor 接口的 query
方法。在 query
方法前后,分页插件会先获取分页参数(如页码、每页记录数等),然后根据这些参数动态生成对应的分页 SQL 语句。
责任链设计模式用于实现分页插件的拦截器链。在 MyBatis 中,通过实现 Interceptor 接口,并将拦截器按照顺序添加到拦截器链中,就可以形成一个责任链。当 MyBatis 执行 SQL 语句时,会按照添加拦截器的顺序,依次调用每个拦截器的 intercept
方法。每个拦截器可以在此方法中进行一些前置或后置处理,然后再传递给下一个拦截器。分页插件就是通过拦截器链实现了对 SQL 语句的拦截和修改。
通过 JDK 动态代理和责任链设计模式的结合使用,MyBatis 分页插件能够灵活地拦截并修改 SQL 语句,以实现分页功能。
MyBatis中接口绑定的原理
MyBatis 中接口绑定的原理是通过JDK动态代理实现的。
在 MyBatis 中,接口绑定是指将 Mapper 接口与对应的 XML 配置文件进行关联,实现接口方法与 SQL 语句的映射。这样,我们就可以通过调用接口方法来执行对应的 SQL 操作。
当我们在应用程序中调用 Mapper 接口的方法时,MyBatis 会通过动态代理机制来实现接口方法的调用。它会生成一个实现了该接口的代理对象,并注册到 SqlSession 的 Configuration 对象中。
在代理对象的方法调用过程中,MyBatis 会首先通过 Configuration 对象找到与方法名对应的 MappedStatement 对象,MappedStatement 包含了 SQL 语句的配置信息。然后,MyBatis 将通过 SqlSession 调用相应的 SQL 执行方法,并将 MappedStatement 对象和方法参数传递给底层的 SQL 执行引擎。
使用动态代理的好处是,它可以在不修改原始接口代码的情况下,通过注入逻辑来增强接口的功能。在 MyBatis 中,我们通过在 XML 配置文件中编写对应的 SQL 语句,实现了将接口方法与 SQL 语句的映射关系,从而进行数据访问操作。
总结起来,MyBatis 的接口绑定通过动态代理实现。它通过生成接口的代理对象,在接口方法调用时根据配置文件的映射关系执行对应的 SQL 语句,实现了接口方法与 SQL 的绑定。这种设计方式使得开发者可以在接口层面上进行业务操作,简化了数据访问代码的编写和维护。
Mybatis的预编译
当使用 MyBatis 进行 SQL 操作时,默认情况下,每次执行 SQL 语句时,MyBatis 都会将 SQL 语句发送给数据库进行解析、编译和执行。这个过程需要消耗时间和资源。
为了优化 SQL 执行的效率,MyBatis 引入了预编译的机制。预编译指的是将 SQL 语句提前编译好,并缓存到内存中以供重复使用。这样,在下一次执行相同的 SQL 语句时,MyBatis 就可以直接使用缓存的编译结果,而不需要再次进行解析和编译过程。
具体来说,当使用 MyBatis 进行 SQL 操作时,我们可以通过在 Mapper XML 文件中使用占位符或 #{}
表达式来构建 SQL 语句。MyBatis 在执行 SQL 语句之前,会将占位符替换为具体的参数值,并将最终生成的 SQL 语句缓存到内存中。下一次如果执行相同的 SQL 语句,MyBatis 就可以直接使用缓存的编译结果。
这里的关键是,MyBatis 会将执行后的 SQL 语句预编译并缓存到内存中。下一次如果我们再次调用相同的 SQL 语句,只需要传递不同的参数值,MyBatis 将直接使用缓存的编译结果,而不需要再进行解析和编译的过程。