02 MyBatis CRUD
1. 本章目标与前置阅读
本章目标
- 在 Spring Boot 3 中集成 MyBatis(
mybatis-spring-boot-starter) - 配置数据源与
@MapperScan - 采用统一扁平包结构:
controller/、service/+service/imp/、entity/、mapper/;XML 平铺在resources/mapper/ - 为
user、order两张表完成 XML 方式完整 CRUD - 使用 Lombok 简化 Entity / 构造注入(
@Data、@RequiredArgsConstructor) - 理解 Entity、Mapper 接口、XML 三者关系
- 掌握逻辑删除、乐观锁、
JSON字段的基本写法 - 初步区分
#{}与${}的安全差异
前置阅读
- 01 Spring Boot 项目创建
- SQL + MySQL 部分
- 数据库已就绪(本地 MySQL 或 Docker;无环境可用 H2 见 §7)
2. 为什么项目里需要它
Controller 只处理 HTTP;业务数据在 MySQL 等关系库中。MyBatis 负责:
- 把 Java 方法调用映射为 SQL 执行
- SQL 写在 XML(或注解)中,便于 DBA 审查与动态 SQL 扩展
- 与 Spring 集成后,Mapper 接口由容器管理,Service 直接注入
不用持久层框架,就要手写 JDBC、关连接、防注入,团队成本极高。
flowchart LR
Ctrl[controller] --> Svc[service 接口]
Svc --> Imp[service.imp 实现]
Imp --> MapU[UserMapper]
Imp --> MapO[OrderMapper]
MapU --> XmlU[UserMapper.xml]
MapO --> XmlO[OrderMapper.xml]
XmlU --> DBu[(`user`)]
XmlO --> DBo[(`order`)]
3. 依赖与配置
3.1 Maven 依赖
在 pom.xml 的 <dependencies> 中添加
<!-- MyBatis Spring Boot Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version><!-- 与 Boot 3.3.x 兼容,版本以官方为准 -->
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 连接池(Boot 3 默认 HikariCP) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Lombok:编译期生成 getter/setter 等,version 由 Boot 父 POM 管理 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
IDEA:安装 Lombok 插件,并开启 Annotation Processing(Settings → Build → Compiler → Annotation Processors),否则编辑器会报「找不到 getter」。
3.2 数据源配置
application-local.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username: root
password: your-password
driver-class-name: com.mysql.cj.jdbc.Driver
sql:
init:
mode: always
schema-locations: classpath:schema.sql
continue-on-error: true
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发环境打印 SQL,生产应关闭
application.yml 中确保 spring.profiles.active: local
3.3 建表(本章主表)
在 MySQL 库 demo 中执行(也可放进 src/main/resources/schema.sql 由 Boot 初始化):
user、order是 MySQL 保留字,SQL 中必须用反引号`user`/`order`。
CREATE DATABASE demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL,
`email` VARCHAR(100) DEFAULT NULL,
`status` TINYINT DEFAULT 1 COMMENT '1启用 0禁用',
`dept_id` BIGINT DEFAULT NULL COMMENT '部门ID',
`extra` JSON DEFAULT NULL COMMENT '扩展 JSON 字段',
`version` INT DEFAULT 0 COMMENT '乐观锁版本号',
`deleted` TINYINT DEFAULT 0 COMMENT '逻辑删除 0未删 1已删',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_dept_id` (`dept_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS `order` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`order_no` VARCHAR(32) NOT NULL,
`user_id` BIGINT NOT NULL,
`amount` DECIMAL(10, 2) NOT NULL DEFAULT 0,
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已取消',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
| 表 | 本章要点 |
|---|---|
user | 完整 CRUD;逻辑删除(deleted);乐观锁(version);extra 用 String 存 JSON 文本 |
order | 完整 CRUD;按 user_id / order_no 查询;amount 用 BigDecimal |
3.4 @MapperScan
可写在启动类(示例工程采用此方式),或单独配置类:
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.demo.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
扫描 com.example.demo.mapper 包中的接口,注册为 Spring Bean。
3.5 本章目录结构
本章约定(扁平分层,不再按业务域拆子目录):
- Controller →
controller/ - Service 接口 →
service/,实现 →service/imp/ - Entity →
entity/(全部放一起) - Mapper 接口 →
mapper/(Java) - Mapper XML →
resources/mapper/(全部平铺,不再拆user/、order/子目录)
demo/
├── pom.xml
└── src/main/
├── java/com/example/demo/
│ ├── DemoApplication.java
│ ├── common/
│ ├── config/
│ ├── security/
│ ├── controller/
│ │ ├── HelloController.java
│ │ ├── UserController.java
│ │ └── OrderController.java
│ ├── service/
│ │ ├── UserService.java
│ │ ├── OrderService.java
│ │ └── imp/
│ │ ├── UserServiceImpl.java
│ │ └── OrderServiceImpl.java
│ ├── entity/
│ │ ├── User.java
│ │ └── Order.java
│ └── mapper/
│ ├── UserMapper.java
│ └── OrderMapper.java
└── resources/
├── application.yml
├── application-local.yml
├── schema.sql
└── mapper/
├── UserMapper.xml
└── OrderMapper.xml
flowchart TB
CTRL[controller.*]
SVC[service.* 接口]
IMP[service.imp.* 实现]
MAP[mapper.*]
ENT[entity.*]
CTRL --> SVC
SVC -.-> IMP
IMP --> MAP
MAP --> ENT
4. 最小可运行示例 — User 完整 CRUD
4.1 Entity(Lombok)
entity/User.java:
package com.example.demo.entity;
import lombok.Data;
import java.time.LocalDateTime;
/**
* {@code @Data} 生成 getter / setter / toString / equals / hashCode。
* 无自定义构造时仍保留无参构造,满足 MyBatis 反射实例化。
*/
@Data
public class User {
private Long id;
private String username;
private String email;
private Integer status;
private Long deptId;
/** 对应 JSON 列,Java 侧用字符串;也可后续换 Map / 自定义 TypeHandler */
private String extra;
private Integer version;
private Integer deleted;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
常用注解对照:
| 注解 | 作用 | 本章用法 |
|---|---|---|
@Data | getter/setter + toString + equals/hashCode | Entity |
@Getter / @Setter | 只生成访问器(比 @Data 更克制) | 可选替代 @Data |
@RequiredArgsConstructor | 为 final 字段生成构造 | Service / Controller 注入 |
deptId ↔ dept_id、createTime ↔ create_time 由 map-underscore-to-camel-case: true 自动映射;resultMap 仍建议显式写出,便于审查。
4.2 Mapper 接口
mapper/UserMapper.java:
package com.example.demo.mapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserMapper {
int insert(User user);
/** 乐观锁更新:WHERE id + version,成功则 version+1 */
int updateById(User user);
/** 逻辑删除:deleted = 1 */
int deleteById(@Param("id") Long id);
User selectById(@Param("id") Long id);
List<User> selectList(@Param("status") Integer status,
@Param("deptId") Long deptId);
}
4.3 XML Mapper
src/main/resources/mapper/UserMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
<resultMap id="UserMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="email" property="email"/>
<result column="status" property="status"/>
<result column="dept_id" property="deptId"/>
<result column="extra" property="extra"/>
<result column="version" property="version"/>
<result column="deleted" property="deleted"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<sql id="User_Column_List">
id, username, email, status, dept_id, extra,
version, deleted, create_time, update_time
</sql>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `user` (username, email, status, dept_id, extra, version, deleted)
VALUES (#{username}, #{email}, #{status}, #{deptId},
#{extra, jdbcType=OTHER}, #{version}, #{deleted})
</insert>
<!-- 乐观锁:带上旧 version;SET 时 version = version + 1 -->
<update id="updateById">
UPDATE `user`
SET username = #{username},
email = #{email},
status = #{status},
dept_id = #{deptId},
extra = #{extra, jdbcType=OTHER},
version = version + 1
WHERE id = #{id}
AND version = #{version}
AND deleted = 0
</update>
<update id="deleteById">
UPDATE `user`
SET deleted = 1
WHERE id = #{id}
AND deleted = 0
</update>
<select id="selectById" resultMap="UserMap">
SELECT <include refid="User_Column_List"/>
FROM `user`
WHERE id = #{id}
AND deleted = 0
</select>
<select id="selectList" resultMap="UserMap">
SELECT <include refid="User_Column_List"/>
FROM `user`
WHERE deleted = 0
<if test="status != null">
AND status = #{status}
</if>
<if test="deptId != null">
AND dept_id = #{deptId}
</if>
ORDER BY id DESC
</select>
</mapper>
逻辑删除 vs 物理删除:用户表用 UPDATE ... SET deleted = 1,查询一律带 deleted = 0。订单表本章用物理 DELETE(表结构无 deleted 列)。
乐观锁:更新条件含 version = #{version};若并发下版本已变,影响行数为 0,Service 应提示「数据已被他人修改」。
#{} vs ${}(点到为止):
| 写法 | 行为 | 安全 |
|---|---|---|
#{username} | 预编译占位符 | 默认使用 |
${columnName} | 字符串直接拼进 SQL | 仅白名单列名/表名,否则注入风险 |
4.4 Service 接口 + imp 实现
接口:service/UserService.java:
package com.example.demo.service;
import com.example.demo.entity.User;
import java.util.List;
public interface UserService {
Long create(User user);
User getById(Long id);
List<User> list(Integer status, Long deptId);
void update(User user);
void delete(Long id);
}
实现:service/imp/UserServiceImpl.java(@Service 写在实现类上):
package com.example.demo.service.imp;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
@Override
@Transactional
public Long create(User user) {
if (user.getStatus() == null) {
user.setStatus(1);
}
if (user.getVersion() == null) {
user.setVersion(0);
}
if (user.getDeleted() == null) {
user.setDeleted(0);
}
userMapper.insert(user);
return user.getId();
}
@Override
public User getById(Long id) {
return userMapper.selectById(id);
}
@Override
public List<User> list(Integer status, Long deptId) {
return userMapper.selectList(status, deptId);
}
@Override
@Transactional
public void update(User user) {
int rows = userMapper.updateById(user);
if (rows == 0) {
throw new IllegalStateException("更新失败:记录不存在、已删除或版本冲突");
}
}
@Override
@Transactional
public void delete(Long id) {
int rows = userMapper.deleteById(id);
if (rows == 0) {
throw new IllegalStateException("删除失败:记录不存在或已删除");
}
}
}
Controller 只依赖
UserService接口;Spring 注入唯一的UserServiceImpl。@RequiredArgsConstructor为final字段生成构造器。
4.5 Controller(统一放 controller 包)
controller/UserController.java:
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping
public Long create(@RequestBody User user) {
return userService.create(user);
}
@GetMapping("/{id}")
public User getById(@PathVariable Long id) {
return userService.getById(id);
}
@GetMapping
public List<User> list(@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long deptId) {
return userService.list(status, deptId);
}
@PutMapping("/{id}")
public void update(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
userService.update(user);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
userService.delete(id);
}
}
4.6 验证 User
# 创建(extra 传 JSON 字符串)
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"username":"alice","email":"alice@example.com","deptId":10,"extra":"{\"level\":\"vip\"}"}'
# 查询
curl http://localhost:8080/api/users/1
curl "http://localhost:8080/api/users?status=1&deptId=10"
# 更新(必须带当前 version,首次一般为 0)
curl -X PUT http://localhost:8080/api/users/1 \
-H "Content-Type: application/json" \
-d '{"username":"alice2","email":"a2@example.com","status":1,"deptId":10,"version":0}'
# 逻辑删除
curl -X DELETE http://localhost:8080/api/users/1
5. Order 完整 CRUD
订单归属用户:order.user_id → user.id(本章可不建外键约束,由业务保证)。
5.1 Entity(Lombok)
package com.example.demo.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
public class Order {
private Long id;
private String orderNo;
private Long userId;
private BigDecimal amount;
private Integer status;
private LocalDateTime createTime;
}
5.2 Mapper 接口
package com.example.demo.mapper;
import com.example.demo.entity.Order;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface OrderMapper {
int insert(Order order);
int updateById(Order order);
int deleteById(@Param("id") Long id);
Order selectById(@Param("id") Long id);
Order selectByOrderNo(@Param("orderNo") String orderNo);
List<Order> selectList(@Param("userId") Long userId,
@Param("status") Integer status);
}
5.3 XML Mapper
src/main/resources/mapper/OrderMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.OrderMapper">
<resultMap id="OrderMap" type="com.example.demo.entity.Order">
<id column="id" property="id"/>
<result column="order_no" property="orderNo"/>
<result column="user_id" property="userId"/>
<result column="amount" property="amount"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
</resultMap>
<sql id="Order_Column_List">
id, order_no, user_id, amount, status, create_time
</sql>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO `order` (order_no, user_id, amount, status)
VALUES (#{orderNo}, #{userId}, #{amount}, #{status})
</insert>
<update id="updateById">
UPDATE `order`
SET order_no = #{orderNo},
user_id = #{userId},
amount = #{amount},
status = #{status}
WHERE id = #{id}
</update>
<delete id="deleteById">
DELETE FROM `order` WHERE id = #{id}
</delete>
<select id="selectById" resultMap="OrderMap">
SELECT <include refid="Order_Column_List"/>
FROM `order`
WHERE id = #{id}
</select>
<select id="selectByOrderNo" resultMap="OrderMap">
SELECT <include refid="Order_Column_List"/>
FROM `order`
WHERE order_no = #{orderNo}
LIMIT 1
</select>
<select id="selectList" resultMap="OrderMap">
SELECT <include refid="Order_Column_List"/>
FROM `order`
<where>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY id DESC
</select>
</mapper>
5.4 Service + Controller(摘要)
- 接口:
service/OrderService.java - 实现:
service/imp/OrderServiceImpl.java(@Service) - 控制器:
controller/OrderController.java,路径/api/orders
创建订单时:默认 status = 0(待支付);若未传 orderNo,可用 ORD + 时间戳简单生成(生产应换雪花/号段)。
// OrderServiceImpl.create 要点
if (order.getStatus() == null) {
order.setStatus(0);
}
if (order.getOrderNo() == null || order.getOrderNo().isBlank()) {
order.setOrderNo("ORD" + System.currentTimeMillis());
}
orderMapper.insert(order);
方法与 User 对称(POST / GET / PUT / DELETE)。完整代码见 examples/02-mybatis/。
5.5 验证 Order
# 先有用户 id=1,再下单
curl -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"userId":1,"amount":99.90,"status":0}'
curl http://localhost:8080/api/orders/1
curl "http://localhost:8080/api/orders?userId=1&status=0"
curl -X PUT http://localhost:8080/api/orders/1 \
-H "Content-Type: application/json" \
-d '{"orderNo":"ORD1001","userId":1,"amount":88.00,"status":1}'
curl -X DELETE http://localhost:8080/api/orders/1
6. 使用规范 / 团队约定
| Do | Don't |
|---|---|
SQL 主线放 XML,复杂动态 SQL 用 <if> / <where>(见 12 章) | 在 Controller 写 SQL 或堆砌 @Select |
参数占位用 #{} | 用 ${} 拼接用户输入 |
namespace 与 Mapper 接口全限定名一致 | XML 文件名/路径随意导致找不到语句 |
保留字表名写 `user` / `order` | 裸写 FROM user 导致语法错误 |
| Controller / Service / Entity / Mapper 各自平铺;XML 不按域分子目录 | 再按业务域拆多套 controller/service/entity 目录 |
@Service 标在 实现类;Controller 只依赖接口 | 在接口上标 @Service;Controller 依赖 *Impl |
有 deleted 的表:查改都过滤未删;删用逻辑删除 | 逻辑删后查询仍返回「已删」行 |
有 version 的表:更新带版本校验 | 覆盖更新却忽略并发 |
| Entity 映射表字段;接口出入参后续可用 DTO | 把 Entity 当万能 VO 到处传 |
Entity 用 @Data(或 @Getter/@Setter);注入用 @RequiredArgsConstructor | 手写大段 getter/setter;Entity 上滥用 @Builder 却忘无参构造 |
开发可开 SQL 日志;生产关闭 StdOutImpl | 生产打印全量 SQL |
| Mapper 只做数据访问,业务在 Service 实现类 | 在 XML 里写「是否发邮件」类逻辑 |
注解 SQL 对比(小查询可用,团队大项目仍推荐 XML):
@Select("SELECT id, username FROM `user` WHERE id = #{id} AND deleted = 0")
User selectById(@Param("id") Long id);
7. 原理简述
MyBatis 运行时:
- Spring 为 Mapper 接口创建 JDK 动态代理
- 调用
userMapper.selectById(1)时,按接口全名 + 方法 id 匹配 XML 中的<select id="selectById"> #{}绑定参数,经 JDBC PreparedStatement 执行resultMap将ResultSet映射为 Entity
sequenceDiagram
participant Ctrl as UserController
participant Svc as UserService
participant Imp as UserServiceImpl
participant Map as UserMapper 代理
participant XML as UserMapper.xml
participant DB as MySQL
Ctrl->>Svc: getById(1)
Svc->>Imp: getById(1)
Imp->>Map: selectById(1)
Map->>XML: 匹配 select id
XML->>DB: PreparedStatement
DB-->>Map: ResultSet
Map-->>Imp: User
Imp-->>Ctrl: User
8. 常见错误与排查
| 现象 | 可能原因 | 排查 |
|---|---|---|
BindingException: Invalid bound statement | XML namespace / id 不一致 | 核对全限定名与方法名 |
| 找不到 XML | mapper-locations 错 | 确认 XML 在 resources/mapper/ 下且文件名正确 |
You have an error in your SQL syntax ... near 'user' | 保留字未加反引号 | 改为 `user` / `order` |
Communications link failure | MySQL 未启动或密码错 | Docker/your-password |
| 列映射为 null | 未开驼峰或缺 resultMap | 查 map-underscore-to-camel-case |
Property 'id' not found | insert 未回填主键 | useGeneratedKeys + keyProperty="id" |
| 更新总返回 0 行 | 乐观锁 version 过期或已逻辑删 | 先 selectById 取最新 version |
| 删了还能查到 | 查询未过滤 deleted = 0 | 补条件 |
| JSON 写入失败 | 驱动/类型 | jdbcType=OTHER 或改用字符串列 |
| 启动无 Mapper Bean | 未 @MapperScan | 检查 @MapperScan("com.example.demo.mapper") |
| IDEA 红字找不到 getter | 未装 Lombok 插件 / 未开 Annotation Processing | 装插件并开启;mvn compile 仍可通过则属 IDE 问题 |
运行期 NoSuchMethodError / 无参构造缺失 | 自定义了全参构造却未保留无参 | Entity 加 @NoArgsConstructor(或勿手写破坏默认构造) |
无 Docker / 无 MySQL:H2 内存库,schema.sql 建表,url 设 jdbc:h2:mem:demo;MODE=MySQL;extra 可改为 VARCHAR(512)。
9. 练习题
- 为
selectList(User)增加按username模糊查询(LIKE CONCAT('%', #{username}, '%'))。 - 新增
UserMapper.selectByUsername;Service 中不存在时抛异常(正式写法见 04)。 - 订单支付:写
pay(Long orderId),将status从0改为1,并校验当前必须是待支付。 - 把
UserMapper.selectById改成@Select注解实现,对比 XML 与注解场景。 - 打开 SQL 日志,确认
#{}对应?占位符。
练习参考片段
1. username 模糊查询
<select id="selectListByUsernameLike" resultMap="UserMap">
SELECT <include refid="User_Column_List"/>
FROM `user`
WHERE deleted = 0
AND username LIKE CONCAT('%', #{username}, '%')
ORDER BY id DESC
</select>
2. selectByUsername
<select id="selectByUsername" resultMap="UserMap">
SELECT <include refid="User_Column_List"/>
FROM `user`
WHERE deleted = 0
AND username = #{username}
LIMIT 1
</select>
5. 确认 ? 占位符
==> Preparing: SELECT ... FROM `user` WHERE id = ? AND deleted = 0
==> Parameters: 1(Long)
下一篇 MyBatis-Plus
MyBatis-Plus 在 MyBatis 之上提供通用 CRUD(如 BaseMapper<T>)、分页插件、条件构造器,能少写 XML;逻辑删除、乐观锁也有注解支持。
| 对比 | MyBatis(主线) | MyBatis-Plus |
|---|---|---|
| SQL 控制 | 完全手写,透明 | 简单 CRUD 可自动生成 |
| 学习成本 | 先懂 SQL 与映射 | 需额外学 Wrapper/API |
| 适用 | 复杂 SQL、团队 XML 审查 | 管理后台式单表 CRUD |
何时再学:熟练本章 XML CRUD 后,若项目大量单表且无复杂 SQL,可评估 Plus。本手册主线以 MyBatis XML 为准。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>