web-第三次课后作业

web-第三次课后作业

本次作业是在之前Web网站的基础上,连接了数据库(SQL Server)。之前项目把注册信息存在 application 内置对象里(一个 List),本质是内存存储。一旦 Tomcat 重启,所有注册数据全部清空。所以必须用数据库持久化存储数据。

本次作业的核心:数据库、JDBC 与 CRUD


一、为什么需要数据库

之前项目把注册信息存在 application 内置对象里(一个 List),本质是内存存储。一旦 Tomcat 重启,所有注册数据全部清空。真实项目必须用数据库持久化存储数据。

本项目使用 SQL Server + JDBC 实现数据库操作。


二、JDBC ------ Java 连接数据库的统一接口

2.1 架构

JDBC(Java Database Connectivity)是 Java 定义的一套操作数据库的标准接口。不管底层是什么数据库(SQL Server、MySQL、Oracle),Java 代码都用同一套 API 操作,区别只在于加载不同的驱动 jar 包和写不同的连接字符串。

复制代码
你的 Java 代码
    │
    ▼
java.sql.DriverManager  ←── 统一的 JDBC 接口
    │
    ▼
SQLServerDriver (mssql-jdbc.jar)  ←── 具体数据库的驱动实现
    │
    ▼
SQL Server 数据库

2.2 JDBC 操作数据库的标准步骤

复制代码
① Class.forName("驱动类全名")          加载驱动,注册到 DriverManager
② DriverManager.getConnection(url)    建立 TCP 连接到数据库
③ conn.prepareStatement(sql)          预编译 SQL 语句
④ pstmt.setXxx(index, value)          给占位符 ? 赋值
⑤ pstmt.executeQuery/Update()         执行查询 或 增删改
⑥ rs → pstmt → conn 逐级关闭           释放资源(先开后关)

注册写入示例:

复制代码
// ① 加载 SQL Server 驱动
Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");

// ② 建立连接
String dbUrl = "jdbc:sqlserver://localhost:1433;databaseName=DB1;encrypt=false";
Connection conn = DriverManager.getConnection(dbUrl, "sa", "你猜");

// ③ 预编译 SQL(占位符 ? 防注入)
String sql = "INSERT INTO [User] (Name, Password, Gender, Age, Hobbies) VALUES (?, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);

// ④ 给每个占位符赋值(下标从 1 开始)
pstmt.setString(1, name);
pstmt.setString(2, password);
pstmt.setString(3, gender);
pstmt.setInt(4, Integer.parseInt(ageStr));
pstmt.setString(5, hobbyStr);

// ⑤ 执行更新(INSERT / UPDATE / DELETE 用 executeUpdate)
pstmt.executeUpdate();

// ⑥ 释放资源
pstmt.close();
conn.close();

查询示例:

复制代码
String sql = "SELECT Id FROM [User] WHERE Name = ? AND Password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

if (rs.next()) {
    loginSuccess = true;  // 查到匹配用户
}

// 关闭顺序:rs → pstmt → conn
rs.close();
pstmt.close();
conn.close();

三、CRUD 核心操作

3.1 查(Read)

复制代码
String sql = "SELECT Id, Name, Gender, Age, Hobbies FROM [User] ORDER BY Id";
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery();

while (rs.next()) {
    int id = rs.getInt("Id");
    String name = rs.getString("Name");
    String gender = rs.getString("Gender");

    int age = rs.getInt("Age");
    // getInt 对 NULL 返回 0,必须用 wasNull() 区分
    String ageStr = rs.wasNull() ? "" : String.valueOf(age);

    String hobbies = rs.getString("Hobbies");
    hobbies = hobbies != null ? hobbies : "";
}

ResultSet 关键方法:

方法 说明
rs.next() 光标移到下一行,无数据时返回 false
rs.getString("列名") 获取字符串值
rs.getInt("列名") 获取整数值
rs.wasNull() 上一次 get 的值是否为 NULL(getInt 对 NULL 返回 0,需此方法区分)

模糊搜索(LIKE):

