MyBatis
环境搭建
配置文件(application.properties)
# ========== 数据源 ==========
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/stock?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
# ========== MyBatis 配置 ==========
# 打印 SQL 日志(开发调试用)
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 开启驼峰命名自动映射(如 user_name → userName)
mybatis.configuration.map-underscore-to-camel-case=true
# 实体类别名包(设置后 XML 中可直接用类名,无需写全限定名)
mybatis.type-aliases-package=org.example.bean
# Mapper XML 文件路径(如果 XML 与接口同包同名,可省略此配置)
# mybatis.mapper-locations=classpath:mapper/*Mapper.xml
| 配置项 | 作用 |
|---|---|
mybatis.configuration.log-impl |
指定日志实现,StdOutImpl 将 SQL 打印到控制台 |
mybatis.configuration.map-underscore-to-camel-case |
自动将数据库下划线命名映射为 Java 驼峰命名 |
mybatis.type-aliases-package |
扫描指定包下的实体类,自动注册别名(类名首字母小写) |
mybatis.mapper-locations |
指定 Mapper XML 文件的路径(支持 Ant 风格通配符) |
Mapper XML 基础规则
MyBatis 通过 Mapper 接口 + XML 映射文件 实现 SQL 与 Java 代码的解耦。
| 规则 | 说明 | 示例 |
|---|---|---|
namespace |
必须等于 Mapper 接口的全限定名 | org.example.mapper.UserMapper |
id |
必须等于 Mapper 接口的方法名 | selectById |
parameterType |
传入参数的类型(全限定名或别名) | User、int、map |
resultType |
返回值的类型(全限定名或别名) | User、org.example.bean.User |
| XML 存放位置 | resources 下的路径必须与 Mapper 接口的包路径一致 |
org/example/mapper/UserMapper.xml |
完整示例对照
Java 接口:
package org.example.mapper;
public interface UserMapper {
User selectById(long id);
}
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">
<mapper namespace="org.example.mapper.UserMapper">
<!-- id = 方法名,resultType = 返回值类型(配置了别名包,直接用 User) -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{id}
</select>
</mapper>
参数绑定
MyBatis 通过 #{} 占位符来绑定参数(底层使用 PreparedStatement,防止 SQL 注入)。
注意:
#{}和${}的区别 ——#{}是预编译占位符(安全),${}是字符串拼接(有 SQL 注入风险,仅在动态表名/列名等特殊场景使用)。
单参数(简单类型)
当方法只有一个简单类型参数(如 int、long、String)时,#{} 中的名称可以任意写:
// Mapper 接口
User selectById(long id);
<!-- #{} 中的名称任意,这里写 abc 也能正确取值 -->
<select id="selectById" resultType="User">
SELECT * FROM user WHERE id = #{abc}
</select>
多参数 — 序号绑定
MyBatis 内部使用 arg0, arg1, ... 或 param1, param2, ... 来引用参数:
// Mapper 接口(不使用 @Param)
User selectByNamepwd(String name, String pwd);
<!-- 方式一:arg 序号 -->
<select id="selectByNamepwd" resultType="User">
SELECT * FROM user WHERE userName = #{arg0} AND pwd = #{arg1}
</select>
<!-- 方式二:param 序号 -->
<select id="selectByNamepwd" resultType="User">
SELECT * FROM user WHERE userName = #{param1} AND pwd = #{param2}
</select>
不推荐:序号绑定可读性差,参数顺序一旦改变就会出错。
多参数 — @Param 注解绑定(推荐)
// Mapper 接口
import org.apache.ibatis.annotations.Param;
User selectByNamepwd(
@Param("userName") String name,
@Param("password") String pwd
);
<!-- #{}中使用 @Param 指定的名称 -->
<select id="selectByNamepwd" resultType="User">
SELECT * FROM user WHERE userName = #{userName} AND pwd = #{password}
</select>
推荐:
@Param明确指定参数名,可读性好,维护方便。
Map 参数绑定
参数较多且不固定时,可以使用 Map 传参,#{} 中的名称与 Map 的 key 对应:
// Mapper 接口
List<User> selectByAgeSexMap(Map<String, Object> map);
// 调用
HashMap<String, Object> map = new HashMap<>();
map.put("age", 30);
map.put("sex", 1);
userMapper.selectByAgeSexMap(map).forEach(System.out::println);
<select id="selectByAgeSexMap" resultType="User" parameterType="map">
SELECT * FROM user WHERE age = #{age} AND sex = #{sex}
</select>
模糊查询
使用 MySQL 的 CONCAT 函数拼接 %,可以防止 SQL 注入:
// Mapper 接口
List<User> selectByUsername(String name);
<select id="selectByUsername" resultType="User">
SELECT * FROM user WHERE userName LIKE CONCAT('%', #{username}, '%')
</select>
// 调用
userMapper.selectByUsername("张").forEach(System.out::println);
// 实际执行 SQL:SELECT * FROM user WHERE userName LIKE '%张%'
主键回填
插入数据后,需要获取数据库生成的主键值。MyBatis 提供 3 种方式。
方式一:useGeneratedKeys(自增主键,最常用)
适用于 MySQL 自增主键(AUTO_INCREMENT),插入后自动回填:
<insert id="insert" parameterType="user"
useGeneratedKeys="true"
keyColumn="id"
keyProperty="id">
INSERT INTO user(userNo, userName, pwd, sex, age, hometown, telphone)
VALUES (#{userNo}, #{userName}, #{pwd}, #{sex}, #{age}, #{hometown}, #{telphone})
</insert>
| 属性 | 作用 |
|---|---|
useGeneratedKeys="true" |
启用 JDBC 的 getGeneratedKeys() 获取自增主键 |
keyColumn |
数据库中的主键列名 |
keyProperty |
Java 实体类中对应的属性名 |
// 调用后 user.getId() 即为新生成的主键值
User user = new User(null, "zhangsan", ...);
userMapper.insert(user);
System.out.println(user.getId()); // 输出:新插入记录的自增 ID
方式二:<selectKey> + LAST_INSERT_ID()(自增主键)
与方式一等效,通过 <selectKey> 在插入后查询 last_insert_id():
<insert id="insert" parameterType="user">
<!-- order="AFTER" 表示在 INSERT 之后执行 -->
<selectKey order="AFTER" keyColumn="id" keyProperty="id" resultType="integer">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO user(userNo, userName, pwd, sex, age, hometown, telphone)
VALUES (#{userNo}, #{userName}, #{pwd}, #{sex}, #{age}, #{hometown}, #{telphone})
</insert>
方式三:<selectKey> + UUID(自定义主键,插入前生成)
适用于主键是字符串(如 UUID),在插入前生成主键值:
<insert id="insert" parameterType="product">
<!-- order="BEFORE" 表示在 INSERT 之前执行 -->
<selectKey order="BEFORE" resultType="string" keyColumn="id" keyProperty="id">
SELECT REPLACE(UUID(), '-', '')
</selectKey>
INSERT INTO product(id, name) VALUES (#{id}, #{name})
</insert>
执行流程:先执行
<selectKey>子查询生成 UUID → 将结果赋给product.id→ 再执行 INSERT,#{id}取到的就是刚生成的 UUID。
// 调用
Product product = new Product(null, "鼠标");
productMapper.insert(product);
System.out.println(product.getId()); // 输出:自动生成的 UUID(32位无横线)
<selectKey> 属性说明
| 属性 | 可选值 | 作用 |
|---|---|---|
order |
BEFORE / AFTER |
BEFORE:插入前执行(UUID/雪花ID);AFTER:插入后执行(自增ID) |
keyColumn |
数据库列名 | 主键列 |
keyProperty |
Java 属性名 | 将生成的主键值赋给实体类的哪个属性 |
resultType |
类型 | 主键值的 Java 类型 |
SQL 片段 <sql>
将重复使用的 SQL 片段抽取出来,通过 <include> 引用,减少代码冗余:
<!-- 定义 SQL 片段 -->
<sql id="selectAll">
SELECT * FROM user
</sql>
<!-- 引用 SQL 片段 -->
<select id="selectById" resultType="User">
<include refid="selectAll"/> WHERE id = #{id}
</select>
<select id="selectByCondition" resultType="User">
<include refid="selectAll"/> WHERE age = #{age}
</select>
好处:如果表名变了或需要统一加字段,只需修改一处。
动态 SQL
根据传入参数动态拼接 SQL 语句,是 MyBatis 最强大的特性之一。
<if> + <where> — 多条件查询
<where> 标签会自动处理 多余的 AND / OR(至少有一个条件成立时才插入 WHERE 关键字):
<select id="selectByCondition" resultType="User">
SELECT * FROM user
<where>
<if test="userName != null and userName.length > 0">
AND userName = #{userName}
</if>
<if test="pwd != null and pwd.length > 0">
AND pwd = #{pwd}
</if>
<if test="sex != null and sex > 0">
AND sex = #{sex}
</if>
<if test="hometown != null and hometown.length > 0">
AND hometown = #{hometown}
</if>
</where>
</select>
<if> 标签的 test 属性:写 OGNL 表达式,判断条件是否成立。
<where> 行为示例:
- 所有条件都不满足 → 生成
SELECT * FROM user(没有 WHERE 子句) - 只有 userName 满足 → 生成
SELECT * FROM user WHERE userName = ?(自动去掉AND)
<choose>、<when>、<otherwise>
if—-else if—-else if —-else 只选一个进行查询
如果用户填写了名字,这个搜索就按照名字来搜索,你即使填写了别的条件也不去拼接,
如果没有填写名字,填写了年龄,就按照年龄来搜索,
如果名字和年龄都没有填写,但是填写了性别就按照性别来查找
name、age、gender搜索时候只能按照其中一个来搜索,但是优先级name>age>gender
List selectByCondition(Student student);
<select id="selectByCondition2" parameterType="Student" resultMap="studentMap">
SELECT <include refid="studentColumns"/>
FROM student
WHERE
<choose>
<when test="name!=null and name!=''">
name LIKE concat('%', #{name}, '%')
</when>
<when test="age!=null">
age=#{age}
</when>
<when test="gender!=null and gender!=''">
gender=#{gender}
</when>
<otherwise>
</otherwise>
</choose>
</select>
<trim> 代替 <where>
<trim> 更灵活,可以自定义前缀和要去除的前缀字符:
<select id="selectByCondition" resultType="User">
SELECT * FROM user
<trim prefix="where" prefixOverrides="and|or">
<if test="userName != null and userName.length > 0">
AND userName = #{userName}
</if>
<if test="pwd != null and pwd.length > 0">
AND pwd = #{pwd}
</if>
</trim>
</select>
<set> — 动态更新
<set> 标签自动添加 SET 关键字,并去除尾部多余的逗号:
<update id="update" parameterType="user">
UPDATE user
<set>
<if test="userName != null and userName.length > 0">
userName = #{userName},
</if>
<if test="pwd != null and pwd.length > 0">
pwd = #{pwd},
</if>
<if test="hometown != null and hometown.length > 0">
hometown = #{hometown},
</if>
</set>
WHERE id = #{id}
</update>
注意:
<set>会自动处理末尾逗号,但你需要确保至少有一个条件成立,否则会生成错误的 SQLUPDATE user SET WHERE id = ?。
<trim> 代替 <set>
<update id="update" parameterType="user">
UPDATE user
<trim prefix="set" suffixOverrides=",">
<if test="userName != null and userName.length > 0">
userName = #{userName},
</if>
<if test="pwd != null and pwd.length > 0">
pwd = #{pwd},
</if>
<if test="hometown != null and hometown.length > 0">
hometown = #{hometown}
</if>
</trim>
WHERE id = #{id}
</update>
<foreach> — 批量操作
批量插入
// Mapper 接口
int insert1(List<User> users);
<insert id="insert1" parameterType="user">
INSERT INTO user(userNo, userName, pwd, sex, age, hometown, telphone) VALUES
<foreach collection="list" item="u" separator=",">
(#{u.userNo}, #{u.userName}, #{u.pwd}, #{u.sex}, #{u.age}, #{u.hometown}, #{u.telphone})
</foreach>
</insert>
// 调用
List<User> userList = Arrays.asList(user1, user2, user3);
userMapper.insert1(userList);
// 生成 SQL:
// INSERT INTO user(...) VALUES (?,?,...), (?,?,...), (?,?,...)
IN 查询
<select id="seletByids" resultType="User">
SELECT * FROM user
<where>
<foreach collection="list" item="id" open="id IN (" close=")" separator=",">
#{id}
</foreach>
</where>
</select>
// 调用后生成:SELECT * FROM user WHERE id IN (?, ?, ?)
<foreach> 属性详解
| 属性 | 含义 | 示例 |
|---|---|---|
collection |
传入的集合。默认可用 list(List 类型)、array(数组类型)、collection(通用),或 @Param 指定的名称 |
list、arg0、ids |
item |
每次迭代的元素变量名,在循环体内通过 #{item名.属性} 取值 |
u → #{u.userName} |
open |
整个循环结果开头拼接的字符串 | "id IN (" |
close |
整个循环结果结尾拼接的字符串 | ")" |
separator |
每次迭代之间的分隔符 | "," |
resultMap — 列名与属性名映射
当数据库列名(如 deptNo)与 Java 属性名(如 no)不一致时,有两种解决方案。
方式一:SQL 起别名(简单场景)
<select id="selectAll" resultType="Dept">
SELECT id, deptNo no, deptName name FROM dept
</select>
通过
deptNo no别名,将列deptNo映射为属性no。
方式二:<resultMap>(推荐,功能更强)
<!-- 定义 resultMap -->
<resultMap id="deptMap" type="dept">
<!-- 主键列用 <id> 标签(可优化性能) -->
<id column="id" property="id"/>
<!-- 普通列用 <result> 标签 -->
<result column="deptNo" property="no"/>
<result column="deptName" property="name"/>
</resultMap>
<!-- 使用 resultMap -->
<select id="selectAll1" resultMap="deptMap">
SELECT id, deptNo, deptName FROM dept
</select>
<resultMap> 子元素说明
| 子标签 | 用途 | 属性 |
|---|---|---|
<id> |
标记主键字段映射(MyBatis 用主键做缓存优化) | column(列名)、property(属性名) |
<result> |
标记普通字段映射 | column(列名)、property(属性名) |
<association> |
一对一关联映射 | 见第十章 |
<collection> |
一对多关联映射 | 见第十一章 |
一对一关联查询 — <association>
场景:一个订单(Order)对应一个用户(AUser),Order 中包含 AUser 对象。
数据库关系:Order 表有外键
uid指向 AUser 表的主键id。
实体类
// Order.java — 多方,持有 AUser 引用
@Data
public class Order {
private Integer id;
private Date ordertime;
private double total;
private Integer uid; // 外键
private AUser aUser; // 一对一关联对象
}
// AUser.java — 一方
@Data
public class AUser {
private Integer id;
private String username;
private String password;
private List<Order> orderList; // 一对多(见第十一章)
}
方式一:直接映射嵌套属性
直接在 <resultMap> 中用 . 语法映射嵌套对象的属性:
<resultMap id="orderMap" type="order">
<id column="id" property="id"/>
<result column="ordertime" property="ordertime"/>
<result column="total" property="total"/>
<result column="uid" property="uid"/>
<!-- 直接用 property="aUser.xxx" 映射嵌套属性 -->
<result column="uid" property="aUser.id"/>
<result column="username" property="aUser.username"/>
<result column="password" property="aUser.password"/>
</resultMap>
<select id="selectALL" resultMap="orderMap">
SELECT o.*, a.username, a.password
FROM `order` o, auser a
WHERE o.uid = a.id
</select>
方式二:<association> 嵌套结果映射
将关联对象单独映射,结构更清晰:
<resultMap id="orderMap1" type="order">
<id column="id" property="id"/>
<result column="ordertime" property="ordertime"/>
<result column="total" property="total"/>
<result column="uid" property="uid"/>
<!-- association 映射一对一关联 -->
<association property="aUser" javaType="aUser">
<id column="uid" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
</association>
</resultMap>
<select id="selectAll1" resultMap="orderMap1">
SELECT o.*, a.username, a.password
FROM `order` o, auser a
WHERE o.uid = a.id
</select>
方式三:<association> 嵌套查询(延迟加载 / 分步查询)
将一个 SQL 拆成两步(查 Order → 查 AUser),支持按需加载:
<!-- 第一步:查所有 Order -->
<resultMap id="orderMap2" type="order">
<id column="id" property="id"/>
<result column="ordertime" property="ordertime"/>
<result column="total" property="total"/>
<result column="uid" property="uid"/>
<!-- column="uid" 作为参数传给 select 指定的查询 -->
<!-- select 指定另一个 Mapper 的查询方法(全限定名.方法名) -->
<association column="uid" property="aUser" javaType="aUser"
select="org.example.mapper.AUserMapper.selectById"/>
</resultMap>
<select id="selectAll2" resultMap="orderMap2">
SELECT * FROM `order`
</select>
<!-- 第二步:被嵌套调用的 AUserMapper.selectById -->
<select id="selectById" resultType="AUser">
SELECT * FROM auser WHERE id = #{id}
</select>
执行流程:先执行
selectAll2查询所有 Order → 对每条 Order 结果,用uid列的值作为参数,调用AUserMapper.selectById查询对应的 AUser → 将结果赋给Order.aUser。
N+1 问题:如果查询了 N 条 Order,会额外执行 N 次
selectById。在批量查询场景下应优先使用方式二(JOIN 查询)。
一对多关联查询 — <collection>
场景:一个用户(AUser)拥有多个订单(Order),AUser 中持有 List<Order>。
方式一:嵌套结果映射
通过 SQL JOIN 一次性查出所有数据,用 <collection> 将结果组装成一对多结构:
<resultMap id="aUserMap" type="aUser">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<!-- collection 映射一对多关联 -->
<!-- ofType:集合中元素的类型(不是 javaType!) -->
<collection property="orderList" ofType="order">
<id column="oid" property="id"/> <!-- 注意:oid 是别名,与用户 id 区分 -->
<result column="ordertime" property="ordertime"/>
<result column="total" property="total"/>
<result column="id" property="uid"/>
</collection>
</resultMap>
<select id="selectAll" resultMap="aUserMap">
SELECT a.*, o.id oid, o.ordertime, o.total
FROM auser a, `order` o
WHERE o.uid = a.id
</select>
关键点:
ofType指定的是集合中元素的类型(order),不是javaType="list"。
方式二:嵌套查询(分步查询)
<resultMap id="aUserMap1" type="aUser">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<!-- column="id" 作为参数传给 select 指定的查询 -->
<collection column="id" property="orderList" ofType="order"
select="org.example.mapper.OrderMapper.selectByID"/>
</resultMap>
<select id="selectAll1" resultMap="aUserMap1">
SELECT * FROM auser
</select>
<!-- OrderMapper 中对应的查询 -->
<select id="selectByID" resultType="Order">
SELECT * FROM `order` WHERE uid = #{uid}
</select>
<association> vs <collection> 对比
<association> |
<collection> |
|
|---|---|---|
| 对应关系 | 一对一(多方持有一方引用) | 一对多(一方持有多方集合) |
| Java 属性类型 | 单个对象(如 AUser aUser) |
集合类型(如 List<Order> orderList) |
| 类型属性 | javaType — 指定对象类型 |
ofType — 指定集合元素类型 |
column |
传给嵌套查询的参数列 | 传给嵌套查询的参数列 |
select |
嵌套调用的查询 ID | 嵌套调用的查询 ID |
MyBatis 各标签/属性速查表
XML 映射文件顶层标签
| 标签 | 作用 |
|---|---|
<mapper> |
根标签,namespace 对应 Mapper 接口全限定名 |
<select> |
定义查询语句 |
<insert> |
定义插入语句 |
<update> |
定义更新语句 |
<delete> |
定义删除语句 |
<sql> |
定义可复用的 SQL 片段 |
<resultMap> |
定义结果集映射规则 |
SQL 语句标签通用属性
| 属性 | 作用 | 示例 |
|---|---|---|
id |
唯一标识,对应 Mapper 接口的方法名 | "selectById" |
parameterType |
参数类型(全限定名或别名),可省略让 MyBatis 自动推断 | "User"、"map"、"int" |
resultType |
返回值类型(用于简单映射,列名须与属性名一致) | "User"、"int"、"org.example.bean.User" |
resultMap |
引用 <resultMap> 定义的映射规则(与 resultType 互斥) |
"userMap" |
useGeneratedKeys |
(仅 <insert>)启用 JDBC 自增主键回填 |
"true" / "false" |
keyColumn |
(仅 <insert>)数据库主键列名 |
"id" |
keyProperty |
(仅 <insert>)主键对应的 Java 属性名 |
"id" |
flushCache |
执行后是否清空缓存 | "true" / "false" |
statementType |
Statement 类型 | STATEMENT / PREPARED(默认) / CALLABLE |
timeout |
超时时间(秒) | "30" |
动态 SQL 标签
| 标签 | 作用 | 关键属性 |
|---|---|---|
<if> |
条件判断 | test:OGNL 表达式,如 "userName != null" |
<where> |
自动添加 WHERE,去除开头多余的 AND/OR |
无 |
<set> |
自动添加 SET,去除尾部多余的逗号 |
无 |
<trim> |
通用的字符串修剪(可代替 <where>/<set>) |
prefix、suffix、prefixOverrides、suffixOverrides |
<foreach> |
遍历集合生成批量 SQL | collection、item、open、close、separator |
<choose> |
多条件分支(类似 Java switch) |
包含 <when> 和 <otherwise> |
<when> |
<choose> 中的条件分支 |
test |
<otherwise> |
<choose> 中的默认分支 |
无 |
<bind> |
创建变量绑定到上下文 | name、value |
<trim> 属性
| 属性 | 作用 | 示例 | ||
|---|---|---|---|---|
prefix |
在内容前添加的前缀 | "where"、"set" |
||
suffix |
在内容后添加的后缀 | ")" |
||
prefixOverrides |
去除内容开头的多余字符(`\ | ` 分隔多个) | `”and\ | or”` |
suffixOverrides |
去除内容末尾的多余字符(`\ | ` 分隔多个) | "," |
<foreach> 属性
| 属性 | 含义 | 默认可用值 |
|---|---|---|
collection |
要遍历的集合 | list(List)、array(数组)、collection(通用)、@Param 别名 |
item |
当前迭代元素的变量名 | 自定义,如 "u"、"id" |
index |
当前迭代的索引(可选) | 自定义,如 "idx" |
open |
拼接在整个循环结果的开头 | "("、"id IN (" |
close |
拼接在整个循环结果的结尾 | ")" |
separator |
每次迭代之间的分隔符 | "," |
<resultMap> 子标签
| 子标签 | 用途 | 关键属性 |
|---|---|---|
<id> |
主键列映射(底层用于缓存优化) | column(列名)、property(属性名) |
<result> |
普通列映射 | column、property |
<association> |
一对一关联映射 | property、javaType、column(传给嵌套查询)、select(嵌套查询 ID) |
<collection> |
一对多关联映射 | property、ofType(元素类型)、column、select |
<selectKey> 属性
| 属性 | 可选值/说明 |
|---|---|
order |
BEFORE(插入前生成主键) / AFTER(插入后获取自增主键) |
keyColumn |
数据库中的主键列名 |
keyProperty |
Java 实体类中的主键属性名 |
resultType |
主键的 Java 类型,如 "integer"、"string" |
MyBatis 配置项(application.properties)
| 配置项 | 作用 |
|---|---|
mybatis.configuration.log-impl |
指定 SQL 日志输出实现 |
mybatis.configuration.map-underscore-to-camel-case |
自动驼峰命名映射(user_name → userName) |
mybatis.type-aliases-package |
实体类别名包扫描路径 |
mybatis.mapper-locations |
Mapper XML 文件路径 |
mybatis.configuration.cache-enabled |
是否开启二级缓存 |
mybatis.configuration.lazy-loading-enabled |
是否开启延迟加载 |
mybatis.configuration.aggressive-lazy-loading |
是否激进加载(true 则访问任一属性即加载全部延迟属性) |
Lombok 注解
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data // 等价于:@Getter + @Setter + @ToString + @EqualsAndHashCode
@NoArgsConstructor // 生成无参构造方法
@AllArgsConstructor // 生成全参构造方法
public class User {
private Integer id;
private String userName;
// ...
}
| 注解 | 作用 |
|---|---|
@Data |
自动生成 getter/setter、toString()、equals()、hashCode() |
@NoArgsConstructor |
生成无参构造器(MyBatis 反射创建对象时必须) |
@AllArgsConstructor |
生成包含所有字段的构造器 |
@Getter / @Setter |
单独生成 getter/setter |
@ToString |
单独生成 toString() |
@Builder |
生成建造者模式的构建器 |
重要:MyBatis 通过反射创建实体对象时需要无参构造器,所以
@NoArgsConstructor是必须的(如果用@Data但未显式声明,Lombok 默认不会生成无参构造——@Data只含@RequiredArgsConstructor,不含@NoArgsConstructor)。
附录:常见问题
Q1:#{} vs ${}
#{} |
${} |
|
|---|---|---|
| 机制 | 预编译占位符 ?(PreparedStatement) |
字符串直接拼接 |
| SQL 注入 | 安全,自动转义 | 不安全,不转义 |
| 使用场景 | 普通参数值 | 动态表名、列名、ORDER BY 字段(必须自行校验) |
Q2:resultType vs resultMap
resultType |
resultMap |
|
|---|---|---|
| 适用场景 | 列名与属性名一致(或通过别名一致) | 列名与属性名不一致、关联查询、嵌套映射 |
| 配置复杂度 | 简单 | 需要定义 <resultMap> |
| 关联映射 | 不支持 | 支持 <association> / <collection> |
Q3:Mapper 注入失败的常见原因
- 未添加
@MapperScan注解或包路径写错 - XML 的
namespace与 Mapper 接口全限定名不一致 - XML 文件放在
resources下的路径与接口包路径不一致