JavaSE_MySQL_JDBC综合项目实战

O泡李华 9

JavaSE + MySQL + JDBC 综合项目实战:学生信息管理系统

本章位置:Java 核心阶段综合练习
项目类型:控制台项目
技术栈:Java 17、MySQL、JDBC、Maven
项目目标:把前面学过的 Java 基础知识真正串成一个完整项目,而不是继续零散练习。


一、为什么要做这个综合项目

前面已经学习了:

变量和运算符

选择语句和循环语句

数组

面向对象

常用类

异常机制

集合框架

IO

多线程

MySQL

JDBC

DAO 思想

如果这些知识一直分开练习,很容易出现:

每一章看懂了

但真正做项目时不知道怎么组合

所以这一章的目标不是继续增加新知识,而是:

用一个完整项目把前面的知识连起来。


二、项目名称

学生信息管理系统

英文可以写:

Student Management System

项目采用:

控制台菜单
+
MySQL 数据库
+
JDBC
+
DAO / Service 分层

完成。


三、项目功能

系统包含:

1. 添加学生

2. 删除学生

3. 修改学生

4. 根据学号查询学生

5. 查询全部学生

6. 按姓名模糊查询

7. 按成绩排序

8. 分页查询

9. 统计学生数量

10. 统计平均分

11. 导出学生数据到文本文件

0. 退出系统

四、这个项目会用到哪些知识

Java 基础

变量

if

switch

while

for

面向对象

Student 类

构造方法

封装

Getter / Setter

toString()

集合

List<Student>

用于保存数据库查询结果。


异常

参数异常

数据库异常

文件异常

IO

导出学生数据到 txt 文件

JDBC

Connection

PreparedStatement

ResultSet

事务基础

分层

entity

dao

service

util

app

五、项目最终结构

建议目录:

student-management
│
├─ pom.xml
│
└─ src
   └─ main
      ├─ java
      │  └─ com.example.student
      │     │
      │     ├─ app
      │     │  └─ StudentApplication.java
      │     │
      │     ├─ entity
      │     │  └─ Student.java
      │     │
      │     ├─ dao
      │     │  ├─ StudentDao.java
      │     │  └─ impl
      │     │     └─ StudentDaoImpl.java
      │     │
      │     ├─ service
      │     │  └─ StudentService.java
      │     │
      │     └─ util
      │        └─ JdbcUtil.java
      │
      └─ resources
         └─ jdbc.properties

六、第一步:创建 Maven 项目

在 IDEA 中:

File

New

Project

选择:

Maven

JDK:

17

项目名:

student-management

七、pom.xml

加入 MySQL 驱动。

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="
         http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>student-management</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.4.0</version>
        </dependency>

    </dependencies>

</project>

如果你本地项目已经在使用其他兼容版本的:

mysql-connector-j

不需要为了这篇笔记强制改版本。


八、第二步:创建数据库

打开 MySQL。

创建数据库:

CREATE DATABASE IF NOT EXISTS student_db
CHARACTER SET utf8mb4;

使用数据库:

USE student_db;

九、创建学生表

