17 KiB
第三阶段教案:ORM 双轨 —— JPA + MyBatis-Plus(第 3 周)
学习周期:7 天 每日用时:2-3 小时 最终产出:同时掌握 JPA 和 MyBatis-Plus 两种 ORM 框架,附带 HTML/CSS 前端页面
前置准备:初始化数据库
在开始之前,先执行 SQL 脚本创建数据库和表:
- 打开 MySQL 客户端(命令行 / Navicat / DataGrip)
- 执行
sql/init.sql的全部内容 - 验证:
SELECT * FROM week3_student.student;应看到 10 条預置数据 - 修改
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,逐行理解每条语句的作用:
-- 创建数据库(字符集 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—— 列注释(给人看的,不影响数据库行为)
动手
- 执行
init.sql,验证数据是否插入成功 - 用命令行或 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 是什么?
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
@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
}
动手
- 启动项目,用 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 —— 统计 - 观察控制台输出的 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
@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, "张三"); // ✅ 写错了编译期直接报红
动手
- 启动项目,用 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 —— 高分学生 - 观察控制台输出的 MP SQL 日志
- 对比同一操作在 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 即可上手 |
今日任务
- 完成上面的对比测试表格(填入你的观察)
- 分别在两个 Controller 中新增一个接口:
- JPA 版:
GET /api/jpa/students/above/{score}—— 查询大于指定分数的学生 - MP 版:
GET /api/mp/students/above/{score}—— 同上
- JPA 版:
- 观察两者生成的 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)
今日任务
- 故意在新增逻辑后
throw new RuntimeException("模拟异常"),验证数据是否回滚 - 分别用 Postman 测试 JPA 和 MP 的分页接口,对比返回字段名
- 在 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 获取数据并渲染表格。
今日任务
- 访问
http://localhost:8080/index.html,确认页面正常加载 - 点击 "JPA 模式" / "MP 模式" Tab,观察数据差异(如果有的话)
- 尝试新增一个学生,查看是否成功
- 在
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(内容) │ │
│ │ └──────────────────────┘ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
今日任务
- 在浏览器中打开
index.html,F12 打开开发者工具 - 用"元素选择器"工具点击表格某一行,查看它的 CSS 规则
- 修改
style.css:把侧边栏主色#1890ff改成你喜欢的颜色 - 给统计卡片增加一个"鼠标悬停时轻微放大"的动画效果:
.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 工具箱齐全的开发者。