MyBatis 是什么
MyBatis 是一个优秀的持久层框架,它对JDBC的操作数据库的过程进行封装,让开发者只需要关注 SQL 本身,而不需要花费精力去处理例如注册驱动、创建connection、创建statement、手动设置参数、结果集检索等JDBC繁琐的过程代码。 MyBatis 的主要特点和优势包括:
- 支持定制化SQL、存储过程和高级映射:MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的工作。它可以使用简单的 XML 或注解用于配置和原始映射,将接口和 Java 的 POJOs(Plain Old Java Objects)映射成数据库中的记录。
- 灵活的映射规则:它提供了多种映射标签,支持复杂类型的映射和关联查询,对于一对一、一对多、多对多等关系都可以轻松应对。
- 松耦合:MyBatis 不会像一些完全的ORM框架那样强迫你使用某种编程模型,它的SQL写在XML里,便于统一管理和优化,对于开发者而言,数据库表的变化只需要修改XML文件即可。
- 动态SQL:MyBatis 提供了强大的动态SQL功能,这对于拼接复杂查询语句非常方便,可以根据不同的条件动态生成SQL语句。
- 易于上手和集成:MyBatis 易于学习和上手,它可以很容易地与Spring等框架集成,同时也支持代码生成器来生成重复的CRUD代码。
- 插件的支持:MyBatis 允许你通过插件来拦截的方法调用的过程,比如Executor、ParameterHandler、ResultSetHandler和StatementHandler等,这为开发者提供了极大的灵活性。
MyBatis 的工作流程大致如下:
- 读取配置文件:MyBatis 使用 XML 或注解来配置和映射原生信息,将 SQL 语句配置在 XML 文件中,并通过 Java 对象和 SQL 的映射关系来封装数据库记录。
- 创建SqlSessionFactory:通过配置文件的信息,MyBatis 创建 SqlSessionFactory,这是 MyBatis 的核心对象,用于创建 SqlSession 对象。
- SqlSession:SqlSession 对象包含了面向数据库执行 SQL 命令所需的所有方法。可以通过 SqlSession 实例来直接运行映射的 SQL 语句,并返回数据。
- 数据操作:通过 SqlSession 的实例,执行定义好的 SQL 语句,进行数据的增加、删除、修改和查询操作。
- 提交事务和关闭会话:操作完成后,需要提交事务,并关闭 SqlSession。 MyBatis 的使用,使得数据库的操作变得更加简单和灵活,大大提高了开发效率。在中国,MyBatis 也是非常流行的Java持久层框架之一,被广泛应用于各种企业级开发中。
#{}和${}的区别
在 MyBatis 中,#{}
和 ${}
都是用于动态 SQL 语句的占位符,但它们的用途和解析方式有所不同。
#{}
:这是 MyBatis 的参数占位符,用于替换预编译语句(PreparedStatements)中的参数。#{}
的参数会被预处理并放入到 SQL 语句中,这样可以防止 SQL 注入攻击。当 MyBatis 执行 SQL 语句时,它会将#{}
内的参数替换为?
,然后使用预编译语句的参数设置功能来安全地设置 SQL 参数。例如:
<select id="selectUsers" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
在这个例子中,#{username}
会被替换为 SQL 参数,并且 MyBatis 会使用预编译语句来设置这个参数,从而避免了 SQL 注入的风险。 2. ${}
:这是 MyBatis 的字符串替换符,用于替换 SQL 语句中的静态文本。${}
的内容不会被预处理,而是直接替换到 SQL 语句中。因此,使用 ${}
时需要特别小心,因为它可能会导致 SQL 注入攻击。通常,${}
用于替换表名、列名或其他 SQL 语句的结构部分,而不是用户输入的数据。例如:
<select id="selectUsersByYear" resultType="User">
SELECT * FROM ${tableName} WHERE year = #{year}
</select>
在这个例子中,${tableName}
会被直接替换到 SQL 语句中,而 #{year}
则会被作为预编译语句的参数来处理。
总结一下,#{}
用于参数替换,可以防止 SQL 注入,而 ${}
用于字符串替换,不会防止 SQL 注入,因此在实际使用中应该尽量使用 #{}
,只在必要时使用 ${}
,并且确保 ${}
中不包含用户输入的数据。
Dao 接口的工作原理,接口里的方法方法能重载吗?
在 MyBatis 中,DAO 接口也被称为 Mapper 接口。这些接口不需要你实现具体的业务逻辑,因为 MyBatis 会使用动态代理技术来自动实现这些接口。
当你调用 Mapper 接口中的一个方法时,MyBatis 会根据接口的全限定名和方法名,以及方法的参数和返回类型,去找到对应的 SQL 语句并执行。
工作原理如下:
- 动态代理:MyBatis 利用了 Java 的动态代理机制,当应用程序调用 Mapper 接口中的方法时,MyBatis 会动态地创建一个代理实现类,这个代理类会拦截接口方法的调用。
- SQL 映射:代理类根据方法签名(方法名和参数类型)来查找对应的 SQL 语句。这个查找过程是通过 MyBatis 的配置信息来完成的,配置信息通常定义在 XML 映射文件中或者使用注解直接在接口方法上。
- 参数处理和 SQL 执行:找到 SQL 语句后,MyBatis 会处理方法参数(如果有的话),将其绑定到 SQL 语句中的占位符(
#{}
)。然后,MyBatis 会创建 SQL 会话(SqlSession),并通过会话执行 SQL 语句。 - 结果映射:执行 SQL 语句后,MyBatis 会处理结果集,将其映射到方法的返回类型上。如果返回类型是集合或者自定义对象,MyBatis 会根据配置信息进行对象关系映射(ORM)。
关于方法重载,MyBatis 的 Mapper 接口不支持传统意义上的方法重载。因为 MyBatis 通过接口的全限定名和方法名来确定唯一的 SQL 语句,如果接口中有多个方法名相同但参数不同的方法,MyBatis 将无法确定哪个方法对应哪个 SQL 语句,这会导致编译错误。因此,在 MyBatis 的 Mapper 接口中,你应该避免方法重载,每个方法应该有唯一的签名。
Mybatis 分页插件
MyBatis 本身不直接支持分页功能,但是它提供了一种插件的机制,可以通过分页插件来实现分页功能。分页插件的原理主要是通过拦截 SQL 语句执行过程,在 SQL 执行前后添加特定的分页语句来实现分页效果。 以下是 MyBatis 分页的一些常见方法和分页插件的原理:
使用 LIMIT 语句分页:
对于支持
LIMIT
语句的数据库(如 MySQL),可以直接在 SQL 语句中使用LIMIT
子句来实现分页。例如:sqlSELECT * FROM users LIMIT #{start}, #{pageSize}
其中
#{start}
是分页的起始位置,#{pageSize}
是每页显示的记录数。分页插件:
MyBatis 分页插件(如 PageHelper)利用了 MyBatis 提供的插件接口(Interceptor),在 SQL 执行之前拦截 SQL 语句,动态地修改 SQL 语句,添加分页语句。例如,对于 MySQL,插件可能会在 SQL 语句后添加
LIMIT
子句。分页插件的原理大致如下:
- 拦截器:分页插件通过实现 MyBatis 的
Interceptor
接口来创建一个拦截器,这个拦截器会拦截特定的方法调用,比如Executor.query()
方法。 - 修改 SQL:在拦截方法中,插件会分析传入的 SQL 语句,然后在其后添加分页语句。对于不同的数据库,分页语句可能不同,例如 MySQL 使用
LIMIT
,Oracle 使用ROWNUM
等。 - 执行分页 SQL:修改后的 SQL 语句会被传递给数据库执行,返回分页后的结果集。
- 总记录数查询:分页插件通常还会提供一个查询总记录数的方法,以便前端可以计算出总页数和当前页码。
- 拦截器:分页插件通过实现 MyBatis 的
分页参数:
分页插件通常会提供一个简单的 API 来设置分页参数,如设置每页显示的记录数和当前页码。在查询执行前,调用这些 API 来设置参数,插件会根据这些参数来构建分页 SQL。
分页结果处理:
分页插件还会对查询结果进行处理,通常会将结果封装成一个包含分页信息的对象,如
Page<T>
,其中T
是查询结果的类型。这个对象会包含当前页的数据列表、总记录数、总页数等信息。 使用分页插件可以简化分页逻辑的实现,使得开发者不需要手动编写分页 SQL,同时也能够提供统一的分页接口,方便在不同数据库之间进行切换。
动态 sql
MyBatis 的动态 SQL 是其强大功能之一,它允许我们在 XML 映射文件中编写可适应不同条件的 SQL 语句。动态 SQL 可以根据不同的条件动态地构建 SQL 语句,从而提高了 SQL 的灵活性和重用性。MyBatis 提供了多种元素来支持动态 SQL,包括 <if>
、<choose>
、<when>
、<otherwise>
、<foreach>
等。
以下是一些常用的动态 SQL 元素及其用法:
<if>
:<if>
元素用于条件判断,它可以根据 test 属性中的表达式结果来决定是否包含其中的 SQL 片段。xml<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 语句,用于实现多条件选择。
<choose>
元素包含多个<when>
元素和一个可选的<otherwise>
元素。<when>
元素的 test 属性用于条件判断,<otherwise>
元素用于定义默认情况。查看代码
xml<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>
<foreach>
:<foreach>
元素用于遍历集合,生成一系列的值。它通常用于构建 IN 条件语句或者在插入多条记录时生成值列表。xml<select id="selectPostIn" resultType="domain.blog.Post"> SELECT * FROM POST P WHERE ID in <foreach item="item" index="index" collection="list" open="(" separator="," close=")"> #{item} </foreach> </select>
<bind>
:<bind>
元素用于在 SQL 语句中创建一个变量,并将其绑定到上下文中,以便在后续的 SQL 中使用。xml<select id="selectBlogsLike" resultType="Blog"> <bind name="pattern" value="'%' + _parameter.getTitle() + '%'"/> SELECT * FROM BLOG WHERE title LIKE #{pattern} </select>
MyBatis缓存
MyBatis 提供了两种缓存机制:一级缓存和二级缓存,用于提高查询性能,减少数据库的访问次数。
一级缓存(Session 缓存)
一级缓存是默认开启的,它基于 SqlSession 的生命周期。在同一个 SqlSession 中,执行相同的查询 SQL,第一次会去数据库查询,得到结果后,MyBatis 会将查询结果存储在一级缓存中。后续的相同查询会直接从一级缓存中获取结果,而不会再次访问数据库。一级缓存是线程不安全的。
一级缓存的工作流程如下:
- 查询执行前,MyBatis 会检查一级缓存中是否已有相同查询的结果。
- 如果缓存命中,则直接返回缓存中的结果。
- 如果缓存未命中,则执行 SQL 查询,并将结果存储在一级缓存中。
一级缓存可以在以下情况下失效:
- SqlSession commit 或 rollback 时。
- 执行了增删改操作(DML)后,一级缓存会被清空,以避免脏读。
- 手动清空缓存,例如调用
SqlSession.clearCache()
方法。
二级缓存(Global 缓存)
二级缓存是跨多个 SqlSession 的缓存,它是基于 namespace 的。二级缓存默认是关闭的,需要手动开启。在同一个 namespace 下,不同的 SqlSession 对同一个查询执行多次时,第二次及以后的查询会从二级缓存中获取数据,从而减少数据库的访问次数。
二级缓存的工作流程如下:
- 查询执行前,MyBatis 会检查当前 namespace 的二级缓存中是否已有相同查询的结果。
- 如果缓存命中,则直接返回缓存中的结果。
- 如果缓存未命中,则检查一级缓存。
- 如果一级缓存也未命中,则执行 SQL 查询,并将结果存储在一级缓存和二级缓存中。
二级缓存可以在以下情况下失效:
- 当执行增删改操作(DML)时,对应的二级缓存会被清空。
- 可以通过配置设置缓存的过期时间,缓存对象超过过期时间会被清除。
- 手动清空缓存,例如调用
SqlSession.clearCache()
方法。
注意事项
- MyBatis 的缓存默认是不会存储空结果的,即如果查询结果为空,则不会放入缓存。
- 使用缓存时需要注意数据的一致性问题,特别是在并发环境下。
- MyBatis 提供了事务性缓存的概念,确保了在同一事务中的读操作都得到相同的数据视图。
为啥mybatis的mapper只有接口没有实现类,但它却能工作?
MyBatis 的 Mapper 接口不需要实现类的原因在于 MyBatis 内部使用动态代理技术来创建 Mapper 接口的实现。动态代理是一种 Java 语言提供的功能,允许你为某个接口创建一个代理对象,这个代理对象在运行时会自动实现该接口。MyBatis 通过这种方式,可以自动生成 Mapper 接口的实现,无需开发者手动编写。
MyBatis 的 Mapper 接口工作原理
当调用 Mapper 接口中的方法时,实际上是调用了 MapperProxy
的 invoke
方法。这个方法利用 Java 的反射机制来处理方法的调用。在 invoke
方法内部,MyBatis 会根据方法名和参数类型来查找对应的 SQL 语句,然后执行 SQL 语句并返回结果。
这个过程大致可以分为以下几个步骤:
- 启动和配置解析:MyBatis 启动时,会读取配置文件(如 mybatis-config.xml)和 Mapper 映射文件(如 UserMapper.xml),解析这些文件并构建出 Configuration 对象,这个对象包含了所有的配置信息,如数据源、事务、映射器等。
- 代理对象的创建:当 Spring 容器启动时,通过
@MapperScan
注解指定的路径,Spring 会为每个 Mapper 接口创建一个MapperFactoryBean
。这个工厂 Bean 实现了 Spring 的FactoryBean
接口,其getObject()
方法返回一个代理对象,这个代理对象是MapperProxy
的实例。 - 方法调用和代理处理:当调用 Mapper 接口的方法时,实际上是调用了
MapperProxy
的invoke
方法。在这个方法中,MyBatis 会查找对应的MappedStatement
对象,这个对象包含了 SQL 语句和执行 SQL 所需的其他信息。 - SQL 执行和结果处理:找到
MappedStatement
后,MyBatis 会创建一个SqlSession
来执行 SQL 语句。执行完成后,MyBatis 会处理返回的结果集,将其映射到 POJO 对象中,然后返回给调用者。
在动态代理中,有实现类时,我们通常通过实现类的 getInterfaces()
方法获取所有实现的接口;无实现类时,我们可以直接使用接口的 Class 对象数组来创建代理。
有实现类时,invoke
方法通过反射可以调用实现类的方法;无实现类时,invoke
方法直接执行所需的逻辑,如 MyBatis 中的 SQL 执行。