C++转Java日记(10)

DAO

把写在JdbcDemo.main()里的数据库操作,整理成菜单可以调用的方法。

1.是什么

Data Access Object,意思是负责访问数据的对象。在这个项目里,StudentDao是自己写的Java类,负责连接MySQL,执行SQL,再把查询结果组装成Student对象。

你已经写过这个方法:

复制代码
Student found = manager.findByName(name);

它在 ArrayList 里找学生。现在可以写一个用法类似的方法:

复制代码
Student found = dao.findByName(name);

它去 MySQL 的 students 表里找学生。两种方法都可以约定:找到就返回 Student,找不到就返回 null 。这样,菜单里原来判断 found == null 的代码就能继续用。

2.实践操作

把查询封装到 StudentDao

在 src\main\java 下新建 StudentDao.java,和 Main.java 放在一起。下面的写法沿用你现在的项目结构,直接使用 Main.java 里定义的 Student 类。

java 复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class StudentDao {
    private final String url;
    private final String user;
    private final String password;

    public StudentDao(String url, String user, String password) {
        this.url = url;
        this.user = user;
        this.password = password;
    }

    public Student findByName(String name) throws SQLException {
        String sql = "SELECT name, score FROM students WHERE name = ?";

        try (Connection connection = DriverManager.getConnection(url, user, password);
             PreparedStatement statement = connection.prepareStatement(sql)) {

            statement.setString(1, name);

            try (ResultSet rows = statement.executeQuery()) {
                if (rows.next()) {
                    return new Student(
                            rows.getString("name"),
                            rows.getInt("score")
                    );
                }
                return null;
            }
        }
    }
}

把 JdbcDemo.main() 换成下面这样。它只需要导入 java.sql.SQLException

java 复制代码
public static void main(String[] args) {
    StudentDao dao = new StudentDao(
            "jdbc:mysql://localhost:3306/student_demo",
            System.getenv("MYSQL_USER"),
            System.getenv("MYSQL_PASSWORD")
    );

    try {
        Student found = dao.findByName("小红");

        if (found == null) {
            System.out.println("查无此人");
        } else {
            found.showInfo();
        }
    } catch (SQLException e) {
        System.out.println("查询失败:" + e.getMessage());
    }
}

提问:这个rows是什么?getString getInt是怎么做的准确读出来内容的?

rows 是你给查询结果起的变量名,类型是 ResultSet。 它可以理解成一张查询结果表,加上一个记录"当前读到哪一行"的位置,这个位置叫游标。

1. executeQuery() 拿到什么?

假设执行的是这条 SQL,并且给 ? 传入了 "小红":

复制代码
SELECT name, score FROM students WHERE name = ?

MySQL 筛选后返回的结果是:

name score
小红 85

statement.executeQuery() 返回一个 ResultSet 对象,让 Java 可以读取这份结果。`rows` 就是引用这个对象的变量,你也可以把它命名为 result。

2. next() 选行,`getString()` 和 getInt() 选列。

刚拿到结果时,游标在第一行之前 。`rows.next()` 把它移到第一行,有这一行就返回 true。进入 if 后,两次取值都发生在同一行:

复制代码
String name = rows.getString("name"); // 当前行的 name 列,读成 String,得到 "小红"
int score = rows.getInt("score");     // 当前行的 score 列,读成 int,得到 85

括号里的 "name" 和 "score" 对应的是 SQL 查询结果的列标签 。JDBC 驱动根据标签找到这一列,再按你选的方法把值读成相应的 Java 类型。比如你在 SQL 里用 SELECT name AS student_name, score ...,就可以用 rows.getString("student_name") 读取姓名。

3.配置插曲

插曲1:再次复习一下数据库服务怎么打开。

Start-Service -Name MySQL80

Get-Service -Name MySQL80
$env:MYSQL_USER = 'root'

$env:MYSQL_PASSWORD = (Get-Credential -UserName 'root' -Message '请输入 MySQL 密码').GetNetworkCredential().Password

插曲2:字体存在问题

查询成功了。`С�죺85` 本应显示为 小红:85,这是终端输出编码不一致。 这次是 Maven 启动 JdbcDemo,所以之前为 java Main 配置的 PowerShell 函数没有参与这次运行。