复制代码
String keyword = request.getParameter("keyword");
if (keyword != null && !keyword.isEmpty()) {
    String sql = "SELECT * FROM [User] WHERE Name LIKE ? ORDER BY Id";
    pstmt.setString(1, "%" + keyword + "%");  // % 拼在 Java 侧,不拼在 SQL 字符串中
}

3.2 增(Create)

复制代码
String sql = "INSERT INTO [User] (Name, Password, Gender, Age, Hobbies) VALUES (?, ?, ?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
pstmt.setString(2, password);
pstmt.setString(3, gender);

if (ageStr != null && !ageStr.isEmpty()) {
    pstmt.setInt(4, Integer.parseInt(ageStr));
} else {
    pstmt.setNull(4, Types.INTEGER);   // 空值插入 NULL
}

if (!hobbyStr.isEmpty()) {
    pstmt.setString(5, hobbyStr);
} else {
    pstmt.setNull(5, Types.NVARCHAR);
}

pstmt.executeUpdate();

3.3 改(Update)

复制代码
String sql = "UPDATE [User] SET Name=?, Password=?, Gender=?, Age=?, Hobbies=? WHERE Id=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
pstmt.setString(2, password);
pstmt.setString(3, gender);
pstmt.setInt(4, Integer.parseInt(ageStr));
pstmt.setString(5, hobbyStr);
pstmt.setInt(6, Integer.parseInt(idStr));   // WHERE 条件用主键 Id
pstmt.executeUpdate();

关键:UPDATE 和 DELETE 的 WHERE 条件必须使用主键(Id),否则可能误改/误删多条记录。

可选字段处理:密码在编辑时选填(留空=不修改),通过判断是否为空选择不同的 SQL:

复制代码
if (password != null && !password.isEmpty()) {
    sql = "UPDATE [User] SET Name=?, Password=?, Gender=?, Age=?, Hobbies=? WHERE Id=?";
} else {
    sql = "UPDATE [User] SET Name=?, Gender=?, Age=?, Hobbies=? WHERE Id=?";
}

3.4 删(Delete)

复制代码
String sql = "DELETE FROM [User] WHERE Id=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, Integer.parseInt(idStr));
pstmt.executeUpdate();

前端删除确认:

复制代码
function delUser(id, name) {
    if (confirm('确定要删除用户「' + name + '」吗?此操作不可恢复。')) {
        document.getElementById('deleteId').value = id;
        document.getElementById('deleteForm').submit();
    }
}

3.5 请求处理流程(GET/POST 合一)

同一个 JSP 文件处理全部请求:

复制代码
用户浏览器
    │
    ├─ GET  page4.jsp              → 查询并展示用户列表(支持搜索)
    ├─ POST page4.jsp?action=add    → 新增用户
    ├─ POST page4.jsp?action=update → 修改用户
    └─ POST page4.jsp?action=delete → 删除用户

if ("POST".equalsIgnoreCase(request.getMethod())) {
    // 处理增/删/改
    String action = request.getParameter("action");
    if ("add".equals(action))    { /* INSERT */ }
    if ("update".equals(action)) { /* UPDATE */ }
    if ("delete".equals(action)) { /* DELETE */ }
}
// GET 或 POST 处理完后,总是执行查询并渲染页面

核心思路:先处理写操作,再执行查询。POST 后页面立即展示最新数据。


四、PreparedStatement vs Statement(SQL 注入防护)

特性 Statement PreparedStatement
写法 字符串拼接 SQL 占位符 ? + setXxx()
SQL 注入防护 ❌ 不安全 ✅ 安全,参数和 SQL 结构分离
预编译 每次发送完整 SQL 一次编译,多次执行,效率更高
可读性 引号拼接混乱 占位符清晰

SQL 注入攻击示例(永远不要这样做):

复制代码
// 危险写法:字符串拼接
String sql = "SELECT * FROM [User] WHERE Name='" + username + "' AND Password='" + password + "'";

// 如果用户输入 username = "admin' --"
// 实际执行的 SQL 变成:
// SELECT * FROM [User] WHERE Name='admin' --' AND Password='xxx'
//                   注释掉了密码校验部分 → 无需密码即可登录