CREATE TABLE student (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    student_no VARCHAR(30) NOT NULL UNIQUE,
    name VARCHAR(50) NOT NULL,
    age INT NOT NULL,
    gender VARCHAR(10),
    major VARCHAR(100),
    score DECIMAL(5,2) NOT NULL DEFAULT 0,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

十、字段说明

id
数据库主键

student_no
学号

name
姓名

age
年龄

gender
性别

major
专业

score
成绩

create_time
创建时间

十一、插入测试数据

INSERT INTO student
(student_no, name, age, gender, major, score)
VALUES
('20260001', '张三', 20, '男', '软件工程', 90.50),
('20260002', '李四', 21, '男', '计算机科学与技术', 85.00),
('20260003', '王五', 19, '女', '软件工程', 95.00),
('20260004', '赵六', 22, '女', '人工智能', 78.50);

检查:

SELECT *
FROM student;

十二、第三步:创建 jdbc.properties

路径:

src/main/resources/jdbc.properties

内容:

jdbc.url=jdbc:mysql://localhost:3306/student_db
jdbc.username=root
jdbc.password=123456

注意:

把 root 和密码改成你自己 MySQL 的实际配置

十三、为什么数据库配置放 properties

不推荐:

String password = "123456";

写死在每个 DAO 里。

配置文件可以做到:

集中管理

修改方便

减少重复

后续迁移 SpringBoot 更自然

十四、第四步:创建 JdbcUtil

包:

com.example.student.util

类:

package com.example.student.util;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public final class JdbcUtil {

    private static final String URL;

    private static final String USERNAME;

    private static final String PASSWORD;

    static {

        Properties properties =
                new Properties();

        try (
                InputStream input =
                        JdbcUtil.class
                                .getClassLoader()
                                .getResourceAsStream(
                                        "jdbc.properties"
                                )
        ) {

            if (input == null) {

                throw new IllegalStateException(
                        "未找到 jdbc.properties"
                );
            }

            properties.load(input);

            URL =
                    properties.getProperty(
                            "jdbc.url"
                    );

            USERNAME =
                    properties.getProperty(
                            "jdbc.username"
                    );

            PASSWORD =
                    properties.getProperty(
                            "jdbc.password"
                    );

        } catch (IOException e) {

            throw new ExceptionInInitializerError(
                    e
            );
        }
    }

    private JdbcUtil() {

    }

    public static Connection
    getConnection()
            throws SQLException {

        return DriverManager
                .getConnection(
                        URL,
                        USERNAME,
                        PASSWORD
                );
    }
}

十五、测试数据库连接

临时创建:

public class TestConnection {

    public static void main(
            String[] args
    ) throws Exception {

        Connection connection =
                JdbcUtil.getConnection();

        System.out.println(
                connection
        );

        connection.close();
    }
}

如果能够打印连接对象:

说明 JDBC 基本连接成功

测试结束后可以删除这个临时类。


十六、第五步:创建 Student 实体类

包:

com.example.student.entity

代码:

package com.example.student.entity;

import java.math.BigDecimal;
import java.time.LocalDateTime;

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;

    public Student() {

    }

    public Student(
            String studentNo,
            String name,
            Integer age,
            String gender,
            String major,
            BigDecimal score
    ) {

        this.studentNo = studentNo;

        this.name = name;

        this.age = age;

        this.gender = gender;

        this.major = major;

        this.score = score;
    }

    public Long getId() {

        return id;
    }

    public void setId(
            Long id
    ) {

        this.id = id;
    }

    public String getStudentNo() {

        return studentNo;
    }

    public void setStudentNo(
            String studentNo
    ) {

        this.studentNo = studentNo;
    }

    public String getName() {

        return name;
    }

    public void setName(
            String name
    ) {

        this.name = name;
    }

    public Integer getAge() {

        return age;
    }

    public void setAge(
            Integer age
    ) {

        this.age = age;
    }

    public String getGender() {

        return gender;
    }

    public void setGender(
            String gender
    ) {

        this.gender = gender;
    }

    public String getMajor() {

        return major;
    }

    public void setMajor(
            String major
    ) {

        this.major = major;
    }

    public BigDecimal getScore() {

        return score;
    }

    public void setScore(
            BigDecimal score
    ) {

        this.score = score;
    }

    public LocalDateTime
    getCreateTime() {

        return createTime;
    }

    public void setCreateTime(
            LocalDateTime createTime
    ) {

        this.createTime =
                createTime;
    }

    @Override
    public String toString() {

        return "Student{"
                + "id="
                + id
                + ", studentNo='"
                + studentNo
                + '\''
                + ", name='"
                + name
                + '\''
                + ", age="
                + age
                + ", gender='"
                + gender
                + '\''
                + ", major='"
                + major
                + '\''
                + ", score="
                + score
                + ", createTime="
                + createTime
                + '}';
    }
}

十七、为什么数据库整数使用 Integer

数据库字段理论上可能:

NULL

Java 基本类型:

int

不能表示:

null

包装类型:

Integer

可以。

这也是实体类中经常看到:

Long
Integer

的原因。


十八、第六步:设计 StudentDao

包:

com.example.student.dao

代码:

package com.example.student.dao;

import com.example.student.entity.Student;

import java.math.BigDecimal;
import java.util.List;

public interface StudentDao {

    int insert(
            Student student
    );

    int deleteByStudentNo(
            String studentNo
    );

    int update(
            Student student
    );

    Student findByStudentNo(
            String studentNo
    );

    List<Student> findAll();

    List<Student> findByName(
            String keyword
    );

    List<Student> findOrderByScoreDesc();

    List<Student> findPage(
            int offset,
            int pageSize
    );

    long count();

    BigDecimal averageScore();
}

十九、为什么先写接口

接口只负责定义:

StudentDao 能做什么

具体 JDBC 怎么写:

交给实现类

这样做的价值:

职责清晰

降低耦合

后续切换 MyBatis 更容易理解

二十、第七步:创建 StudentDaoImpl

包:

com.example.student.dao.impl

基础:

package com.example.student.dao.impl;

import com.example.student.dao.StudentDao;
import com.example.student.entity.Student;
import com.example.student.util.JdbcUtil;

import java.math.BigDecimal;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class StudentDaoImpl
        implements StudentDao {

}

接下来逐个实现。


二十一、先封装 ResultSet 映射方法

DAO 中很多查询都会重复:

ResultSet
→
Student

