Mybatis
Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注sql语句本身,
不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。
开发人员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。
MyBatis可以使用xml或注解来配置和映射原生信息,将POJO映射成数据库中的记录,避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。
通过xml文件或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的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优化和移植性
Hibernate对sql语句封装,提供了日志、缓存、级联(级联比MyBatis强大)等特性,</br> 此外还提供hql(Hibernate 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:多表继承set、where、if、foreach、trim、choose、when、otherwise、bind:一般写动态sql涉及到的标签- 等等
Mybatis $()和#()的区别
${}是字符串替换,是Properties文件中的变量占位符,它可以用于标签属性值和sql内部,属于静态文本替换Mybatis在处理${}时,就是把${}直接替换成变量的值,这种会出现sql注入的风险。
#{}是预编译处理,是sql的参数占位符,Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值
Mybatis 模糊查询
Mybatis的模糊查询一般存在两种写法,一种是使用${},另一种是使用#{}。
使用${}的存在sql注入的风险,一般不推荐使用
一般的写法:
select * from user where name like CONCAT('%', #{name}, '%')
like也可以根据使用情况替换成likeLeft、likeRight
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属性指定了用于查询集合的嵌套查询的ID,column属性指定了嵌套查询使用的外键列。
最后调用selectById方法来查询班级信息,同时会自动执行嵌套查询selectStudentByClazzId,并将结果映射到Clazz对象的allStudents属性中。
Mybatis 缓存
Mybatis中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。
一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中进行相同的sql语句查询时,
第二次以后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存1024条sql。
二级缓存是指可以跨SqlSession的缓存。是mapper级别的缓存,对于mapper级别的缓存不同的SqlSession是可以共享的。
一级缓存原理(SqlSession级别)
第一次发出一个查询sql,sql查询结果写入SqlSession的一级缓存中,缓存使用的数据结构是一个map。
key:MapperID+offset+limit+sql+所有的入参value:用户信息
同一个SqlSession再次发出相同的sql,就从缓存中取出数据。</br>
如果两次中间出现commit操作(修改、添加、删除),本SqlSession中的一级缓存区域全部清空,</br>
下次再去缓存中查询不到,就要从数据库查询,从数据库查询到再写入缓存。
二级缓存原理(mapper基本)
二级缓存的范围是mapper级别(mapper同一个命名空间),mapper以命名空间为单位创建缓存数据结构,结构是map。
key:MapperID+offset+limit+sql+所有的入参
mybatis的二级缓存是通过CacheExecutor实现的。
CacheExecutor其实是Executor的代理对象。
所有的查询操作,在CacheExecutor中都会先匹配缓存中是否存在,不存在则查询数据库。
具体使用需要配置:
Mybatis全局配置中启用二级缓存配置- 在对应的
Mapper.xml中配置cache节点 - 在对应的
select查询节点中添加useCache=true - 属性类需要实现
Serializable序列化接口
Mybatis 工作原理
- 读取
MyBatis配置文件:mybatis-config.xml为MyBatis的全局配置文件,配置了MyBatis的运行环境等信息,例如数据库连接信息。 - 加载
sql映射文件。文件中包含了操作数据库的sql语句,需要在MyBatis配置文件mybatis-config.xml中加载。mybatis-config.xml文件可以加载多个映射文件,每个文件对应数据库中的一张表。
- 构造会话工厂:通过
MyBatis的环境等配置信息构建会话工厂SqlSessionFactory。 - 创建会话对象:由会话工厂创建
SqlSession对象,该对象中包含了执行sql语句的所有方法。 Executor执行器:MyBatis底层定义了一个Executor接口来操作数据库, 根据SqlSession传递的参数动态地生成需要执行的sql语句,同时负责查询缓存的维护。MappedStatement对象:在Executor接口的执行方法中有一个MappedStatement类型的参数, 是对映射信息的封装,用于存储要映射的sql语句的id、参数等信息。- 输入参数映射:输入参数类型可以是
Map、List等集合类型,也可以是基本数据类型和POJO类型。 输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。 - 输出结果映射:输出结果类型可以是
Map、List等集合类型,也可以是基本数据类型和POJO类型。 输出结果映射过程类似于JDBC对结果集的解析过程。
Mybatis 插件
MyBatis可以拦截以下四大核心组件的方法调用:
Executor:执行器,负责sql语句的执行和事务管理(update、query、commit、rollback等方法)。StatementHandler:语句处理器,处理具体的sql语句,包括预编译和参数设置等(prepare、parameterize、batch、updates query等方法)。ParameterHandler:参数处理器,负责将用户传递的参数转换成JDBC可识别的参数(getParameterObject、setParameters等方法)。ResultSetHandler:结果集处理器,负责将JDBC返回的结果集转换成用户所需的对象或集合(handlerResultSet、handleOutputParameters等方法)。
通过拦截这些方法调用,MyBatis插件可以实现:sql重写、日志记录、性能监控、事务管理增强等等功能。
MyBatis插件的原理
MyBatis插件的实现原理基于Java的动态代理机制。
当配置了MyBatis插件后,初始化时MyBatis会使用JDK动态代理,
为目标对象(SqlSession、Executor、StatementHandler等)生成一个代理对象。
这个代理对象会包装原始对象,并在方法调用时执行自定义的拦截逻辑。
当调用这些代理对象的方法时,实际上会触发MyBatis提供的Invocation对象的proceed方法,
这个方法会在执行原始方法逻辑前后执行插件中定义的逻辑。
拦截过程如下:
- 当目标对象的方法被调用时,代理对象会先检查是否存在对应的插件拦截器。
- 如果存在拦截器,且该方法的签名与拦截器配置的方法签名匹配,则调用拦截器的
intercept方法。 - 在
intercept方法中,开发者可以实现自定义的拦截逻辑。通常,这里会包含对原始方法调用的修改或增强。 - 执行完拦截逻辑后,可以选择是否继续执行原始方法。如果继续执行,则通过反射调用原始对象的方法;否则,直接返回自定义的结果。
- 需要注意的是,由于
MyBatis插件是基于方法签名进行拦截的,因此开发者在编写插件时需要谨慎选择需要拦截的方法签名,以避免不必要的性能开销和潜在问题。
MyBatis插件注解
@Intercepts注解的作用是,标记需要拦截的方法列表。
Mybatis通过该注解去判断当前方法是否需要被拦截。@Intercepts其实就是一个数组,用来添加复数个@Signature。- 每个
@Signature都指定了一个需要拦截的方法。
@Signature注解参数说明:
type:就是指定拦截器类型(Executor、StatementHandler、ParameterHandler、ResultSetHandler)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>
当执行Executor的update方法时,插件中定义的intercept方法会被调用,
并且会在原始方法的执行前后打印出相应的日志信息。