哈悠与杂货店绿色中文版
1.89G · 2025-11-17
MyBatis 是一种优秀的 ORM(Object-Relational Mapping)框架,与 JDBC 相比,有以下几点优势:
综上所述,使用 MyBatis 可以大大简化数据库操作的代码,提高 SQL 语句的可维护性和可读性,同时还提供了强大的动态 SQL 功能,易于集成使用。因此,相比于直接使用 JDBC,使用 MyBatis 更为便捷、高效和方便。
然而,也要注意一些缺点:
ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。
主要有以下几点区别:
JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。
首先,我们来聊聊编程模型。MyBatis和JPA采用了不同的方式来处理数据操作。
其次,我们来看一下SQL控制。
接下来是灵活性和控制
关于查询语言
在缓存方面
总的来说,选择使用MyBatis还是JPA取决于项目需求和团队技术背景。如果你需要更多的SQL控制和定制化,MyBatis可能更适合;如果你希望更快速地进行常见数据库操作,JPA可能更适合
首先,Mybatis被称为半ORM框架是因为它在数据库操作方面提供了一些对象关系映射的功能,但相对于全ORM框架,它更加灵活和轻量级。在Mybatis中,我们需要手动编写SQL来执行数据库操作,这跟传统的JDBC方式有点类似。但是,Mybatis通过映射文件来实现Java对象与数据库表之间的映射,这就是它的ORM特性。
区别的话,全ORM框架通常更加自动化,它会完全代替你来生成SQL语句,进行数据库操作。这在某些情况下能够提高开发效率,因为你不需要写太多的SQL代码。但是,全ORM框架也可能在性能方面略有影响,因为它们可能会生成复杂的SQL语句,导致查询效率下降。
相比之下,Mybatis更加灵活,你可以精确地控制要执行的SQL语句,这对于需要优化查询性能的场景很有帮助。另外,Mybatis在映射文件中可以明确指定每个字段的映射关系,这样你能更好地控制数据库表和Java对象之间的对应关系。
优点
缺点
适用场景
MyBatis专注于SQL自身,是一个足够灵活的DAO层解决方案。对性能的要求很高,或者需求变化较多的项目,例如Web项目,那么MyBatis是不二的选择。
MyBatis 的执行原理基于其核心设计思想:通过映射文件(XML 或注解)将 SQL 语句与 Java 对象进行绑定,整个执行流程可以分为以下几个步骤:
MyBatis 的插件机制是通过 动态代理 实现的,主要是在 SQL 执行的关键点(如执行查询、更新、插入)拦截操作并增强功能。
MyBatis的插件可以在MyBatis的执行过程中的多个关键点进行拦截和干预。这些关键点包括:
Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
插件机制的核心是Interceptor接口,你可以实现这个接口,编写自己的插件逻辑。一个插件主要包括以下几个步骤:
<plugins>标签配置你的插件。通常需要指定插件类和一些参数。Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页,先把数据都查出来,然后再做分页。
可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
常用的分页插件和技巧:
分页插件是一种扩展机制,它允许MyBatis在查询过程中,自动应用分页逻辑而不需要手动编写分页查询语句。分页插件的一般原理如下:
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql(SQL 拼接 limit),根据 dialect 方言,添加对应的物理分页语句和物理分页参数,用到了 JDK 动态代理,用到了责任链设计模式。
所谓的延迟加载,其实就是一种优化方法,目标是为了在查数据库的时候,尽量不读取多余的数据,从而提高我们应用的表现和节约资源。在MyBatis里,这个延迟加载的技巧主要是用在处理对象关系映射的时候,也就是ORM。
来个例子帮你理解:假设有两张表,一张是订单表,另一张是商品表。每个订单下面可能有好几个商品。用延迟加载的话,当我们查一个订单的时候,MyBatis不会马上查出这个订单的所有商品,而是等到我们真的要用商品的数据时才去查。这样做就避免了在查订单的时候额外加载了一堆没用的商品。但要注意,虽然延迟加载能提升性能,可别用得过了,免得碰上懒加载的N+1问题,就是要查很多次才能拿到关联数据,结果性能就拖垮了。所以用延迟加载的时候,得根据实际情况合理配置和使用。
Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
延迟加载的基本原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法。
比如调用a.getB().getName(),拦截器 invoke()方法发现 a.getB()是 null 值,那么就会单独发送事务,先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用a.setB(b),于是 a 的对象 b 属性就有值了,接着完成a.getB().getName()方法的调用。
当然了,不光是 Mybatis,几乎所有的ORM框架、包括 Hibernate,支持延迟加载的原理都是一样的。
当谈到MyBatis中的懒加载和预加载时,我们实际上在讨论在获取数据库数据时如何处理关联对象的加载方式。
懒加载是一种延迟加载技术,它在需要访问关联对象的时候才会加载相关数据。这意味着,当你从数据库中获取一个主对象时,它的关联对象并不会立即加载到内存中,只有当你实际调用访问关联对象的方法时,MyBatis才会去数据库中加载并填充这些关联对象的数据。懒加载适用于关联对象较多或者关联对象数据较大的情况,这样可以减少不必要的数据库查询,提升性能。
预加载则是一种在获取主对象时同时加载其关联对象的技术。这样一来,当获取主对象时,它的所有关联对象也会被一并加载到内存中,避免了多次数据库查询。预加载适用于你确定在后续使用中肯定会访问关联对象,这样可以减少每次访问关联对象时的延迟。
选择懒加载还是预加载取决于具体需求和场景。如果希望在尽量少的数据库查询次数下获取数据,懒加载是个不错的选择。如果在获取主对象后会频繁地访问其关联对象,预加载可能更适合,因为它可以减少多次查询带来的性能开销。
两者都是优化数据库访问性能的手段,根据具体的使用场景选择合适的加载方式非常重要。
#{ } 被解析成预编译语句,预编译之后可以直接执行,不需要重新编译sql。
//sqlMap 中如下的 sql 语句
select * from user where name = #{name};
//解析成为预编译语句;编译好SQL语句再取值
select * from user where name = ?;
${ } 仅仅为一个字符串替换,每次执行sql之前需要进行编译,存在 sql 注入问题。
select * from user where name = '${name}'
//传递的参数为 "seven" 时,解析为如下,然后发送数据库服务器进行编译。取值以后再去编译SQL语句。
select * from user where name = "seven";
where 1=1 和 <where> 标签 两种方案,该如何选择?
<where> 标签。当然现在 MySQL Server版本基本都是 5.7以上了,不是的话那赶紧升级吧还是。
数据库接受到sql语句之后,需要词法和语义解析,优化sql语句,制定执行计划。这需要花费一些时间。如果一条sql语句需要反复执行,每次都进行语法检查和优化,会浪费很多时间。预编译语句就是将sql语句中的值用占位符替代,即将sql语句模板化。一次编译、多次运行,省去了解析优化等过程。
mybatis是通过PreparedStatement和占位符来实现预编译的。
mybatis底层使用PreparedStatement,默认情况下,将对所有的 sql 进行预编译,将#{}替换为?,然后将带有占位符?的sql模板发送至mysql服务器,由服务器对此无参数的sql进行编译后,将编译结果缓存,然后直接执行带有真实参数的sql。
预编译的作用:
PreparedStatement 对象缓存下来,下次对于同一个sql,可以直接使用这个缓存的 PreparedState 对象。MyBatis 类型转换主要依赖于 MyBatis 的 类型处理器(TypeHandler)机制。
TypeHandler 的核心作用:
具体操作流程如下:
缓存:合理使用缓存是优化中最常见的方法之一,将从数据库中查询出来的数据放入缓存中,下次使用时不必从数据库查询,而是直接从缓存中读取,避免频繁操作数据库,减轻数据库的压力,同时提高系统性能。
Mybatis里面设计了二级缓存来提升数据的检索效率,避免每次数据的访问都需要去查询数据库。
一级缓存是SqlSession级别的缓存:Mybatis对缓存提供支持,默认情况下只开启一级缓存,一级缓存作用范围为同一个SqlSession。在SQL和参数相同的情况下,我们使用同一个SqlSession对象调用同一个Mapper方法,往往只会执行一次SQL。因为在使用SqlSession第一次查询后,Mybatis会将结果放到缓存中,以后再次查询时,如果没有声明需要刷新,并且缓存没超时的情况下,SqlSession只会取出当前缓存的数据,不会再次发送SQL到数据库。若使用不同的SqlSession,因为不同的SqlSession是相互隔离的,不会使用一级缓存。
二级缓存是mapper级别的缓存:可以使缓存在各个SqlSession之间共享。当多个用户在查询数据的时候,只要有任何一个SqlSession拿到了数据就会放入到二级缓存里面,其他的SqlSession就可以从二级缓存加载数据。
主要区别就在于作用范围:一级缓存只在一个会话内部有效,而二级缓存可以在不同会话之间共享数据。
二级缓存默认不开启,需要在mybatis-config.xml开启二级缓存:
<!-- 通知 MyBatis 框架开启二级缓存 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
并在相应的Mapper.xml文件添加cache标签,表示对哪个mapper 开启缓存:
<cache/>
二级缓存要求返回的POJO必须是可序列化的,即要求实现Serializable接口。
当开启二级缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
二级缓存是以 namespace(mapper) 为单位的,不同 namespace 下的操作互不影响。且 insert/update/delete 操作会清空所在 namespace 下的全部缓存。
那么问题就出来了,假设现在有 ItemMapper 以及 XxxMapper,在 XxxMapper 中做了表关联查询,且做了二级缓存。此时在 ItemMapper 中将 item 信息给删了,由于不同 namespace 下的操作互不影响,XxxMapper 的二级缓存不会变,那之后再次通过 XxxMapper 查询的数据就不对了,非常危险。
例如:
@Mapper
@Repository
@CacheNamespace
publicinterfaceXxxMapper{
@Select("select i.id itemId,i.name itemName,p.amount,p.unit_price unitPrice " +
"from item i JOIN payment p on i.id = p.item_id where i.id = #{id}")
List<PaymentVO> getPaymentVO(Long id);
}
@Autowired
private XxxMapper xxxMapper;
@Test
voidtest(){
System.out.println("==================== 查询PaymentVO ====================");
List<PaymentVO> voList = xxxMapper.getPaymentVO(1L);
System.out.println(JSON.toJSONString(voList.get(0)));
System.out.println("==================== 更新item表的name ==================== ");
Item item = itemMapper.selectById(1);
item.setName("java并发编程");
itemMapper.updateById(item);
System.out.println("==================== 重新查询PaymentVO ==================== ");
List<PaymentVO> voList2 = xxxMapper.getPaymentVO(1L);
System.out.println(JSON.toJSONString(voList2.get(0)));
}
由于 itemMapper 与 xxxMapper 不是同一个命名空间,所以 itemMapper 执行的更新操作不会影响到 xxxMapper 的二级缓存;
再次调用 xxxMapper.getPaymentVO,发现取出的值是走缓存的,itemName 还是老的。但实际上 itemName 在上面已经被改了
最佳实践中,通常一个 xml 映射文件,都会写一个 Dao 接口与之对应。Dao 接口就是人们常说的 Mapper 接口,接口的全限名,就是映射文件中的 namespace 的值,接口的方法名,就是映射文件中 MappedStatement 的 id 值,接口方法内的参数,就是传递给 sql 的参数。 Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement ,举例:com.mybatis3.mappers.StudentDao.findStudentById ,可以唯一找到 namespace 为 com.mybatis3.mappers. StudentDao 下面 id = findStudentById 的 MappedStatement 。在 MyBatis 中,每一个 <select>、 <insert>、 <update>、 <delete> 标签,都会被解析为一个 MappedStatement 对象。
Dao 接口里的方法可以重载,但是 Mybatis 的 xml 里面的 ID 不允许重复。并且需要满足以下条件:
@Param ,或者使用 param1 这种Mybatis 版本 3.3.0:
/**
* Mapper接口里面方法重载
*/
public interface StuMapper {
List<Student> getAllStu();
List<Student> getAllStu(@Param("id") Integer id);
}
然后在 StuMapper.xml 中利用 Mybatis 的动态 sql 就可以实现。
<select id="getAllStu" resultType="com.pojo.Student">
select * from student
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>
能正常运行,并能得到相应的结果,这样就实现了在 Dao 接口中写重载方法。
Mybatis 的 Dao 接口可以有多个重载方法,但是多个接口对应的映射必须只有一个,否则启动会报错。
Dao 接口的工作原理是 JDK 动态代理,MyBatis 运行时会使用 JDK 动态代理为 Dao 接口生成代理 proxy 对象,代理对象 proxy 会拦截接口方法,转而执行 MappedStatement 所代表的 sql,然后将 sql 执行结果返回。
核心原理是 JDBC 的能力 和 动态代理,通过解析 XML映射文件和动态生成 DAO 接口实现类来完成 SQL的执行。
以下是详细的执行原理:
动态SQL是在 MyBatis中根据不同的条件、需求动态生成 so!语句的一种机制。它的主要目的是提高 sql 的灵活性和复用性,在复杂的查询或更新场景中,根据参数动态构建不同的 sqL语句,
动态 SQL的执行基于XML映射文件中定义的 SOL片段与标签,如 if、choose、when、otherwise、where、foreach 等,这些标签被解析,在运行时根据传入的参数值评估,最终形成完整的 SQL 语句发送到傲数据库
执行MyBatis 解析动态 SQL 的流程如下:
常见动态sql:
Mybatis有三种基本的Executor执行器,SimpleExecutor、ReuseExecutor、BatchExecutor。
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。
BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。
作用范围:Executor的这些特点,都严格限制在SqlSession生命周期范围内。
除了常见的select、insert、update和delete标签,MyBatis的XML映射文件中还有一些其他标签用于更复杂的操作和配置。以下是一些常见的额外标签:
不同的 xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复;毕竟 namespace 不是必须的,只是最佳实践而已。
原因就是 namespace+id 是作为 Map<String, MappedStatement> 的 key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同。
第一种是使用 <resultMap> 标签,逐一定义列名和对象属性名之间的映射关系。第二种是使用 sql 列的别名功能,将列别名书写为对象属性名,比如 T_NAME AS NAME,对象属性名一般是 name,小写,但是列名不区分大小写,MyBatis 会忽略列名大小写,智能找到与之对应对象属性名,你甚至可以写成 T_NAME AS NaMe,MyBatis 一样可以正常工作。
有了列名与属性名的映射关系后,MyBatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
MyBatis 可以映射枚举类,不单可以映射枚举类,MyBatis 可以映射任何对象到表的一列上。映射方式为自定义一个 TypeHandler ,实现 TypeHandler 的 setParameter() 和 getResult() 接口方法。 TypeHandler 有两个作用:
setParameter() 和 getResult() 两个方法,分别代表设置 sql 问号占位符参数和获取列查询结果。虽然 MyBatis 解析 xml 映射文件是按照顺序解析的,但是,被引用的 B 标签依然可以定义在任何地方,MyBatis 都可以正确识别。
原理是,MyBatis 解析 A 标签,发现 A 标签引用了 B 标签,但是 B 标签尚未解析到,尚不存在,此时,MyBatis 会将 A 标签标记为未解析状态,然后继续解析余下的标签,包含 B 标签,待所有标签解析完毕,MyBatis 会重新解析那些被标记为未解析的标签,此时再解析 A 标签时,B 标签已经存在,A 标签也就可以正常解析完成了。
在MyBatis中,要执行模糊查询(使用LIKE语句),可以使用SQL语句的字符串拼接或使用动态SQL来构建查询语句。
假设你要在一个查询中执行模糊查询,搜索用户的用户名包含特定关键字的情况。
字符串拼接方式:
<select id="searchUsers" resultMap="userResultMap">
SELECT * FROM users
WHERE username LIKE CONCAT('%', #{keyword}, '%')
</select>
在这个例子中,#{keyword}是参数占位符,表示要搜索的关键字。CONCAT('%', #{keyword}, '%')用于构建模糊匹配的字符串。
动态SQL方式:
<select id="searchUsers" resultMap="userResultMap">
SELECT * FROM users
<where>
<if test="keyword != null">
AND username LIKE CONCAT('%', #{keyword}, '%')
</if>
</where>
</select>
在这个例子中,使用了<if>标签来创建动态条件。只有在keyword参数不为null时,才会添加AND username LIKE CONCAT('%', #{keyword}, '%')这个条件到查询语句中。
MyBatis 自带的连接池是 PooledDataSource 类实现的,是一个简单的连接池实现,提供了连接复用和基本的资源管理功能
执行原理:
关键配置:
在 MyBatis 中,实现一对一和一对多的关联査询主要是通过 resultMap 来完成的。MyBatis 提供了两种方式来处理关联关系
嵌套结果映射(Nested Result Mapping)
<association>标签表示一对一的关系。<collection>标签表示一对多的关系。嵌套查询(Nested Select)
<association>或<collection>的select 属性指定子查询在实现动态数据源切换方面,MyBatis有几种方法,让你能够在不同的数据库之间轻松切换。比如,你可能会在开发环境和生产环境中使用不同的数据库。下面是一些可以考虑的方法:
就可以看成 MyBatis 能实现的 MyBatis Plus都实现了,是增强版工具