Mybatis中SQL的执行过程详解
作者:骑个小蜗牛
Mybatis 框架
SQL执行过程
数据库操作映射方式
MyBatis支持两种方式进行数据库操作映射:
- 映射文件:通过XML文件来定义SQL语句和映射关系
- 注解方式:通过在Java代码中使用注解来定义SQL语句和映射关系
这两种方式都可以实现数据库操作的映射,具体使用哪种方式取决于个人的喜好和项目需求。本文都以映射文件的方式进行分析。
SQL的执行过程
SQL解析
MyBatis 会将SQL 语句解析为内部的数据结构。这一过程中,MyBatis 会根据 SQL 中的参数类型、数量等信息,生成一个 MappedStatement 对象,表示当前的 SQL 语句及其相关的执行元数据。
参数绑定
在 SQL 解析完成后,MyBatis 会在 SQL 参数映射阶段,将 SQL 语句中的参数与 Java 对象进行映射,并将参数值处理成符合数据库预编译要求的形式。这样,在 SQL 预编译和执行阶段,就可以正确地传递参数并执行 SQL 语句。
SQL预编译
MyBatis 利用 JDBC 的 PreparedStatement 接口进行 SQL 语句的预编译。预编译过程会将 SQL 中的占位符(?)替换为实际参数,并将其转化为数据库可以优化执行的形式,从而提高 SQL 执行效率。
执SQL行
完成预编译后,MyBatis 会通过 JDBC API 将 SQL 语句提交给数据库执行,并获取查询结果或执行结果。数据库执行完成后,结果会返回给 MyBatis 进行进一步处理。
结果映射
MyBatis 会将数据库返回的结果集转换为相应的 Java 对象。这一过程包括类型转换、字段匹配等,通常通过 TypeHandler 来完成。TypeHandler 负责将数据库中的列值与 Java 对象的属性进行映射。
事务处理
如果启用了手动事务管理,MyBatis 会在 SQL 执行完成后,根据配置的事务管理策略,提交或回滚事务,确保数据一致性和事务的完整性。
缓存处理
若启用了缓存,MyBatis 会将查询结果存储到缓存中,以便下次执行相同的 SQL 语句时能够直接从缓存中获取结果,从而减少数据库的访问频率,提高性能。MyBatis 支持一级缓存(SqlSession 范围内)和二级缓存(跨 SqlSession 的共享缓存)。
日志记录与监控
MyBatis 会通过集成日志框架(如 Log4j、SLF4J)记录 SQL 执行的详细日志。这些日志信息可以帮助开发人员分析 SQL 执行的性能,发现潜在的问题,优化查询效率。
最后,将查询结果返回给调用方,自此,完成整个 SQL 执行过程。
下面详细介绍每一个过程:
SQL解析
当 MyBatis 执行一个查询时,首先会对传入的 SQL 语句进行解析,解析 SQL 语句的结构和参数信息,为后续的参数绑定和执行做准备。
- 解析过程会进行语法分析和语义分析,以确保SQL语句的正确性
- 分析 SQL 语句中的参数占位符并提取出需要传入的参数类型和数量。
- 将 SQL 语句转化为 MyBatis 内部可识别的数据结构
MyBatis 使用 XML 或注解中的 SQL 语句,结合映射文件中的 MappedStatement 对象来表示 SQL 信息。
MappedStatement 是 MyBatis 核心的配置对象,包含了 SQL 的元数据、类型处理器、缓存策略等。
解析过程涉及到标签的解析、参数的解析和动态SQL的处理
标签解析:
- 首先会解析映射文件的根节点(<select>、<insert>、<update>、<delete>等),然后逐个解析其中的标签。
- 解析标签时,MyBatis会根据标签的不同类型(如查询、插入、更新、删除)执行相应的解析逻辑。
动态SQL处理:
- MyBatis支持动态SQL,即根据不同的条件在运行时动态生成SQL语句。\
- 动态SQL使用一系列XML标签(如<if>、<choose>、<when>、<otherwise>等)进行条件判断和 SQL片段的组装。
- 在解析映射文件时,MyBatis会对动态SQL片段进行解析,并根据条件判断生成最终的SQL语句。
- 动态SQL的处理涉及到条件判断、循环、字符串拼接等操作,以根据运行时的条件生成最终的SQL语句。
参考:
<select id="findUserById" resultType="User"> SELECT * FROM users WHERE id = #{id} </select>
- 在上述例子中,findUserById 是一个 MappedStatement 对象,#{id} 是一个占位符。
SQL参数映射
MyBatis 在 SQL 参数映射阶段,会将用户提供的参数绑定到 SQL 语句中的占位符。根据映射文件或注解中的 SQL 语句,MyBatis 会分析哪些占位符需要绑定哪些参数。这样,在 SQL 预编译和执行阶段,就可以正确地传递参数并执行 SQL 语句。
在 MappedStatement 中,有一个 BoundSql 对象,它包含了实际的 SQL 语句以及绑定的参数。BoundSql 会根据 SQL 的占位符(如 ? 或 #{})将传入的参数与 SQL 语句进行绑定。
- MyBatis 首先会解析 SQL 语句,识别其中的占位符(#{})或者字符串替换(${})形式的参数。
- 对于 ${} 形式的参数,MyBatis 会直接将参数值替换到 SQL 语句中(需要谨防SQL注入问题)。
参考:
Map<String, Object> params = new HashMap<>(); params.put("id", 1); userMapper.findUserById(params);
- params 中的 id 将会绑定到 SQL 语句中的 #{id} 占位符。
SQL预编译
MyBatis使用JDBC的PreparedStatement接口创建预编译的SQL语句,预编译的SQL语句中使用占位符(如?)代替参数。通过 SQL 预编译提升执行效率,并确保参数的安全性。
- 预编译有助于提高执行效率,因为 SQL 语句只需要解析一次,数据库会优化执行计划,避免每次都解析 SQL。
- PreparedStatement 允许 MyBatis 在数据库执行时才将参数值填充到预编译的 SQL 中,从而提高执行效率并避免 SQL 注入攻击。
参考:
PreparedStatement ps = connection.prepareStatement("SELECT * FROM users WHERE id = ?"); ps.setInt(1, 1);
- 将第一个参数(id)设置为 1
SQL执行
MyBatis 会通过 JDBC API 将编译后的 SQL 语句提交到数据库执行。执行过程会获取执行结果,例如查询结果或更新、删除操作的影响行数。
- 执行时,MyBatis 会通过 Statement 或 PreparedStatement 接口执行 SQL。查询操作会返回一个 ResultSet,更新、删除等操作会返回受影响的行数。
- 如果执行的是查询操作,MyBatis 会进一步处理返回的 ResultSet,并将其转换为 Java 对象。
参考:
ResultSet rs = ps.executeQuery();
结果映射
执行 SQL 后,MyBatis 会将数据库返回的 ResultSet 映射为相应的 Java 对象。这个映射过程会根据 SQL 查询的字段与 Java 对象的属性进行转换。
- MyBatis 使用 ResultMap 来定义如何将查询结果映射到 Java 对象。ResultMap 可以指定每个字段与对象属性之间的映射关系。
- 例如,使用 TypeHandler 进行类型转换(比如将数据库中的 VARCHAR 类型转换为 String 类型)。
- 对于复杂的结果映射,MyBatis 允许嵌套的映射以及集合类型的处理。
参考:
<resultMap id="userResultMap" type="User"> <result property="id" column="id"/> <result property="name" column="name"/> </resultMap>
事务处理
MyBatis 支持手动和自动事务管理。默认情况下,MyBatis 使用 JDBC 提供的事务控制。在执行数据库操作后,MyBatis 会根据配置决定是否提交或回滚事务。
- 如果使用自动提交(autocommit=true),事务会在每个操作后立即提交。
- 如果使用手动事务管理,开发者需要显式地调用 commit() 或 rollback() 来提交或回滚事务。
- 事务处理保证了操作的一致性,即在一个事务中的多个操作要么全部成功,要么全部失败。
参考:
sqlSession.commit(); // 提交事务 sqlSession.rollback(); // 回滚事务
缓存处理
MyBatis 提供了一级缓存和二级缓存机制来提高查询效率。一级缓存是 SqlSession 级别的缓存,而二级缓存是跨 SqlSession 的共享缓存。
- 一级缓存:在同一个 SqlSession 中,查询的结果会被缓存。只要没有关闭 SqlSession,再次查询相同的 SQL 会直接从缓存中获取结果,而不是重新查询数据库。
- 二级缓存:不同 SqlSession 之间可以共享缓存。二级缓存的使用需要在 MyBatis 配置文件中显式启用,并且每个映射器可以配置自己的缓存策略。
- 缓存机制有助于提高性能,尤其是在查询频繁但数据变化较少的场景中。
日志记录与监控
MyBatis 可以集成各种日志框架(如 Log4j、SLF4J 等)来记录 SQL 执行过程中的信息。开发人员可以通过日志输出 SQL 执行的详细信息,包括查询语句、执行时间等。
- MyBatis 的日志记录对于开发人员调试 SQL 和优化性能非常有帮助。
- 日志框架可以配置为不同的日志级别,例如 DEBUG、INFO、ERROR 等,方便查看不同详细度的信息。
参考:
<settings> <setting name="logImpl" value="SLF4J"/> </settings>
扩展
#与$的区别
$ 符号
- $符号占位符是简单的字符串替换,不进行预编译和参数类型处理,也不会进行转义。
- $符号占位符直接将参数的值替换到SQL语句中,可以用于动态拼接SQL语句的部分内容。
- $符号占位符存在SQL注入的风险,因为参数值直接替换到SQL语句中,可能导致恶意注入攻击。
- 最终SQL的动态参数值一定不会有引号包裹
# 符号
- #符号占位符是预编译的占位符,会对参数进行类型处理和安全处理。
- #符号占位符将参数值作为预编译参数传递给数据库,可以防止SQL注入攻击。
- #符号占位符可以用于动态生成SQL语句的条件部分,例如WHERE子句、ORDER BY子句等。
- 最终SQL的动态参数值可能会有引号包裹(不是一定都会有引号包裹)
- 字符串类型:会自动将其包裹在单引号中。
- 数字类型:不会自动加单引号。
- 日期类型:会格式化日期并加上单引号。
总结示例
示例:
username的值为haha(String)
1. 使用$占位符
SELECT id, username, email FROM users WHERE username = ${username} # 最终实际执行的sql SELECT id, username, email FROM users WHERE username = haha
2. 使用#占位符
SELECT id, username, email FROM users WHERE username = #{username} # 最终实际执行的sql SELECT id, username, email FROM users WHERE username = 'haha'
Mybatis SQL分类
根据 SQL 查询的特性来区分的,可以将SQL分为动态 SQL 和静态 SQL。
动态 SQL
使用动态SQL,可以根据不同的条件生成不同的SQL语句,从而实现灵活的查询和更新操作。
动态SQL可以使用if、choose、when、otherwise等标签来实现条件判断和循环操作,同时还可以使用foreach标签来实现对集合类型参数的遍历操作。
这样可以避免在代码中使用大量的字符串拼接,提高代码的可读性和维护性。
动态 SQL 的特点:
- 可以根据不同的条件生成不同的 SQL 查询。
- 可以在查询语句中动态地添加、删除或修改部分 SQL 逻辑。
- 提供了更大的灵活性和动态性,可以根据运行时的条件来动态生成查询语句。
常见的动态 SQL 的示例:
- 使用条件语句(如 IF、CASE)来动态选择不同的查询分支。
- 使用循环语句(如 FOR、WHILE)来生成重复或动态数量的查询条件。
- 使用动态连接条件来构建动态查询条件。
- 使用动态排序来指定不同的排序方式。
静态 SQL
静态 SQL 的特点:
- 查询语句的结构和逻辑是固定的,在执行查询时不会发生变化。
- SQL 查询不受外部参数的影响,参数值在查询中直接拼接。
常见的静态 SQL 的示例:
- 简单的选择查询语句,不需要根据条件改变查询逻辑。
- 固定的更新语句,不受外部参数影响。
- 预定义的查询模板,参数值直接拼接在查询语句中。
- 创建表格、索引等静态结构定义。
静态SQL和动态SQL选择
由于静态sql是在应用启动的时候就解析,而动态sql是在执行该sql相关操作的时候才根据传入的参数进行解析的,所以静态sql效率会比动态sql好。
${}、#{}与SQL是否静态SQL、动态SQL无直接关系
- ${} 和 #{} 只是占位符,用来插入参数,它们本身并不决定 SQL 是静态还是动态。
- 动态 SQL 是通过 条件判断(如 、)来决定 SQL 的结构是否变化。
- 即使 SQL 中使用了 ${} 或 #{},如果没有动态条件,它仍然是静态 SQL。
示例1:
使用了${} 、#{},不是动态SQL
<select id="getUserById" resultType="User"> SELECT id, username, email FROM users WHERE id = #{id} </select>
- 这里的 #{id} 用来传递参数,但 SQL 本身 是固定的,没有根据任何条件变化。
- 这个查询并不是动态 SQL,因为它没有任何动态部分(比如没有使用 标签)。
示例2:
使用 ${} 来动态拼接表名,不是动态SQL
<select id="getUsersByTableName" resultType="User"> SELECT id, username, email FROM ${tableName} WHERE id = #{id} </select>
- 这里的 ${tableName} 是动态的,会根据传入的 tableName 参数拼接成不同的表名。
- 但 SQL 结构本身依然是 固定的,只是表名不同。
示例3:
使用了${} 、#{},是动态SQL
<select id="selectUsers" resultType="User"> SELECT id, username, email FROM users <where> <if test="username != null">AND username = #{username}</if> <if test="email != null">AND email = #{email}</if> </where> </select>
- 使用了 标签来动态地添加 SQL 条件,因此整个 SQL 是动态的。
- 如果 username 和 email 参数存在,SQL 语句就会加入对应的 AND 条件;如果不存在,这些条件会被忽略。
- 这个查询才是真正的动态 SQL,因为它的结构根据输入的参数变化。但 #{} 只是用来传递参数,它和是否动态没有直接关系
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。