// 安全写法:PreparedStatement 参数化
String sql = "SELECT * FROM [User] WHERE Name = ? AND Password = ?";
pstmt.setString(1, username);  // 无论用户输入什么,都只当作"值",不会变成 SQL 的一部分
pstmt.setString(2, password);

原理:PreparedStatement 在数据库端先编译 SQL 模板,再将参数作为纯数据传入------参数值无论包含什么特殊字符('、--、;),都不会被当作 SQL 语法解析。

原则:所有来自用户输入的值,都必须通过 ? 占位符 + setXxx() 方法传入,绝不拼接到 SQL 字符串中。


五、常用方法速查

方法 用途
executeQuery() 执行 SELECT,返回 ResultSet
executeUpdate() 执行 INSERT/UPDATE/DELETE,返回影响行数
setString(index, value) 设置第 index 个 ? 为字符串
setInt(index, value) 设置第 index 个 ? 为整数
setNull(index, java.sql.Types.XXX) 设置第 index 个 ? 为数据库 NULL
getParameter(name) 获取单个请求参数(text/radio/select)
getParameterValues(name) 获取多个请求参数(checkbox 多选),无选中时返回 null

? 占位符索引从 1 开始(不是 0)。

空字符串和 NULL 是不同的概念:用户没填的字段应插入 NULL。

Types.INTEGER / Types.NVARCHAR 需与数据库列类型匹配。


六、请求参数处理

复制代码
request.setCharacterEncoding("UTF-8");   // 处理中文参数(仅 POST 有效)
String name   = request.getParameter("name");          // 单值参数
String[] hobbies = request.getParameterValues("hobbies"); // 多值参数(checkbox)

搜索使用 GET 方法,与 CRUD 的 POST 互不干扰。搜索参数拼接在 URL 中(?keyword=张三),方便分享和书签。


七、数据表结构

复制代码
CREATE TABLE [User] (
    Id        INT IDENTITY(1,1) PRIMARY KEY,   -- 自增主键
    Name      NVARCHAR(50)  NOT NULL,           -- 姓名
    Password  NVARCHAR(50)  NOT NULL,           -- 密码
    Gender    NVARCHAR(2)   NOT NULL,           -- 男/女
    Age       INT           NULL,               -- 年龄(可空)
    Hobbies   NVARCHAR(200) NULL,               -- 爱好(用、拼接)
    CreatedAt DATETIME      DEFAULT GETDATE()   -- 创建时间
);

IDENTITY(1,1) 表示从 1 开始,每次自增 1。插入时不需要指定 Id 值。

NVARCHAR 的 N 表示 Unicode,能存中文。永远用 NVARCHAR 存可能有中文的字段。


八、SQL Server JDBC 连接字符串

复制代码
jdbc:sqlserver://localhost:1433;databaseName=DB1;encrypt=false
│              │              │   │              │
协议标识         主机/IP         端口  数据库名        关闭 SSL 加密
参数 说明 生产环境建议
encrypt=false 关闭 SSL 加密,本地开发用 encrypt=true
trustServerCertificate=true 信任自签证书 用正式 CA 证书
integratedSecurity=true Windows 集成认证 视环境而定

不同数据库的连接字符串对比:

数据库 连接字符串格式
SQL Server jdbc:sqlserver://localhost:1433;databaseName=DB1
MySQL jdbc:mysql://localhost:3306/DB1?useSSL=false
PostgreSQL jdbc:postgresql://localhost:5432/DB1
Oracle jdbc:oracle:thin:@localhost:1521:DB1

结构统一:jdbc:数据库类型://主机:端口/数据库名?参数


九、SQL Server 常用数据类型(与 Java 类型对照)

SQL Server 类型 Java 类型 说明 本项目使用
INT int / Integer 整数 Age
NVARCHAR(n) String 可变长 Unicode 字符串 Name, Password, Gender, Hobbies
DATETIME java.sql.Timestamp 日期时间 CreatedAt
BIT boolean 布尔值 未使用
DECIMAL(p,s) java.math.BigDecimal 精确小数 未使用

十、常见错误与调试

问题 原因 解决
ClassNotFoundException 未导入 JDBC 驱动 jar mssql-jdbc.jar 放入 WEB-INF/lib/ 或配置 Maven 依赖
SQLException: 拒绝连接 SQL Server 未启动或端口不对 检查 SQL Server 服务,确认端口 1433
此列名无效 表结构不匹配 核对 SQL 中的列名与数据库是否一致
getInt 返回 0 但数据库是 NULL getInt 对 NULL 返回 0 调用 rs.wasNull() 判断
中文乱码 字符编码不统一 request.setCharacterEncoding("UTF-8") + 数据库列用 NVARCHAR
删除操作没反应 隐藏表单的 actionid 未正确赋值 在 JS 中用 console.log 检查值

十一、关键类一览

类 / 接口 所属包 作用 获取方式
DriverManager java.sql 管理驱动,创建数据库连接 DriverManager.getConnection(url, user, password)
Connection java.sql 代表一个数据库 TCP 连接 DriverManager 创建
PreparedStatement java.sql 预编译 SQL,安全传参 conn.prepareStatement(sql)
ResultSet java.sql 查询结果集,逐行读取 pstmt.executeQuery()
SQLServerDriver com.microsoft.sqlserver.jdbc SQL Server 的 JDBC 驱动实现 通过 Class.forName() 加载

十二、当前架构的隐患与改进方向

问题 隐患 改进方向
每次请求新建连接 浪费资源 使用连接池(如 HikariCP、Druid)
数据库密码明文写在代码里 泄露风险 外置到 .properties 配置文件
密码明文存储 数据库泄露则密码全暴露 改为哈希存储(BCrypt、SHA-256 + 盐)
每个 JSP 重复写连接代码 维护困难 抽取 DBUtil 工具类统一管理

理想的目标架构

复制代码
JSP 页面(只负责显示)
    ↓ 调用
Servlet(处理请求、校验参数、调用业务逻辑)
    ↓ 调用
DAO 类(封装 SQL,通过 DBUtil 获取连接)
    ↓ 使用
DBUtil 工具类(管理连接池、提供 getConnection())
    ↓ 读取
db.properties(数据库连接参数)
    ↓ 连接
SQL Server

当前阶段直接在 JSP 里写 JDBC 代码是可以接受的------代码量小,概念清晰,适合理解数据库操作的基本流程。后续项目变大再重构不迟。


十三、关键概念一句话总结

概念 一句话
JDBC Java 操作数据库的统一接口,一套 API 操作所有数据库
DriverManager 管理数据库驱动的"接线员",负责创建连接
Connection 一个数据库 TCP 连接,用完必须关闭
PreparedStatement 预编译 + 参数化的 SQL 执行器,防注入
ResultSet 查询结果集,像 Excel 表一样逐行读取
mssql-jdbc 微软官方提供的 SQL Server JDBC 驱动
连接池 预先创建好一批连接反复使用,避免频繁建立/销毁
SQL 注入 用户输入被当成 SQL 代码执行,PreparedStatement 可以杜绝
硬编码 把配置直接写在代码里,应外置到配置文件
CRUD Create / Read / Update / Delete,数据库四大基本操作
IDENTITY SQL Server 的自增列,插入时不需手动指定值
NVARCHAR Unicode 可变长字符串,存中文必须用它

十四、具体效果

相关推荐
遗憾随她而去.1 小时前
Web地图全体系深度梳理:引擎、数据源、图层、投影核心知识
前端
爱因斯坦乐2 小时前
Vue项目整合
前端·javascript·vue.js
无风听海2 小时前
IndexedDB 深度指南 浏览器中的事务型对象数据库
前端·数据库
ct9783 小时前
组件间的通信
前端·javascript·vue.js
MageGojo3 小时前
天气 API 接入实战:基于 ApiZero 实现实时天气、分钟级降水和 15 天预报查询
java·后端·spring·api 接口接入·接口实战
左手吻左脸。3 小时前
Vue 全栈面试题大全(2026 最新版最详细)
前端·javascript·vue.js
Aphasia3113 小时前
手写KeepAlive组件
前端·react.js·面试
两个西柚呀3 小时前
js中的同步和异步,三种处理异步任务的方式
前端·javascript
pe7er4 小时前
软件设计不要“既要又要”
前端·后端·架构