CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Spring Boot 实战入门/课时/JDBC 与 MyBatis
RawBlame
课时目录16 节

Spring Boot 实战入门

  1. 01Spring Boot 是什么
  2. 02Starter 与自动配置
  3. 03配置与 Profile
  4. 04IoC 与依赖注入
  5. 05Bean 作用域与生命周期
  6. 06Spring AOP 入门
  7. 07Spring MVC 与 REST
  8. 08参数校验与异常处理
  9. 09JDBC 与 MyBatis
  10. 10Spring Data JPA 入门
  11. 11事务管理
  12. 12Spring Security 基础
  13. 13JWT 与 API 鉴权
  14. 14单元测试与 MockMvc
  15. 15Actuator 与健康检查
  16. 16综合练习与进阶路线

JDBC 与 MyBatis

Mapper、SQL 映射;对照本站 MyBatis-Flex 用法。

40 分钟

JDBC 与 MyBatis

这一课你将能做到

  1. 配置数据源并用 JdbcTemplate 完成增删改查,理解 DataSource 与连接池的角色
  2. 用 MyBatis 注解和 XML 两种方式写 Mapper,会用 <if>/<where> 写动态 SQL
  3. 说清 #{} 与 ${} 的区别,避免 SQL 注入这个最高频的安全漏洞

为什么要学

到目前为止任务数据都存在内存 Map 里,应用一重启就没了。真实系统要落库,而Java 后端绕不开 SQL:复杂报表、多表关联、性能调优,最终都落在 SQL 上。

MyBatis 是国内企业的事实标准(本站后端用的 MyBatis-Flex 也是 MyBatis 系的增强框架):SQL 完全由你掌控、可读可调优,正好补足 ORM 框架对复杂 SQL 的短板。学它的前置是理解 JDBC 这套地基——DataSource、Connection、参数占位符这些概念在所有持久层框架里通用。

先建立图像

你的代码            MyBatis 帮你干的活              数据库
─────────          ──────────────────            ──────
TaskMapper          ① 根据接口方法找到对应 SQL
  .findById(id)  →  ② 把 id 填进 SQL 占位符     →   SELECT ... WHERE id = ?
                    ③ 执行查询,拿到 ResultSet
   Task 对象    ←   ④ 按映射规则把行转成对象      ←   一行行数据

人话:JDBC 是 Java 操作数据库的原始标准(手动开关连接、手动填参数、手动取结果);MyBatis 把「填参数、取结果、转对象」这些机械劳动自动化,SQL 本身还是你写。

名词人话
DataSource连接池 + 配置(URL、账号、密码),管着一池子可复用的连接
Connection到数据库的一条 TCP 会话,用完归还池子,不 new 不 close 原始对象
MapperMyBatis 的数据访问接口,方法 ↔ SQL 的一一映射
ResultMap「查询结果列 → Java 字段」的映射规则

跟着做

步骤 1:引入依赖与数据源配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- H2:本课零安装练手用;生产换 MySQL 驱动 -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
<!-- 生产 MySQL 时启用:
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
-->

application-dev.yml 配置数据源(Boot 检测到 jdbc 依赖会自动装配 HikariCP 连接池):

spring:
  datasource:
    url: jdbc:h2:mem:taskflow;DB_CLOSE_DELAY=-1
    driver-class-name: org.h2.Driver
    username: sa
    password: ""
  h2:
    console:
      enabled: true          # 提供 /h2-console 网页端查看数据(仅开发用)

步骤 2:用 schema.sql 建表

Boot 对内嵌数据库会自动执行 classpath 下的 schema.sql 和 data.sql。新建 src/main/resources/schema.sql:

