1、Mybatis基本介绍
一种持久层框架的ORM(Object-Relational Mapping,对象关系映射,是一种编程技术,用于在关系数据库和对象之间做一个映射)框架。
特点:
- 易于配置和使用:使用XML或注解配置数据库操作,只需定义SQL语句和参数映射。
- 灵活的SQL编写:允许开发人员直接编写原生SQL语句。更好的优化查询,处理复杂的操作。
- 提供了ORM功能:简化了数据的处理和操作,帮助转换。
- 支持动态SQL:根据不同条件生成不同SQL语句。MyBatis 使用
${}
和#{}
来实现动态 SQL 的功能,但它们的用法和目的有所不同-
#{}
用于在 SQL 语句中传递参数,它可以防止 SQL 注入,因为 MyBatis 会将 SQL 与参数值分开处理,并通过预处理语句(PreparedStatement
)的参数占位符机制来设置参数值。 -
${}
用于直接将参数值插入 SQL 语句中,这种方式可能会导致 SQL 注入问题,因为它仅仅是字符串替换。因此,使用${}
时需要格外小心,确保替换的值是安全的。 -
防止SQL注入:
PreparedStatement
可以有效防止SQL注入的原因在于它使用了参数化查询。参数化查询与构建SQL字符串的传统方式不同,在使用PreparedStatement
时,SQL语句在编译时就已经被确定下来,且每个参数的位置也被标记为一个占位符(通常是?
)。这些占位符将在之后被实际的参数值所替换,但这些参数值是作为查询的一部分发送给数据库的,而不是直接拼接到SQL字符串中。这就是PreparedStatement
防止SQL注入的核心原理。String query = "SELECT * FROM users WHERE name = ?"; PreparedStatement pstmt = connection.prepareStatement(query); pstmt.setString(1, name); // 此处的name即使是恶意输入,也只会被当作字符串处理
-
mybatis和jdbc哪个快?
- 在绝对性能方面,理论上,直接使用JDBC应该能提供略微更好的性能,因为它允许开发者进行更精细的优化,避免了任何可能的额外开销。
- 然而,实际应用中的性能差异通常非常小,而且MyBatis提供了许多功能,如动态SQL、结果映射和缓存支持,这些功能可以帮助优化性能,甚至在某些情况下可能使得使用MyBatis比直接使用JDBC更有效率。
2、Mybatis架构设计
3、和MyBatisPlus的关系
总的来说,MyBatis是一个轻量级的持久层框架,而MyBatis Plus是在MyBatis基础上进行了扩展,提供了更多的便捷功能和增强特性.
4、启动过程
public void start() throws Exception{
// 1. 加载全局配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2.获取SqlSessionFactory
// DefaultSqlSessionFactory
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
// 3.获取SqlSession对象 DefaultSqlSession Executor--> SimpleExecutor --> CachingExecutor --> 插件的逻辑植入
SqlSession sqlSession = factory.openSession();
// 4.通过sqlSession的API接口实现数据库操作
List<User> list = sqlSession.selectList("com.bobo.vip.mapper.UserMapper.selectUserById",2);
for (User user : list) {
System.out.println(user);
}
// 关闭会话
sqlSession.close();
}
解析config文件,创建sqlSessionFactory对象,生成sqlSession对象,根据配置文件中的信息加载映射文件,动态生成Mapper实现类,进行操作
SqlSessionFactory
- 作用:
SqlSessionFactory
是一个工厂类,负责创建SqlSession
对象。它是MyBatis的入口点,用于从配置文件或Java配置中构建SqlSession
。 - 关系:开发者通过
SqlSessionFactory
获取SqlSession
实例,进而进行数据库操作。
SqlSession
- 作用:
SqlSession
代表和数据库的一次会话。通过SqlSession
,可以执行命令、获取Mapper
接口的实例和管理事务。 - 关系:每次数据库操作(执行SQL语句)实际上都是通过
SqlSession
发起的。SqlSession
内部使用Executor
来执行具体的SQL命令。
Executor
- 作用:
Executor
是MyBatis的核心之一,负责SQL语句的生成和执行。它管理着SQL执行的所有细节,包括处理StatementHandler
、ParameterHandler
和ResultHandler
。 - 关系:当
SqlSession
接收到执行SQL的请求后,它将该请求委托给Executor
处理。
StatementHandler
- 作用:负责使用数据库连接(从
SqlSession
提供)准备Statement
对象,可能是PreparedStatement
或CallableStatement
,并执行SQL语句。 - 关系:
Executor
在执行SQL时,会创建并使用StatementHandler
来准备和执行SQL语句。
ParameterHandler
- 作用:用于为SQL语句设置参数。它负责将方法调用的参数与SQL命令中的占位符匹配起来。
- 关系:在
StatementHandler
准备Statement
时,ParameterHandler
被用来处理SQL语句的参数绑定。
ResultHandler
- 作用:处理
Statement
执行后返回的结果集。它将ResultSet
映射成Java对象或对象集合。 - 关系:
Executor
执行查询后,使用ResultHandler
来处理并映射查询结果。
总结关系和作用
- 从会话到执行:
SqlSessionFactory
创建SqlSession
,代表了与数据库的一次会话;SqlSession
则是执行SQL操作的直接接口,内部通过Executor
实现SQL命令的实际执行。 - 执行与处理:
Executor
负责整个SQL执行过程,它创建StatementHandler
来准备和执行SQL语句,使用ParameterHandler
来设置SQL语句的参数,最后通过ResultHandler
处理执行结果。
5、Mybatis的缓存设计
缓存和缓冲是有区别的,详情见主页腾讯面经
- 作用:MyBatis缓存的作用是提高数据库访问性能。它将查询结果缓存到内存中,当下次有相同的查询请求时,直接从缓存中取出结果,避免了再次访问数据库,从而提高了查询的响应速度。
- 一级缓存:一级缓存是SqlSession级别的缓存,它默认是开启的。当同一个SqlSession执行相同的SQL语句时,会先从缓存中查找,如果找到了对应的结果,则直接返回缓存中的结果,而不会再次访问数据库。注意:相同的sql,和mysql中的缓存很相似,但是mysql8弃用了,原因是因为带来的问题远比能解决的问题多!!
- 二级缓存:二级缓存是Mapper级别(使用同一个Mapper接口)的缓存,它默认是关闭的。当不同的SqlSession执行相同的SQL语句时,如果开启了二级缓存,则会先从缓存中查找,如果找到了对应的结果,则直接返回缓存中的结果,而不会再次访问数据库。 注意:不同的sqlSession执行相同的sql语句!!!配置文件中开启Cache-Enable:true,然后配置。
- 三级缓存:三级缓存是在分布式环境下把存储在内存中的数据持久化到第三方数据源的过程。比如Redis中。这块需要重写
Cache
接口,自定义行为替代了二级缓存,二三级缓存不存在同时出现。
6、对于拦截器的理解
- 上面那三个Handler,分别是执行sql语句的,参数设置的,结果集映射的。
- 应用:拦截器可以用于实现一些通用的功能,如日志记录、权限校验、性能监控等。它可以拦截SQL的执行、参数的设置、结果的处理等环节。要实现一个拦截器,需要实现MyBatis提供的Interceptor接口,并重写其中的方法。Interceptor接口中定义了3个方法:
-
intercept:拦截方法,用于在SQL执行前后进行一些操作。在该方法中可以通过Invocation.proceed()方法调用下一个拦截器或执行目标方法。
-
plugin:用于包装目标对象,返回一个代理对象。可以通过该方法为目标对象生成一个代理对象,以便拦截对目标对象的方法调用。
-
setProperties:用于设置拦截器的属性。可以通过该方法获取配置文件中的属性,并进行相应的初始化操作。
拦截器在MyBatis的配置文件中进行配置,可以通过<plugins>标签将拦截器添加到MyBatis的拦截链中。使用拦截器可以方便地扩展MyBatis的功能,实现一些通用的需求,并且可以灵活地控制拦截器的顺序。示例:假设我们想要创建一个简单的拦截器,用于记录SQL语句的执行时间:
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExampleInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
// 继续执行原有操作
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
System.out.println("SQL执行时间:" + (endTime - startTime) + "ms");
return result;
}
// 其他方法省略...
}
在这个示例中,@Intercepts
注解定义了该拦截器应该拦截Executor
接口的update
和query
方法。intercept
方法中的逻辑用于计算和打印SQL语句的执行时间。
<configuration>
<plugins>
<plugin interceptor="com.example.ExampleInterceptor">
<!-- 这里可以配置一些属性 -->
</plugin>
</plugins>
</configuration>
7、sqlSession的安全问题
1. 状态管理
SqlSession
持有一系列状态信息,包括打开的连接、正在执行的SQL语句、未提交或未回滚的事务等。在多线程环境中共享同一个SqlSession
,会导致这些状态信息被并发访问和修改,引起数据不一致、事务管理混乱甚至数据库连接错误等问题。
2. 一级缓存
SqlSession
内部具有一级缓存(也称为会话缓存),用于缓存一个会话期间执行的查询操作的结果。如果多个线程共享同一个SqlSession
,那么一个线程的操作可能会影响到另一个线程的查询结果,因为它们可能从缓存中读取到旧数据或预期之外的数据。
3. 事务管理
在SqlSession
中执行的所有操作都是在一个单独的数据库事务中进行的。在多线程环境下共享同一个SqlSession
会导致无法精确控制事务的边界,比如,一个线程意图提交或回滚某个事务,可能会不经意地影响到其他线程的操作。
解决方案
为了避免这些问题,应该遵循以下最佳实践:
- 线程隔离:确保每个线程都使用独立的
SqlSession
实例,这可以通过使用线程局部变量(ThreadLocal
)或在每个请求的处理中创建和销毁SqlSession
来实现。会话管理:在需要的情况下,使用Spring或其他容器框架来管理SqlSession
的生命周期,这些框架通常提供了集成的事务管理和会话管理,可以简化SqlSession
的使用。
4.spring解决方案:
在Spring中,可以通过使用 SqlSessionTemplate
来解决SqlSession数据不安全的问题。SqlSessionTemplate
是MyBatis-Spring提供的一个实现了 SqlSession
接口的类,它会自动管理SqlSession的生命周期,并保证每个线程都有自己的SqlSession实例。使用 SqlSessionTemplate
时,只需要将其配置为Spring的Bean,并注入到需要使用SqlSession的地方即可,Spring会自动为每个线程提供一个独立的SqlSession实例。
8、一个xml都会对应一个Mapper接口。这个Mapper接口的工作原理是什么?
在MyBatis中,MappedStatement
是一个非常核心的组件,它代表了一个映射语句。更具体地说,每当你在MyBatis的映射文件中定义一个<select>
、<insert>
、<update>
或<delete>
标签时,MyBatis会为每个这样的标签创建一个MappedStatement
对象。这个对象包含了执行SQL语句所需的所有信息,如SQL语句本身、输入参数的映射配置、输出结果的映射配置等。
通过JDK动态代理,单例对象拦截接口方法,转而执行MappedStatement代表的sql,所以要统统对应。Mapper接口里的方法,是不能重载的,因为是 全限名+方法名 的保存和寻找策略。
9、延迟加载
MyBatis的延迟加载(懒加载)是一种优化技术,用于提高应用程序访问数据库时的性能。在延迟加载策略下,只有当真正需要使用到关联对象的数据时,MyBatis才会执行相应的SQL语句去查询这些数据。这与立即加载(即时加载)形成对比,在立即加载策略下,一旦加载了主实体,就会立即加载其关联的所有实体,不管这些数据是否立即需要。
通过一个具体的例子来展示它是如何工作的,以及它在实际应用中的作用。假设我们有一个博客系统,其中包含两个主要实体:Post
(帖子)和Comment
(评论)。每个Post
都可以有多个Comment
,形成一对多的关系。
在没有启用延迟加载的情况下,当我们从数据库中查询一个Post
对象时,为了满足对象关系映射(ORM)的需要,我们可能会同时加载与这个Post
相关的所有Comment
对象。这种情况下,即使我们只需要查看Post
的内容,而不立即需要查看评论,相关的所有评论数据也会被加载。这可能导致不必要的性能开销,特别是当一个Post
有很多Comment
时。
配置:
<configuration>
<settings>
<!-- 启用延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 当访问非延迟加载对象的任何属性时,就加载所有延迟加载的属性 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 使用列名替代列索引 -->
<setting name="useColumnLabel" value="true"/>
<!-- 允许单个语句返回多结果集 -->
<setting name="multipleResultSetsEnabled" value="true"/>
</settings>
</configuration>
<mapper namespace="com.example.mapper.PostMapper">
<!-- Post的基本信息查询 -->
<resultMap id="postResultMap" type="Post">
<id property="id" column="id"/>
<result property="title" column="title"/>
<!-- 延迟加载Post的Comments -->
<collection property="comments"
column="id"
select="com.example.mapper.CommentMapper.selectCommentsByPostId"
fetchType="lazy"/>
</resultMap>
<!-- 根据ID查询Post,使用postResultMap来处理结果 -->
<select id="selectPostById" resultMap="postResultMap">
SELECT * FROM post WHERE id = #{id}
</select>
</mapper>
<mapper namespace="com.example.mapper.CommentMapper">
<select id="selectCommentsByPostId" resultType="Comment">
SELECT * FROM comment WHERE post_id = #{postId}
</select>
</mapper>
原理:
1. 创建代理对象
当加载一个对象(如Post
对象)时,如果配置了该对象的某些属性(如comments
集合)为延迟加载,MyBatis会为这个对象创建一个代理(Proxy)对象。这个代理对象控制对Post
对象的访问。
2. 拦截属性访问
当程序尝试访问Post
对象的comments
属性时,访问操作会被代理对象拦截。
3. 执行延迟加载
拦截访问时,代理对象会检查对应的属性是否已加载。如果尚未加载,代理对象会执行配置好的SQL查询(如通过调用CommentMapper.selectCommentsByPostId
方法)来加载数据。然后,它会将加载的数据设置到原始对象的对应属性中。
4. 返回结果
之后,当再次访问该属性时,因为数据已经被加载,代理对象就直接返回属性值,不再执行SQL查询。
代理技术
MyBatis使用了Java的动态代理技术来实现延迟加载。动态代理允许MyBatis在运行时创建对象的代理,并拦截对代理对象方法的调用。对于集合属性(如List<Comment>
),MyBatis可能会使用CGLIB或Javassist等字节码操作库来创建目标对象的子类,并在子类中加入延迟加载的逻辑。
事务考虑
需要注意的是,延迟加载发生时,必须保证还在原始查询所在的事务范围内(如果有事务控制)。如果在访问延迟加载属性时,原始事务已经关闭,可能会遇到懒加载异常,因为无法再执行SQL查询来加载数据。
性能优化
延迟加载是一个重要的性能优化手段。通过仅在需要时加载关联数据,它可以避免加载大量不必要的数据,从而减少内存消耗和数据库查询开销。然而,不当的使用(如在循环中不断触发延迟加载)可能会导致性能问题,因此应当谨慎使用。
10、如何记录JDBC相关日志?
MyBatis 定义了一个简单的日志接口org.apache.ibatis.logging.Log
,这个接口包含了不同级别的日志方法,如debug
、warn
、error
等。每个支持的日志框架都会有一个对应的实现这个接口的适配器。
11、MyBatis的数据源模块是怎么设计的?
提供了两种方式:
-
非连接池的方式
-
连接池的方式
12、MyBatis中是如何处理事务的?
-
JDBC处理
-
Managed:交给当前的WEB容器处理
13、大量数据插入
从文件中读取:从哪来的?
-
估算文件大小:根据每条记录的字段信息。最直观的把这1000w写入文件中查看大小
-
如何批量插入:文件比较大的情况下。需要分批次的处理 insert into()values(),,,,,,
-
数据完整性:1000W的数据来源我们需要保存每条数据都是完整的
-
数据库是否支持批次数据:mysql(max_allowed_packet)
-
中途出错的问题:如果中途出现错误,比如数据刚好插入到900w的时候,数据库连接失败,这种情况不可能重新来插一遍,所有需要记录每次插入数据的位置,并且需要保证和批次插入的数据在同一个事务中,这样恢复之后可以从记录的位置开始继续插入
14、异常处理
-
异常捕获:在代码的关键位置,使用try-catch语句捕获可能发生的异常。
-
异常分类:根据异常的类型进行分类,例如业务异常、系统异常等。
-
统一封装:针对不同类型的异常,我们会封装统一的异常类,例如BusinessException、SystemException等,这些异常类继承自通用的Exception类。
-
异常处理器:在项目中定义一个全局的异常处理器,用于捕获和处理所有未被try-catch语句捕获的异常。
-
异常处理逻辑:在异常处理器中,我们会根据异常的类型进行相应的处理逻辑,例如记录异常日志、返回友好的错误信息等。
-
统一返回结果:无论是业务异常还是系统异常,我们都会将异常信息封装成统一的返回结果对象,用于给前端或其他调用方返回异常信息。
-
异常信息国际化:为了支持多语言环境,我们会将异常信息进行国际化处理,根据请求的语言环境返回相应的异常信息。
通过以上的方式,我们能够实现异常的统一处理,提高代码的可维护性和可读性,并且能够更好地进行日志记录和错误信息返回。