WebCRUD_MVC项目实战详解

O泡李华 5

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 优化

存储引擎