CREATE TABLE IF NOT EXISTS task (
    id          BIGINT AUTO_INCREMENT PRIMARY KEY,
    title       VARCHAR(50)  NOT NULL,
    priority    INT          NOT NULL DEFAULT 3,
    status      VARCHAR(20)  NOT NULL DEFAULT 'TODO',
    created_at  TIMESTAMP    NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO task (title, priority, status) VALUES
('学习 Spring Boot 配置', 1, 'DONE'),
('学习 MyBatis 动态 SQL', 2, 'DOING'),
('复习事务传播行为', 3, 'TODO');

启动后打开 http://localhost:8080/h2-console(JDBC URL 填 jdbc:h2:mem:taskflow)能直接看到表和数据——先确认库通了,再写代码。

步骤 3:JdbcTemplate 版 CRUD

package com.example.taskflow.repository;

import com.example.taskflow.model.Task;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public class TaskJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    public TaskJdbcRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    private static final RowMapper<Task> MAPPER = (rs, rowNum) -> mapRow(rs);

    private static Task mapRow(ResultSet rs) throws SQLException {
        Timestamp ts = rs.getTimestamp("created_at");
        LocalDateTime createdAt = ts != null ? ts.toLocalDateTime() : null;
        return new Task(
                rs.getLong("id"),
                rs.getString("title"),
                rs.getInt("priority"),
                rs.getString("status"),
                createdAt
        );
    }

    public List<Task> findAll() {
        return jdbcTemplate.query("SELECT * FROM task ORDER BY priority, id", MAPPER);
    }

    public Optional<Task> findById(long id) {
        List<Task> list = jdbcTemplate.query(
                "SELECT * FROM task WHERE id = ?", MAPPER, id);
        return list.stream().findFirst();
    }

    public int insert(String title, int priority) {
        return jdbcTemplate.update(
                "INSERT INTO task(title, priority, status) VALUES (?, ?, 'TODO')",
                title, priority);
    }

    public int updateStatus(long id, String status) {
        return jdbcTemplate.update("UPDATE task SET status = ? WHERE id = ?", status, id);
    }

    public int delete(long id) {
        return jdbcTemplate.update("DELETE FROM task WHERE id = ?", id);
    }
}

体会三点:? 占位符防注入;query 返回 List、update 返回影响行数;RowMapper 负责「行 → 对象」的手工转换——这种模板代码正是 MyBatis 要消灭的。

步骤 4:MyBatis 注解版 Mapper

添加依赖(Boot 3 对应 mybatis-spring-boot-starter 3.x):

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.4</version>
</dependency>

写 Mapper 接口(只有接口,没有实现类):

package com.example.taskflow.mapper;

import com.example.taskflow.model.Task;
import org.apache.ibatis.annotations.*;

import java.util.List;
import java.util.Optional;

@Mapper
public interface TaskMapper {

    @Select("SELECT * FROM task WHERE id = #{id}")
    Optional<Task> findById(long id);

    @Select("SELECT * FROM task ORDER BY priority, id")
    List<Task> findAll();

    @Insert("INSERT INTO task(title, priority, status) VALUES(#{title}, #{priority}, 'TODO')")
    @Options(useGeneratedKeys = true, keyProperty = "id")   // 回填自增主键
    int insert(Task task);

    @Update("UPDATE task SET status = #{status} WHERE id = #{id}")
    int updateStatus(@Param("id") long id, @Param("status") String status);

    @Delete("DELETE FROM task WHERE id = #{id}")
    int delete(long id);
}

Controller/Service 里直接注入使用:

@RestController
@RequestMapping("/api/tasks")
public class TaskDbController {

    private final TaskMapper taskMapper;

    public TaskDbController(TaskMapper taskMapper) {
        this.taskMapper = taskMapper;
    }

    @GetMapping("/{id}")
    public Task detail(@PathVariable long id) {
        return taskMapper.findById(id)
                .orElseThrow(() -> new BusinessException(ErrorCode.TASK_NOT_FOUND));
    }
}

为什么接口不用写实现?MyBatis 在启动时为每个 @Mapper 接口生成动态代理,调用方法时代理根据「注解或 XML 里同 id 的 SQL」去执行——和上一课 AOP 的代理思想同源。