可以先写:

private Student mapStudent(
        ResultSet rs
) throws SQLException {

    Student student =
            new Student();

    student.setId(
            rs.getLong(
                    "id"
            )
    );

    student.setStudentNo(
            rs.getString(
                    "student_no"
            )
    );

    student.setName(
            rs.getString(
                    "name"
            )
    );

    student.setAge(
            rs.getInt(
                    "age"
            )
    );

    student.setGender(
            rs.getString(
                    "gender"
            )
    );

    student.setMajor(
            rs.getString(
                    "major"
            )
    );

    student.setScore(
            rs.getBigDecimal(
                    "score"
            )
    );

    student.setCreateTime(
            rs.getObject(
                    "create_time",
                    java.time
                            .LocalDateTime
                            .class
            )
    );

    return student;
}

二十二、实现新增学生

@Override
public int insert(
        Student student
) {

    String sql =
            """
            INSERT INTO student
            (
                student_no,
                name,
                age,
                gender,
                major,
                score
            )
            VALUES
            (?, ?, ?, ?, ?, ?)
            """;

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            )
    ) {

        ps.setString(
                1,
                student
                        .getStudentNo()
        );

        ps.setString(
                2,
                student.getName()
        );

        ps.setInt(
                3,
                student.getAge()
        );

        ps.setString(
                4,
                student.getGender()
        );

        ps.setString(
                5,
                student.getMajor()
        );

        ps.setBigDecimal(
                6,
                student.getScore()
        );

        return ps.executeUpdate();

    } catch (SQLException e) {

        throw new RuntimeException(
                "新增学生失败",
                e
        );
    }
}

二十三、实现删除学生

@Override
public int deleteByStudentNo(
        String studentNo
) {

    String sql =
            """
            DELETE FROM student
            WHERE student_no = ?
            """;

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            )
    ) {

        ps.setString(
                1,
                studentNo
        );

        return ps.executeUpdate();

    } catch (SQLException e) {

        throw new RuntimeException(
                "删除学生失败",
                e
        );
    }
}

二十四、实现修改学生

@Override
public int update(
        Student student
) {

    String sql =
            """
            UPDATE student
            SET name = ?,
                age = ?,
                gender = ?,
                major = ?,
                score = ?
            WHERE student_no = ?
            """;

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            )
    ) {

        ps.setString(
                1,
                student.getName()
        );

        ps.setInt(
                2,
                student.getAge()
        );

        ps.setString(
                3,
                student.getGender()
        );

        ps.setString(
                4,
                student.getMajor()
        );

        ps.setBigDecimal(
                5,
                student.getScore()
        );

        ps.setString(
                6,
                student
                        .getStudentNo()
        );

        return ps.executeUpdate();

    } catch (SQLException e) {

        throw new RuntimeException(
                "修改学生失败",
                e
        );
    }
}

二十五、实现按学号查询

@Override
public Student findByStudentNo(
        String studentNo
) {

    String sql =
            """
            SELECT
                id,
                student_no,
                name,
                age,
                gender,
                major,
                score,
                create_time
            FROM student
            WHERE student_no = ?
            """;

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            )
    ) {

        ps.setString(
                1,
                studentNo
        );

        try (
                ResultSet rs =
                        ps.executeQuery()
        ) {

            if (rs.next()) {

                return mapStudent(
                        rs
                );
            }
        }

        return null;

    } catch (SQLException e) {

        throw new RuntimeException(
                "查询学生失败",
                e
        );
    }
}

二十六、实现查询全部

@Override
public List<Student> findAll() {

    String sql =
            """
            SELECT
                id,
                student_no,
                name,
                age,
                gender,
                major,
                score,
                create_time
            FROM student
            ORDER BY id
            """;

    List<Student> list =
            new ArrayList<>();

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            );

            ResultSet rs =
                    ps.executeQuery()
    ) {

        while (rs.next()) {

            list.add(
                    mapStudent(rs)
            );
        }

        return list;

    } catch (SQLException e) {

        throw new RuntimeException(
                "查询学生列表失败",
                e
        );
    }
}

二十七、实现姓名模糊查询

@Override
public List<Student> findByName(
        String keyword
) {

    String sql =
            """
            SELECT
                id,
                student_no,
                name,
                age,
                gender,
                major,
                score,
                create_time
            FROM student
            WHERE name LIKE ?
            ORDER BY id
            """;

    List<Student> list =
            new ArrayList<>();

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            )
    ) {

        ps.setString(
                1,
                "%"
                        + keyword
                        + "%"
        );

        try (
                ResultSet rs =
                        ps.executeQuery()
        ) {

            while (rs.next()) {

                list.add(
                        mapStudent(rs)
                );
            }
        }

        return list;

    } catch (SQLException e) {

        throw new RuntimeException(
                "模糊查询学生失败",
                e
        );
    }
}