1. 先在当前 PowerShell 验证。 在 student-manager 目录执行:

java 复制代码
chcp 65001 > $null
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$env:MAVEN_OPTS = "$env:MAVEN_OPTS -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8"

4.对Update的理解

有一个行为和原来的 ArrayList 不一样

以前 manager.findByName() 返回的是列表里已有的对象 ,所以 found.setScore(95) 会改到列表里的那个学生。现在 dao.findByName() 每次都根据查询结果创建一个 Java 对象。对它调用 setScore(95),只会修改这个对象的字段。要把新成绩保存到 MySQL,需要再执行一条 UPDATE。

因此,先在 JdbcDemo 验证查询,再把菜单的查询分支 接到 dao.findByName(name)。菜单里查询和改分原本合在 case "2", "3",这一步只拆出 case "2" 来验证。下一步再写 dao.updateScore(name, score),把改分也接到同一张表。

先读懂这条 SQL

复制代码
UPDATE students SET score = ? WHERE name = ?

如果你要把小红改成 95 分,第一个 ? 填 95,第二个 ? 填 "小红"。参数位置由 SQL 里问号的顺序决定,所以即使 Java 方法写成 updateScore(name, score),也要先绑定成绩、再绑定姓名。`WHERE name = ?` 限定了要修改的学生。省掉 WHERE 会更新整张表。

在 StudentDao 里实现改分

java 复制代码
public boolean updateScore(String name, int score) throws SQLException {
    if (score < 0 || score > 100) {
        throw new IllegalArgumentException("成绩应在 0~100 之间");
    }

    String sql = "UPDATE students SET score = ? WHERE name = ?";

    try (Connection connection = DriverManager.getConnection(url, user, password);
         PreparedStatement statement = connection.prepareStatement(sql)) {

        statement.setInt(1, score);
        statement.setString(2, name);

        int affectedRows = statement.executeUpdate();
        return affectedRows > 0;
    }
}

用 JdbcDemo 验证分数真的写进去了

java 复制代码
try {
    if (dao.updateScore("小红", 95)) {
        Student updated = dao.findByName("小红");
        if (updated != null) {
            updated.showInfo();
        }
    } else {
        System.out.println("查无此人");
    }
} catch (IllegalArgumentException e) {
    System.out.println(e.getMessage());
} catch (SQLException e) {
    System.out.println("数据库操作失败:" + e.getMessage());
}

5.整体拉通

补齐 StudentDao 的三个方法

你已经写了 findByName() 和 updateScore()。把下面的方法放进 StudentDao 类里面 ,与这两个方法并列。在 StudentDao.java 顶部补上 import java.util.ArrayList; 和 import java.util.List;。

java 复制代码
public List<Student> findAll() throws SQLException {
    String sql = "SELECT name, score FROM students ORDER BY name";
    List<Student> students = new ArrayList<>();

    try (Connection connection = DriverManager.getConnection(url, user, password);
         PreparedStatement statement = connection.prepareStatement(sql);
         ResultSet rows = statement.executeQuery()) {

        while (rows.next()) {
            String name = rows.getString("name");
            int score = rows.getInt("score");
            students.add(new Student(name, score));
        }
    }

    return students;
}

public void addStudent(Student student) throws SQLException {
    String sql = "INSERT INTO students (name, score) VALUES (?, ?)";

    try (Connection connection = DriverManager.getConnection(url, user, password);
         PreparedStatement statement = connection.prepareStatement(sql)) {

        statement.setString(1, student.getName());
        statement.setInt(2, student.getScore());
        statement.executeUpdate();
    }
}

public boolean deleteByName(String name) throws SQLException {
    String sql = "DELETE FROM students WHERE name = ?";

    try (Connection connection = DriverManager.getConnection(url, user, password);
         PreparedStatement statement = connection.prepareStatement(sql)) {

        statement.setString(1, name);
        return statement.executeUpdate() > 0;
    }
}

感觉多手写一下这些函数的规律还是很明显的。

原来的main类和现在的main类对比:

