MyBatis 核心原理:SQL Mapper 到对象映射的实现
各位好,我是你们的老朋友,一个在代码堆里摸爬滚打多年的老码农。今天咱们聊聊 MyBatis,这个在 Java 世界里鼎鼎大名的持久层框架。 MyBatis 的核心价值,就是帮我们把那些枯燥乏味的 SQL 语句,优雅地映射成 Java 对象,让程序员们从繁琐的 JDBC 代码中解放出来,有更多时间去喝咖啡,哦不,是思考人生。
1. MyBatis 到底是啥? 为啥这么火?
简单来说,MyBatis 是一个半自动化的 ORM (Object-Relational Mapping) 框架。 啥叫半自动化? 意思就是它不像 Hibernate 那样,可以帮你自动生成 SQL,而是需要你手动编写 SQL,但 MyBatis 会帮你把 SQL 的执行结果,自动映射成 Java 对象。
为啥 MyBatis 这么火?原因很简单:
- 灵活: 让你完全掌控 SQL,可以根据业务需求进行优化,避免了 ORM 框架生成的 SQL 效率低下的问题。
- 简单易学: 相比 Hibernate 复杂的配置,MyBatis 的配置简单明了,上手很快。
- 轻量级: MyBatis 的核心 jar 包很小,不会给你的项目带来额外的负担。
- 性能好: 由于可以手动优化 SQL,MyBatis 的性能通常比其他 ORM 框架要好。
2. SQL Mapper: MyBatis 的灵魂
SQL Mapper 是 MyBatis 的核心概念,它定义了 SQL 语句与 Java 方法之间的映射关系。 我们通过 XML 文件或者注解来定义 SQL Mapper,告诉 MyBatis 哪个 SQL 语句对应哪个 Java 方法,以及如何将 SQL 的执行结果映射成 Java 对象。
2.1 XML Mapper 文件
这是最常用的方式,通过 XML 文件来定义 SQL Mapper。 一个典型的 XML Mapper 文件长这样:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.dao.UserDao">
<select id="getUserById" parameterType="java.lang.Long" resultType="com.example.model.User">
SELECT id, username, email FROM users WHERE id = #{id}
</select>
<insert id="insertUser" parameterType="com.example.model.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO users (username, email) VALUES (#{username}, #{email})
</insert>
<update id="updateUser" parameterType="com.example.model.User">
UPDATE users SET username = #{username}, email = #{email} WHERE id = #{id}
</update>
<delete id="deleteUserById" parameterType="java.lang.Long">
DELETE FROM users WHERE id = #{id}
</delete>
</mapper>
<mapper namespace="com.example.dao.UserDao">
: 指定了该 Mapper 文件对应的 Java 接口,也就是com.example.dao.UserDao
。<select id="getUserById"...>
: 定义了一个查询语句,id
属性指定了该语句的唯一标识,parameterType
指定了输入参数的类型,resultType
指定了返回结果的类型。#{id}
: 这是一个占位符,MyBatis 会将它替换成实际的参数值。
2.2 注解 Mapper
除了 XML 文件,我们还可以使用注解来定义 SQL Mapper。 例如:
package com.example.dao;
import com.example.model.User;
import org.apache.ibatis.annotations.*;
public interface UserDao {
@Select("SELECT id, username, email FROM users WHERE id = #{id}")
User getUserById(@Param("id") Long id);
@Insert("INSERT INTO users (username, email) VALUES (#{username}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insertUser(User user);
@Update("UPDATE users SET username = #{username}, email = #{email} WHERE id = #{id}")
int updateUser(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteUserById(Long id);
}
@Select
,@Insert
,@Update
,@Delete
: 这些注解分别对应了 SQL 的四种操作类型。@Param("id")
: 如果方法有多个参数,需要使用@Param
注解来指定参数名,以便在 SQL 语句中使用。@Options(useGeneratedKeys = true, keyProperty = "id")
: 用于指定自增主键的生成方式。
2.3 XML vs 注解: 选哪个?
选择哪种方式,取决于你的个人喜好和项目需求。
- XML: SQL 语句与 Java 代码分离,更清晰,更易于维护,特别是对于复杂的 SQL 语句。
- 注解: 代码更简洁,更易于阅读,适合简单的 SQL 语句。
一般来说,建议对于复杂的 SQL 语句使用 XML 文件,对于简单的 SQL 语句可以使用注解。 当然,也可以混合使用,根据实际情况灵活选择。
3. 对象映射: MyBatis 的核心功能
对象映射是 MyBatis 的核心功能,它负责将 SQL 的执行结果,转换成 Java 对象。 MyBatis 提供了多种方式来实现对象映射,包括:
- 自动映射: MyBatis 会自动将 SQL 结果集中的列名,与 Java 对象的属性名进行匹配,并自动赋值。
- 手动映射: 我们可以通过
<resultMap>
元素来手动指定 SQL 结果集中的列名,与 Java 对象的属性名之间的映射关系。
3.1 自动映射
自动映射是最简单的映射方式,MyBatis 会自动将 SQL 结果集中的列名,与 Java 对象的属性名进行匹配,并自动赋值。 例如:
public class User {
private Long id;
private String username;
private String email;
// 省略 getter 和 setter 方法
}
<select id="getUserById" parameterType="java.lang.Long" resultType="com.example.model.User">
SELECT id, username, email FROM users WHERE id = #{id}
</select>
在这个例子中,SQL 结果集中的 id
, username
, email
列,会自动映射到 User
对象的 id
, username
, email
属性。
自动映射的规则:
- SQL 结果集中的列名,必须与 Java 对象的属性名一致(忽略大小写)。
- Java 对象的属性必须有对应的 getter 和 setter 方法。
- 如果 SQL 结果集中的列名,在 Java 对象中找不到对应的属性,则该列会被忽略。
3.2 手动映射: ResultMap
当自动映射无法满足需求时,我们可以使用 <resultMap>
元素来手动指定 SQL 结果集中的列名,与 Java 对象的属性名之间的映射关系。 例如:
<resultMap id="UserResultMap" type="com.example.model.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
</resultMap>
<select id="getUserById" parameterType="java.lang.Long" resultMap="UserResultMap">
SELECT id, username, email FROM users WHERE id = #{id}
</select>
<resultMap id="UserResultMap" type="com.example.model.User">
: 定义了一个名为UserResultMap
的 ResultMap,指定了映射的 Java 对象类型为com.example.model.User
。<id property="id" column="id"/>
: 指定了id
列对应User
对象的id
属性,并且该属性是主键。<result property="username" column="username"/>
: 指定了username
列对应User
对象的username
属性。
为什么要用 ResultMap?
- 列名与属性名不一致: 当 SQL 结果集中的列名,与 Java 对象的属性名不一致时,需要使用 ResultMap 来进行映射。 例如,数据库中的列名为
user_name
,而 Java 对象的属性名为username
。 - 处理复杂关系: 当需要处理一对一、一对多、多对多等复杂关系时,需要使用 ResultMap 来进行映射。
- 自定义类型转换: 当需要对 SQL 结果集中的值进行自定义类型转换时,需要使用 ResultMap 来进行映射。
3.3 ResultMap 的高级用法
ResultMap 除了可以进行简单的列名映射,还可以处理更复杂的关系,例如:
- 一对一关系: 一个用户对应一个地址。
- 一对多关系: 一个用户对应多个订单。
- 多对多关系: 一个学生可以选择多门课程,一门课程可以被多个学生选择。
一对一关系:<association>
<resultMap id="UserResultMap" type="com.example.model.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
<association property="address" column="address_id" javaType="com.example.model.Address"
select="com.example.dao.AddressDao.getAddressById"/>
</resultMap>
<association property="address" column="address_id" javaType="com.example.model.Address" select="com.example.dao.AddressDao.getAddressById"/>
: 定义了一个一对一关系,property
指定了User
对象的address
属性,column
指定了users
表中的address_id
列,javaType
指定了address
属性的类型,select
指定了用于查询Address
对象的 SQL Mapper 方法。 MyBatis 会根据address_id
的值,自动调用com.example.dao.AddressDao.getAddressById
方法,查询对应的Address
对象,并将结果赋值给User
对象的address
属性。
一对多关系:<collection>
<resultMap id="UserResultMap" type="com.example.model.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="email" column="email"/>
<collection property="orders" column="id" ofType="com.example.model.Order"
select="com.example.dao.OrderDao.getOrdersByUserId"/>
</resultMap>
<collection property="orders" column="id" ofType="com.example.model.Order" select="com.example.dao.OrderDao.getOrdersByUserId"/>
: 定义了一个一对多关系,property
指定了User
对象的orders
属性,column
指定了users
表中的id
列,ofType
指定了orders
属性的类型,select
指定了用于查询Order
对象的 SQL Mapper 方法。 MyBatis 会根据id
的值,自动调用com.example.dao.OrderDao.getOrdersByUserId
方法,查询对应的Order
对象列表,并将结果赋值给User
对象的orders
属性。
多对多关系: 结合 <collection>
和中间表
多对多关系通常需要借助中间表来实现。 例如,students
表和 courses
表之间,通过 student_course
表建立多对多关系。
<resultMap id="StudentResultMap" type="com.example.model.Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="courses" column="id" ofType="com.example.model.Course"
select="com.example.dao.CourseDao.getCoursesByStudentId"/>
</resultMap>
<select id="getStudentById" parameterType="java.lang.Long" resultMap="StudentResultMap">
SELECT id, name FROM students WHERE id = #{id}
</select>
<resultMap id="CourseResultMap" type="com.example.model.Course">
<id property="id" column="id"/>
<result property="name" column="name"/>
</resultMap>
<select id="getCoursesByStudentId" parameterType="java.lang.Long" resultMap="CourseResultMap">
SELECT c.id, c.name
FROM courses c
INNER JOIN student_course sc ON c.id = sc.course_id
WHERE sc.student_id = #{studentId}
</select>
在这个例子中,StudentResultMap
使用 <collection>
元素,通过 student_course
表,查询与该学生关联的所有课程。
4. 参数处理: MyBatis 的灵活之处
MyBatis 提供了多种方式来处理 SQL 语句中的参数,包括:
#{}
: 预编译占位符,MyBatis 会将它替换成实际的参数值,并进行类型转换,防止 SQL 注入。${}
: 字符串替换,MyBatis 会直接将它替换成实际的参数值,不会进行类型转换,容易导致 SQL 注入。
4.1 #{}
: 预编译占位符
#{}
是最常用的参数处理方式,MyBatis 会将它替换成实际的参数值,并进行类型转换,防止 SQL 注入。 例如:
<select id="getUserById" parameterType="java.lang.Long" resultType="com.example.model.User">
SELECT id, username, email FROM users WHERE id = #{id}
</select>
MyBatis 会将 #{id}
替换成实际的 id
值,并进行类型转换,例如,如果 id
是一个 Long
类型的值,MyBatis 会将它转换成字符串,并用单引号括起来,例如 '123'
。
#{}
的优点:
- 防止 SQL 注入: MyBatis 会对参数进行转义,防止恶意用户通过 SQL 注入来攻击系统。
- 类型转换: MyBatis 会自动将参数转换成数据库需要的类型。
- 性能优化: 使用预编译语句,可以提高 SQL 的执行效率。
4.2 ${}
: 字符串替换
${}
会直接将它替换成实际的参数值,不会进行类型转换,容易导致 SQL 注入。 因此,强烈建议不要使用 ${}
,除非你知道你在做什么,并且已经采取了必要的安全措施。
例如:
<select id="getUserByColumn" parameterType="java.util.Map" resultType="com.example.model.User">
SELECT id, username, email FROM users ORDER BY ${columnName} ${order}
</select>
在这个例子中,${columnName}
和 ${order}
会直接被替换成实际的参数值,例如 username
和 ASC
。 如果用户可以控制这些参数的值,就可以通过 SQL 注入来攻击系统。
什么时候可以使用 ${}
?
- 排序字段: 当需要动态指定排序字段时,可以使用
${}
。 但是,需要对排序字段进行严格的校验,防止 SQL 注入。 - 表名: 当需要动态指定表名时,可以使用
${}
。 但是,同样需要对表名进行严格的校验,防止 SQL 注入。
总之,除非你有充分的理由,否则请尽量使用 #{}
来处理 SQL 语句中的参数。
5. 动态 SQL: MyBatis 的强大之处
MyBatis 提供了强大的动态 SQL 功能,可以根据不同的条件,生成不同的 SQL 语句。 这使得我们可以编写更灵活、更通用的 SQL Mapper。
MyBatis 提供了多种动态 SQL 标签,包括:
<if>
: 条件判断。<choose>
,<when>
,<otherwise>
: 多路选择。<trim>
,<where>
,<set>
: 用于处理 SQL 语句中的多余字符。<foreach>
: 用于循环遍历集合。
5.1 <if>
: 条件判断
<if>
标签用于判断条件是否成立,如果成立,则执行标签内的 SQL 片段。 例如:
<select id="findUsers" parameterType="com.example.model.User" resultType="com.example.model.User">
SELECT id, username, email FROM users
<where>
<if test="username != null and username != ''">
AND username LIKE #{username}
</if>
<if test="email != null and email != ''">
AND email LIKE #{email}
</if>
</where>
</select>
在这个例子中,如果 username
不为空,则会添加 AND username LIKE #{username}
条件;如果 email
不为空,则会添加 AND email LIKE #{email}
条件。
5.2 <choose>
, <when>
, <otherwise>
: 多路选择
<choose>
, <when>
, <otherwise>
标签用于实现多路选择,类似于 Java 中的 switch
语句。 例如:
<select id="findUsers" parameterType="com.example.model.User" resultType="com.example.model.User">
SELECT id, username, email FROM users
<where>
<choose>
<when test="username != null and username != ''">
AND username LIKE #{username}
</when>
<when test="email != null and email != ''">
AND email LIKE #{email}
</when>
<otherwise>
AND 1 = 1
</otherwise>
</choose>
</where>
</select>
在这个例子中,如果 username
不为空,则会添加 AND username LIKE #{username}
条件;如果 email
不为空,则会添加 AND email LIKE #{email}
条件;否则,会添加 AND 1 = 1
条件。
5.3 <trim>
, <where>
, <set>
: 处理多余字符
<trim>
, <where>
, <set>
标签用于处理 SQL 语句中的多余字符,例如 AND
, OR
, ,
等。
<where>
<where>
标签会自动添加 WHERE
关键字,并且会智能地去除多余的 AND
或 OR
关键字。 例如:
<select id="findUsers" parameterType="com.example.model.User" resultType="com.example.model.User">
SELECT id, username, email FROM users
<where>
<if test="username != null and username != ''">
AND username LIKE #{username}
</if>
<if test="email != null and email != ''">
AND email LIKE #{email}
</if>
</where>
</select>
如果 username
和 email
都为空,则 <where>
标签会生成 WHERE 1=1
。
<set>
<set>
标签会自动添加 SET
关键字,并且会智能地去除多余的 ,
关键字。 例如:
<update id="updateUser" parameterType="com.example.model.User">
UPDATE users
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="email != null and email != ''">
email = #{email},
</if>
</set>
WHERE id = #{id}
</update>
如果 username
和 email
都为空,则 <set>
标签会生成 SET
。
<trim>
<trim>
标签可以自定义前缀、后缀、前缀覆盖和后缀覆盖。 例如:
<select id="findUsers" parameterType="com.example.model.User" resultType="com.example.model.User">
SELECT id, username, email FROM users
<trim prefix="WHERE" prefixOverrides="AND |OR">
<if test="username != null and username != ''">
AND username LIKE #{username}
</if>
<if test="email != null and email != ''">
AND email LIKE #{email}
</if>
</trim>
</select>
在这个例子中,prefix="WHERE"
指定了前缀为 WHERE
,prefixOverrides="AND |OR"
指定了需要覆盖的前缀为 AND
或 OR
。
5.4 <foreach>
: 循环遍历集合
<foreach>
标签用于循环遍历集合,例如 List
, Array
, Map
等。 例如:
<select id="findUsersByIds" parameterType="java.util.List" resultType="com.example.model.User">
SELECT id, username, email FROM users
WHERE id IN
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</select>
item="id"
: 指定了集合中的元素名为id
。collection="list"
: 指定了集合为list
。open="("
: 指定了循环开始时的字符串为(
。separator=","
: 指定了元素之间的分隔符为,
。close=")"
: 指定了循环结束时的字符串为)
。
在这个例子中,如果 list
的值为 [1, 2, 3]
,则生成的 SQL 语句为:
SELECT id, username, email FROM users WHERE id IN (1, 2, 3)
6. MyBatis 的工作流程
简单来说,MyBatis 的工作流程可以概括为以下几个步骤:
- 加载 MyBatis 配置文件: MyBatis 会加载
mybatis-config.xml
文件,读取数据库连接信息、Mapper 文件路径等配置信息。 - 构建 SqlSessionFactory: MyBatis 会根据配置文件,构建
SqlSessionFactory
对象,SqlSessionFactory
是创建SqlSession
的工厂。 - 获取 SqlSession: 通过
SqlSessionFactory
创建SqlSession
对象,SqlSession
是 MyBatis 与数据库交互的接口。 - 执行 SQL 语句: 通过
SqlSession
的方法,例如selectOne
,selectList
,insert
,update
,delete
等,执行 SQL 语句。 - 处理结果集: MyBatis 会将 SQL 的执行结果,按照 Mapper 文件中定义的映射关系,映射成 Java 对象。
- 关闭 SqlSession: 使用完毕后,需要关闭
SqlSession
,释放资源。
7. 总结
MyBatis 作为一个优秀的持久层框架,凭借其灵活、简单、轻量级、高性能等优点,赢得了广大 Java 程序员的喜爱。 掌握 MyBatis 的核心原理,可以帮助我们更好地使用 MyBatis,提高开发效率,编写更健壮、更高效的应用程序。
希望这篇文章能够帮助你更好地理解 MyBatis 的核心原理。 记住,实践是检验真理的唯一标准,多写代码,多思考,才能真正掌握 MyBatis。 祝你在编程的道路上越走越远!