二十八、为什么 LIKE 参数仍然用 PreparedStatement

不要:

"WHERE name LIKE '%"
+ keyword
+ "%'"

推荐:

WHERE name LIKE ?

然后:

ps.setString(
        1,
        "%" + keyword + "%"
);

这样仍然是:

参数绑定

二十九、按成绩降序查询

@Override
public List<Student>
findOrderByScoreDesc() {

    String sql =
            """
            SELECT
                id,
                student_no,
                name,
                age,
                gender,
                major,
                score,
                create_time
            FROM student
            ORDER BY score DESC,
                     student_no ASC
            """;

    List<Student> list =
            new ArrayList<>();

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            );

            ResultSet rs =
                    ps.executeQuery()
    ) {

        while (rs.next()) {

            list.add(
                    mapStudent(rs)
            );
        }

        return list;

    } catch (SQLException e) {

        throw new RuntimeException(
                "成绩排序查询失败",
                e
        );
    }
}

三十、分页查询

@Override
public List<Student> findPage(
        int offset,
        int pageSize
) {

    String sql =
            """
            SELECT
                id,
                student_no,
                name,
                age,
                gender,
                major,
                score,
                create_time
            FROM student
            ORDER BY id
            LIMIT ?, ?
            """;

    List<Student> list =
            new ArrayList<>();

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            )
    ) {

        ps.setInt(
                1,
                offset
        );

        ps.setInt(
                2,
                pageSize
        );

        try (
                ResultSet rs =
                        ps.executeQuery()
        ) {

            while (rs.next()) {

                list.add(
                        mapStudent(rs)
                );
            }
        }

        return list;

    } catch (SQLException e) {

        throw new RuntimeException(
                "分页查询失败",
                e
        );
    }
}

三十一、统计学生总数

@Override
public long count() {

    String sql =
            """
            SELECT COUNT(*)
            FROM student
            """;

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            );

            ResultSet rs =
                    ps.executeQuery()
    ) {

        if (rs.next()) {

            return rs.getLong(1);
        }

        return 0;

    } catch (SQLException e) {

        throw new RuntimeException(
                "统计学生数量失败",
                e
        );
    }
}

三十二、统计平均分

@Override
public BigDecimal averageScore() {

    String sql =
            """
            SELECT AVG(score)
            FROM student
            """;

    try (
            Connection connection =
                    JdbcUtil
                            .getConnection();

            PreparedStatement ps =
                    connection
                            .prepareStatement(
                                    sql
                            );

            ResultSet rs =
                    ps.executeQuery()
    ) {

        if (rs.next()) {

            BigDecimal result =
                    rs.getBigDecimal(1);

            return result == null
                    ? BigDecimal.ZERO
                    : result;
        }

        return BigDecimal.ZERO;

    } catch (SQLException e) {

        throw new RuntimeException(
                "统计平均分失败",
                e
        );
    }
}

三十三、第八步:创建 StudentService

包:

com.example.student.service

代码骨架:

package com.example.student.service;

import com.example.student.dao.StudentDao;
import com.example.student.dao.impl.StudentDaoImpl;
import com.example.student.entity.Student;

import java.math.BigDecimal;
import java.util.List;

public class StudentService {

    private final StudentDao
            studentDao =
            new StudentDaoImpl();

}

三十四、Service 负责什么

DAO 负责:

怎么操作数据库

Service 负责:

业务规则

例如:

学号不能为空

学号不能重复

年龄范围

成绩范围

查询不到学生怎么办

三十五、添加学生业务

public void addStudent(
        Student student
) {

    validateStudent(
            student
    );

    Student old =
            studentDao
                    .findByStudentNo(
                            student
                                    .getStudentNo()
                    );

    if (old != null) {

        throw new IllegalArgumentException(
                "学号已存在"
        );
    }

    int rows =
            studentDao.insert(
                    student
            );

    if (rows != 1) {

        throw new RuntimeException(
                "学生添加失败"
        );
    }
}

三十六、学生参数校验

private void validateStudent(
        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(
                "年龄不合法"
        );
    }

    if (
        student.getScore()
                == null
        || student.getScore()
                .compareTo(
                        BigDecimal.ZERO
                ) < 0
        || student.getScore()
                .compareTo(
                        new BigDecimal(
                                "100"
                        )
                ) > 0
    ) {

        throw new IllegalArgumentException(
                "成绩必须在 0~100 之间"
        );
    }
}

三十七、删除学生业务

public void deleteStudent(
        String studentNo
) {

    Student student =
            getByStudentNo(
                    studentNo
            );

    int rows =
            studentDao
                    .deleteByStudentNo(
                            student
                                    .getStudentNo()
                    );

    if (rows != 1) {

        throw new RuntimeException(
                "删除学生失败"
        );
    }
}

