Mapper接口与映射方式
导学
通过本节学习,你将能够:
- 掌握 MyBatis 的两种映射方式:XML 映射文件与注解映射
- 理解 XML 映射文件的完整结构(DOCTYPE、namespace、CRUD 标签)
- 熟练使用
@Select、@Insert、@Update、@Delete完成注解映射 - 根据项目场景正确选择 XML 或注解方式,避免混合使用带来的维护灾难
定义
什么是映射方式
映射方式指 SQL 语句与 Java Mapper 接口方法之间的绑定机制。MyBatis 提供两种绑定途径:
| 方式 | SQL 存放位置 | 绑定机制 |
|---|---|---|
| XML 映射文件 | src/main/resources/mapper/*.xml | 通过 namespace + id 与接口方法匹配 |
| 注解映射 | Mapper 接口的方法上 | 通过 @Select/@Insert/@Update/@Delete 直接声明 SQL |
解决了什么痛点
在 JDBC 时代,SQL 与 Java 代码深度耦合:
// JDBC 的噩梦:SQL 字符串散落在业务代码中
String sql = "SELECT * FROM student WHERE major = '" + major + "'";
// 修改 SQL 需要改 Java 代码 → 重新编译 → 重新部署
MyBatis 的两种映射方式都实现了 SQL 与 Java 代码的解耦,但解耦程度和适用场景不同:
- XML 方式:SQL 完全独立于 Java 文件,DBA 可直接审阅,适合复杂 SQL、动态 SQL
- 注解方式:SQL 与接口同处一个文件,开发时无需切换窗口,适合简单 CRUD
核心原理
XML 方式 vs 注解方式对比流程图
流程解读:
- XML 方式:接口与 SQL 分离,MyBatis 启动时解析 XML 文件,将
namespace.id与 SQL 语句注册到Configuration中 - 注解方式:接口与 SQL 合一,MyBatis 启动时扫描接口上的注解,同样将
接口全限定名.方法名与 SQL 注册到Configuration中 - 殊途同归:无论哪种方式,最终都生成相同的
MappedStatement,后续由Executor执行时无差异
完整示例
场景说明
乐途公司学生管理系统的 StudentMapper 需要支持查询所有学生、按 ID 查询、新增、更新、删除五个操作。本节分别用 XML 方式和注解方式实现同一套功能,便于直观对比。
操作前的数据库表结构及初始数据
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
公共部分(两种方式共用)
实体类 Student.java
package com.fly.entity;
import java.math.BigDecimal;
public class Student {
private Integer id;
private String name;
private Integer age;
private String major;
private BigDecimal score;
public Student() {}
public Student(String name, Integer age, String major, BigDecimal score) {
this.name = name; this.age = age; this.major = major; this.score = score;
}
// Getter / Setter 省略...
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public String getMajor() { return major; }
public void setMajor(String major) { this.major = major; }
public BigDecimal getScore() { return score; }
public void setScore(BigDecimal score) { this.score = score; }
@Override
public String toString() {
return "Student{id=" + id + ", name='" + name + "', age=" + age
+ ", major='" + major + "', score=" + score + "}";
}
}
工具类 MyBatisUtil.java
package com.fly.util;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
public class MyBatisUtil {
private static final SqlSessionFactory factory;
static {
try (InputStream is = Resources.getResourceAsStream("mybatis-config.xml")) {
factory = new SqlSessionFactoryBuilder().build(is);
} catch (Exception e) { throw new ExceptionInInitializerError(e); }
}
public static SqlSessionFactory getFactory() { return factory; }
}
方式一:XML 映射文件
Mapper 接口 StudentMapper.java
package com.fly.mapper;
import com.fly.entity.Student;
import java.util.List;
public interface StudentMapper {
Student findById(Integer id);
List<Student> findAll();
int insert(Student student);
int update(Student student);
int deleteById(Integer id);
}
映射文件 StudentMapper.xml
<?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">
<!-- namespace 必须与接口全限定名一致 -->
<mapper namespace="com.fly.mapper.StudentMapper">
<select id="findById" resultType="com.fly.entity.Student">
SELECT id, name, age, major, score
FROM student
WHERE id = #{id}
</select>
<select id="findAll" resultType="com.fly.entity.Student">
SELECT id, name, age, major, score FROM student
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO student (name, age, major, score)
VALUES (#{name}, #{age}, #{major}, #{score})
</insert>
<update id="update">
UPDATE student
SET name = #{name}, age = #{age}, major = #{major}, score = #{score}
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM student WHERE id = #{id}
</delete>
</mapper>
全局配置中注册方式
<mappers>
<!-- XML 方式:按资源路径注册 -->
<mapper resource="mapper/StudentMapper.xml"/>
</mappers>
方式二:注解映射
Mapper 接口 StudentMapper.java(SQL 直接写在注解中)
package com.fly.mapper;
import com.fly.entity.Student;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface StudentMapper {
@Select("SELECT id, name, age, major, score FROM student WHERE id = #{id}")
@Results(id = "studentMap", value = {
@Result(property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "age", column = "age"),
@Result(property = "major", column = "major"),
@Result(property = "score", column = "score")
})
Student findById(Integer id);
@Select("SELECT id, name, age, major, score FROM student")
@ResultMap("studentMap")
List<Student> findAll();
@Insert("INSERT INTO student (name, age, major, score) VALUES (#{name}, #{age}, #{major}, #{score})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Student student);
@Update("UPDATE student SET name = #{name}, age = #{age}, major = #{major}, score = #{score} WHERE id = #{id}")
int update(Student student);
@Delete("DELETE FROM student WHERE id = #{id}")
int deleteById(Integer id);
}
全局配置中注册方式
<mappers>
<!-- 注解方式:按接口类注册 -->
<mapper class="com.fly.mapper.StudentMapper"/>
</mappers>
测试代码与执行结果
测试入口(两种方式测试逻辑相同,仅 Mapper 注册方式不同)
package com.fly.test;
import com.fly.entity.Student;
import com.fly.mapper.StudentMapper;
import com.fly.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import java.math.BigDecimal;
import java.util.List;
public class MapperTest {
public static void main(String[] args) {
// 1. 查询所有
try (SqlSession session = MyBatisUtil.getFactory().openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
List<Student> all = mapper.findAll();
System.out.println("【查询全部】共 " + all.size() + " 条记录:");
all.forEach(s -> System.out.println(" " + s));
}
// 2. 按 ID 查询
try (SqlSession session = MyBatisUtil.getFactory().openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
Student s = mapper.findById(3);
System.out.println("\n【按 ID 查询】id=3 => " + s);
}
// 3. 插入
try (SqlSession session = MyBatisUtil.getFactory().openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
Student newbie = new Student("赵六", 23, "人工智能", new BigDecimal("91.00"));
mapper.insert(newbie);
session.commit();
System.out.println("\n【插入】生成主键: " + newbie.getId());
}
// 4. 更新
try (SqlSession session = MyBatisUtil.getFactory().openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
Student update = mapper.findById(1);
update.setScore(new BigDecimal("99.00"));
mapper.update(update);
session.commit();
System.out.println("\n【更新】大翔分数更新为: " + update.getScore());
}
// 5. 删除
try (SqlSession session = MyBatisUtil.getFactory().openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
int rows = mapper.deleteById(6); // 删除赵六
session.commit();
System.out.println("\n【删除】影响行数: " + rows);
}
// 6. 验证最终状态
try (SqlSession session = MyBatisUtil.getFactory().openSession()) {
StudentMapper mapper = session.getMapper(StudentMapper.class);
List<Student> all = mapper.findAll();
System.out.println("\n【最终数据】共 " + all.size() + " 条记录:");
all.forEach(s -> System.out.println(" " + s));
}
}
}
实际执行结果(控制台输出)
【查询全部】共 5 条记录:
Student{id=1, name='大翔', age=22, major='计算机科学', score=95.50}
Student{id=2, name='白歌', age=21, major='软件工程', score=88.00}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
Student{id=4, name='黄俪', age=21, major='信息安全', score=90.50}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.00}
【按 ID 查询】id=3 => Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
【插入】生成主键: 6
【更新】大翔分数更新为: 99.00
【删除】影响行数: 1
【最终数据】共 5 条记录:
Student{id=1, name='大翔', age=22, major='计算机科学', score=99.00}
Student{id=2, name='白歌', age=21, major='软件工程', score=88.00}
Student{id=3, name='小崔', age=20, major='计算机科学', score=92.00}
Student{id=4, name='黄俪', age=21, major='信息安全', score=90.50}
Student{id=5, name='李眉', age=22, major='软件工程', score=87.00}
最终数据状态:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 99.00 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
XML vs 注解 综合对比
| 对比维度 | XML 映射文件 | 注解映射 |
|---|---|---|
| SQL 位置 | 独立的 .xml 文件 | 接口方法的注解中 |
| 动态 SQL | 强大,支持 <if>、<choose>、<foreach> 等标签 | 较弱,需用 @SelectProvider 等注解,代码冗长 |
| 结果映射 | resultMap 标签功能完善,支持关联映射 | @Results + @Result 可实现,但复杂关联时可读性差 |
| SQL 审阅 | DBA/运维可直接查看 XML,无需看 Java | SQL 散落在 Java 代码中,不利于独立审阅 |
| 开发效率 | 需维护两个文件,切换成本高 | 一个文件搞定,简单 CRUD 开发快 |
| 热部署 | 修改 XML 后无需重新编译(部分容器支持热加载) | 修改注解需重新编译 |
| 适用场景 | 复杂 SQL、动态 SQL、多表关联、需要 DBA 审阅 | 简单 CRUD、快速原型、微服务轻量接口 |
| 注册方式 | <mapper resource="..."/> | <mapper class="..."/> |
易错场景 / 常见误区
| 误区 | 错误表现 | 正解 |
|---|---|---|
| 同一接口混用 XML 和注解 | 接口方法上写 @Select,同时存在同名 XML,导致行为不可预期 | 一个 Mapper 接口只选一种方式,要么纯 XML,要么纯注解 |
注解方式忘记 @Results | 数据库列名与属性名不一致时,映射结果为 null | 使用 @Results 显式指定 column 与 property 映射关系,或用 @ResultMap 复用 |
| XML 的 namespace 写错 | Invalid bound statement (not found) | namespace 必须是接口的全限定名,含包名 |
注解的 @Insert 忘记 @Options | 插入后实体对象的 id 仍为 null | 需要 @Options(useGeneratedKeys = true, keyProperty = "id") 回填主键 |
注解方式用 <mapper resource="..."/> 注册 | 启动不报错,但执行时 not found | 注解方式必须用 <mapper class="接口全限定名"/> 注册 |
| XML 中 resultType 用别名但未配置 | ClassNotFoundException 或解析失败 | 在 mybatis-config.xml 的 <typeAliases> 中注册包路径或类别名 |
面试考点
Q1:MyBatis 的 XML 映射方式和注解映射方式有什么区别?如何选择?
A: XML 方式将 SQL 写在独立的 XML 文件中,与 Java 代码完全解耦,支持强大的动态 SQL(
<if>、<foreach>等),适合复杂查询、需要 DBA 审阅 SQL、或 SQL 频繁变更的场景。注解方式将 SQL 直接写在接口方法的注解上(@Select、@Insert等),开发效率高,一个文件即可完成,适合简单 CRUD、快速原型或微服务轻量接口。生产级项目推荐以 XML 为主,复杂逻辑用 XML,极简单点查询可酌情用注解,但同一 Mapper 不要混用两种方式。
Q2:为什么 Mapper 接口的 namespace 必须与接口全限定名一致?
A: MyBatis 通过
namespace + id作为唯一键,从Configuration中定位MappedStatement。当调用session.getMapper(StudentMapper.class)时,动态代理会根据接口全限定名和方法名拼接出com.fly.mapper.StudentMapper.findById去查找对应的 SQL。如果namespace不一致,就无法建立接口方法与 XML 中 SQL 的绑定关系,执行时会抛出Invalid bound statement (not found)。
Q3:注解方式中 @ResultMap("studentMap") 是如何被识别和复用的?
A: 在 MyBatis 注解中,
@Results注解可以指定id属性(如@Results(id = "studentMap", ...)),这相当于定义了一个具名的结果映射。同一接口的其他方法可以通过@ResultMap("studentMap")引用它,避免重复书写列与属性的映射关系。这个id只在当前 Mapper 接口的命名空间内有效。
Q4:如果项目中有 100 个 Mapper,如何简化 <mappers> 的注册配置?
A: 可以使用包扫描方式批量注册:
<package name="com.fly.mapper"/>。使用此方式时,MyBatis 会自动扫描com.fly.mapper包下的所有接口。对于 XML 方式,要求 XML 文件与接口同名且同路径(如com/fly/mapper/StudentMapper.xml放在resources下);对于注解方式,直接扫描接口类即可。这比逐个写<mapper resource="..."/>简洁得多。
小结
本节详细对比了 MyBatis 的两种映射方式:XML 映射文件和注解映射。XML 方式以 "SQL 与代码解耦" 为核心优势,配合强大的动态 SQL 能力,是企业级项目的首选。注解方式以 "开发效率" 为核心优势,适合简单场景。无论哪种方式,namespace 与接口全限定名的一致性、正确的注册方式、以及避免混用,都是保证项目可维护性的关键。
关键记忆点:
- XML 方式:
<mapper resource="..."/>,SQL 独立,动态 SQL 强大 - 注解方式:
<mapper class="..."/>,@Select/@Insert/@Update/@Delete,开发快捷 - 同一 Mapper 严禁混用两种方式
namespace= 接口全限定名,id= 接口方法名- 包扫描
<package name="..."/>可大幅简化配置
下一章引子
至此,第 01 章 "MyBatis 概述与快速上手" 已全部完成。你已经掌握了 MyBatis 的定位、环境搭建、核心对象生命周期、第一个完整 CRUD 程序,以及两种映射方式的选择策略。第 02 章将深入 MyBatis 配置详解——从 properties、settings、typeAliases 到插件和缓存配置,带你把 mybatis-config.xml 的每一项都理解透彻,为复杂项目打下坚实基础。