java 复制代码
public class Main{
    public static void main(String[] args){
        Path path = Path.of("students.txt");
        StudentManager manager = new StudentManager();
        try {
            manager.loadFromFile(path);
        }catch(NoSuchFileException e){
            System.out.println("找不到学生文件,先从空列表开始。");
        }catch(IOException e){
            System.out.println("加载失败:"+e.getMessage());
            return ;
        }
 
        try(Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8)){
            while(true){
                System.out.println("\\n1. 添加  2. 查询  3. 改分  4. 查看全部  5. 保存  0. 退出");
                System.out.print("请选择:");
                if(!scanner.hasNextLine()){
                    return ;
                }
                String choice = scanner.nextLine().strip();
                try{
                    switch(choice){
                        case "1"->{
                            System.out.print("姓名:");
                            String name = scanner.nextLine().strip();
                            if (name.isBlank() || name.contains(",")) {
                                System.out.println("姓名不能为空,也不能包含英文逗号。");
                            } else if (manager.findByName(name) != null) {
                                System.out.println("该姓名已存在。");
                            } else {
                                manager.addStudent(name, readScore(scanner));
                                System.out.println("添加成功。");
                            }
                        }
                        case "2", "3"->{
                            System.out.println("要查找的姓名:");
                            String name = scanner.nextLine().strip();
                            Student found = manager.findByName(name);
                            if(found == null){
                                System.out.println("查无此人。");
                            }else if(choice.equals("3")){
                                found.setScore(readScore(scanner));
                                found.showInfo();
                            }else{
                                found.showInfo();
                            }
                        }
                        case "4"->{
                            manager.showAll();
                        }
                        case "5"->{
                            manager.saveToFile(path);
                            System.out.println("已保存到 "+path.toAbsolutePath());
                        }
                        case "0"->{
                            return;
                        }
                        default -> System.out.println("请输入菜单里的选项。");
                    }
                } catch (NumberFormatException e) {
                    System.out.println("成绩请输入整数。");
                } catch (IllegalArgumentException e) {
                    System.out.println(e.getMessage());
                } catch (IOException e) {
                    System.out.println("保存失败:" + e.getMessage());
                }
            }
        }
    }
    private static int readScore(Scanner scanner){
        System.out.print("成绩:");
        return Integer.parseInt(scanner.nextLine().strip());
    }
}
java 复制代码
public class Main {
    public static void main(String[] args) {
        String url = "jdbc:mysql://localhost:3306/student_demo";
        String user = System.getenv("MYSQL_USER");
        String password = System.getenv("MYSQL_PASSWORD");

        if (user == null || user.isBlank() || password == null || password.isEmpty()) {
            System.out.println("请先在当前 PowerShell 设置 MySQL 用户名和密码。");
            return;
        }

        StudentDao dao = new StudentDao(url, user, password);

        try (Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8)) {
            while (true) {
                System.out.println("\n1. 添加  2. 查询  3. 改分  4. 查看全部  5. 删除  0. 退出");
                System.out.print("请选择:");

                if (!scanner.hasNextLine()) {
                    return;
                }

                String choice = scanner.nextLine().strip();

                try {
                    switch (choice) {
                        case "1" -> {
                            String name = readText(scanner, "姓名:");
                            int score = readScore(scanner);

                            dao.addStudent(new Student(name, score));
                            System.out.println("添加成功");
                        }
                        case "2" -> {
                            String name = readText(scanner, "要查询的姓名:");
                            Student found = dao.findByName(name);

                            if (found == null) {
                                System.out.println("查无此人");
                            } else {
                                found.showInfo();
                            }
                        }
                        case "3" -> {
                            String name = readText(scanner, "要改分的姓名:");
                            int score = readScore(scanner);

                            if (dao.updateScore(name, score)) {
                                System.out.println("修改成功");
                            } else {
                                System.out.println("查无此人");
                            }
                        }
                        case "4" -> {
                            for (Student student : dao.findAll()) {
                                student.showInfo();
                            }
                        }
                        case "5" -> {
                            String name = readText(scanner, "要删除的姓名:");

                            if (dao.deleteByName(name)) {
                                System.out.println("删除成功");
                            } else {
                                System.out.println("查无此人");
                            }
                        }
                        case "0" -> {
                            return;
                        }
                        default -> System.out.println("请输入菜单里的选项");
                    }
                } catch (NumberFormatException e) {
                    System.out.println("成绩请输入整数");
                } catch (IllegalArgumentException e) {
                    System.out.println(e.getMessage());
                } catch (SQLException e) {
                    System.out.println("数据库操作失败:" + e.getMessage());
                }
            }
        }
    }

    private static String readText(Scanner scanner, String prompt) {
        System.out.print(prompt);
        String text = scanner.nextLine().strip();

        if (text.isEmpty()) {
            throw new IllegalArgumentException("输入不能为空");
        }

        return text;
    }

    private static int readScore(Scanner scanner) {
        System.out.print("成绩:");
        String text = scanner.nextLine().strip();
        return Integer.parseInt(text);
    }
}