三十八、根据学号查询业务

public Student getByStudentNo(
        String studentNo
) {

    if (
        studentNo == null
        || studentNo.isBlank()
    ) {

        throw new IllegalArgumentException(
                "学号不能为空"
        );
    }

    Student student =
            studentDao
                    .findByStudentNo(
                            studentNo
                    );

    if (student == null) {

        throw new IllegalArgumentException(
                "学生不存在"
        );
    }

    return student;
}

三十九、修改学生

public void updateStudent(
        Student student
) {

    validateStudent(
            student
    );

    getByStudentNo(
            student.getStudentNo()
    );

    int rows =
            studentDao.update(
                    student
            );

    if (rows != 1) {

        throw new RuntimeException(
                "修改学生失败"
        );
    }
}

四十、查询全部

public List<Student>
getAllStudents() {

    return studentDao.findAll();
}

四十一、姓名模糊查询

public List<Student>
searchByName(
        String keyword
) {

    if (
        keyword == null
    ) {

        keyword = "";
    }

    return studentDao
            .findByName(
                    keyword.trim()
            );
}

四十二、按成绩排序

public List<Student>
getRanking() {

    return studentDao
            .findOrderByScoreDesc();
}

四十三、分页查询

public List<Student>
getPage(
        int pageNum,
        int pageSize
) {

    if (
        pageNum < 1
    ) {

        throw new IllegalArgumentException(
                "页码不能小于 1"
        );
    }

    if (
        pageSize < 1
        || pageSize > 100
    ) {

        throw new IllegalArgumentException(
                "每页数量必须在 1~100"
        );
    }

    int offset =
            (pageNum - 1)
                    * pageSize;

    return studentDao
            .findPage(
                    offset,
                    pageSize
            );
}

四十四、统计

public long countStudents() {

    return studentDao.count();
}

平均分:

public BigDecimal
averageScore() {

    return studentDao
            .averageScore();
}

四十五、第九步:添加数据导出功能

为了复习 IO,

在 Service 中加入:

public void exportStudents(
        Path path
) {

}

四十六、导出学生数据

public void exportStudents(
        Path path
) {

    List<Student> students =
            studentDao.findAll();

    StringBuilder content =
            new StringBuilder();

    content.append(
            "学号,姓名,年龄,性别,专业,成绩"
    );

    content.append(
            System.lineSeparator()
    );

    for (
        Student student :
        students
    ) {

        content.append(
                student
                        .getStudentNo()
        );

        content.append(",");

        content.append(
                student.getName()
        );

        content.append(",");

        content.append(
                student.getAge()
        );

        content.append(",");

        content.append(
                student.getGender()
        );

        content.append(",");

        content.append(
                student.getMajor()
        );

        content.append(",");

        content.append(
                student.getScore()
        );

        content.append(
                System.lineSeparator()
        );
    }

    try {

        Path parent =
                path.getParent();

        if (parent != null) {

            Files.createDirectories(
                    parent
            );
        }

        Files.writeString(
                path,
                content.toString(),
                StandardCharsets.UTF_8
        );

    } catch (IOException e) {

        throw new RuntimeException(
                "导出学生数据失败",
                e
        );
    }
}

需要导入:

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

四十七、为什么这里使用 StringBuilder

因为需要循环拼接:

多行学生数据

使用:

StringBuilder

比不断:

String result = result + ...

更合适。


四十八、第十步:创建主程序 StudentApplication

包:

com.example.student.app

基础:

package com.example.student.app;

import com.example.student.entity.Student;
import com.example.student.service.StudentService;

import java.math.BigDecimal;
import java.nio.file.Path;
import java.util.List;
import java.util.Scanner;

public class StudentApplication {

    private static final Scanner
            SCANNER =
            new Scanner(
                    System.in
            );

    private static final StudentService
            STUDENT_SERVICE =
            new StudentService();

    public static void main(
            String[] args
    ) {

        while (true) {

            printMenu();

            String choice =
                    SCANNER
                            .nextLine()
                            .trim();

            try {

                switch (choice) {

                    case "1" ->
                            addStudent();

                    case "2" ->
                            deleteStudent();

                    case "3" ->
                            updateStudent();

                    case "4" ->
                            queryOne();

                    case "5" ->
                            queryAll();

                    case "6" ->
                            searchByName();

                    case "7" ->
                            showRanking();

                    case "8" ->
                            showPage();

                    case "9" ->
                            showStatistics();

                    case "10" ->
                            exportData();

                    case "0" -> {

                        System.out.println(
                                "系统已退出"
                        );

                        SCANNER.close();

                        return;
                    }

                    default ->
                            System.out.println(
                                    "请输入正确的菜单编号"
                            );
                }

            } catch (
                    Exception e
            ) {

                System.out.println(
                        "操作失败:"
                                + e.getMessage()
                );
            }
        }
    }
}

