通过功能猜测源码
,比通过源码猜测功能
要简单多了
不要为了看源码而看源码,源码会告诉你如何做,但不会告诉你为什么这样做
了解和意识到,是两个概念
相关术语
exploring-mapped-sql-statements
mapper:名词,映射器
mapped:映射(ed 结尾,可做动词和形容词,而这里应该是用作形容词,修饰 sql-statements )
statements 有两种定义:XML、注解,而这里使用了 XML 定义 SQL 语句,如下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.IUserService">
<select id="queryNameById" parameterType="java.lang.Integer" resultType="java.lang.String">
SELECT name FROM user WHERE id = #{id}
</select>
<select id="queryAgeById" parameterType="java.lang.Integer" resultType="java.lang.String">
SELECT age FROM user WHERE id = #{id}
</select>
</mapper>
映射,在数学中指:两个集合内,元素一一对应,如 y = x + 1 ( x = 1 对应 y = 2 , x = 2 对应 y = 3 ) y = x + 1(x = 1 ~对应~ y = 2,x = 2 ~ 对应 ~ y = 3) y=x+1(x=1 对应 y=2,x=2 对应 y=3)
映射语句:与数据库的 SQL 语句进行映射的语句,比如这里的<select>
标签
cn.IUserService.queryNameById
对应SELECT name FROM user WHERE id = #{id}
cn.IUserService.queryAgeById
对应SELECT age FROM user WHERE id = #{id}
为什么要将 SQL 放到 XML 文件中?
方便对 SQL 语句的统一管理
大多数 ORM 框架,会将 Java 对象
和 数据库表
关联起来
Mybaits 没有这样做,而是是将 Java 方法
和 SQL语句
关联起来
使得可以直接在映射文件中,编写近乎原生的 SQL 语句
ORM (Object-Relational Mapping):一种技术,允许你使用
面向对象的方式
,从数据库中查询和操作数据
那么
Java 是如何调用映射文件内的 SQL 语句呢?
通过映射文件内的 mapper 标签中的 namespace 属性
,可以找到一个java文件,该文件是个接口,也是调用 SQL 语句的入口
package cn
public interface IUserService {
String queryNameById();
String queryAgeById();
}
这种接口(称为映射接口文件)不需要具体的实现类,Mybatis 会通过代理的方式,将接口内的方法
与 XML文件内的语句
相互关联
具体使用有两种方式
// 第一种方式:类似于反射中的 Class.form,缺点:1 全限定名,容易拼错 2 无 IDE 自动补全,错误不易发现
String name1 = sqlSession.selectOne("cn.IUserService.queryNameById", 1);
// 第二种方式,有 IDE 的智能提示,推荐
IUserService userService = sqlSession.getMapper(IUserService.class);
String name2 = userService.queryNameById(1);
模拟接口与XML文件进行关联
参考:Mybatis 手撸专栏
这里假设:解析映射文件,将解后的内容放入 HashMap 中,其中:key = namespace + id, value = SQL 语句
HashMap<String, String> mapper = new HashMap<>();
mapper.put("cn.zhang.IUserService.queryNameById", "SELECT name FROM user WHERE id = 1");
mapper.put("cn.zhang.IUserService.queryAgeById", "SELECT age FROM user WHERE id = 1");
现在我们有:(1)接口 (2)Map
那么,现在想办法,如何将映射接口文件和 mapper 联系起来呢?
⭐️代理
创建代理对象的代理类中,必定要包含(1)接口 (2)Map
这个代理类就是MapperProxy,如图
public class MapperProxy implements InvocationHandler {
private Class mapperInterface;
private HashMap<String, String> mapper;
public MapperProxy(Class mapperInterface, HashMap<String, String> mapper) {
this.mapperInterface = mapperInterface;
this.mapper = mapper;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return mapper.get(mapperInterface.getName() + "." + method.getName());
}
}
通过代理类,将接口和 XML 文件联系起来
测试如下
// 模拟解析 XML 之后的形式
HashMap<String, String> mapper = new HashMap<>();
mapper.put("cn.zhang.IUserService.queryNameById", "SELECT name FROM user WHERE id = 1");
mapper.put("cn.zhang.IUserService.queryAgeById", "SELECT age FROM user WHERE id = 1");
MapperProxy mapperProxy = new MapperProxy(IUserService.class, mapper);
IUserService o = (IUserService) Proxy.newProxyInstance(IUserService.class.getClassLoader(), new Class[]{IUserService.class}, mapperProxy);
System.out.println(o.queryNameById()); // SELECT name FROM user WHERE id = 1
测试的时候,每次都要调用 Proxy.newProxyInstance,很麻烦;于是,封装到 MapperProxy 中
public Object getProxyObject(){
return Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, this);
}
// 之后就可以
MapperProxy mapperProxy = new MapperProxy(IUserService.class, mapper);
IUserService o = (IUserService) mapperProxy.getProxyObject();
System.out.println(o.queryNameById()); // SELECT name FROM user WHERE id = 1
MapperProxyFactory
相同的IDAO接口,可有不同的实现方式,甚至,所使用的数据库也可能不同
因此,干脆将IDAO封装到一个类中,之后通过 newInstance(xmlFIle) 的方式生成 MapperProxy 代理对象
另外,Proxy.newProxyInstance 的逻辑也封装到 newInstance 中
这个类就是MapperProxyFactory
class MapperProxyFactory<T> {
private Class<T> mapperInterface;
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
public T newInstance(HashMap<String, String> mapper) {
final MapperProxy mapperProxy = new MapperProxy(mapperInterface, mapper);
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
}
使用如下
IUserService o = new MapperProxyFactory<>(IUserService.class).newInstance(mapper);
System.out.println(o.queryNameById());
MethodCache
其实在源码中,MapperProxy 一共有三个属性
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
我们已经知道,一个 MapperProxy,对应一个 mapperInterface
但其实, methodCache
是一个Map
,该Map
下的值: MapperMethod
, 对应接口方法的具体实现
一个 MapperMethod
,对应一个接口中的方法
当第一次调用某个方法的时候,比如userService.queryNameById()
,会先尝试从methodCache
(方法缓存)中获取,如果获取失败,会构造 MapperMethod
并存入methodCache
methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
构造 MapperMethod
的过程中
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
在执行 new SqlCommand
的时候,会将当前 接口 + 方法
作为参数,向 configuration
中(解析的 xml 信息会存入其中)查询是否有对应 SQL 语句:即最终执行的是 mappedStatements.get(mapperInterface.getName() + "." + method.getName())
如果 get 失败,会报错,因为这说明该接口的方法,没有对应 xml SQL 语句:Invalid bound statement (not found)
MapperRegistry 及 SqlSession 封装
MapperRegistry
MapperProxyFactory 只是针对一个IDAO接口,并不能添加、删除;那么,就必定需要一个集中式管理的类
这个类就是 MapperRegistry,相当于封装了多个 IDAO 接口,用于集中式管理IDAO接口
SqlSession
之前定义的 sqlsession (xmlFile)为一个 HashMap,现将其封装成一个对象,并由 SqlSessionFactory 构造,该工厂持有 MapperRegistry(封装多个IDAO服务)
通过该工厂,可创建 SqlSession,但此时 SqlSession 中未定义具体实现
为何要创建一个 SqlSessionFactory ?直接 new SqlSession 不行吗?
一般,使用 new 创建对象,适用于创建一些简单的对象
如果要创建一个复杂的对象,建议使用工厂方法,优点如下
- 语义清晰
- 代码整洁,可使业务逻辑和技术细节相分离
一些术语解释
01_SqlSession 负责连接
Mybatis 中的 SqlSession 是什么
SqlSession 是 Mybatis 中的一个接口,用于
- 管理数据库连接 🔗⭐⭐⭐
- 提供操作数据库的方法 👨🏭
核心的类之一⭐,负责与数据库进行交互,执行 SQL 语句并返回结果
在使用 Mybatis 进行数据访问时,首先需要创建 SqlSession 对象
SqlSession 对象的创建通常是通过 SqlSessionFactory 工厂类的 openSession 方法完成的
SqlSession 对象可以使用该方法创建一个新的实例,这个实例是一个完全独立
的、和线程安全
的会话。SqlSession 会维护一个独立的数据库连接
👨🏭,负责与数据库进行交互
02_Executor 负责执行
作用
Executor 为一个接口
用于执行数据库操作的方法
Executor 负责
- 管理和执行 SQL 语句
- 处理输入参数、结果集
- 提供缓存功能
三种类型
- SimpleExecutor
- 每次执行 SQL 语句时,都会创建一个新的 Statement 对象,执行完毕后立即关闭该对象
- 因此,SimpleExecutor 是一种非常简单的 Executor,它不会进行缓存操作。
- ResueExecutor
- 在执行 SQL 语句时,会检查缓存中是否已经存在该 SQL 语句的 Statement 对象,如果存在就重用该对象
- 这种方式可以减少 Statement 对象的创建和销毁操作,提高性能
- BatchExecutor
- 用于批量执行 SQL 语句的
- 将多条 SQL 语句合并成一个批处理操作,一次性执行
- 这种方式可以减少
数据库连接
和Statement 对象的创建和销毁
操作,提高性能
Executor 接口提供的方法
- update:用于执行 insert、update、delete 等更新操作。
- query:用于执行 select 查询操作。
- flushStatements:用于将缓存的 SQL 语句刷入到数据库中
- commit 和 rollback:用于提交和回滚事务
03_MappedStatement 负责存储
用于保存 SQL 语句
及其它相关的配置信息
用于和其它一些类一起,负责解析映射文件、生成执行 SQL 语句的代码
MappedStatement 对应 Mybatis 映射文件中的一个 select、insert、update、delete 等节点
- 保存 SQL 语句
- 输入参数
- 输出结果类型
- 缓存配置信息
- …
Mybatis 通过这种方式,实现了将 SQL 语句
和 Java 代码
的解耦,方便开发人员进行 SQL 语句的维护和修改
public class MappedStatement {
// SQL 语句
private String sql;
// 输入参数类型
private Class<?> parameterType;
// 输出结果类型
private Class<?> resultType;
// 缓存配置信息
private Cache cache;
// MappedStatement 提供了一个 execute 方法,用于执行 SQL 语句,并返回结果
public <T> List<T> execute(SqlSession sqlSession, Object parameter) {
List<T> result = sqlSession.selectList(sql, parameter); // 执行 SQL 语句
return result; // 返回查询结果
}
// getter 和 setter 方法
}