理论知识

假设加一个功能:「把小明的成绩改成 95,同时把小红的成绩改成 90,要求两项一起生效。」如果第一条 UPDATE 成功、第二条失败,默认自动提交会让第一条的修改已经留在数据库里。

要让这两条 SQL 一起成功或一起撤销,可以把它们放到同一条 Connection 的同一个事务里:

复制代码
connection.setAutoCommit(false)
    执行第一条 UPDATE
    执行第二条 UPDATE
    两条都成功 → connection.commit()
    中途失败   → connection.rollback()

这里和你的代码直接相关的一点是,`dao.updateScore()` 每调用一次就打开一条新连接 。连续调用两次 dao.updateScore(),就会用两条连接。因此,如果要实现上面的批量改分,需要让两条 SQL 在同一个方法里共用一条连接。现在能解释这点,就足够进入下一阶段。

把控制台菜单换成 HTTP 请求

你现在的 Main 通过 Scanner 等输入。做成 Web 后端以后,输入改由浏览器、前端或其他客户端通过 HTTP 请求 发来,结果用 HTTP 响应返回。DAO 仍然负责访问 MySQL。

例如,控制台中「选 2 → 输入小红」这个操作,可以设计成:

java 复制代码
GET /students?name=%E5%B0%8F%E7%BA%A2

GET 表示读取,`/students` 是请求路径,`?name=` 后面是查询参数。这里 %E5%B0%8F%E7%BA%A2 是"小红"在 URL 中的编码。后端取得姓名后,仍然调用你写的 dao.findByName(name)。查到后可以返回状态码 200,响应正文用 JSON 表示:

复制代码
{
  "name": "小红",
  "score": 95
}

添加学生则可以设计成 POST /students,由客户端把姓名和成绩放在 JSON 请求体里。HTTP 负责传递请求和响应,JSON 是其中承载数据的一种文本格式。

HTTP 规定客户端和后端怎么交换消息,JSON 规定消息里的数据怎么写,事务规定几条数据库操作怎么一起生效。

你之前用 Scanner 等待用户在终端输入。做成 Web 后端以后,输入来自客户端发来的 HTTP 请求 ,处理结果通过 HTTP 响应送回客户端。

比如,查询小红的成绩可以设计成 GET /students?name=小红。这里 GET 表示读取,`/students` 是路径,`name` 是查询参数。实际发送时,URL 中的中文会进行编码。后端读出姓名,再调用你写过的 dao.findByName(name)。

响应里有两个你现在需要认识的部分:

部分 例子 客户端怎么用
状态码 200、`404`、`500` 判断处理结果。查询成功可以返回 200,学生不存在可以返回 404,服务器遇到意外错误可以返回 500
响应正文 {"name":"小红","score":95} 读取实际数据,比如把 95 显示在页面上

常见的请求方法有 GET(读取)、`POST`(提交数据,常用于新增)、`PATCH`(修改部分字段)和 DELETE(删除)。这些方法表达客户端的意图,具体执行哪段 Java 代码、哪条 SQL,由你写的后端决定。 比如,`GET` 请求本身不会自动执行 SELECT。

JSON

是一种文本数据格式。

假设客户端要提交两条改分记录,请求正文可以写成:

复制代码
[
  {"name": "小明", "score": 95},
  {"name": "小红", "score": 90}
]

