Java(十)

46 阅读4分钟

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;
}

常用注解对照:

注解作用本章用法
@Datagetter/setter + toString + equals/hashCodeEntity
@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. 使用规范 / 团队约定

DoDon'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 运行时:

  1. Spring 为 Mapper 接口创建 JDK 动态代理
  2. 调用 userMapper.selectById(1) 时,按接口全名 + 方法 id 匹配 XML 中的 <select id="selectById">
  3. #{} 绑定参数,经 JDBC PreparedStatement 执行
  4. 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 statementXML namespace / id 不一致核对全限定名与方法名
找不到 XMLmapper-locations 错确认 XML 在 resources/mapper/ 下且文件名正确
You have an error in your SQL syntax ... near 'user'保留字未加反引号改为 `user` / `order`
Communications link failureMySQL 未启动或密码错Docker/your-password
列映射为 null未开驼峰或缺 resultMap查 map-underscore-to-camel-case
Property 'id' not foundinsert 未回填主键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. 练习题

  1. 为 selectList(User)增加按 username 模糊查询(LIKE CONCAT('%', #{username}, '%'))。
  2. 新增 UserMapper.selectByUsername;Service 中不存在时抛异常(正式写法见 04)。
  3. 订单支付:写 pay(Long orderId),将 status 从 0 改为 1,并校验当前必须是待支付。
  4. 把 UserMapper.selectById 改成 @Select 注解实现,对比 XML 与注解场景。
  5. 打开 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>

源代码

gitee.com/mschanges/s…