MyBatis知识点
目录
基础
说说什么是MyBatis
ORM是什么
为什么MyBatis是半自动ORM映射工具,它与全自动的区别在哪里?
Hibernate和MyBatis的区别?
MyBatis的使用过程,生命周期
MyBatis的生命周期
在Mapper中如何传递多个参数?
实体类属性名和表中字段名不一样?
#{}和${}的区别
模糊查询like怎么写?
MyBatis是否支持延迟加载
MyBatis支持动态sql吗
MyBatis如何执行批量操作
MyBatis的一级和二级缓存
MyBatis的工作原理
MyBatis的功能架构
编辑
Mapper接口没有实现类的原因
Executor执行器
编辑
指定Executor
MyBatis的插件运行原理
分页的实现
JDBC的执行流程
Statement和PreparedStatement的区别
基础
说说什么是MyBatis
- Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。
- MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
缺点:
- SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求
- SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库
ORM是什么
ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单 Java 对象(POJO)的映射关系的技术。
为什么MyBatis是半自动ORM映射工具,它与全自动的区别在哪里?
Hibernate属于全自动ORM映射工具,使用Hibernate查询对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
而MyBatis在查询关联对象或关联集合对象时,需要手动编写SQL来完成,所以,被称为半自动ORM映射工具。
JDBC编程有哪些不足之处,MyBatis是如何解决的?
1、数据连接创建,释放频繁造成资源浪费从而影响系统性能,在MyBatis-config.xml中配置数据连接池,使用连接池统一管理数据库连接。
2、sql语句写在代码中造成代码不易维护,将sql语句配置在XXXXmapper.xml文件中与Java代码分离。
3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。MyBatis自动将Java对象映射到sql语句。
4、对结果集解析麻烦,sql语句变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便,MyBatis自动将sql执行结果映射到Java对象。
Hibernate和MyBatis的区别?
相同点:
都是对JDBC的封装,都是应用于持久层的框架。
不同点:
映射关系:
MyBatis 是一个半自动映射的框架,配置 Java 对象与 sql 语句执行结果的对应关系,多表关联关系配置简单
Hibernate 是一个全表映射的框架,配置 Java 对象与数据库表的对应关系,多表关联关系配置复杂
SQL优化和移植性
- Hibernate 对 SQL 语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供 HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需要支持多种数据库,代码开发量少,但 SQL 语句优化困难。
- MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相对大些。直接使用 SQL 语句操作数据库,不支持数据库无关性,但 sql 语句优化容易。
MyBatis和Hibernate适用场景
- Hibernate 是标准的 ORM 框架,SQL 编写量较少,但不够灵活,适合于需求相对稳定,中小型的软件项目,比如:办公自动化系统
- MyBatis 是半 ORM 框架,需要编写较多 SQL,但是比较灵活,适合于需求变化频繁,快速迭代的项目,比如:电商网站
MyBatis的使用过程,生命周期
创建 SqlSessionFactory
可以从配置或者直接编码来创建 SqlSessionFactory
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = newSqlSessionFactoryBuilder().build(inputStream);
通过 SqlSessionFactory 创建 SqlSession
SqlSession(会话)可以理解为程序和数据库之间的桥梁
SqlSession session = sqlSessionFactory.openSession();
通过sqlsession执行数据库操作
可以通过Sqlsession实例来直接执行已映射的sql语句
Blog blog = (Blog)session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
更常用的方式是先获取 Mapper(映射),然后再执行 SQL 语句:
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
调用session.commit提交事务
如果是更新,删除语句,我们还需要提交一下事务
调用session.close关闭会话
MyBatis的生命周期
SqlSessionFactoryBuilder
一旦创建了SqlSessionFactoryBuilder,就不再需要它了。因此SqlSessionFactoryBuilder实例的生命周期只存在方法的内部。
SqlSessionFactroy
SqlSessionFactory 是用来创建 SqlSession 的,相当于一个数据库连接池,每次创建 SqlSessionFactory 都会使用数据库资源,多次创建和销毁是对资源的浪费。所以 SqlSessionFactory 是应用级的生命周期,而且应该是单例的。
SqlSession
SqlSession 相当于 JDBC 中的 Connection,SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的生命周期是一次请求或一个方法。
Mapper
映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的,它的生命周期在 sqlsession 事务方法之内,一般会控制在方法级
在Mapper中如何传递多个参数?
方法 1:顺序传参法
public User selectUser(String name, int deptId);<select id="selectUser" resultMap="UserResultMap">select * from userwhere user_name = #{0} and dept_id = #{1}
</select>
\#{}
里面的数字代表传入参数的顺序。- 这种方法不建议使用,sql 层表达不直观,且一旦顺序调整容易出错。
方法 2:@Param 注解传参法
public User selectUser(@Param("userName") String name, int @Param("deptId") deptId);<select id="selectUser" resultMap="UserResultMap">select * from userwhere user_name = #{userName} and dept_id = #{deptId}
</select>
\#{}
里面的名称对应的是注解@Param 括号里面修饰的名称。- 这种方法在参数不多的情况还是比较直观的,(推荐使用)。
方法 3:Map 传参法
public User selectUser(Map<String, Object> params);<select id="selectUser" parameterType="java.util.Map" resultMap="UserResultMap">select * from userwhere user_name = #{userName} and dept_id = #{deptId}
</select>
\#{}里面的名称对应的是 Map 里面的 key 名称。
这种方法适合传递多个参数,且参数易变能灵活传递的情况。
方法 4:Java Bean 传参法
public User selectUser(User user);
<select id="selectUser" parameterType="com.jourwon.pojo.User" resultMap="UserResultMap">
select * from user
where user_name = #{userName} and dept_id = #{deptId}
</select>
\#{}
里面的名称对应的是 User 类里面的成员属性。- 这种方法直观,需要建一个实体类,扩展不容易,需要加属性,但代码可读性强,业务逻辑处理方便,推荐使用。(推荐使用)。
实体类属性名和表中字段名不一样?
- 第 1 种: 通过在查询的 SQL 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
- 第 2 种: 通过 resultMap 中的<result>来映射字段名和实体类属性名的一一对应的关系。
#{}和${}的区别
#{}
是预编译处理,${}
是字符串替换。
当使用 #{}
时,MyBatis 会在 SQL 执行之前,将占位符替换为问号 ?
,并使用参数值来替代这些问号。由于 #{}
使用了预处理,所以能有效防止 SQL 注入,确保参数值在到达数据库之前被正确地处理和转义。
②、当使用 ${}
时,参数的值会直接替换到 SQL 语句中去,而不会经过预处理。存在sql注入的风险
模糊查询like怎么写?
- 1 ’
%${question}%
’ 可能引起 SQL 注入,不推荐 - 2
"%"#{question}"%"
注意:因为#{…}
解析成 sql 语句时候,会在变量外侧自动加单引号’ ',所以这里 % 需要使用双引号" ",不能使用单引号 ’ ',不然会查不到任何结果。 - 3
CONCAT('%',#{question},'%')
使用 CONCAT()函数,(推荐 ✨) - 4 使用 bind 标签(不推荐)
MyBatis是否支持延迟加载
- 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支持动态sql吗
- if
根据条件来组成 where 子句
<select id="findActiveBlogWithTitleLike"resultType="Blog">
SELECT * FROM BLOG
WHERE state = ‘ACTIVE’
<if test="title != null">AND title like #{title}
</if>
</select>
- choose (when, otherwise)
这个和 Java 中的 switch 语句有点像
<select id="findActiveBlogLike"resultType="Blog">
SELECT * FROM BLOG WHERE state = ‘ACTIVE’
<choose><when test="title != null">AND title like #{title}</when><when test="author != null and author.name != null">AND author_name like #{author.name}</when><otherwise>AND featured = 1</otherwise>
</choose>
</select>
-
trim (where, set)
-
<where>可以用在所有的查询条件都是动态的情况
<select id="findActiveBlogLike"resultType="Blog">
SELECT * FROM BLOG
<where><if test="state != null">state = #{state}</if><if test="title != null">AND title like #{title}</if><if test="author != null and author.name != null">AND author_name like #{author.name}</if>
</where>
</select>
- <set> 可以用在动态更新的时候
<update id="updateAuthorIfNecessary">update Author<set><if test="username != null">username=#{username},</if><if test="password != null">password=#{password},</if><if test="email != null">email=#{email},</if><if test="bio != null">bio=#{bio}</if></set>where id=#{id}
</update>
-
foreach
看到名字就知道了,这个是用来循环的,可以对集合进行遍历
<select id="selectPostIn" resultType="domain.blog.Post">
SELECT *
FROM POST P
<where><foreach item="item" index="index" collection="list"open="ID in (" separator="," close=")" nullable="true">#{item}</foreach>
</where>
</select>
MyBatis如何执行批量操作
第一种:使用foreach标签
foreach 的主要用在构建 in 条件中,它可以在 SQL 语句中进行迭代一个集合。foreach 标签的属性主要有 item,index,collection,open,separator,close。
- item 表示集合中每一个元素进行迭代时的别名,随便起的变量名;
- index 指定一个名字,用于表示在迭代过程中,每次迭代到的位置,不常用;
- open 表示该语句以什么开始,常用“(”;
- separator 表示在每次进行迭代之间以什么符号作为分隔符,常用“,”;
- close 表示以什么结束,常用“)”。
在使用 foreach 的时候最关键的也是最容易出错的就是 collection 属性,该属性是必须指定的,但是在不同情况下,该属性的值是不一样的,主要有以下 3 种情况:
- 如果传入的是单参数且参数类型是一个 List 的时候,collection 属性值为 list
- 如果传入的是单参数且参数类型是一个 array 数组的时候,collection 的属性值为 array
- 如果传入的参数是多个的时候,我们就需要把它们封装成一个 Map 了,当然单参数也可以封装成 map,实际上如果你在传入参数的时候,在 MyBatis 里面也是会把它封装成一个 Map 的,map 的 key 就是参数名,所以这个时候 collection 属性值就是传入的 List 或 array 对象在自己封装的 map 里面的 key
<!-- MySQL下批量保存,可以foreach遍历 mysql支持values(),(),()语法 --> //推荐使用
<insert id="addEmpsBatch">INSERT INTO emp(ename,gender,email,did)VALUES<foreach collection="emps" item="emp" separator=",">(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})</foreach>
</insert>
<!-- 这种方式需要数据库连接属性allowMutiQueries=true的支持如jdbc.url=jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true -->
<insert id="addEmpsBatch"><foreach collection="emps" item="emp" separator=";">INSERT INTO emp(ename,gender,email,did)VALUES(#{emp.eName},#{emp.gender},#{emp.email},#{emp.dept.id})</foreach>
</insert>
第二种方法:使用 ExecutorType.BATCH
- Mybatis 内置的 ExecutorType 有 3 种,默认为 simple,该模式下它为每个语句的执行创建一个新的预处理语句,单条提交 sql;而 batch 模式重复使用已经预处理的语句,并且批量执行所有更新语句,显然 batch 性能将更优; 但 batch 模式也有自己的问题,比如在 Insert 操作时,在事务没有提交之前,是没有办法获取到自增的 id,在某些情况下不符合业务的需求。
MyBatis的一级和二级缓存
- 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 SqlSession,各个 SqlSession 之间的缓存相互隔离,当 Session flush 或 close 之后,该 SqlSession 中的所有 Cache 就将清空,MyBatis 默认打开一级缓存。
- 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同之处在于其存储作用域为 Mapper(Namespace),可以在多个 SqlSession 之间共享,并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置。
MyBatis的工作原理
MyBatis的功能架构
API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层-接收到调用请求就会调用数据处理层来完成具体的数据处理
数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作
基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑
Mapper接口没有实现类的原因
动态代理
Mapper类的获取过程
Executor执行器
SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象
ResuseExecutor:执行update或select,以SQL作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是防止于Map<String,Statement>内,供下一次使用。简而言之,就是重复使用Statement对象
BatchExecutor:执行update,将所有SQL都添加到批处理中,它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同
指定Executor
在 Mybatis 配置文件中,在设置(settings)可以指定默认的 ExecutorType 执行器类型,也可以手动给 DefaultSqlSessionFactory 的创建 SqlSession 的方法传递 ExecutorType 类型参数,如SqlSession openSession(ExecutorType execType)。
配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。
MyBatis的插件运行原理
分页的实现
MyBatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的原理
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,实现Executor的query方法
在执行查询的时候,拦截待执行的SQL,然后重写SQL,根据dialect方言,添加对应的物理分页语句和物理分页参数
例子:select * from student,拦截 sql 后重写为:select t.* from (select * from student) t limit 0, 10
JDBC的执行流程
1.加载数据库驱动
在于数据库建立连接之前,首先需要通过Class.forName()方法加载对应的数据库驱动。这一步确保JDNC驱动注册到了DriverManager类中
Class.forName("com.mysql.cj.jdbc.Driver");
2.建立数据库连接
使用DriverManager.getConnection()方法建立到数据库的连接。这一步需要提供数据库的URL、用户名和密码作为参数
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/databaseName", "username", "password");
3.创建Statement对象
通过建立的数据库连接对象Connection创建Statement、PreparedStatement或CallableStatement对象,用于执行SQL语句
Statement stmt = conn.createStatement();
或者创建PreparedStatement对象(预编译SQL语句,适用于带参数的SQL):
PreparedStatement pstmt = conn.prepareStatement("SELECT * FROM tableName WHERE column = ?");
pstmt.setString(1, "value");
4.执行SQL语句
使用Statement或PreparedStatement对象执行SQL语句
执行查询语句时,使用executeQuery()方法,它返回ResultSet对象
执行更新语句时,使用executeUpdate()方法,它返回一个整数表示受影响的行数
ResultSet rs = stmt.executeQuery("SELECT * FROM tableName");
或者
int affectedRows = stmt.executeUpdate("UPDATE tableName SET column = 'value' WHERE condition");
5.处理结果集
如果执行的是查询操作,需要处理ResultSet对象来获取数据
6.关闭资源最后,需要依次关闭ResultSet、Statement和Connection等资源,释放数据库连接等资源
Statement和PreparedStatement的区别
Statement和PreparedStatement都是用于执行 SQL 语句的接口,但它们之间存在几个关键的区别:
①、每次执行Statement对象的executeQuery或executeUpdate方法时,SQL 语句在数据库端都需要重新编译和执行。这适用于一次性执行的 SQL 语句。
Statement 不支持参数化查询。如果需要在 SQL 语句中插入变量,通常需要通过字符串拼接的方式来实现,这会增加 SQL 注入攻击的风险。
②、PreparedStatement 代表预编译的 SQL 语句的对象。这意味着 SQL 语句在PreparedStatement对象创建时就被发送到数据库进行预编译。
之后,可以通过设置参数值来多次高效地执行这个 SQL 语句。这不仅减少了数据库编译 SQL 语句的开销,也提高了性能,尤其是对于重复执行的 SQL 操作。
PreparedStatement 支持参数化查询,即可以在 SQL 语句中使用问号(?)作为参数占位符。通过setXxx方法(如setString、setInt)设置参数,可以有效防止 SQL 注入。
总的来说,PreparedStatement相比Statement有着更好的性能和更高的安全性,是执行 SQL 语句的首选方式,尤其是在处理含有用户输入的动态查询时。