四十九、菜单

private static void printMenu() {

    System.out.println();
    System.out.println(
            "===== 学生信息管理系统 ====="
    );

    System.out.println(
            "1. 添加学生"
    );

    System.out.println(
            "2. 删除学生"
    );

    System.out.println(
            "3. 修改学生"
    );

    System.out.println(
            "4. 根据学号查询"
    );

    System.out.println(
            "5. 查询全部学生"
    );

    System.out.println(
            "6. 按姓名模糊查询"
    );

    System.out.println(
            "7. 成绩排行榜"
    );

    System.out.println(
            "8. 分页查询"
    );

    System.out.println(
            "9. 数据统计"
    );

    System.out.println(
            "10. 导出数据"
    );

    System.out.println(
            "0. 退出系统"
    );

    System.out.print(
            "请选择:"
    );
}

五十、添加学生

private static void addStudent() {

    Student student =
            readStudent();

    STUDENT_SERVICE
            .addStudent(
                    student
            );

    System.out.println(
            "添加成功"
    );
}

五十一、读取学生输入

private static Student
readStudent() {

    System.out.print(
            "学号:"
    );

    String studentNo =
            SCANNER
                    .nextLine()
                    .trim();

    System.out.print(
            "姓名:"
    );

    String name =
            SCANNER
                    .nextLine()
                    .trim();

    System.out.print(
            "年龄:"
    );

    int age =
            Integer.parseInt(
                    SCANNER
                            .nextLine()
                            .trim()
            );

    System.out.print(
            "性别:"
    );

    String gender =
            SCANNER
                    .nextLine()
                    .trim();

    System.out.print(
            "专业:"
    );

    String major =
            SCANNER
                    .nextLine()
                    .trim();

    System.out.print(
            "成绩:"
    );

    BigDecimal score =
            new BigDecimal(
                    SCANNER
                            .nextLine()
                            .trim()
            );

    return new Student(
            studentNo,
            name,
            age,
            gender,
            major,
            score
    );
}

五十二、为什么这里统一 nextLine()

不建议混用:

nextInt()

nextLine()

因为很容易遇到:

换行符残留

导致输入跳过。

这里统一:

nextLine()

再自己:

Integer.parseInt()

更容易控制。


五十三、删除学生

private static void
deleteStudent() {

    System.out.print(
            "请输入学号:"
    );

    String studentNo =
            SCANNER
                    .nextLine()
                    .trim();

    STUDENT_SERVICE
            .deleteStudent(
                    studentNo
            );

    System.out.println(
            "删除成功"
    );
}

五十四、修改学生

简化做法:

private static void
updateStudent() {

    System.out.println(
            "请输入修改后的完整学生信息"
    );

    Student student =
            readStudent();

    STUDENT_SERVICE
            .updateStudent(
                    student
            );

    System.out.println(
            "修改成功"
    );
}

后续可以自己升级成:

只修改指定字段

五十五、按学号查询

private static void
queryOne() {

    System.out.print(
            "请输入学号:"
    );

    String studentNo =
            SCANNER
                    .nextLine()
                    .trim();

    Student student =
            STUDENT_SERVICE
                    .getByStudentNo(
                            studentNo
                    );

    System.out.println(
            student
    );
}

五十六、查询全部

private static void
queryAll() {

    List<Student> students =
            STUDENT_SERVICE
                    .getAllStudents();

    printStudents(
            students
    );
}

五十七、统一打印列表

private static void
printStudents(
        List<Student> students
) {

    if (
        students.isEmpty()
    ) {

        System.out.println(
                "暂无数据"
        );

        return;
    }

    for (
        Student student :
        students
    ) {

        System.out.println(
                student
        );
    }
}

五十八、姓名模糊查询

private static void
searchByName() {

    System.out.print(
            "请输入姓名关键词:"
    );

    String keyword =
            SCANNER
                    .nextLine()
                    .trim();

    List<Student> students =
            STUDENT_SERVICE
                    .searchByName(
                            keyword
                    );

    printStudents(
            students
    );
}

五十九、排行榜

private static void
showRanking() {

    List<Student> students =
            STUDENT_SERVICE
                    .getRanking();

    for (int i = 0;
         i < students.size();
         i++) {

        Student student =
                students.get(i);

        System.out.println(
                "第 "
                        + (i + 1)
                        + " 名:"
                        + student.getName()
                        + ",成绩:"
                        + student.getScore()
        );
    }
}

六十、分页查询

