WebCRUD_MVC项目实战详解
Web CRUD + MVC 项目实战详解
本章位置:第二阶段 Java 核心框架
前置知识:Servlet、JSP、EL、JSTL、MyBatis、JavaBean、MySQL
下一篇:MySQL 进阶
项目目标:从零完成一个“学生管理系统”,把 Servlet、JSP、EL、JSTL、MyBatis、Service、Mapper 真正串成完整 Web 项目。
一、项目要完成什么
本项目实现:
学生列表
学生详情
新增学生
修改学生
删除学生
姓名/学号搜索
分页查询
表单回显
错误提示
真正要掌握的不是“学生管理”本身,而是:
一个 Java Web CRUD 项目应该怎么分层
请求应该怎么流转
每一层应该负责什么
二、CRUD 是什么
CRUD:
C = Create 新增
R = Read 查询
U = Update 修改
D = Delete 删除
用户管理、商品管理、订单管理、文章管理,本质上都大量使用 CRUD。
三、MVC 是什么
MVC:
Model
View
Controller
本项目中的对应关系:
Controller
Servlet
View
JSP + EL + JSTL
Model
Student + Service + Mapper + Database
完整请求链:
浏览器
↓
Servlet
↓
Service
↓
Mapper
↓
MyBatis
↓
MySQL
↓
Servlet
↓
request.setAttribute()
↓
forward
↓
JSP
↓
EL + JSTL
↓
HTML
↓
浏览器
四、为什么一定要分层
如果所有代码都写进 Servlet:
获取参数
参数校验
业务判断
SQL
数据库连接
HTML
页面跳转
一个类会越来越乱。
推荐职责:
Servlet
负责 Web 请求与响应
Service
负责业务规则和事务
Mapper
负责数据库操作
JSP
负责页面展示
五、项目技术栈
JDK 17
Maven
Tomcat 8.5
Servlet
JSP
EL
JSTL
MyBatis
MySQL
HTML/CSS
如果使用 Tomcat 8.5:
javax.servlet
不要混用 Tomcat 10+ 常见的:
jakarta.servlet
六、推荐目录结构
student-web
│
├─ pom.xml
│
└─ src
└─ main
├─ java
│ └─ com.example.student
│ ├─ entity
│ │ ├─ Student.java
│ │ └─ PageResult.java
│ ├─ mapper
│ │ └─ StudentMapper.java
│ ├─ service
│ │ └─ StudentService.java
│ ├─ servlet
│ │ ├─ StudentListServlet.java
│ │ ├─ StudentDetailServlet.java
│ │ ├─ StudentAddPageServlet.java
│ │ ├─ StudentAddServlet.java
│ │ ├─ StudentEditServlet.java
│ │ ├─ StudentUpdateServlet.java
│ │ └─ StudentDeleteServlet.java
│ └─ util
│ └─ MyBatisUtil.java
│
├─ resources
│ ├─ jdbc.properties
│ ├─ mybatis-config.xml
│ └─ mapper
│ └─ StudentMapper.xml
│
└─ webapp
├─ css
│ └─ style.css
└─ WEB-INF
└─ views
├─ student-list.jsp
├─ student-form.jsp
└─ student-detail.jsp
七、Maven 依赖
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.19</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.4.0</version>
</dependency>
</dependencies>
八、数据库设计
CREATE DATABASE IF NOT EXISTS student_web
CHARACTER SET utf8mb4;
USE student_web;
创建表:
CREATE TABLE student (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
student_no VARCHAR(30) NOT NULL UNIQUE,
name VARCHAR(50) NOT NULL,
age INT,
gender VARCHAR(10),
major VARCHAR(100),
score DECIMAL(5,2),
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
);
测试数据:
INSERT INTO student
(
student_no,
name,
age,
gender,
major,
score
)
VALUES
(
'20260001',
'张三',
20,
'男',
'软件工程',
90.50
),
(
'20260002',
'李四',
21,
'女',
'计算机科学与技术',
85.00
);
九、Student 实体类
public class Student {
private Long id;
private String studentNo;
private String name;
private Integer age;
private String gender;
private String major;
private BigDecimal score;
private LocalDateTime createTime;
private LocalDateTime updateTime;
public Student() {
}
// Getter / Setter
}
IDEA:
Alt + Insert
生成:
Getter / Setter
Constructor
toString
十、jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/student_web
jdbc.username=root
jdbc.password=123456
十一、mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="jdbc.properties"/>
<settings>
<setting
name="mapUnderscoreToCamelCase"
value="true"/>
<setting
name="logImpl"
value="STDOUT_LOGGING"/>
</settings>
<typeAliases>
<package
name="com.example.student.entity"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property
name="driver"
value="${jdbc.driver}"/>
<property
name="url"
value="${jdbc.url}"/>
<property
name="username"
value="${jdbc.username}"/>
<property
name="password"
value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper
resource="mapper/StudentMapper.xml"/>
</mappers>
</configuration>
十二、MyBatisUtil
public final class MyBatisUtil {
private static final SqlSessionFactory FACTORY;
static {
try {
InputStream input =
Resources.getResourceAsStream(
"mybatis-config.xml"
);
FACTORY =
new SqlSessionFactoryBuilder()
.build(
input
);
} catch (
Exception e
) {
throw new ExceptionInInitializerError(
e
);
}
}
private MyBatisUtil() {
}
public static SqlSession openSession() {
return FACTORY.openSession();
}
}
记住:
SqlSessionFactory
应用级复用
SqlSession
一次业务/事务使用
用完关闭
十三、StudentMapper
public interface StudentMapper {
Student findById(
Long id
);
List<Student> findPage(
@Param("keyword")
String keyword,
@Param("offset")
int offset,
@Param("pageSize")
int pageSize
);
long countByKeyword(
@Param("keyword")
String keyword
);
int insert(
Student student
);
int update(
Student student
);
int deleteById(
Long id
);
int countByStudentNo(
@Param("studentNo")
String studentNo
);
int countByStudentNoExcludeId(
@Param("studentNo")
String studentNo,
@Param("id")
Long id
);
}
十四、为什么新增要检查学号
数据库:
student_no UNIQUE
业务层应该提前告诉用户:
学号已存在
数据库的 UNIQUE 约束仍然保留,作为最终保护。
十五、Mapper XML resultMap
<resultMap
id="studentResultMap"
type="Student">
<id
property="id"
column="id"/>
<result
property="studentNo"
column="student_no"/>
<result
property="name"
column="name"/>
<result
property="age"
column="age"/>
<result
property="gender"
column="gender"/>
<result
property="major"
column="major"/>
<result
property="score"
column="score"/>
<result
property="createTime"
column="create_time"/>
<result
property="updateTime"
column="update_time"/>
</resultMap>
十六、查询详情 SQL
<select
id="findById"
resultMap="studentResultMap">
SELECT
id,
student_no,
name,
age,
gender,
major,
score,
create_time,
update_time
FROM student
WHERE id = #{id}
</select>
十七、搜索 + 分页 SQL
<select
id="findPage"
resultMap="studentResultMap">
SELECT
id,
student_no,
name,
age,
gender,
major,
score,
create_time,
update_time
FROM student
<where>
<if test="keyword != null and keyword != ''">
(
name LIKE CONCAT(
'%',
#{keyword},
'%'
)
OR
student_no LIKE CONCAT(
'%',
#{keyword},
'%'
)
)
</if>
</where>
ORDER BY id DESC
LIMIT
#{offset},
#{pageSize}
</select>
这里已经提前接触:
MyBatis 动态 SQL
后面 MyBatis 进阶再系统学习。
十八、分页总数 SQL
<select
id="countByKeyword"
resultType="long">
SELECT
COUNT(*)
FROM student
<where>
<if test="keyword != null and keyword != ''">
(
name LIKE CONCAT(
'%',
#{keyword},
'%'
)
OR
student_no LIKE CONCAT(
'%',
#{keyword},
'%'
)
)
</if>
</where>
</select>
十九、新增 SQL
<insert
id="insert"
useGeneratedKeys="true"
keyProperty="id">
INSERT INTO student
(
student_no,
name,
age,
gender,
major,
score
)
VALUES
(
#{studentNo},
#{name},
#{age},
#{gender},
#{major},
#{score}
)
</insert>
二十、修改 SQL
<update
id="update">
UPDATE student
SET
student_no = #{studentNo},
name = #{name},
age = #{age},
gender = #{gender},
major = #{major},
score = #{score}
WHERE id = #{id}
</update>
二十一、删除 SQL
<delete
id="deleteById">
DELETE
FROM student
WHERE id = #{id}
</delete>
二十二、学号唯一判断
新增:
<select
id="countByStudentNo"
resultType="int">
SELECT COUNT(*)
FROM student
WHERE student_no = #{studentNo}
</select>
修改:
<select
id="countByStudentNoExcludeId"
resultType="int">
SELECT COUNT(*)
FROM student
WHERE
student_no = #{studentNo}
AND id != #{id}
</select>
修改时必须排除自己。
二十三、PageResult 分页对象
public class PageResult<T> {
private List<T> records;
private long total;
private int pageNum;
private int pageSize;
private int totalPages;
// Constructor
// Getter / Setter
}
二十四、分页公式
int offset =
(pageNum - 1)
* pageSize;
总页数:
int totalPages =
(int)
(
(
total
+ pageSize
- 1
)
/ pageSize
);
例如:
total = 21
pageSize = 10
totalPages = 3
二十五、StudentService 查询详情
public Student findById(
Long id
) {
try (
SqlSession sqlSession =
MyBatisUtil.openSession()
) {
StudentMapper mapper =
sqlSession.getMapper(
StudentMapper.class
);
return mapper.findById(
id
);
}
}
二十六、StudentService 分页
public PageResult<Student> findPage(
String keyword,
int pageNum,
int pageSize
) {
if (
pageNum < 1
) {
pageNum = 1;
}
if (
pageSize <= 0
) {
pageSize = 10;
}
int offset =
(pageNum - 1)
* pageSize;
try (
SqlSession sqlSession =
MyBatisUtil.openSession()
) {
StudentMapper mapper =
sqlSession.getMapper(
StudentMapper.class
);
long total =
mapper.countByKeyword(
keyword
);
int totalPages =
(int)
(
(
total
+ pageSize
- 1
)
/ pageSize
);
List<Student> records =
mapper.findPage(
keyword,
offset,
pageSize
);
return new PageResult<>(
records,
total,
pageNum,
pageSize,
totalPages
);
}
}
二十七、Service 参数校验
例如:
private void validate(
Student student
) {
if (
student == null
) {
throw new IllegalArgumentException(
"学生不能为空"
);
}
if (
student.getStudentNo() == null
||
student.getStudentNo().isBlank()
) {
throw new IllegalArgumentException(
"学号不能为空"
);
}
if (
student.getName() == null
||
student.getName().isBlank()
) {
throw new IllegalArgumentException(
"姓名不能为空"
);
}
if (
student.getAge() != null
&&
(
student.getAge() < 1
||
student.getAge() > 150
)
) {
throw new IllegalArgumentException(
"年龄必须在 1~150 之间"
);
}
}
二十八、新增 Service
public void add(
Student student
) {
validate(
student
);
try (
SqlSession sqlSession =
MyBatisUtil.openSession()
) {
try {
StudentMapper mapper =
sqlSession.getMapper(
StudentMapper.class
);
if (
mapper.countByStudentNo(
student.getStudentNo()
)
> 0
) {
throw new IllegalArgumentException(
"学号已存在"
);
}
mapper.insert(
student
);
sqlSession.commit();
} catch (
RuntimeException e
) {
sqlSession.rollback();
throw e;
}
}
}
事务边界适合放在 Service,因为:
Service 代表完整业务操作
二十九、修改 Service
public void update(
Student student
) {
validate(
student
);
if (
student.getId() == null
) {
throw new IllegalArgumentException(
"id 不能为空"
);
}
try (
SqlSession sqlSession =
MyBatisUtil.openSession()
) {
try {
StudentMapper mapper =
sqlSession.getMapper(
StudentMapper.class
);
Student old =
mapper.findById(
student.getId()
);
if (
old == null
) {
throw new IllegalArgumentException(
"学生不存在"
);
}
if (
mapper.countByStudentNoExcludeId(
student.getStudentNo(),
student.getId()
)
> 0
) {
throw new IllegalArgumentException(
"学号已被其他学生使用"
);
}
mapper.update(
student
);
sqlSession.commit();
} catch (
RuntimeException e
) {
sqlSession.rollback();
throw e;
}
}
}
三十、删除 Service
public void deleteById(
Long id
) {
if (
id == null
) {
throw new IllegalArgumentException(
"id 不能为空"
);
}
try (
SqlSession sqlSession =
MyBatisUtil.openSession()
) {
try {
StudentMapper mapper =
sqlSession.getMapper(
StudentMapper.class
);
if (
mapper.findById(
id
)
== null
) {
throw new IllegalArgumentException(
"学生不存在"
);
}
mapper.deleteById(
id
);
sqlSession.commit();
} catch (
RuntimeException e
) {
sqlSession.rollback();
throw e;
}
}
}
三十一、学生列表 Servlet
@WebServlet(
"/student/list"
)
public class StudentListServlet
extends HttpServlet {
private final StudentService
studentService =
new StudentService();
@Override
protected void doGet(
HttpServletRequest request,
HttpServletResponse response
)
throws ServletException,
IOException {
String keyword =
request.getParameter(
"keyword"
);
String pageNumText =
request.getParameter(
"pageNum"
);
int pageNum = 1;
if (
pageNumText != null
&&
!pageNumText.isBlank()
) {
try {
pageNum =
Integer.parseInt(
pageNumText
);
} catch (
NumberFormatException ignored
) {
pageNum = 1;
}
}
PageResult<Student> page =
studentService.findPage(
keyword,
pageNum,
10
);
request.setAttribute(
"page",
page
);
request.setAttribute(
"keyword",
keyword
);
request
.getRequestDispatcher(
"/WEB-INF/views/student-list.jsp"
)
.forward(
request,
response
);
}
}
三十二、详情 Servlet
流程:
GET
↓
获取 id
↓
格式校验
↓
查询 Student
↓
不存在 → 404
↓
存在 → request
↓
forward detail.jsp
核心代码:
Student student =
studentService.findById(
id
);
if (
student == null
) {
response.sendError(
404,
"学生不存在"
);
return;
}
request.setAttribute(
"student",
student
);
request
.getRequestDispatcher(
"/WEB-INF/views/student-detail.jsp"
)
.forward(
request,
response
);
三十三、新增页面 Servlet
@WebServlet(
"/student/add-page"
)
public class StudentAddPageServlet
extends HttpServlet {
@Override
protected void doGet(
HttpServletRequest request,
HttpServletResponse response
)
throws ServletException,
IOException {
request
.getRequestDispatcher(
"/WEB-INF/views/student-form.jsp"
)
.forward(
request,
response
);
}
}
三十四、新增处理 Servlet
新增使用:
POST
步骤:
设置 UTF-8
↓
读取表单
↓
字符串转 Integer / BigDecimal
↓
构造 Student
↓
调用 Service
↓
成功 redirect
↓
失败 forward
成功:
studentService.add(
student
);
response.sendRedirect(
request.getContextPath()
+ "/student/list"
);
失败:
request.setAttribute(
"error",
e.getMessage()
);
request.setAttribute(
"student",
student
);
request
.getRequestDispatcher(
"/WEB-INF/views/student-form.jsp"
)
.forward(
request,
response
);
三十五、为什么成功 redirect,失败 forward
成功:
POST
↓
Redirect
↓
GET
这就是:
PRG
Post Redirect Get
优点:
刷新页面不会重复新增
失败使用 forward:
保留当前 request
可以继续保留:
用户输入
错误信息
三十六、编辑流程
编辑分两步:
GET /student/edit?id=1
查询旧数据
↓
表单回显
POST /student/update
读取新数据
↓
update
↓
redirect list
三十七、删除流程
推荐:
POST /student/delete
而不是:
GET /student/delete?id=1
因为删除:
会修改服务器状态
至少学习阶段不要用普通 GET 做删除。
三十八、student-list.jsp
核心头部:
<%@ page
contentType="text/html;charset=UTF-8"
pageEncoding="UTF-8"
%>
<%@ taglib
prefix="c"
uri="http://java.sun.com/jsp/jstl/core"
%>
搜索:
<form
action="${pageContext.request.contextPath}/student/list"
method="get"
>
<input
type="text"
name="keyword"
value="${keyword}"
placeholder="姓名或学号"
>
<button
type="submit"
>
搜索
</button>
</form>
三十九、列表循环
<c:choose>
<c:when
test="${empty page.records}"
>
<p>
暂无数据
</p>
</c:when>
<c:otherwise>
<table>
<c:forEach
items="${page.records}"
var="student"
>
<tr>
<td>
${student.id}
</td>
<td>
<c:out
value="${student.studentNo}"
/>
</td>
<td>
<c:out
value="${student.name}"
/>
</td>
<td>
${student.age}
</td>
</tr>
</c:forEach>
</table>
</c:otherwise>
</c:choose>
四十、详情链接
<c:url
value="/student/detail"
var="detailUrl"
>
<c:param
name="id"
value="${student.id}"
/>
</c:url>
<a
href="${detailUrl}"
>
详情
</a>
四十一、编辑链接
<c:url
value="/student/edit"
var="editUrl"
>
<c:param
name="id"
value="${student.id}"
/>
</c:url>
<a
href="${editUrl}"
>
编辑
</a>
四十二、删除表单
<form
action="${pageContext.request.contextPath}/student/delete"
method="post"
style="display:inline"
>
<input
type="hidden"
name="id"
value="${student.id}"
>
<button
type="submit"
>
删除
</button>
</form>
注意:
hidden 不代表安全
用户仍然可以修改 hidden 值。
后端必须重新校验。
四十三、分页 JSP
<c:if
test="${page.totalPages > 1}"
>
<c:forEach
begin="1"
end="${page.totalPages}"
var="p"
>
<c:url
value="/student/list"
var="pageUrl"
>
<c:param
name="pageNum"
value="${p}"
/>
<c:param
name="keyword"
value="${keyword}"
/>
</c:url>
<c:choose>
<c:when
test="${p == page.pageNum}"
>
<strong>
${p}
</strong>
</c:when>
<c:otherwise>
<a
href="${pageUrl}"
>
${p}
</a>
</c:otherwise>
</c:choose>
</c:forEach>
</c:if>
四十四、为什么分页必须带 keyword
用户搜索:
张
如果翻第二页只传:
pageNum=2
搜索条件会丢失。
所以分页 URL 要保留:
pageNum
+
keyword
四十五、student-form.jsp
新增和编辑可以共用一个页面。
判断:
${empty student.id}
如果 id 为空:
新增
否则:
编辑
四十六、表单错误显示
<c:if
test="${not empty error}"
>
<p>
<c:out
value="${error}"
/>
</p>
</c:if>
四十七、表单回显
例如:
<input
type="text"
name="name"
value="${student.name}"
>
新增失败后:
用户刚输入的数据还在
不用重新填。
四十八、详情 JSP
Servlet:
request.setAttribute(
"student",
student
);
JSP:
<p>
学号:
<c:out
value="${student.studentNo}"
/>
</p>
<p>
姓名:
<c:out
value="${student.name}"
/>
</p>
<p>
年龄:
${student.age}
</p>
四十九、为什么分页应该在数据库做
错误思路:
SELECT 全部学生
↓
Java Stream skip
↓
limit
数据量大时:
浪费数据库传输
浪费内存
浪费 CPU
正确:
LIMIT
#{offset},
#{pageSize}
让数据库只返回当前页。
五十、为什么分页需要 COUNT
页面需要知道:
总记录数
总页数
当前页
所以常见:
SELECT COUNT(*)
+
SELECT 当前页
两条 SQL。
五十一、前端校验不能代替后端校验
HTML:
<input
required
>
只能改善用户体验。
用户完全可以使用:
Postman
curl
浏览器开发者工具
绕过前端。
所以后端必须再验证。
五十二、推荐四层数据保护
前端校验
↓
Servlet 格式校验
↓
Service 业务校验
↓
数据库约束
例如:
学号 UNIQUE
Service 提前判断只是为了:
更友好的错误信息
数据库 UNIQUE 才是:
最终数据约束
五十三、为什么事务放 Service
假设以后一个业务:
新增学生
+
新增学生档案
+
记录操作日志
三个数据库操作应该:
一起成功
一起失败
业务整体属于 Service。
所以:
Service 是更合理的事务边界
五十四、不要在 Servlet 长期保存 SqlSession
错误:
private SqlSession sqlSession;
Servlet 会:
被多个请求线程共享
而 SqlSession:
关联连接
事务
一级缓存
不是全局共享线程安全对象
应该:
一次业务打开
用完关闭
五十五、不要在 JSP 访问数据库
错误:
<%
SqlSession session = ...
StudentMapper mapper = ...
%>
这会破坏 MVC。
正确:
Servlet
↓
Service
↓
Mapper
↓
数据库
↓
request
↓
JSP
五十六、不要在 Servlet 拼 SQL
Servlet 应该不知道:
SELECT
INSERT
UPDATE
DELETE
SQL 应该放:
Mapper XML
五十七、不要在 Mapper 写业务逻辑
Mapper 只负责:
数据库访问
例如:
学生年龄是否合法
不属于 Mapper。
五十八、常见错误:404
检查:
Tomcat 是否启动
端口
Deployment
Context Path
@WebServlet 路径
浏览器 URL
五十九、常见错误:405
例如:
form method=post
但 Servlet 只写:
doGet()
可能:
405 Method Not Allowed
六十、常见错误:500
重点看:
Tomcat 控制台
Caused by
可能来自:
Servlet
Service
MyBatis
数据库
JSP
六十一、常见错误:中文乱码
POST:
request.setCharacterEncoding(
"UTF-8"
);
必须在:
第一次 getParameter 之前
JSP:
pageEncoding="UTF-8"
Tomcat GET 参数:
URIEncoding="UTF-8"
六十二、常见错误:数据库修改没生效
检查:
sqlSession.commit();
六十三、常见错误:页面拿不到数据
检查:
Servlet 是否 setAttribute
attribute 名是否一致
是不是 forward
JSP 是否被直接访问
Service 是否真的返回数据
六十四、常见错误:搜索第二页失效
原因:
分页链接忘记 keyword
必须保留搜索条件。
六十五、常见错误:修改自己时学号提示重复
检查 SQL:
student_no = #{studentNo}
AND id != #{id}
修改唯一校验必须:
排除自己
六十六、常见错误:删除用 GET
例如:
/student/delete?id=1
风险:
误点击
爬虫访问
预加载
安全问题
学习阶段至少使用:
POST
六十七、Web CRUD 排错顺序
推荐严格按:
浏览器
↓
F12 Network
↓
URL
↓
HTTP Method
↓
Servlet
↓
参数
↓
Service
↓
Mapper
↓
MyBatis SQL 日志
↓
MySQL
↓
request attribute
↓
JSP
不要一出错就:
所有文件一起改
六十八、F12 Network 看什么
Request URL
Request Method
Status Code
Query String
Form Data
Response
六十九、MyBatis 日志看什么
Preparing
Parameters
Total
如果 SQL 有问题:
复制到 MySQL 客户端直接运行
通常非常有效。
七十、项目测试清单
列表:
空列表
正常列表
搜索
分页
搜索 + 分页
新增:
正常新增
空学号
空姓名
年龄非法
成绩非法
重复学号
编辑:
正常修改
不存在 id
学号改成别人已有学号
自己的学号保持不变
删除:
正常删除
不存在 id
非法 id
详情:
正常 id
空 id
非数字 id
不存在 id
七十一、为什么要测试边界值
成绩:
0
100
-1
101
年龄:
1
150
0
151
只测试正常值:
不能证明校验正确
七十二、推荐开发顺序
1. 建库建表
2. Maven Web 项目
3. 配依赖
4. 配 Tomcat
5. Student
6. MyBatis 配置
7. Mapper findById
8. Service findById
9. DetailServlet
10. detail.jsp
11. 列表
12. 新增
13. 修改
14. 删除
15. 搜索
16. 分页
17. 校验
18. 页面优化
七十三、为什么先做 findById
因为:
参数只有一个 id
SQL 简单
返回一个 Student
最容易验证整条链:
浏览器
→ Servlet
→ Service
→ Mapper
→ MySQL
→ JSP
七十四、不要一次写完十个类再运行
这样报错时:
不知道错在哪一层
正确方式:
每完成一层
马上运行
马上验证
七十五、IDEA 快捷操作
生成 Getter / Setter:
Alt + Insert
重写 Servlet 方法:
Ctrl + O
可以选择:
doGet
doPost
抽取方法:
Ctrl + Alt + M
七十六、这个项目之后 Spring MVC 会帮你省什么
现在:
request.getParameter(
"name"
);
以后 Spring MVC:
public String add(
Student student
) {
}
自动完成:
请求参数
→ JavaBean
七十七、request.setAttribute 以后会变什么
现在:
request.setAttribute(
"student",
student
);
Spring MVC:
model.addAttribute(
"student",
student
);
思想没有变:
给 View 准备数据
七十八、forward 以后会变什么
现在:
request
.getRequestDispatcher(
"/WEB-INF/views/student.jsp"
)
.forward(
request,
response
);
Spring MVC:
return "student";
框架帮你:
解析 View
完成转发
七十九、MyBatisUtil 以后会变什么
现在:
MyBatisUtil.openSession()
以后 SpringBoot + MyBatis:
Spring 管理 DataSource
Spring 管理 SqlSession
Spring 管理 Mapper
Spring 管理事务
所以 Mapper 可以直接注入使用。
八十、为什么这章必须学扎实
如果这一章理解了:
请求
参数
Servlet
Service
Mapper
SQL
事务
request
forward
JSP
后面 SpringBoot 会变成:
框架帮你省代码
而不是:
突然出现一堆看不懂的注解
八十一、必须掌握的请求设计
GET /student/list
GET /student/detail?id=1
GET /student/add-page
POST /student/add
GET /student/edit?id=1
POST /student/update
POST /student/delete
八十二、必须掌握的数据流
查询:
GET
↓
Servlet
↓
Service
↓
Mapper
↓
SELECT
↓
Student/List
↓
request.setAttribute
↓
forward JSP
↓
EL/JSTL
新增:
POST Form
↓
getParameter
↓
Student
↓
Service
↓
Mapper
↓
INSERT
↓
commit
↓
redirect
八十三、必须回答的问题
学完后应该能够回答:
1. CRUD 是什么?
2. MVC 是什么?
3. Servlet 在 MVC 中负责什么?
4. JSP 负责什么?
5. Service 为什么存在?
6. Mapper 为什么存在?
7. 为什么事务更适合放 Service?
8. 为什么新增成功使用 redirect?
9. 为什么新增失败常用 forward?
10. PRG 是什么?
11. 为什么删除不推荐 GET?
12. 为什么分页要 COUNT + LIMIT?
13. 为什么不能把全部数据查到 Java 再分页?
14. 为什么前端校验不能代替后端校验?
15. 为什么 Service 校验后数据库仍然需要 UNIQUE?
16. SqlSessionFactory 和 SqlSession 生命周期有什么区别?
17. 为什么 JSP 不应该访问数据库?
18. 为什么 Servlet 不应该直接写 SQL?
19. 搜索翻页时为什么要保留 keyword?
20. Web CRUD 出错应该按什么顺序排查?
八十四、Web CRUD 知识结构
Web CRUD
│
├─ Controller
│ ├─ ListServlet
│ ├─ DetailServlet
│ ├─ AddServlet
│ ├─ UpdateServlet
│ └─ DeleteServlet
│
├─ Service
│ ├─ 参数校验
│ ├─ 业务规则
│ └─ 事务
│
├─ Mapper
│ ├─ SELECT
│ ├─ INSERT
│ ├─ UPDATE
│ └─ DELETE
│
├─ View
│ ├─ JSP
│ ├─ EL
│ └─ JSTL
│
├─ 功能
│ ├─ 列表
│ ├─ 详情
│ ├─ 新增
│ ├─ 修改
│ ├─ 删除
│ ├─ 搜索
│ └─ 分页
│
└─ Web 设计
├─ GET / POST
├─ Forward
├─ Redirect
├─ PRG
└─ MVC
八十五、本章总结
这一章最重要的是建立:
完整 Web 项目思维
而不是只记几个 Servlet API。
核心架构:
Browser
↓
Servlet
↓
Service
↓
Mapper
↓
MyBatis
↓
MySQL
↓
Servlet
↓
JSP
职责:
Servlet
处理 Web
Service
处理业务和事务
Mapper
处理数据库
JSP
处理页面
新增、修改、删除成功后推荐:
POST
↓
Redirect
↓
GET
即:
PRG
分页应该:
COUNT
+
LIMIT
优先在数据库完成。
校验应该:
前端
+
Servlet
+
Service
+
数据库约束
多层协作。
到这里,你已经把前面学过的:
Servlet
JSP
EL
JSTL
MyBatis
MySQL
真正组成了一个完整 CRUD Web 项目。
按照课程表,下一篇进入:
MySQL 进阶
后面会开始系统整理:
索引
事务
锁
多表查询
执行计划
SQL 优化
存储引擎