第一部分:自定义持久层框架
在该部分中主要学习到了以下几个关键知识点:
1.1 分析手动编写JDBC操作面临的问题
- 数据库连接频繁创建、销毁造成资源浪费,影响系统性能;
- sql语句硬编码,不易维护;
- 使用preparedStatement传递sql参数存在硬编码,不易维护;
- 对查询结果集解析以及封装成实体类存在硬编码,不易维护;
1.2 针对性的给出问题的解决思路
- 针对问题1,可以使用“享元模式”即“池化”技术,使用数据库连接池来维护数据库连接的创建、回收等;
- 针对问题2,可以将sql语句从代码中抽离出来,放到单独的xml配置文件中;
- 针对问题3和4,可以使用反射、内省等技术自动将查询结果集绑定到实体类上;
1.3 尝试编写自定义框架解决面临的问题
- 配置文件:分为Mybatis全局配置文件和mapper映射文件;
- Configuration:上述配置文件对应的Java对象;
- SqlSessionFactoryBuilder:拥有一个
build()
方法,它会将上述的配置文件字节流进行xml解析封装成Configuration
对象,再将Configuration
对象设置到new出来的SqlSessionFactory
并返回; - SqlSessionFactory:拥有一个
openSession()
方法,用来根据Configuration
创建SqlSession
对象; - SqlSession:持有
Executor
的引用,封装了数据库的查询方法; - Executor:真正去实现JDBC的CRUD操作;
上面自定义的框架会发现和工作中使用的Mybatis框架用法有点出入,这是因为考虑到:
- 自定义框架使用中存在重复的代码,整个操作的过程模板重复(创建sqlsession,调用sqlsession方 法,
关闭 sqlsession);- 自定义框架使用中存在硬编码,调用sqlsession的方法时,参数statement的id硬编码;
因此可以针对xml文件定义对应的mapper接口,然后使用动态代理来生成具体的mapper实现类来帮我们自动完成上述操作。
第二部分:Mybatis相关概念
2.1 简介
Mybatis是一个半自动化的ORM(对象/关系映射)框架,它支持定制化SQL、存储过程以及高级映
射。MyBatis避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。MyBatis可以使用简单的
XML或注解来配置和映射原生类型、接口和Java的POJO (Plain Old Java Objects,普通老式Java对 象)
为数据库中的记录。
2.2 历史
原是apache的一个开源项目iBatis, 2010年6月这个项目由apache software foundation 迁移到了
google code,随着开发团队转投Google Code旗下,ibatis3.x正式更名为Mybatis ,代码于2013年11
月迁移到Github。
iBATIS一词来源于“internet”和“abatis”的组合,是一个基于Java的持久层框架。iBATIS提供的持久层框
架包括SQL Maps和Data Access Objects(DAO)。
2.3 优势
Mybatis是一个半自动化的持久层框架,对开发人员开说,核心sql还是需要自己进行优化,sql和java编
码进行分离,功能边界清晰,一个专注业务,一个专注数据。
分析图示如下:
相比于Hibernate这种全自动化的ORM框架它更灵活、更轻量。
第三部分:Mybatis基本应用
3.1 入门
基本开发步骤:
①添加MyBatis的pom依赖
②创建user数据表
③编写User实体类
④编写映射文件UserMapper.xml
⑤编写核心文件SqlMapConfig.xml
⑥编写测试类
3.2 常用开发方式
3.2.1 传统开发方式
- 编写Dao接口;
- 实现Dao接口,和基本开发步骤类似,主要是通过statementId来定位sql并执行数据库操作;
3.2.2 代理开发方式
Mapper 接口开发方法只需要程序员编写Mapper 接口(相当于Dao 接口),由Mybatis 框架根据接口
定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。
Mapper 接口开发需要遵循以下规范:
- Mapper.xml文件中的namespace与mapper接口的全限定名相同;
- Mapper接口方法名和Mapper.xml中定义的每个statement的id相同;
- Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql的parameterType的类型相同;
- Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同;
第四部分:Mybatis配置文件深入
4.1 核心配置文件
SqlMapConfig.xml
4.1.1 层级关系
4.1.2 常用配置
1)environments标签
数据库环境的配置,支持多环境配置
其中,事务管理器(transactionManager)类型有两种:
•JDBC:这个配置就是直接使用了JDBC 的提交和回滚设置,它依赖于从数据源得到的连接来管理事务作
用域。
•MANAGED:这个配置几乎没做什么。它从来不提交或回滚一个连接,而是让容器来管理事务的整个生
命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接,然而一些容器并不希望这样,因
此需要将 closeConnection 属性设置为 false 来阻止它默认的关闭行为。
其中,数据源(dataSource)类型有三种:
•UNPOOLED:这个数据源的实现只是每次被请求时打开和关闭连接。
•POOLED:这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来。
•JNDI:这个数据源的实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置
数据源,然后放置一个 JNDI 上下文的引用。
2)mapper标签
该标签的作用是加载映射的,加载方式有如下几种:
•使用相对于类路径的资源引用,例如:
•使用完全限定资源定位符(URL),例如:
•使用映射器接口实现类的完全限定类名,例如:
•将包内的映射器接口实现全部注册为映射器,例如:
3)Properties标签
实际开发中,习惯将数据源的配置信息单独抽取成一个properties文件,该标签可以加载额外配置的
properties文件,该标签必须放在配置文件顶部!
4)typeAliases标签
类型别名,是为Java 类型设置一个短的名字。配置方式有:
- 单个配置,
<typeAliases><typeAlias type="com.User" alias="user"></typeAlias></typeAliases>
; - 包配置,
<typeAliases><package name="com.jarry.entity"></typeAlias></typeAliases>
,这种方式的别名为类名,不区分大小写;
4.2 映射配置文件
xxxMapper.xml
4.2.1 动态sql之if标签
<select id="findByCondition" parameterType="user" resultType="user">
select * from User
<where>
<if test="id!=0">
and id=#{id}
</if>
<if test="username!=null">
and username=#{username}
</if>
</where>
</select>
4.2.2 动态sql之foreach标签
<select id="findByIds" parameterType="list" resultType="user">
select * from User
<where>
<foreach collection="list" open="id in(" close=")" item="id"
separator=",">
#{id}
</foreach>
</where>
</select>
4.2.3 动态sql之SQL片段抽取
<sql id="selectUser" select * from User</sql>
<select id="findById" parameterType="int" resultType="user">
<include refid="selectUser"></include> where id=#{id}
</select>
<select id="findByIds" parameterType="list" resultType="user">
<include refid="selectUser"></include>
<where>
<foreach collection="array" open="id in(" close=")" item="id"
separator=",">
#{id}
</foreach>
</where>
</select>
第五部分:Mybatis复杂映射开发
5.1 一对一查询
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪一个客户
private User user;
} p
ublic class User {
private int id;
private String username;
private String password;
private Date birthday;
}
想要从数据库中查询出Order对象,而Oder对象持有一个User对象。
对应的mapper.xml如下:
<mapper namespace="com.jarry.mapper.OrderMapper">
<resultMap id="orderMap" type="com.jarry.domain.Order">
<result column="uid" property="user.id"></result>
<result column="username" property="user.username"></result>
<result column="password" property="user.password"></result>
<result column="birthday" property="user.birthday"></result>
</resultMap>
<select id="findAll" resultMap="orderMap">
select * from orders o,user u where o.uid=u.id
</select>
</mapper>
或者
<resultMap id="orderMap" type="com.jarry.domain.Order">
<result property="id" column="id"></result>
<result property="ordertime" column="ordertime"></result>
<result property="total" column="total"></result>
<!--该标签为单个对象封装标签,javaType表示实体该字段的类型-->
<association property="user" javaType="com.jarry.domain.User">
<result column="uid" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
</association>
</resultMap>
5.2 一对多查询
public class Order {
private int id;
private Date ordertime;
private double total;
//代表当前订单从属于哪一个客户
private User user;
} p
ublic class User {
private int id;
private String username;
private String password;
private Date birthday;
//代表当前用户具备哪些订单
private List<Order> orderList;
}
从数据库中查询User对象,该User对象持有一个Oder对象的list。
对应的mapper.xml如下:
<mapper namespace="com.jarry.mapper.UserMapper">
<resultMap id="userMap" type="com.jarry.domain.User">
<result column="id" property="id"></result>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<!--该标签为集合封装标签, ofType表示该实体字段集合的泛型类型-->
<collection property="orderList" ofType="com.jarry.domain.Order">
<result column="oid" property="id"></result>
<result column="ordertime" property="ordertime"></result>
<result column="total" property="total"></result>
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
select *,o.id oid from user u left join orders o on u.id=o.uid
</select>
</mapper>
5.3 多对多查询
双向一对多,参考5.2。
第六部分:Mybatis注解开发
6.1 常用注解
@Insert:实现新增
@Update:实现更新
@Delete:实现删除
@Select:实现查询
@Result:实现结果集封装
@Results:可以与@Result 一起使用,封装多个结果集
@One:实现一对一结果集封装
@Many:实现一对多结果集封装
6.2 一对一
Mapper接口方法写法如下:
@Select("select * from orders")
@Results({
@Result(id=true,property = "id",column = "id"),
@Result(property = "ordertime",column = "ordertime"),
@Result(property = "total",column = "total"),
@Result(property = "user",column = "uid",
javaType = User.class,
one = @One(select = "com.lagou.mapper.UserMapper.findById"))
})
List<Order> findAll();
6.3 一对多
Mapper接口方法写法如下:
public interface UserMapper {
@Select("select * from user")
@Results({
@Result(id = true,property = "id",column = "id"),
@Result(property = "username",column = "username"),
@Result(property = "password",column = "password"),
@Result(property = "birthday",column = "birthday"),
@Result(property = "orderList",column = "id",
javaType = List.class,
many = @Many(select =
"com.lagou.mapper.OrderMapper.findByUid"))
})
List<User> findAllUserAndOrder();
} p
ublic interface OrderMapper {
@Select("select * from orders where uid=#{uid}")
List<Order> findByUid(int uid);
}
6.4多对多
双向一对多,参考6.3。
第七部分:Mybatis缓存
为了加快查询效率,Mybatis引入了缓存机制。
①、一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数
据结构(HashMap)用于存储缓存数据。不同的sqlSession
之间的缓存数据区域(HashMap)是互相不影响的。
②、二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession
可以共用二级缓存,二级缓存是跨SqlSession的。
缓存查询顺序:二级缓存------>一级缓存------->数据库
7.1 一级缓存
- 默认开启。
- SqlSession级别,不同的SqlSession不共享缓存。
- 底层是一个HashMap结构。
- 一级缓存的维护是在
Executor
类中执行。 - 执行SqlSession的commit或者clear会导致一级缓存清空,并转存到二级缓存。
- 在sql标签上配置flushCache=true,可以使一级缓存失效。
7.2 二级缓存
- 默认不生效,需要在mapper.xml使用
标签,或者在mapper接口上添加@CacheNamespace注解使其生效。 - Mapper/Namespace级别,和SqlSession无关,不同的SqlSession可以共享同一个Mapper/Namespace的二级缓存。
- 底层也默认是HashMap结构
- 开启二级缓存后pojo要实现Serializable接口,使其可以持久化到其他存储介质。
- 执行增删改,并且提交SqlSession以后会清空对应的二级缓存。
7.3 二级缓存接入Redis
Mybatis提供了一个Cache接口,可以实现自己的缓存策略。
基于HashMap实现换粗无法适用集群或分布式环境,因为他是JVM进程级别的。因此需要整合分布式缓存框架,如Redis。
mybatis提供了一个针对cache接口的redis实现类。
使用步骤:
- 添加如下maven依赖:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency
- 在mapper.xml加入二级缓存开启标签
或者在mapper接口上添加@CacheNamespace(implementation = RedisCache.class)注解 - 添加redis配置文件:
redis.properties
redis.host=localhost
redis.port=6379
redis.connectionTimeout=5000
redis.password=
redis.database=0
第八部分:Mybatis插件
8.1 Mybatis插件介绍
mybatis提供四大扩展点:
- 执行器Executor (update、query、commit、rollback等方法);
- SQL语法构建器StatementHandler (prepare、parameterize、batch、updates query等方 法);
- 参数处理器ParameterHandler (getParameterObject、setParameters方法);
- 结果集处理器ResultSetHandler (handleResultSets、handleOutputParameters等方法);
8.2 插件原理
- 基于JDK动态代理;
- 插件会被保存到interceptorChain;
- 在执行SQL时,需要先通过DefaultSqlSessionFactory 创
建 SqlSession。Executor 实例会在创建 SqlSession 的过程中被创建, Executor实例创建完毕后,
MyBatis会通过JDK动态代理为实例生成代理类。这样,插件逻辑即可在 Executor相关方法被调用前执
行;
8.3 自定义插件
Intercepts ({//注意看这个大花括号,也就这说这里可以定义多个@Signature对多个地方拦截,都用这
个拦截器
@Signature (type = StatementHandler .class , //这是指拦截哪个接口
method = "prepare",//这个接口内的哪个方法名,不要拼错了
args = { Connection.class, Integer .class}),//// 这是拦截的方法的入参,按顺序写到这,不要多也不要少,如果方法重载,可是要通过方法名和入参来确定唯一的
})
public class MyPlugin implements Interceptor {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// //这里是每次执行操作的时候,都会进行这个拦截器的方法内
Override
public Object intercept(Invocation invocation) throws Throwable {
//增强逻辑
System.out.println("对方法进行了增强....");
return invocation.proceed(); //执行原方法
}
/**获取配置文件的属性**/
//插件初始化的时候调用,也只调用一次,插件配置的属性从这里设置进来
Override
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数:"+properties );
}
}
sqlMapConfig.xml
<plugins>
<plugin interceptor="com.lagou.plugin.MySqlPagingPlugin">
<!--配置参数-->
<property name="name" value="Bob"/>
</plugin>
</plugins>
第九部分:Mybatis架构原理
9.1 总体架构
Mybatis的功能架构分为三层:
(1) API接口层:提供给外部使用的接口 API,开发人员通过这些本地API来操纵数据库。接口层一接收到
调用请求就会调用数据处理层来完成具体的数据处理。
MyBatis和数据库的交互有两种方式:
a. 使用传统的MyBati s提供的API ;
b. 使用Mapper代理的方式
(2) 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根
据调用的请求完成一次数据库操作。
(3) 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是
共 用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑
9.2 主要组件
9.3 总体流程
(1) 加载配置并初始化
触发条件:加载配置文件
配置来源于两个地方,一个是配置文件(主配置文件conf.xml,mapper文件*.xml),—个是java代码中的 注
解,将主配置文件内容解析封装到Configuration,将sql的配置信息加载成为一个mappedstatement 对
象,存储在内存之中
(2) 接收调用请求
触发条件:调用Mybatis提供的API
传入参数:为SQL的ID和传入参数对象
处理过程:将请求传递给下层的请求处理层进行处理。
(3) 处理操作请求
触发条件:API接口层传递请求过来
传入参数:为SQL的ID和传入参数对象
处理过程:
(A) 根据SQL的ID查找对应的MappedStatement对象。
(B) 根据传入参数对象解析MappedStatement对象,得到最终要执行的SQL和执行传入参数。
(C) 获取数据库连接,根据得到的最终SQL语句和执行传入参数到数据库执行,并得到执行结果。
(D) 根据MappedStatement对象中的结果映射配置对得到的执行结果进行转换处理,并得到最终的处理
结果。
(E) 释放连接资源。
(4) 返回处理结果
将最终的处理结果返回。
第十部分:Mybatis源码剖析
10.1 延迟加载
就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
- 优点:
先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表
速度要快。 - 缺点:
因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时
间,所以可能造成用户等待时间变长,造成用户体验下降。 - 在多表中:
一对多,多对多:通常情况下采用延迟加载
一对一(多对一):通常情况下采用立即加载 - 注意:
延迟加载是基于嵌套查询来实现的
10.1.1 局部延迟加载
在association和collection标签中都有一个fetchType属性,通过修改它的值,可以修改局部的加载策
略。
<!-- 开启一对多 延迟加载 -->
<resultMap id="userMap" type="user">
<id column="id" property="id"></id>
<result column="username" property="username"></result>
<result column="password" property="password"></result>
<result column="birthday" property="birthday"></result>
<!--
fetchType="lazy" 懒加载策略
fetchType="eager" 立即加载策略
-->
<collection property="orderList" ofType="order" column="id"
select="com.lagou.dao.OrderMapper.findByUid" fetchType="lazy">
</collection>
</resultMap>
<select id="findAll" resultMap="userMap">
SELECT * FROM `user`
</select>
10.1.2 全局延迟加载
在Mybatis的核心配置文件中可以使用setting标签修改全局的加载策略。
<settings>
<!--开启全局延迟加载功能-->
<setting name="lazyLoadingEnabled" value="true"/>
</settings>
局部配置优先于全局配置。
10.2 延迟加载原理
- 基于动态代理技术,主要使用:Javassist,Cglib实现。
- 为pojo生成代理,在调用字段的getter方法时,调用代理方法完成sql的延迟执行。
第十一部分:设计模式
Mybati s至少用到了以下的设计模式:
11.1 Builder模式
Builder模式的定义是"将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表
示。”,它属于创建类模式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范
围,就可以使用工厂模式和Builder模式,相对于工厂模式会产出一个完整的产品,Builder应用于更加
复杂的对象的构建,甚至只会构建产品的一个部分,直白来说,就是使用多个简单的对象一步一步构建
成一个复杂的对象
Mybatis中的体现:
SqlSessionFactory 的构建过程:
Mybatis的初始化工作非常复杂,不是只用一个构造函数就能搞定的。所以使用了建造者模式,使用了
大 量的Builder,进行分层构造,核心对象Configuration使用了 XmlConfigBuilder来进行构造
在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的
MybatisMapConfig.xml 和所有的 *Mapper.xml 文件,构建 Mybatis 运行的核心对象 Configuration
对 象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。
11.2 工厂模式
在Mybatis中比如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂
模式。
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于创
建型模式。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建
其他类的实例,被创建的实例通常都具有共同的父类
Mybatis 体现:
Mybatis中执行Sql语句、获取Mappers、管理事务的核心接口SqlSession的创建过程使用到了工厂模
式。
有一个 SqlSessionFactory 来负责 SqlSession 的创建
可以看到,该Factory的openSession ()方法重载了很多个,分别支
持autoCommit、Executor、Transaction等参数的输入,来构建核心的SqlSession对象。
11.3 代理模式
代理模式(Proxy Pattern):给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式 的
英文叫做Proxy,它是一种对象结构型模式,代理模式分为静态代理和动态代理,我们来介绍动态代 理。
Mybatis中实现:
代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper.java接
口,不需要实现,由Mybati s后台帮我们完成具体SQL的执行。
当我们使用Configuration的getMapper方法时,会调用mapperRegistry.getMapper方法,而该方法又
会调用 mapperProxyFactory.newInstance(sqlSession)来生成一个具体的代理:
public class MapperProxyFactory<T> {
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache = new
ConcurrentHashMap<Method, MapperMethod>();
public MapperProxyFactory(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
} p
ublic Class<T> getMapperInterface() {
return mapperInterface;
} p
ublic Map<Method, MapperMethod> getMethodCache() {
return methodCache;
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(),
new
Class[] { mapperInterface },
mapperProxy);
} p
ublic T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession,
mapperInterface, methodCache);
return newInstance(mapperProxy);
}
}
在这里,先通过T newInstance(SqlSession sqlSession)方法会得到一个MapperProxy对象,然后调用
newInstance(MapperProxy mapperProxy)生成代理对象然后返回,该MapperProxy类实现了InvocationHandler接口,并且实现了该接口的invoke方法。通
过这种方式,我们只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给
MapperProxy.invoke方法,而该方法则会调用后续的
sqlSession.cud>executor.execute>prepareStatement 等一系列方法,完成 SQL 的执行和返回。
文章来源:拉勾教育Java高新训练营15期