private static void
showPage() {

    System.out.print(
            "页码:"
    );

    int pageNum =
            Integer.parseInt(
                    SCANNER
                            .nextLine()
                            .trim()
            );

    System.out.print(
            "每页数量:"
    );

    int pageSize =
            Integer.parseInt(
                    SCANNER
                            .nextLine()
                            .trim()
            );

    List<Student> students =
            STUDENT_SERVICE
                    .getPage(
                            pageNum,
                            pageSize
                    );

    printStudents(
            students
    );
}

六十一、统计

private static void
showStatistics() {

    long count =
            STUDENT_SERVICE
                    .countStudents();

    BigDecimal average =
            STUDENT_SERVICE
                    .averageScore();

    System.out.println(
            "学生总数:"
                    + count
    );

    System.out.println(
            "平均分:"
                    + average
    );
}

六十二、导出数据

private static void
exportData() {

    Path path =
            Path.of(
                    "data",
                    "students.csv"
            );

    STUDENT_SERVICE
            .exportStudents(
                    path
            );

    System.out.println(
            "导出成功:"
                    + path
                            .toAbsolutePath()
    );
}

六十三、运行项目

启动:

StudentApplication.main()

应该看到:

===== 学生信息管理系统 =====
1. 添加学生
2. 删除学生
3. 修改学生
4. 根据学号查询
5. 查询全部学生
6. 按姓名模糊查询
7. 成绩排行榜
8. 分页查询
9. 数据统计
10. 导出数据
0. 退出系统
请选择:

六十四、建议测试顺序

先测试:

数据库连接

再测试:

查询全部

然后:

按学号查询

再:

新增
修改
删除

最后:

分页
统计
导出

不要所有代码一次写完才第一次运行。


六十五、推荐的开发顺序

做项目时建议:

数据库
↓
实体类
↓
JdbcUtil
↓
DAO 接口
↓
DAO 查询
↓
DAO 增删改
↓
Service
↓
控制台菜单
↓
IO 导出
↓
异常处理

这种顺序更容易排错。


六十六、为什么先把 DAO 测通

如果:

数据库连接
SQL
DAO

都没验证,

就直接写 Service 和控制台,

最后出错时很难知道:

问题到底在哪一层

所以应该:

一层一层验证

六十七、项目中可能遇到的错误 1:找不到驱动

如果出现类似:

No suitable driver

检查:

pom.xml 是否有 mysql-connector-j

Maven 是否成功下载

JDBC URL 是否正确

六十八、错误 2:Access denied

类似:

Access denied for user

检查:

用户名

密码

MySQL 用户权限

六十九、错误 3:Unknown database

类似:

Unknown database 'student_db'

说明:

数据库不存在

先执行:

CREATE DATABASE student_db;

七十、错误 4:Table doesn’t exist

检查:

当前数据库

表名

student 表是否创建

七十一、错误 5:Duplicate entry

例如:

Duplicate entry

可能因为:

student_no 有 UNIQUE

而你插入了重复学号。

Service 中虽然提前查询,

数据库的:

UNIQUE 约束

仍然非常重要。


七十二、为什么业务校验和数据库约束都要有

Service:

给用户友好提示

数据库:

提供最终数据完整性保护

不要只依赖:

前端
Service

去保证唯一性。


七十三、错误 6:NumberFormatException

例如输入年龄:

abc

然后:

Integer.parseInt(
        "abc"
);

会异常。

主菜单里已经:

catch (Exception e)

所以不会让整个程序直接退出。


七十四、错误 7:SQL 参数顺序写错

例如 SQL:

UPDATE student
SET name = ?,
    age = ?
WHERE student_no = ?

必须对应:

ps.setString(
        1,
        name
);

ps.setInt(
        2,
        age
);

ps.setString(
        3,
        studentNo
);

参数编号和 SQL 中:

?

顺序必须一致。


七十五、错误 8:忘记 rs.next()

错误:

rs.getString(
        "name"
);

之前没有:

rs.next();

ResultSet 游标还在:

第一行之前

七十六、错误 9:连接没关闭

推荐始终:

try (
    Connection connection = ...;
    PreparedStatement ps = ...
) {

}

不要手动到处:

close()

然后又经常忘记。


七十七、这个项目如何体现面向对象

Student:

实体对象

StudentDao:

数据访问抽象

StudentDaoImpl:

具体 JDBC 实现

StudentService:

业务逻辑

StudentApplication:

用户交互

这就是:

职责分离

七十八、这个项目如何体现集合

数据库查询多条记录后:

List<Student>

保存。

例如:

List<Student> students =
        studentDao.findAll();

七十九、这个项目如何体现异常机制

例如:

throw new IllegalArgumentException(
        "成绩必须在 0~100 之间"
);

DAO 中:

catch (SQLException e) {

    throw new RuntimeException(
            "查询学生失败",
            e
    );
}

