MyBatis通过简单的XML或注解来配置和映射原生信息,将接口与Java的POJO(Plain Old Java Objects,普通的Java对象)映射成数据库中的记录。它免除了几乎所有的JDBC代码和参数设置以及结果集的检索。如果你厌倦了Hibernate的笨重和JPA的复杂,那么MyBatis的简洁和强大一定会让你爱不释手。
本文将带你从MyBatis的基础配置开始,逐步深入到参数处理、结果集映射、动态SQL和二级缓存等核心概念。无论你是初学者还是有一定经验的开发者,相信这篇文章都能让你对MyBatis有更深刻的理解。
一、 核心配置文件:mybatis-config.xml
万事开头难,我们先从MyBatis的心脏——核心配置文件开始。这个文件通常命名为 mybatis-config.xml,它配置了MyBatis的全局行为,如数据库环境、类型别名、事务管理器、映射器等。
一个典型的mybatis-config.xml结构如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="db.properties"/>
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<typeAliases>
<typeAlias type="com.example.model.User" alias="User"/>
<package name="com.example.model"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="mappers/UserMapper.xml"/>
</mappers>
</configuration>
配置项详解:
-
<properties>: 用于引入外部属性文件(如db.properties),方便管理数据库连接等敏感信息。在后续配置中可以使用${key}的形式引用。 -
<settings>: 这是MyBatis中极为重要的调整设置,会改变MyBatis的运行时行为。mapUnderscoreToCamelCase: 自动将数据库中下划线命名的列(如user_name)映射到Java对象中驼峰命名的属性(如userName),非常实用!logImpl: 配置MyBatis使用何种日志实现,STDOUT_LOGGING会将日志输出到标准控制台,便于开发调试。
-
<typeAliases>: 为Java类型设置一个简短的名字。这样做可以减少在Mapper XML中书写冗长的完全限定类名。 -
<environments>: 配置数据库连接环境。可以配置多个environment,通过default属性指定默认使用的环境。transactionManager: 事务管理器,JDBC表示使用JDBC的提交和回滚设置。在Spring集成中通常会使用MANAGED。dataSource: 数据源,POOLED表示使用MyBatis内置的连接池。
-
<mappers>: 注册SQL映射文件。MyBatis会从这些文件中加载SQL语句。
二、 Mapper接口与XML
MyBatis的核心思想是接口绑定。我们只需要定义一个Mapper接口(例如UserMapper.java),然后编写一个对应的XML文件(UserMapper.xml)来提供SQL实现。
UserMapper.java (接口)
package com.example.mapper;
import com.example.model.User;
public interface UserMapper {
User findById(Integer id);
int insertUser(User user);
}
UserMapper.xml (SQL实现)
<?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.mapper.UserMapper">
<select id="findById" resultType="com.example.model.User">
select id, user_name, email from users where id = #{id}
</select>
<insert id="insertUser" parameterType="com.example.model.User">
insert into users (user_name, email) values (#{userName}, #{email})
</insert>
</mapper>
关键点:
- XML文件的
namespace必须与Mapper接口的全限定名完全一致。 select,insert,update,delete等标签的id属性必须与接口中对应的方法名一致。parameterType指定了传入参数的类型,resultType指定了返回结果的类型。
三、 插入数据并获取自增ID
在业务中,我们经常需要在插入一条数据后立刻获取它的自增主键ID。MyBatis提供了非常便捷的方式。
只需要在 insert 标签中增加两个属性:useGeneratedKeys="true" 和 keyProperty="id"。
<insert id="insertUser" parameterType="User" useGeneratedKeys="true" keyProperty="id">
insert into users (user_name, email, password)
values (#{userName}, #{email}, #{password})
</insert>
useGeneratedKeys="true": 告诉MyBatis这个操作会生成主键。keyProperty="id": 指定将生成的主键值设置到传入的User对象的哪个属性上。id对应User类中的id字段。
Java代码调用:
User user = new User();
user.setUserName("Gemini");
user.setEmail("gemini@google.com");
user.setPassword("secret");
System.out.println("插入前,User ID: " + user.getId()); // 输出: null
userMapper.insertUser(user); // 执行插入
System.out.println("插入后,User ID: " + user.getId()); // 输出: (数据库生成的自增ID)
执行insertUser方法后,user对象的id属性会被自动回填。
四、#{} 与 ${} 的天壤之别
这是MyBatis中一个非常重要且常见的面试题。两者都用于参数替换,但机制完全不同。
-
#{}(预编译参数) :-
原理: MyBatis在处理
#{}时,会将其替换为?占位符,并使用PreparedStatement来设置参数。 -
优点: 能有效防止SQL注入。因为参数值是作为
PreparedStatement的参数来处理的,而不是直接拼接到SQL字符串中,数据库驱动会对其进行安全处理。 -
适用场景: 绝大多数情况下都应该使用
#{}来传递业务参数。
select * from users where id = #{userId} -
-
${}(字符串替换) :-
原理: MyBatis在处理
${}时,会直接将${}中的内容原样拼接到SQL语句中。 -
缺点: 存在严重的SQL注入风险。如果传入的参数来自用户输入且未经过严格过滤,攻击者可以构造恶意的SQL片段。
-
适用场景: 仅在需要动态替换SQL的非参数部分时使用,例如动态指定表名、列名或
ORDER BY的排序字段。使用时必须对参数来源进行严格控制和校验。
select * from users order by ${orderByCol} select * from users order by ${orderByCol} -
总结: 能用 #{} 就坚决不用 ${},安全第一!
五、 灵活多变的参数封装
MyBatis支持多种方式向SQL传递参数,以应对不同的业务场景。
1. 单个参数
如果方法只有一个参数(基本类型、包装类型、String),在XML中可以直接使用。
// Mapper接口
User findById(Integer id);
<select id="findById" resultType="User">
select * from users where id = #{id}
</select>
2. 多个参数
当方法有多个参数时,MyBatis默认无法区分它们。需要使用 @Param 注解来为每个参数命名。
// Mapper接口
User findByNameAndEmail(@Param("name") String userName, @Param("emailAddr") String email);
<select id="findByNameAndEmail" resultType="User">
select * from users where user_name = #{name} and email = #{emailAddr}
</select>
3. 对象参数 (POJO)
这是最常用、最推荐的方式。将参数封装到一个Java对象中,可读性和可维护性都非常好。
// Mapper接口
int updateUser(User user);
<update id="updateUser" parameterType="User">
update users set
user_name = #{userName},
email = #{email}
where id = #{id}
</update>
4. List参数
当需要执行 IN 查询时,可以使用List或数组作为参数,并结合动态SQL中的 foreach 标签。
// Mapper接口
List<User> findByIds(List<Integer> ids);
<select id="findByIds" resultType="User">
select * from users where id in
<foreach item="id" collection="list" open="(" separator="," close=")">
#{id}
</foreach>
</select>
collection="list": 当参数是List时,固定写list。如果是数组,则写array。如果使用@Param("ids")注解,则写ids。item="id": 每次迭代的元素别名。open,close,separator: 用于拼接SQL的开头、结尾和分隔符。
5. Map参数
使用Map传递参数也非常灵活,但可读性不如POJO。
// Mapper接口
List<User> searchUsers(Map<String, Object> params);
<select id="searchUsers" resultType="User">
select * from users where user_name like #{name} and email = #{email}
</select>
Java调用:
Map<String, Object> map = new HashMap<>();
map.put("name", "%" + keyword + "%");
map.put("email", "test@example.com");
List<User> users = userMapper.searchUsers(map);
六、强大的结果集封装:ResultMap
当数据库列名与Java对象属性名不一致,或者需要处理复杂的关联查询(一对一、一对多)时,resultType的自动映射就显得力不-从心了。这时,ResultMap 就该登场了!
ResultMap 是MyBatis中最强大、最重要的元素,它能让你完全掌控结果集与对象的映射关系。
1. 基础映射 (解决列名属性名不一致)
假设数据库表users的列是 user_id, user_name, user_email,而User类的属性是 id, userName, email。
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="email" column="user_email"/>
</resultMap>
<select id="findById" resultMap="userResultMap">
select user_id, user_name, user_email from users where user_id = #{id}
</select>
通过resultMap,我们将user_id列映射到了id属性,user_name映射到了userName属性,完美解决了不一致问题。
2. 关联映射:一对一 (association)
假设我们有一个Order订单类,它内部包含一个User对象,表示下单的用户。
public class Order {
private Integer id;
private String orderNo;
private User user; // 关联的用户对象
// getters and setters
}
查询订单信息,同时带出用户信息:
<resultMap id="orderResultMap" type="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
<association property="user" javaType="User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="email" column="user_email"/>
</association>
</resultMap>
<select id="findOrderById" resultMap="orderResultMap">
select
o.id as order_id,
o.order_no,
u.id as user_id,
u.user_name,
u.user_email
from orders o
left join users u on o.user_id = u.id
where o.id = #{orderId}
</select>
MyBatis会根据resultMap的定义,将查询出的扁平化结果集自动封装成嵌套的Order和User对象。
3. 关联映射:一对多 (collection)
假设一个User可以有多个Order订单。
public class User {
private Integer id;
private String userName;
private List<Order> orders; // 关联的订单列表
// getters and setters
}
查询用户信息,同时带出他所有的订单列表:
<resultMap id="userWithOrdersResultMap" type="User">
<id property="id" column="user_id"/>
<result property="userName" column="user_name"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
<select id="findUserWithOrders" resultMap="userWithOrdersResultMap">
select
u.id as user_id,
u.user_name,
o.id as order_id,
o.order_no
from users u
left join orders o on u.id = o.user_id
where u.id = #{userId}
</select>
这样,查询结果就会被封装成一个User对象,其orders属性是一个包含该用户所有订单的List。
七、 动态SQL:让SQL“活”起来
动态SQL是MyBatis的另一个核心特性,它允许我们根据不同的条件动态地拼接SQL语句,极大地提高了SQL的复用性。
1. if (条件判断)
<select id="findActiveUsers" resultType="User">
select * from users where 1=1
<if test="userName != null and userName != ''">
and user_name like #{userName}
</if>
<if test="email != null and email != ''">
and email = #{email}
</if>
</select>
如果传入的userName不为空,则拼接AND user_name LIKE ...条件。
2. where (智能处理AND/OR)
上面的if写法有个小问题,如果所有条件都不满足,SQL会是select * from users where 1=1,虽然也能工作,但不够优雅。如果去掉1=1,第一个if满足时,SQL会变成select * from users where and user_name ...,这会产生语法错误。
<where>标签可以智能地处理这个问题:它只在有条件内容时才插入WHERE子句,并且会自动去掉第一个条件前面的AND或OR。
<select id="findActiveUsers" resultType="User">
select * from users
<where>
<if test="userName != null and userName != ''">
and user_name like #{userName}
</if>
<if test="email != null and email != ''">
and email = #{email}
</if>
</where>
</select>
这才是动态条件查询的正确打开方式!
3. set (智能处理更新语句的逗号)
在update语句中,如果使用if,可能会导致SQL末尾多一个逗号。<set>标签可以解决这个问题。
<update id="updateUserSelective">
update users
<set>
<if test="userName != null">user_name = #{userName},</if>
<if test="email != null">email = #{email},</if>
<if test="password != null">password = #{password},</if>
</set>
where id = #{id}
</update>
<set>会自动插入SET关键字,并移除结尾多余的逗号。
4. choose, when, otherwise (分支选择)
类似于Java中的switch语句,只执行第一个满足条件的分支。
<select id="findByCondition" resultType="User">
select * from users where
<choose>
<when test="title != null">
title = #{title}
</when>
<when test="author != null">
author = #{author}
</when>
<otherwise>
is_active = 1
</otherwise>
</choose>
</select>
5. foreach (循环)
前面在List参数部分已经见过它了,是处理IN查询的神器。
八、 性能优化:缓存机制
为了提升查询性能,MyBatis内置了两级缓存机制。
1. 一级缓存 (SqlSession级别)
- 作用域:
SqlSession。一级缓存是默认开启的,无法关闭。 - 生命周期: 当一个
SqlSession被创建时,它会持有一个本地缓存。当SqlSession关闭或被清空(commit, rollback)时,缓存就会被清空。 - 工作原理: 在同一个
SqlSession中,如果执行了两次完全相同的查询(相同的SQL、相同的参数),第二次查询会直接从本地缓存中获取结果,而不会再次访问数据库。 - 注意: 任何
insert,update,delete操作都会清空当前SqlSession的一级缓存,以防止脏读。
2. 二级缓存 (Mapper Namespace级别)
-
作用域:
Mapper Namespace。二级缓存是跨SqlSession的,多个SqlSession可以共享同一个Mapper的二级缓存。 -
如何开启:
-
在
mybatis-config.xml中全局开启二级缓存:<settings> <setting name="cacheEnabled" value="true"/> </settings> -
在需要开启缓存的Mapper XML文件中添加
<cache/>标签:<mapper namespace="com.example.mapper.UserMapper"> <cache/> </mapper>
-
-
工作原理: 当一个
SqlSession提交事务(sqlSession.commit())后,它所执行的查询结果如果对应的Mapper开启了二级缓存,就会被存入二级缓存中。其他SqlSession在执行相同的查询时,会先去二级缓存中查找,如果找到就直接返回,不再查询数据库。 -
缓存失效: 同样地,该
namespace下的任何insert,update,delete操作都会导致二级缓存被清空。 -
注意事项:
- 放入二级缓存的POJO对象必须实现
java.io.Serializable接口。 - 二级缓存适用于读多写少、数据变更不频繁的场景。对于实时性要求高的业务,要慎用二级缓存,因为它可能会导致数据不一致(脏读)。
<cache>标签有很多属性可以配置,如eviction(回收策略)、flushInterval(刷新间隔)、size(缓存大小)、readOnly(是否只读)等,可以进行精细化控制。
- 放入二级缓存的POJO对象必须实现
总结
MyBatis以其简洁、灵活和高效赢得了众多Java开发者的青睐。它让我们能够专注于SQL本身,通过简单的配置即可完成复杂的数据库操作。
回顾一下,我们今天学习了:
- 核心配置:
mybatis-config.xml是MyBatis的基石。 - 基本使用:通过Mapper接口和XML文件实现SQL的绑定与执行。
- 参数处理:深入理解了
#{}与${}的区别,并掌握了多种参数传递方式。 - 结果映射:学会了使用强大的
ResultMap处理各种复杂的查询结果。 - 动态SQL:利用
if,where,foreach等标签让SQL变得智能和可复用。 - 二级缓存:了解了如何通过二级缓存提升应用性能。
希望这篇详尽的博客能够帮助你构建一个清晰而全面的MyBatis知识体系。当然,MyBatis还有插件、拦截器等更高级的玩法等待你去探索。技术的道路,学无止境,让我们一起不断进步!