在现代软件开发中,数据持久化是一个至关重要的环节。为了方便和高效地访问数据库,ORM框架应运而生。而MyBatis作为一款优秀的ORM框架,不仅提供了强大的数据库访问能力,更注重SQL的灵活性和可维护性。本文将带你深入了解MyBatis的原理,了解它是如何实现高效、灵活的数据访问的。
目录
执行流程
读取配置文件
首先Mybatis会读取配置文件mybatis-config.xml,这之中有两部分内容,一个是当前的环境配置,指定使用的数据库,用户名密码等等。第二个是加载一些映射文件,比如UserMapper。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 数据库连接信息 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydatabase"/>
<property name="username" value="root"/>
<property name="password" value="mypassword"/>
</dataSource>
</environment>
</environments>
<!-- 映射器的配置 -->
<mappers>
<mapper resource="com/example/mappers/UserMapper.xml"/>
</mappers>
</configuration>
操作数据库
Mybatis操作数据库主要是通过一个SqlSession对象完成,所以下一步需要创建SqlSession,而SqlSession有专门的对象进行创建,也就是图中的SqlSessionFactory,这是一个构建绘画的工厂,全局只有一个,可以批量生成SqlSession。
Mybatis会通过这个SqlSession工厂来创建SqlSession,最终这个SqlSession会包含所有的执行sql语句的方法,SqlSession才是真正和数据库打交道的对象,并且每一次操作都有一个会话,SqlSession是有很多个的。
下面的Executor是真正操作数据库的接口,封装这jdbc的一系列操作,这里面同时也维护了一些缓存(比如一级缓存和二级缓存)。Executor会从MappedStatement对象在读取一些数据库的信息。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mappers.UserMapper">
<select id="selectById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
在这个样例映射器文件中,你需要根据你自己的数据库和实体类进行相应的修改。以下是对一些关键配置项的说明:
namespace
属性指定了映射器接口的完全限定名,这里假设为com.example.mappers.UserMapper
。你需要根据实际情况修改为你自己的接口路径。<select>
元素定义了一个查询语句。id
属性指定了方法名,这里是selectById
。resultType
属性指定了查询结果的类型,这里是User
类型。- 在
<select>
元素的内容中,可以编写具体的 SQL 查询语句。这个示例中,通过SELECT * FROM users WHERE id = #{id}
查询指定ID的用户数据。#{id}
是一个占位符,表示参数值。
标签中的这些信息都是由MappedStatement对象来读取存储的。在操作数据库之前,我们还需要进行数据类型的转换,不管是map,list,string还是integer,我们都需要转换为数据库支持的类型,类型转换完成才能去操作数据库。类型转换之后,还要把数据库类型的结果转换为java类型。
总结一下,mybatis的执行流程可以归纳为以下几步:
- 读取配置文件:mybatis-config.xml加载运行环境和映射文件
- 构造会话工厂SqlSessionFactory
- 会话工厂创建SqlSession对象(包含了执行sql语句的所有方法)
- 操作数据库的接口,Executor执行器,同时负责查询缓存数据的维护
- Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
- 输入参数映射
- 输出结果映射
延迟加载
先说结论,mybatis支持延迟加载,但是它默认是没有开启的。
什么是延迟加载?
延迟加载(Lazy Loading)是指在需要使用关联对象时才进行数据库查询,而不是在加载主对象时就立即查询所有关联对象的数据。
有两张表,用户表和订单表,用户和订单是一对多的关系,左侧是用户实体类 ,右边是订单的实体类。用户的实体类有一个成员变量叫做orderList,这里面封装的都是订单,也就是当前用户的订单列表。如果我们查询用户的时候就把用户的订单也查询出来,就属于立即加载。如果查询用户的时候,先不查询订单数据,而是当需要订单的时候再查询订单,就是延迟加载。
我们可以在mapper文件中添加属性fetchType=“lazy”来定义延迟加载的属性。
延迟加载原理
首先使用CGLIB创建目标对象的代理对象(orderList),主要是用代理对象来完成延迟加载。当我们调用目标方法(getOrderList),就会进入到代理对象的invoke方法,在这里去判断orderList是否为空,如果使用的是延迟加载,那么orderList肯定是空的,此时mybatis才回去真正的执行sql查询。拿到结果后再把结果封装到user中的属性(orderList)
一二级缓存
我们回忆一下之前redis的缓存是怎么用的:
假如一个请求来了之后我们首先回去判断是否命中了缓存,如果缓存中没有数据我们才回去查询数据库,同时再把数据保存在缓存中,再返回给用户,这是缓存的基本使用方式。
mybatis是支持一级和二级缓存的。缓存都是保存在本地叫做本地缓存,有一个缓存类叫做PerpetualCache,本质是一个HashMap。一级和二级缓存都是基于PerpetualCache来做的。
一级缓存的作用域是session级别的,相对来说是比较小的。
二级缓存的作用域会更大些,是命名空间和mapper的作用域,不依赖于SqlSession。
这两种缓存的基本思路和上面redis缓存的思路大致一样,不同的是不同的作用域下保存数据的实际是不一样的,下面我们来相信的说明下一二级缓存的作用效果。
一级缓存
基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session进行flush或close之后,该session中所有cache就将清空,默认打开一级缓存。
首先我们open了一个session,我们拿到了两个userMapper,如何使用userMapper1去查询用户id为6的数据,返回的是用户,然后用userMapper2也去查询用户为6的用户,此时这两条查询的是相同的id,这时一级缓存就会起作用了,因为第一次查询的时候他会把数据保存在本地的缓存中(hashMap),使用第二次查询的时候就不会再执行sql了。
二级缓存
二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于sqlSession,也是默认采用PerpetualCache,本质上都是存储到hashMap中。
这段代码中,我们打开了两次session,分别取查询id为6的数据 ,这两次查询属于不同的会话,最终的结果就是会执行两条SQL。
如果就想执行一次的话,就需要去开启我们的二级缓存了。开启二级缓存需要两步操作,因为而二级缓存默认是关闭的,我们需要手动打开。
首先要在全局配置文件中开启二级缓存:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
然后再映射文件中加一个cache标签:
<select id="selectUserById" resultType="User" useCache="false">
...
</select>
注意事项
- 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存nameSpaces)进行了增删改操作后,默认该作用域下所有select中的缓存将被清空。
- 二级缓存需要缓存的数据需要实现Serilizable接口
- 只有会话提交或关闭以后,一级缓存中的数据才会转移到二级缓存中