Files
gc-plan/week3/教案.md
2026-04-29 23:45:17 +08:00

17 KiB
Raw Permalink Blame History

第三阶段教案ORM 双轨 —— JPA + MyBatis-Plus第 3 周)

学习周期7 天 每日用时2-3 小时 最终产出:同时掌握 JPA 和 MyBatis-Plus 两种 ORM 框架,附带 HTML/CSS 前端页面


前置准备:初始化数据库

在开始之前,先执行 SQL 脚本创建数据库和表:

  1. 打开 MySQL 客户端(命令行 / Navicat / DataGrip
  2. 执行 sql/init.sql全部内容
  3. 验证:SELECT * FROM week3_student.student; 应看到 10 条預置数据
  4. 修改 application.yml 中的数据库密码为你自己的密码

项目导入

IDEAFile → Open → 选择 week3/pom.xml → Open as Project。

本项目同时引入 JPA 和 MyBatis-Plus 两套依赖,共用同一个 Student 实体类。


第 1 天MySQL 基础 + 表设计

为什么需要数据库?

第 2 周的通讯录用内存存储应用重启后数据就没了。数据库MySQL是持久化存储数据不会丢失。

核心概念

术语 解释 类比
数据库Database 存放一组相关表的容器 一个 Excel 文件
Table 数据以行列存储 Excel 中的一个 Sheet
Column/字段) 表的一个属性 Sheet 中的一列
Row/记录) 一条完整数据 Sheet 中的一行
主键Primary Key 唯一标识一行数据的列 身份证号
索引Index 加速查询的数据结构 书的目录

阅读 init.sql

打开 sql/init.sql,逐行理解每条语句的作用:

-- 创建数据库(字符集 utf8mb4 支持中文和 emoji
CREATE DATABASE IF NOT EXISTS week3_student
    DEFAULT CHARACTER SET utf8mb4;

-- 创建表
CREATE TABLE student (
    id          BIGINT  NOT NULL AUTO_INCREMENT  COMMENT '主键ID',
    name        VARCHAR(20) NOT NULL             COMMENT '姓名',
    ...
    PRIMARY KEY (id)                             -- 主键约束
) ENGINE=InnoDB;                                  -- InnoDB 引擎支持事务

关键点:

  • AUTO_INCREMENT —— 插入时不必指定 ID数据库自动递增
  • VARCHAR(20) —— 可变长度字符串,最多 20 个字符
  • NOT NULL —— 该列不能为空
  • DEFAULT —— 未指定时的默认值
  • COMMENT —— 列注释(给人看的,不影响数据库行为)

动手

  1. 执行 init.sql,验证数据是否插入成功
  2. 用命令行或 GUI 工具执行以下查询:
    SELECT * FROM student WHERE score >= 85 ORDER BY score DESC;
    SELECT COUNT(*) FROM student;
    SELECT AVG(score) FROM student;
    SELECT name, score FROM student WHERE name LIKE '%张%';
    

第 2 天Spring Data JPA —— 声明式数据访问

JPA 是什么?

JPAJava Persistence API是 Java 官方的 ORM 标准Hibernate 是它的最流行实现。

ORMObject-Relational Mapping:把数据库的表映射为 Java 对象。让你用操作对象的方式操作数据库,不用手写 SQL。

数据库表student (id, name, age, email, score)
     ↕  JPA 映射
Java 类Student { id, name, age, email, score }

JPA 的核心流程

StudentJpaRepository  ← 继承 JpaRepository<Student, Long>
       ↓ 自动生成
SQL → 执行 → 结果映射为 Student 对象

打开以下文件,对照阅读:

1. 实体:entity/Student.java

@Entity                        // 标记为 JPA 实体
@Table(name = "student")       // 映射到 student 表
public class Student {

    @Id                                                  // 主键
    @GeneratedValue(strategy = GenerationType.IDENTITY)  // 自增
    private Long id;

    @Column(name = "name", length = 20)  // 映射到 name 列
    private String name;
    // ...
}

理解@Entity + @Table 告诉 JPA"这个类对应哪张表"。@Column 告诉 JPA"这个属性对应哪个列"。有了这些注解JPA 就知道如何把查询结果自动填充为 Student 对象。

2. 仓库:repository/jpa/StudentJpaRepository.java

@Repository
public interface StudentJpaRepository extends JpaRepository<Student, Long> {

    List<Student> findByName(String name);                // ← 方法名即查询
    List<Student> findByNameContaining(String keyword);   // ← 自动模糊搜索
    List<Student> findByScoreGreaterThan(int score);      // ← 自动 > 条件

    @Query("SELECT s FROM Student s WHERE s.name LIKE %:kw%")  // ← 自定义 JPQL
    List<Student> searchByKeyword(@Param("kw") String kw);
}

理解你只需要定义接口和方法名JPA 自动生成 SQL。这就是"声明式"的含义——声明你想查什么,框架负责怎么查。

3. 服务:service/jpa/StudentJpaService.java

@Transactional  // 声明式事务:方法内所有 DB 操作在同一事务中
public Student add(Student student) {
    return repository.save(student);  // 一句代码完成 INSERT
}

动手

  1. 启动项目,用 Postman 测试 JPA 接口:
    GET  /api/jpa/students              —— 查全部
    GET  /api/jpa/students?keyword=张    —— 模糊搜索
    GET  /api/jpa/students/1            —— 查单个
    POST /api/jpa/students              —— 新增
    PUT  /api/jpa/students/1            —— 更新
    DELETE /api/jpa/students/1          —— 删除
    GET  /api/jpa/students/page?pageNum=1&pageSize=3  —— 分页
    GET  /api/jpa/students/stats?min=80&max=100  —— 统计
    
  2. 观察控制台输出的 JPA SQLapplication.ymlshow-sql: true

第 3 天MyBatis-Plus —— 灵活的 SQL 利器

MyBatis-Plus 是什么?

MyBatis 是半自动 ORM你需要自己写 SQL在 XML 或注解中但框架帮你做结果映射。MyBatis-Plus 在 MyBatis 基础上做了增强:常见 CRUD 不用写 SQL。

JPA vs MyBatis-Plus 设计哲学

JPA MyBatis-Plus
设计理念 面向对象 → 自动生成 SQL SQL 为核心 → 辅助生成
你控制什么 定义实体和方法名 直接写 SQL 或用 Lambda 构建条件
适用场景 常规 CRUD、表关联规范 复杂查询、报表、性能优化

MP 的核心流程

StudentMapper ← 继承 BaseMapper<Student>
       ↓ LambdaQueryWrapper 构建条件 / @Select 写 SQL
SQL → 执行 → 结果映射为 Student 对象

打开以下文件,对照阅读:

1. Mapperrepository/mp/StudentMapper.java

@Mapper
public interface StudentMapper extends BaseMapper<Student> {

    // 方式一:@Select 直接写 SQL最灵活
    @Select("SELECT * FROM student WHERE name LIKE CONCAT('%', #{kw}, '%')")
    List<Student> searchByKeyword(@Param("kw") String kw);

    // 方式二:自定义分页 + SQL
    @Select("SELECT * FROM student WHERE name LIKE CONCAT('%', #{kw}, '%')")
    IPage<Student> searchByKeywordPage(Page<Student> page, @Param("kw") String kw);
}

2. 服务:service/mp/StudentMpService.java

重点LambdaQueryWrapper —— MP 的杀手级功能

// 查全部、按成绩降序
LambdaQueryWrapper<Student> wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(Student::getScore);        // 基于 Lambda列名不会写错
mapper.selectList(wrapper);

// 模糊搜索 + 范围 + 排序(链式调用)
wrapper.like(Student::getName, keyword)         // WHERE name LIKE '%keyword%'
       .ge(Student::getScore, 60)              // AND score >= 60
       .orderByDesc(Student::getScore);
mapper.selectList(wrapper);

// 分页
Page<Student> page = new Page<>(1, 5);         // 第 1 页,每页 5 条
mapper.selectPage(page, wrapper);

为什么 Lambda 比字符串安全?

wrapper.eq("name", "张三");    // ❌ "name" 写错了编译期不报错
wrapper.eq(Student::getName, "张三");  // ✅ 写错了编译期直接报红

动手

  1. 启动项目,用 Postman 测试 MP 接口(注意 /api/mp/ 前缀):
    GET  /api/mp/students              —— 查全部
    GET  /api/mp/students?keyword=张    —— 模糊搜索
    GET  /api/mp/students/1            —— 查单个
    POST /api/mp/students              —— 新增
    PUT  /api/mp/students/1            —— 更新
    DELETE /api/mp/students/1          —— 删除
    GET  /api/mp/students/page?pageNum=1&pageSize=3  —— 分页
    GET  /api/mp/students/excellent?threshold=85  —— 高分学生
    
  2. 观察控制台输出的 MP SQL 日志
  3. 对比同一操作在 JPA 和 MP 下生成的 SQL

第 4 天JPA vs MyBatis-Plus 全面对比

同步测试

用 Postman 分别调同一个操作,对比差异:

操作 JPA 接口 MP 接口 观察点
查全部 GET /api/jpa/students GET /api/mp/students 返回格式是否一致?
新增 POST /api/jpa/students POST /api/mp/students 控制台 SQL 有何不同?
分页 GET /api/jpa/students/page GET /api/mp/students/page 分页字段名一样吗?

对比总结(动手写)

维度 Spring Data JPA MyBatis-Plus
风格 声明式,方法名即查询 Lambda 条件构造器 + 原生 SQL
简单 CRUD JpaRepository 已提供 BaseMapper 已提供
复杂查询 @Query 写 JPQL / 方法命名 @Select 写原生 SQL / LambdaWrapper
分页 PageRequest + findAll() Page<T> + selectPage()
SQL 可见性 框架生成,不直观 控制台清晰输出,更透明
上手难度 需理解 JPA 规范 会 SQL 即可上手

今日任务

  1. 完成上面的对比测试表格(填入你的观察)
  2. 分别在两个 Controller 中新增一个接口:
    • JPA 版:GET /api/jpa/students/above/{score} —— 查询大于指定分数的学生
    • MP 版:GET /api/mp/students/above/{score} —— 同上
  3. 观察两者生成的 SQL体会"声明式"和"手动式"的差异

第 5 天:分页与事务

分页对比

JPA 分页

PageRequest pr = PageRequest.of(0, 5, Sort.by(Direction.DESC, "score"));
Page<Student> page = repository.findAll(pr);
// page.getContent()      → 当前页数据
// page.getTotalElements() → 总记录数
// page.getTotalPages()   → 总页数

MP 分页

Page<Student> page = new Page<>(1, 5);          // 页码从 1 开始
IPage<Student> result = mapper.selectPage(page, wrapper);
// result.getRecords() → 当前页数据
// result.getTotal()   → 总记录数
// result.getPages()   → 总页数

差异JPA 的 PageRequest 页码从 0 开始MP 从 1 开始。

@Transactional 详解

@Transactional
public Student add(Student student) {
    repository.save(student);   // 操作 1
    // ... 如果这里抛异常,
    //     操作 1 自动回滚,数据不会写入数据库
}

关键属性:

  • readOnly = true —— 只读事务,性能更好(跳过脏检查)
  • rollbackFor —— 指定哪些异常触发回滚(默认 RuntimeException 和 Error

今日任务

  1. 故意在新增逻辑后 throw new RuntimeException("模拟异常"),验证数据是否回滚
  2. 分别用 Postman 测试 JPA 和 MP 的分页接口,对比返回字段名
  3. 在 MySQL 中直接插入一条数据,用 API 验证能否查到

第 6 天HTML 基础 —— 为后端配一个前端

核心概念

概念 解释
HTML 网页的骨架 —— 定义"有什么"(标题、表格、按钮)
标签 <tag> 开始,</tag> 结束,如 <h1>标题</h1>
属性 标签的附加信息,如 <input type="text" placeholder="搜索...">
表单 <form> + <input>,用于收集用户输入

阅读 index.html

打开 src/main/resources/static/index.html,对照理解:

<!DOCTYPE html>         → 声明文档类型
<html>                  → 根元素
  <head>                → 元数据(标题、样式引用)
  <body>                → 页面可见内容
    <header>            → 头部区域
    <table>             → 表格:<thead> 表头 + <tbody> 数据行
    <form>              → 表单:<input> 各种输入框

如何在浏览器中访问

Spring Boot 默认把 src/main/resources/static/ 下的文件作为静态资源暴露。

启动项目后访问:http://localhost:8080/index.html

页面中的 JavaScriptjs/app.js)会调用 fetch() 访问后端 API 获取数据并渲染表格。

今日任务

  1. 访问 http://localhost:8080/index.html,确认页面正常加载
  2. 点击 "JPA 模式" / "MP 模式" Tab观察数据差异如果有的话
  3. 尝试新增一个学生,查看是否成功
  4. index.html 中的表格表头增加一列"成绩排名"(暂时填 "-"

第 7 天CSS 基础 —— 让页面好看

核心概念

概念 解释
CSS 网页的皮肤 —— 定义"长什么样"(颜色、间距、布局)
选择器 指定 CSS 样式作用于哪些元素(如 .tab 作用于 class="tab" 的元素)
盒模型 每个元素 = content + padding + border + margin
Flexbox 弹性布局,轻松实现水平排列和居中对齐
伪类 元素的状态,如 :hover(鼠标悬停)、:focus(获得焦点)

阅读 style.css

打开 src/main/resources/static/css/style.css,找出以下模式:

/* 类选择器 —— 作用于 class="xxx" 的元素 */
.toolbar {
    display: flex;        /* ← Flexbox 横向排列 */
    gap: 12px;            /* ← 子元素间距 */
}

/* 标签选择器 —— 作用于所有该标签 */
table {
    border-collapse: collapse;  /* ← 合并表格边框 */
}

/* 伪类 —— 鼠标悬停效果 */
.btn:hover {
    background: #0070d2;       /* ← 鼠标悬停时变色 */
}

/* 子元素选择器 —— 只作用于 table 下的 th */
thead th {
    font-weight: 600;
}

CSS 盒模型速查

┌──────── margin外边距元素与元素之间─────────┐
│  ┌─── border边框───┐                        │
│  │  ┌── padding内边距──┐                     │
│  │  │    content内容   │                     │
│  │  └──────────────────────┘                     │
│  └─────────────────────────────────┘             │
└─────────────────────────────────────────────────┘

今日任务

  1. 在浏览器中打开 index.htmlF12 打开开发者工具
  2. 用"元素选择器"工具点击表格某一行,查看它的 CSS 规则
  3. 修改 style.css:把侧边栏主色 #1890ff 改成你喜欢的颜色
  4. 给统计卡片增加一个"鼠标悬停时轻微放大"的动画效果:
    .stat-card:hover {
        transform: scale(1.05);
        box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    }
    

本周产出清单

✅ SQL 数据库表设计与创建student 表 + 10 条测试数据)
✅ JPA 完整 CRUD + 分页 + 模糊搜索 + 统计
✅ MyBatis-Plus 完整 CRUD + 分页 + Lambda 条件查询
✅ 同一实体类兼容两套 ORM 框架
✅ 对比测试笔记JPA vs MP 的 API 风格、SQL 差异、分页差异)
✅ @Transactional 事务验证
✅ HTML 前端页面(表格 + 表单 + 分页 + 统计数据)
✅ CSS 样式Flexbox 布局 + 颜色体系 + 响应式卡片)
✅ JavaScript fetch API 前后端联调

常见问题

问题 原因 解决
启动报 "Access denied for user" MySQL 密码不对 修改 application.yml 中的 password
启动报 "Unknown database" 没执行 init.sql 先在 MySQL 中执行 sql/init.sql
启动报 "Table 'student' doesn't exist" 执行 SQL 时没选对库 确保 USE week3_student; 后执行建表
JPA 查询中文返回空 字符集问题 检查 URL 参数:characterEncoding=utf-8
前端页面 404 静态资源路径不对 确认文件在 src/main/resources/static/
前端调接口跨域报错 前后端同源 本项目前后端同端口,不存在跨域问题
MP 分页不生效 没配置分页插件 确认 MyBatisPlusConfig 中注册了 PaginationInnerInterceptor
JPA 的 show-sql 不打印 日志级别太高 确认 logging.level.com.learn: DEBUG

本周核心JPA 和 MP 不是二选一的对手,而是一个工具箱里的两把刀。简单 CRUD 用 JPA 省力,复杂 SQL 用 MP 掌控。把两个都学透,你就是 ORM 工具箱齐全的开发者。