步骤 5:XML 版 + 动态 SQL(企业里最常用的形态)

复杂 SQL 都走 XML。配置扫描路径:

mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true    # created_at → createdAt 自动映射

src/main/resources/mapper/TaskMapper.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="com.example.taskflow.mapper.TaskMapper">

    <!-- 动态 SQL:条件拼装,解决「筛选条件可空」的经典问题 -->
    <select id="search" resultType="com.example.taskflow.model.Task">
        SELECT * FROM task
        <where>
            <if test="status != null and status != ''">
                AND status = #{status}
            </if>
            <if test="keyword != null and keyword != ''">
                AND title LIKE CONCAT('%', #{keyword}, '%')
            </if>
            <if test="minPriority != null">
                AND priority &lt;= #{minPriority}
            </if>
        </where>
        ORDER BY priority, id
    </select>

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO task(title, priority, status)
        VALUES(#{title}, #{priority}, #{status})
    </insert>
</mapper>

接口补对应方法:

List<Task> search(@Param("status") String status,
                  @Param("keyword") String keyword,
                  @Param("minPriority") Integer minPriority);

<where> 标签会自动处理开头多余的 AND;&lt;= 是 XML 转义的小于等于(或用 <![CDATA[ <= ]]>)。map-underscore-to-camel-case 开启后,SELECT * 的下划线列自动对上驼峰字段,resultMap 都省了。

步骤 6:用日志亲眼看到 SQL

logging:
  level:
    com.example.taskflow.mapper: DEBUG

调用 /api/tasks/1,控制台输出:

==>  Preparing: SELECT * FROM task WHERE id = ?
==> Parameters: 1(Long)
<==      Total: 1

「Preparing + Parameters」两行是排查数据问题的第一现场:SQL 长什么样、参数填了什么,一目了然。

讲清楚要点

  1. 占位符是安全底线:#{} 预编译参数化,值永远当「数据」处理;${} 是字符串拼接,值直接混进 SQL 文本。只有动态表名/排序字段这类「结构拼接」才允许用 ${},且必须白名单校验。WHERE title = '${keyword}' 传入 ' OR '1'='1 即注入成功——这是面试和代码审查双重必考。
  2. 连接池是性能基石:HikariCP 复用 TCP 连接,省去每次建连的握手开销。配置里最重要的是 maximum-pool-size(默认 10,按数据库承载能力调),连接泄漏(借了不还)是典型事故源。
  3. Mapper 是接口不是类:动态代理 + SQL 定位(注解或 XML 的 namespace.method)。报错 Invalid bound statement (not found) 意味着接口方法找不到对应 SQL——十有八九是 XML 路径、namespace 或方法 id 对不上。
  4. 注解 SQL 适合简单 CRUD,XML 适合一切复杂的:动态 SQL、多表关联、结果映射,XML 的可读性和可维护性碾压注解里的字符串拼接。企业实践:能写 XML 就写 XML。
  5. JdbcTemplate 没过时:临时运维脚本、复杂动态查询、不想引 MyBatis 的小服务,它依然是最轻量的选择。学它是为了理解持久层的地基动作。

你可能会懵的地方

  • MyBatis 和 JPA 到底选哪个? 国内业务系统主流是 MyBatis 系:SQL 可控、便于调优和 DBA 审查;JPA 在标准 CRUD 和领域建模上更省力。两者可以共存:报表走 MyBatis、标准 CRUD 走 JPA,下一课详细对比。

  • MyBatis-Flex 是什么?和本课学的什么关系? 它是基于 MyBatis 的增强框架(同族还有 MyBatis-Plus),提供 QueryWrapper 链式构造、逻辑删除、分页插件等能力,底层仍是 MyBatis 的 Mapper 机制。本课的 @Mapper、XML、动态 SQL 概念完全适用,学完直接能看懂本站后端代码。

  • 数据库下划线命名和 Java 驼峰为什么对不上? SQL 习惯 created_at,Java 习惯 createdAt。开启 map-underscore-to-camel-case: true 自动映射;或者在 XML 里显式写 <resultMap> 逐列映射,控制更精细。

  • 分页是 LIMIT 自己写还是用插件? 手写 LIMIT #{offset}, #{size} 需要再写一条 COUNT,繁琐;MyBatis 系项目通常引分页插件(PageHelper / MyBatis-Flex 的 paginate),一行代码返回「数据 + 总数」。原理都是拦截 SQL 自动改写。

  • 为什么我不能在 Mapper 方法里重载? 动态代理靠「方法名 + 参数位置」定位 SQL id,重载会造成多个同名方法无法区分。Mapper 的方法签名必须唯一。

错法对照

错法问题正解
${keyword} 拼接用户输入SQL 注入漏洞用 #{};仅结构拼接用 ${} + 白名单
忘记 mapper-locations 配置Invalid bound statementXML 放对路径并配置扫描
XML 里直接写 <=XML 解析报错&lt;= 或 CDATA 包裹
实体字段 camelCase 但没开驼峰映射查出来的字段全是 null开启 map-underscore-to-camel-case
Connection 用完手动 close 到底连接池被掏空交给 JdbcTemplate/MyBatis 自动归还

卡点急救

  1. Invalid bound statement (not found):按序检查——XML 文件在不在 mapper-locations 指向的路径;namespace 是不是接口全限定名;方法 id 与接口方法名是否一致;项目是否重新编译(target 里有没有该 XML)。
  2. H2 建表没生效:schema.sql 放在 src/main/resources 根目录;确认连接 URL 的库名一致;非内嵌库需显式配 spring.sql.init.mode: always。
  3. 插入后拿不到自增 id:注解方式确认 @Options(useGeneratedKeys = true, keyProperty = "id"),XML 方式确认 useGeneratedKeys="true" keyProperty="id",且传参是实体对象。
  4. 查询字段全为 null:列名与字段名不一致,开驼峰映射或补 resultMap;顺便看步骤 6 的 SQL 日志确认查到了数据。

面试常问

  • #{} 和 ${} 的区别? #{} 预编译占位,参数以 PreparedStatement 参数传入,防 SQL 注入;${} 直接字符串替换进 SQL,有注入风险,仅用于表名、列名等结构拼接且必须白名单校验。

  • MyBatis 的执行原理? 启动时解析 Mapper 接口与 XML/注解,为每个接口创建动态代理;调用方法时根据 namespace + 方法名定位 MappedStatement,参数对象解析成 SQL 参数,执行后按映射规则把 ResultSet 转成目标类型。

  • 一级缓存和二级缓存? 一级缓存是 SqlSession 级(默认开启,同一会话内相同查询复用;Spring 集成下每次请求新 session,基本感知不到);二级缓存是 namespace 级,跨会话共享,更新频繁场景易脏读,实践中基本不用,缓存交给 Redis。

  • 动态 SQL 有哪些标签? <if>、<where>、<set>、<choose>/<when>/<otherwise>、<foreach>(IN 条件与批量插入)、<trim>。能手写一个「多条件 + IN 查询」的完整 XML。

自测三问

  1. SELECT * FROM user WHERE name = '${name}' 传入 x' OR '1'='1 会发生什么?怎么改?
  2. Mapper 接口为什么不需要实现类?Invalid bound statement 的排查清单是什么?
  3. 动态 SQL 里 <where> 标签替你干了什么?

小结一句话

DataSource 管连接、#{} 防注入、Mapper 接口靠动态代理绑定 SQL——MyBatis 把 JDBC 的机械劳动自动化,而 SQL 的掌控权仍然在你手里。

延伸

  • MyBatis Spring Boot Starter 文档
  • MyBatis 动态 SQL 官方文档
  • 下一课:Spring Data JPA 入门——另一种持久层哲学:不写 SQL 也能干活。
加载本节练习…
← 上一课参数校验与异常处理下一课 →Spring Data JPA 入门