你可以借熟悉的 Java 类型理解这段数据。外层 [] 是数组,结构类似 List<Student>。每个 {} 是一个对象,里面有姓名和成绩两个字段。`"小明" 是字符串,95` 是数字,写成 "95" 就成了字符串。JSON 的字段名和字符串都用双引号。

JSON 到达后端时是文本。 后端需要把文本解析成 Java 能使用的数据。反过来,后端也可以把查到的 Student 对象转换为 JSON,再放进 HTTP 响应。以后 Spring Boot 会帮你完成常见的转换。HTTP 也可以传普通文本或图片,使用 JSON 时通常通过 Content-Type: application/json 告诉接收方正文的格式。

事务

假设用户的要求是 "小明改成 95,小红改成 90,两项必须一起成功" 。后端要执行两条 UPDATE。如果第一条成功后立刻提交,而第二条执行失败,数据库就会只改掉小明的成绩。

用 JDBC 可以在同一条 Connection 上关闭自动提交,执行两条 SQL,再根据结果决定怎么结束:

复制代码
关闭自动提交
执行小明的 UPDATE
执行小红的 UPDATE
两项都满足要求 → commit(),一起提交
其中一项失败   → rollback(),撤销本次事务中的修改

这里的"失败"也包括业务规则没有满足 。例如小红不在表里,`UPDATE` 可能正常执行并返回 0 行,没有抛异常。如果你的要求是两个人都必须存在,就要检查这个结果,并在找不到人时回滚。

你之前的 dao.updateScore() 每调用一次都会打开一条新连接。所以调用两次,第一条修改可能已经自动提交。要实现批量改分,就要让两条 SQL 共用一条连接和一个事务。

事务的理论通常用 ACID 概括:

性质 放到这个例子里理解
原子性(Atomicity) 两笔改分一起提交,或者一起撤销
一致性(Consistency) 提交后数据仍满足规则,例如姓名主键唯一。成绩范围也要由应用校验或数据库约束来保证
隔离性(Isolation) 多个请求同时读写时,隔离级别决定它们何时能看到彼此的修改
持久性(Durability) 提交成功的修改,数据库应当能够在重启后恢复

把三者放进同一次请求

客户端通过 HTTP 提交改分请求,用 JSON 写明两个人的新成绩。后端解析数据、检查成绩,然后用 JDBC 事务 执行两条 UPDATE。只有事务提交成功后,后端才返回表示成功的 HTTP 响应。如果第二个人查无此人,后端先回滚,再返回失败结果。

这里最容易混淆的一点是:一次 HTTP 请求不会自动变成一次数据库事务。 如果后端收到一份包含两条记录的 JSON,却分别调用两次你现在的 updateScore(),仍然可能只成功一条。请求怎么传由 HTTP 决定,两条改分是否一起生效由后端的事务代码决定。

相关推荐
总要学点什么1 小时前
第105篇:三维特效-流动图片墙体效果——让箭头沿墙长滚,它俩竟是同一份代码
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
简 洁 冬冬1 小时前
泛型方法写法全面辨析:从「能编译」到「能跑」到「能用」
java·泛型
LGL6030A1 小时前
Java学习历程31——初识Spring Boot
java·spring boot·学习
xiaoye-duck1 小时前
《C++ 项目:手写大模型接入 SDK》会话模块整合:内存 SessionManager 与 DataManager 持久化联动
开发语言·数据库·c++·语言模型
Funlang2 小时前
龙芯上的 Fun:从跑起来到机器码
开发语言·龙芯
总要学点什么3 小时前
第104篇:三维特效-流动墙体效果——让贴图沿墙流动,它却只肯走一条对角线
开发语言·javascript·ecmascript·gis·webgl·材质·cesium
旖旎夜光3 小时前
【C++ 基础入门】C++11 新特性(中):右值引用、移动语义与完美转发
开发语言·c++·笔记·学习
Lansonli3 小时前
大数据Spark(八十八):Spark任务资源之Standalone集群测试
java·服务器·spark
ShineWinsu3 小时前
对于Qt:QTabWidget常见属性的解析
开发语言·数据库·c++·qt·浏览器·qtabwidget·标签页