Week 1-8: Spring Boot 学习计划完整项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
473
week3/教案.md
Normal file
473
week3/教案.md
Normal file
@@ -0,0 +1,473 @@
|
||||
# 第三阶段教案: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` 中的数据库密码为你自己的密码
|
||||
|
||||
---
|
||||
|
||||
## 项目导入
|
||||
|
||||
IDEA:File → 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 是什么?
|
||||
|
||||
JPA(Java Persistence API)是 Java 官方的 ORM 标准,Hibernate 是它的最流行实现。
|
||||
|
||||
**ORM(Object-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 工具箱齐全的开发者。
|
||||
Reference in New Issue
Block a user