主程序统一:

catch (Exception e) {

    System.out.println(
            "操作失败:"
                    + e.getMessage()
    );
}

八十、这个项目如何体现 IO

导出:

数据库
↓
List<Student>
↓
StringBuilder
↓
Files.writeString()
↓
students.csv

八十一、这个项目如何体现常用类

使用:

String

StringBuilder

BigDecimal

LocalDateTime

Path

Files

八十二、这个项目如何体现数据库知识

使用:

CREATE TABLE

INSERT

SELECT

UPDATE

DELETE

LIKE

ORDER BY

LIMIT

COUNT

AVG

八十三、这个项目如何体现 JDBC

使用:

DriverManager

Connection

PreparedStatement

ResultSet

executeQuery()

executeUpdate()

八十四、为什么不用 Statement

项目统一使用:

PreparedStatement

避免:

用户输入直接拼 SQL

这也是 JDBC 阶段必须养成的习惯。


八十五、项目可以继续做的升级,但当前不要求

这个项目还可以升级:

登录系统

教师表

课程表

选课关系

事务

连接池

日志

MyBatis

Web 页面

但是当前课程阶段不要全部加。

这次综合练习的目标只是:

把 JavaSE + MySQL + JDBC 基础真正练熟。


八十六、当前阶段不要加入哪些东西

暂时不要加入:

Spring

SpringBoot

MyBatis

Redis

Vue

前后端分离

JWT

原因:

那会把重点从 JavaSE 综合练习带偏

先把当前层做扎实。


八十七、推荐自己手写的部分

以下建议不要复制完就算:

Student 实体类

StudentDao 接口

insert()

findByStudentNo()

findAll()

StudentService 参数校验

StudentApplication 菜单

这些最好自己重新敲一遍。


八十八、推荐第二遍自己完成

第二遍可以不看答案,

从空项目开始自己做:

1. 建库

2. 建表

3. 配置 Maven

4. JdbcUtil

5. Student

6. DAO

7. Service

8. Main

如果第二遍能独立写出来,

说明这部分真正掌握了。


八十九、综合练习检查清单

完成后检查:

[ ] Maven 项目可以正常启动

[ ] MySQL 数据库创建成功

[ ] student 表创建成功

[ ] JDBC 能连接数据库

[ ] 查询全部正常

[ ] 按学号查询正常

[ ] 新增正常

[ ] 重复学号会失败

[ ] 删除正常

[ ] 修改正常

[ ] 模糊查询正常

[ ] 排行榜正常

[ ] 分页正常

[ ] COUNT 正常

[ ] AVG 正常

[ ] CSV 导出正常

[ ] 数据库资源正常关闭

[ ] SQL 没有直接拼接用户输入

九十、项目知识关系图

StudentApplication
        │
        ▼
 StudentService
        │
        ▼
   StudentDao
        │
        ▼
 StudentDaoImpl
        │
        ▼
    JdbcUtil
        │
        ▼
     MySQL

数据返回:

MySQL
↓
ResultSet
↓
Student
↓
List<Student>
↓
Service
↓
控制台

九十一、从这个项目到 MyBatis

现在 DAO 中要写:

PreparedStatement

setString

setInt

executeQuery

ResultSet

rs.next

rs.getString

rs.getInt

后面学习 MyBatis 后:

很多重复 JDBC 模板代码
会被框架处理

你主要关注:

SQL
Mapper
参数
结果映射

九十二、从这个项目到 SpringBoot

现在对象之间:

new StudentDaoImpl()

new StudentService()

后面 Spring 会通过:

IOC
DI

帮助管理对象。

现在手动:

异常捕获
事务
连接

后面 SpringBoot 会进一步:

统一异常处理
事务管理
数据源管理

九十三、为什么这个综合练习值得保留

以后学习:

MyBatis
SpringBoot
MVC

时,

如果感觉:

“框架太抽象”

可以回来看这个项目。

因为框架主要是在帮我们处理:

重复代码
对象管理
数据映射
事务
Web 请求

而不是凭空创造新的编程世界。


九十四、本章总结

这个项目把前面的知识真正连了起来:

Java 基础语法
↓
面向对象
↓
集合
↓
异常
↓
IO
↓
MySQL
↓
JDBC
↓
DAO
↓
Service
↓
完整程序

如果只记一个核心:

Java 项目不是把所有代码写在 main 方法里,而是把数据、业务、数据库访问和用户交互按照职责拆开。

这就是从:

“会写 Java 语法”

逐渐走向:

“会组织一个 Java 项目”

的重要一步。

完成这份综合练习之后,第一阶段 Java 核心中的后端基础已经基本串起来。

后面继续进入课程体系中的:

HTML / CSS
JavaScript

开始学习 Web 前端基础。