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

474 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 第三阶段教案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`,逐行理解每条语句的作用:
```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 工具执行以下查询:
```sql
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`
```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`
```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`
```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 SQL`application.yml` 中 `show-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. Mapper`repository/mp/StudentMapper.java`
```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 的杀手级功能
```java
// 查全部、按成绩降序
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 比字符串安全?**
```java
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 分页**
```java
PageRequest pr = PageRequest.of(0, 5, Sort.by(Direction.DESC, "score"));
Page<Student> page = repository.findAll(pr);
// page.getContent() → 当前页数据
// page.getTotalElements() → 总记录数
// page.getTotalPages() → 总页数
```
**MP 分页**
```java
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 详解
```java
@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`**
页面中的 JavaScript`js/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`,找出以下模式:
```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.html`F12 打开开发者工具
2. 用"元素选择器"工具点击表格某一行,查看它的 CSS 规则
3. 修改 `style.css`:把侧边栏主色 `#1890ff` 改成你喜欢的颜色
4. 给统计卡片增加一个"鼠标悬停时轻微放大"的动画效果:
```css
.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 工具箱齐全的开发者。