Java数据库基础与JDBC详解

O泡李华 10

Java 数据库基础与 JDBC 详解

本章位置:Java 核心基础
前置知识:面向对象、异常机制、集合框架
学习目标:掌握 MySQL 基础操作、SQL 常用语句、JDBC 核心 API、预编译、事务基础、DAO 思想、工具类封装,并能够独立完成 Java 连接 MySQL 的增删改查。


一、为什么 Java 开发离不开数据库

前面学习 Java 时,数据通常保存在:

变量
数组
集合
文件

这些方式都可以保存数据,但实际 Web 项目需要:

用户注册后数据长期保存

订单不能程序关闭就消失

商品信息要长期维护

学生信息需要查询和修改

管理员需要统计大量业务数据

因此需要:

数据库

二、什么是数据库

数据库可以理解为:

专门用于长期保存、管理和查询数据的软件系统。

常见数据库:

MySQL
PostgreSQL
Oracle
SQL Server
SQLite

Java Web 开发中:

MySQL

非常常见。


三、数据库管理系统

数据库管理系统:

DBMS
Database Management System

例如:

MySQL

就是一种:

关系型数据库管理系统

四、关系型数据库

关系型数据库通常以:

表

的形式组织数据。

例如用户表:

user

可能是:

id username password age
1 admin 123456 20
2 zhangsan abc123 21

这里:

表
类似 Excel 工作表

行
代表一条数据

列
代表一个字段

五、数据库、表、字段、记录

数据库

例如:

school_db

表

例如:

student
user
course
order

字段

例如 student:

id
name
age
gender
score

记录

一整行数据:

1 张三 20 男 90

就是一条记录。


六、SQL 是什么

SQL:

Structured Query Language

中文:

结构化查询语言

用于操作关系型数据库。

例如:

SELECT * FROM student;

表示:

查询 student 表中的所有数据

七、SQL 的常见分类

SQL 可以大致分:

DDL
DML
DQL
DCL
TCL

初学阶段重点:

DDL
DML
DQL

八、DDL

DDL:

Data Definition Language

数据定义语言。

主要操作:

数据库
表
表结构

例如:

CREATE
ALTER
DROP

九、创建数据库

CREATE DATABASE java_demo;

如果不存在才创建:

CREATE DATABASE IF NOT EXISTS java_demo;

十、指定字符集

例如:

CREATE DATABASE java_demo
CHARACTER SET utf8mb4;

现代 MySQL 项目中:

utf8mb4

比旧的 utf8 更完整。


十一、查看数据库

SHOW DATABASES;

十二、选择数据库

USE java_demo;

之后创建表等操作默认在:

java_demo

中执行。


十三、删除数据库

DROP DATABASE java_demo;

谨慎:

数据库删除后数据会丢失

开发环境也不要养成随手执行 DROP 的习惯。


十四、创建表

例如:

CREATE TABLE student (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    age INT,
    gender VARCHAR(10),
    score DECIMAL(5,2)
);

十五、常见字段类型

整数

TINYINT
SMALLINT
INT
BIGINT

小数

DECIMAL
FLOAT
DOUBLE

金额通常更适合:

DECIMAL

字符串

CHAR
VARCHAR
TEXT

日期时间

DATE
DATETIME
TIMESTAMP

十六、VARCHAR

例如:

username VARCHAR(50)

表示:

最多保存一定长度的可变字符串

适合:

用户名
姓名
标题
手机号

十七、CHAR

固定长度字符串。

例如:

gender CHAR(1)

但实际项目是否使用 CHAR,要根据业务和数据库设计决定。


十八、TEXT

适合:

较长文本

例如:

文章内容
描述
备注

十九、DECIMAL

例如:

price DECIMAL(10,2)

表示:

最多 10 位有效数字
其中 2 位小数

金额字段常使用:

DECIMAL

Java 中通常对应:

BigDecimal

二十、主键

主键:

Primary Key

用于唯一标识一条数据。

例如:

id BIGINT PRIMARY KEY

主键要求:

唯一
不能为空

二十一、自增主键

MySQL:

AUTO_INCREMENT

例如:

id BIGINT PRIMARY KEY AUTO_INCREMENT

插入数据时可以不手动指定 id。


二十二、NOT NULL

name VARCHAR(50) NOT NULL

表示:

name 不能为空

二十三、UNIQUE

例如:

username VARCHAR(50) UNIQUE

表示:

用户名不能重复

二十四、DEFAULT

例如:

status INT DEFAULT 1

如果插入时不指定:

status

默认:

1

二十五、查看表

SHOW TABLES;

二十六、查看表结构

DESC student;

或者:

DESCRIBE student;

二十七、删除表

DROP TABLE student;

如果存在再删除:

DROP TABLE IF EXISTS student;

二十八、修改表结构

添加字段:

ALTER TABLE student
ADD COLUMN phone VARCHAR(20);

二十九、修改字段

例如:

ALTER TABLE student
MODIFY COLUMN name VARCHAR(100);

三十、删除字段

ALTER TABLE student
DROP COLUMN phone;

三十一、DML

DML:

Data Manipulation Language

主要:

INSERT
UPDATE
DELETE

用于修改表中的数据。


三十二、INSERT

插入:

INSERT INTO student
(name, age, gender, score)
VALUES
('张三', 20, '男', 90.5);

三十三、一次插入多条

INSERT INTO student
(name, age, gender, score)
VALUES
('张三', 20, '男', 90),
('李四', 21, '男', 85),
('王五', 19, '女', 95);

三十四、UPDATE

UPDATE student
SET score = 100
WHERE id = 1;

三十五、UPDATE 一定注意 WHERE

危险:

UPDATE student
SET score = 100;

这会:

修改整张表所有记录

所以修改前一定确认:

WHERE 条件

三十六、DELETE

DELETE FROM student
WHERE id = 1;

三十七、DELETE 也要注意 WHERE

危险:

DELETE FROM student;

会:

删除表中所有数据

但表结构还在。


三十八、DQL

DQL:

Data Query Language

主要就是:

SELECT

三十九、查询所有字段

SELECT *
FROM student;

四十、查询指定字段

SELECT id, name, score
FROM student;

真实项目通常更推荐:

明确写字段

而不是长期大量:

SELECT *

四十一、WHERE 条件

SELECT *
FROM student
WHERE age >= 18;

四十二、常见比较条件

=
!=
<>
>
<
>=
<=

例如:

SELECT *
FROM student
WHERE score >= 60;

四十三、AND

SELECT *
FROM student
WHERE age >= 18
AND score >= 60;

四十四、OR

SELECT *
FROM student
WHERE score < 60
OR score >= 90;

四十五、BETWEEN

SELECT *
FROM student
WHERE score BETWEEN 60 AND 90;

通常包含边界。


四十六、IN

SELECT *
FROM student
WHERE id IN (1, 3, 5);

四十七、LIKE

模糊查询。

SELECT *
FROM student
WHERE name LIKE '张%';

表示:

张开头

四十八、LIKE 通配符

%
任意长度字符

_
任意一个字符

例如:

WHERE name LIKE '%三%'

表示:

包含“三”

四十九、IS NULL

判断 null:

SELECT *
FROM student
WHERE phone IS NULL;

不能写:

phone = NULL

五十、IS NOT NULL

SELECT *
FROM student
WHERE phone IS NOT NULL;

五十一、ORDER BY

升序:

SELECT *
FROM student
ORDER BY score ASC;

降序:

SELECT *
FROM student
ORDER BY score DESC;

五十二、多字段排序

SELECT *
FROM student
ORDER BY score DESC,
         age ASC;

表示:

先成绩降序

成绩相同再年龄升序

五十三、LIMIT

MySQL 分页常用:

SELECT *
FROM student
LIMIT 0, 10;

表示:

从第 0 条开始
取 10 条

五十四、分页公式

假设:

pageNum
当前页

pageSize
每页数量

起始位置:

(pageNum - 1) * pageSize

例如:

第 3 页
每页 10 条

起点:

(3 - 1) * 10 = 20

SQL:

LIMIT 20, 10

五十五、聚合函数

常见:

COUNT
SUM
AVG
MAX
MIN

五十六、COUNT

SELECT COUNT(*)
FROM student;

统计:

总记录数

五十七、AVG

SELECT AVG(score)
FROM student;

五十八、MAX / MIN

SELECT MAX(score)
FROM student;
SELECT MIN(score)
FROM student;

五十九、GROUP BY

例如按性别统计人数:

SELECT gender,
       COUNT(*)
FROM student
GROUP BY gender;

六十、HAVING

WHERE:

分组前过滤

HAVING:

分组后过滤

例如:

SELECT gender,
       AVG(score) avg_score
FROM student
GROUP BY gender
HAVING AVG(score) >= 80;

六十一、SQL 别名

字段别名:

SELECT AVG(score)
       AS avg_score
FROM student;

表别名:

SELECT s.id,
       s.name
FROM student s;

六十二、JDBC 是什么

JDBC:

Java Database Connectivity

中文:

Java 数据库连接

它是一套:

Java 操作关系型数据库的标准 API

六十三、为什么需要 JDBC

Java 程序不能直接“凭空”操作 MySQL。

需要:

Java 程序
↓
JDBC API
↓
MySQL 驱动
↓
MySQL 数据库

六十四、JDBC 是接口规范

JDBC 中很多 API:

Connection
Statement
PreparedStatement
ResultSet

属于 Java 标准。

数据库厂商提供:

驱动实现

例如:

MySQL Connector/J

六十五、JDBC 核心对象

最重要:

Driver

Connection

Statement

PreparedStatement

ResultSet

六十六、Connection

表示:

Java 程序和数据库之间的一次连接

例如:

Connection connection;

六十七、Statement

用于:

执行 SQL

但实际开发中更推荐:

PreparedStatement

六十八、PreparedStatement

预编译 SQL 对象。

例如:

SELECT *
FROM student
WHERE id = ?

其中:

?

表示参数占位符。


六十九、ResultSet

查询结果集。

例如:

SELECT *
FROM student;

数据库返回多行数据,

JDBC 中通常通过:

ResultSet

读取。


七十、引入 MySQL JDBC 驱动

Maven 项目:

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>具体版本</version>
</dependency>

版本应结合:

当前项目
JDK
MySQL
Maven 仓库

选择。


七十一、JDBC URL

MySQL 常见格式:

jdbc:mysql://localhost:3306/java_demo

完整可能:

jdbc:mysql://localhost:3306/java_demo?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

但现代驱动对部分参数已有更合理默认值,

不要机械复制十年前的 JDBC URL 参数。


七十二、连接数据库

基础:

String url =
        "jdbc:mysql://localhost:3306/java_demo";

String username =
        "root";

String password =
        "123456";

Connection connection =
        DriverManager
                .getConnection(
                        url,
                        username,
                        password
                );

七十三、DriverManager

DriverManager:

用于获取 JDBC Connection

例如:

DriverManager.getConnection(...)

七十四、现代 JDBC 通常不需要手动 Class.forName

老教材常见:

Class.forName(
        "com.mysql.cj.jdbc.Driver"
);

现代 JDBC 驱动通常支持:

SPI 自动加载

所以很多场景可以直接:

DriverManager.getConnection(...)

但理解驱动加载原理仍然有价值。


七十五、JDBC 查询完整流程

基本步骤:

1. 获取 Connection

2. 准备 SQL

3. 创建 PreparedStatement

4. 设置参数

5. 执行查询

6. 获取 ResultSet

7. 读取结果

8. 关闭资源

七十六、查询一条数据

String sql =
        """
        SELECT id, name, age, score
        FROM student
        WHERE id = ?
        """;

try (
    Connection connection =
            DriverManager
                    .getConnection(
                            url,
                            username,
                            password
                    );

    PreparedStatement ps =
            connection
                    .prepareStatement(
                            sql
                    )
) {

    ps.setLong(
            1,
            1L
    );

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

        if (rs.next()) {

            long id =
                    rs.getLong(
                            "id"
                    );

            String name =
                    rs.getString(
                            "name"
                    );

            int age =
                    rs.getInt(
                            "age"
                    );

            BigDecimal score =
                    rs.getBigDecimal(
                            "score"
                    );

            System.out.println(
                    id
                            + ", "
                            + name
                            + ", "
                            + age
                            + ", "
                            + score
            );
        }
    }
}

七十七、ResultSet.next()

ResultSet 初始:

游标在第一行之前

调用:

rs.next()

向下一行移动。

如果有数据:

true

如果没有更多:

false

七十八、查询多条

while (
    rs.next()
) {

    // 读取当前行
}

这是查询列表最常见写法。


七十九、ResultSet 常用 get 方法

getInt()

getLong()

getString()

getDouble()

getBigDecimal()

getBoolean()

getDate()

getTimestamp()

八十、按列名读取更清晰

例如:

rs.getString(
        "name"
);

相比:

rs.getString(2);

列名通常:

可读性更好

八十一、Java 实体类映射数据库数据

例如:

public class Student {

    private Long id;

    private String name;

    private Integer age;

    private BigDecimal score;

    // Getter / Setter
}

八十二、ResultSet 转 Student

Student student =
        new Student();

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

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

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

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

这个过程叫:

结果映射

后面 MyBatis 会自动帮我们做很多类似工作。


八十三、查询 List

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

while (
    rs.next()
) {

    Student student =
            new Student();

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

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

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

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

    list.add(student);
}

八十四、executeQuery()

用于:

SELECT

返回:

ResultSet

八十五、executeUpdate()

用于:

INSERT

UPDATE

DELETE

返回:

受影响行数

八十六、INSERT 使用 PreparedStatement

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

try (
    Connection connection =
            DriverManager
                    .getConnection(
                            url,
                            username,
                            password
                    );

    PreparedStatement ps =
            connection
                    .prepareStatement(
                            sql
                    )
) {

    ps.setString(
            1,
            "张三"
    );

    ps.setInt(
            2,
            20
    );

    ps.setBigDecimal(
            3,
            new BigDecimal(
                    "90.50"
            )
    );

    int rows =
            ps.executeUpdate();

    System.out.println(
            "影响行数:"
                    + rows
    );
}

八十七、UPDATE

String sql =
        """
        UPDATE student
        SET score = ?
        WHERE id = ?
        """;

PreparedStatement ps =
        connection
                .prepareStatement(
                        sql
                );

ps.setBigDecimal(
        1,
        new BigDecimal(
                "95.00"
        )
);

ps.setLong(
        2,
        1L
);

int rows =
        ps.executeUpdate();

八十八、DELETE

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

PreparedStatement ps =
        connection
                .prepareStatement(
                        sql
                );

ps.setLong(
        1,
        1L
);

int rows =
        ps.executeUpdate();

八十九、为什么推荐 PreparedStatement

相比:

Statement

PreparedStatement 的优点包括:

参数绑定更清晰

更安全

可以防止常见 SQL 注入

数据库可能复用执行计划

代码更易维护

九十、什么是 SQL 注入

错误示例:

String sql =
        "SELECT * FROM user "
        + "WHERE username = '"
        + username
        + "' AND password = '"
        + password
        + "'";

如果用户输入特殊内容:

可能改变 SQL 原本语义

造成:

绕过登录
非法查询
删除数据
泄露数据

这类问题叫:

SQL Injection

九十一、使用 ? 参数

正确:

String sql =
        """
        SELECT *
        FROM user
        WHERE username = ?
        AND password = ?
        """;

PreparedStatement ps =
        connection
                .prepareStatement(
                        sql
                );

ps.setString(
        1,
        username
);

ps.setString(
        2,
        password
);

参数不会直接当 SQL 语法拼进去。


九十二、参数下标从 1 开始

注意:

ps.setString(
        1,
        username
);

PreparedStatement 参数位置:

从 1 开始

不是数组那样从 0 开始。


九十三、setObject()

还可以:

ps.setObject(
        1,
        value
);

但如果类型明确:

setString

setInt

setLong

setBigDecimal

代码更直观。


九十四、获取自增主键

插入后有时需要:

新生成的 id

创建:

PreparedStatement ps =
        connection
                .prepareStatement(
                        sql,
                        Statement
                                .RETURN_GENERATED_KEYS
                );

执行后:

ResultSet keys =
        ps.getGeneratedKeys();

九十五、读取生成主键

if (keys.next()) {

    long id =
            keys.getLong(1);
}

九十六、JDBC 资源关闭顺序

通常:

ResultSet

PreparedStatement

Connection

都需要关闭。

推荐:

try-with-resources

自动关闭。


九十七、不要忘记关闭 Connection

Connection 是:

数据库连接资源

如果不关闭:

连接泄漏
连接数耗尽
程序无法继续访问数据库

真实项目通常会通过:

连接池

管理。


九十八、事务是什么

假设转账:

A 扣 100

B 加 100

如果:

A 扣成功

B 加失败

数据就不一致。

因此需要:

事务

保证多个操作作为一个整体。


九十九、事务四大特性

经典:

ACID

一百、Atomicity 原子性

事务中的操作
要么全部成功
要么全部失败

一百零一、Consistency 一致性

事务前后:

数据必须满足业务规则和约束

一百零二、Isolation 隔离性

多个事务同时执行时:

互相影响需要受到控制

一百零三、Durability 持久性

事务提交后:

数据应持久保存

即使数据库之后重启,

已提交数据也不应随意丢失。


一百零四、JDBC 默认自动提交

通常 JDBC Connection:

connection
        .getAutoCommit();

默认可能是:

true

每条 SQL 执行后:

自动提交

一百零五、手动事务

connection
        .setAutoCommit(
                false
        );

然后执行多条 SQL。

成功:

connection.commit();

失败:

connection.rollback();

一百零六、JDBC 事务模板

Connection connection =
        null;

try {

    connection =
            DriverManager
                    .getConnection(
                            url,
                            username,
                            password
                    );

    connection
            .setAutoCommit(
                    false
            );

    // SQL 1

    // SQL 2

    connection.commit();

} catch (
        Exception e
) {

    if (
        connection != null
    ) {

        try {

            connection.rollback();

        } catch (
                SQLException rollbackException
        ) {

            e.addSuppressed(
                    rollbackException
            );
        }
    }

    throw new RuntimeException(
            "事务执行失败",
            e
    );

} finally {

    if (
        connection != null
    ) {

        try {

            connection.close();

        } catch (
                SQLException e
        ) {

            e.printStackTrace();
        }
    }
}

真实项目中会进一步通过:

Spring 事务

简化。


一百零七、DAO 思想

DAO:

Data Access Object

中文:

数据访问对象

用于:

把数据库访问代码单独封装起来。


一百零八、为什么需要 DAO

如果所有 JDBC 代码都写在:

main()

或者:

Service

里,

很快会变成:

SQL
业务判断
页面逻辑
输入输出
数据库连接

全部混在一起。

DAO 用来负责:

数据库增删改查

让不同职责分开。


一百零九、经典分层

基础项目常见:

controller
↓
service
↓
dao
↓
database

或者更简单:

view
↓
service
↓
dao

一百一十、DAO 接口

例如:

public interface StudentDao {

    int insert(
            Student student
    );

    int update(
            Student student
    );

    int deleteById(
            Long id
    );

    Student findById(
            Long id
    );

    List<Student> findAll();
}

一百一十一、DAO 实现类

public class StudentDaoImpl
        implements StudentDao {

    @Override
    public int insert(
            Student student
    ) {

        // JDBC INSERT

        return 0;
    }

    @Override
    public Student findById(
            Long id
    ) {

        // JDBC SELECT

        return null;
    }
}

一百一十二、DAO 的好处

数据库代码集中

业务代码更清晰

方便替换实现

便于测试

减少重复代码

后面 MyBatis 中:

Mapper

本质上和 DAO 思想非常接近。


一百一十三、JDBC 工具类

如果每个 DAO 都重复:

url

username

password

getConnection()

close()

可以抽取:

JDBC 工具类

一百一十四、基础 JdbcUtil

public final class JdbcUtil {

    private static final String URL =
            "jdbc:mysql://localhost:3306/java_demo";

    private static final String USERNAME =
            "root";

    private static final String PASSWORD =
            "123456";

    private JdbcUtil() {

    }

    public static Connection
    getConnection()
            throws SQLException {

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

一百一十五、为什么工具类构造方法 private

private JdbcUtil() {

}

表示:

不希望外部 new JdbcUtil()

因为工具类主要提供:

static 方法

一百一十六、数据库配置不要硬编码

不推荐长期:

private static final String PASSWORD =
        "123456";

写死在源码里。

应该放到:

配置文件
环境变量
配置中心
密钥管理系统

等。


一百一十七、properties 配置

例如:

jdbc.properties

内容:

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

然后通过:

Properties

加载。


一百一十八、加载 jdbc.properties

Properties properties =
        new Properties();

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

    if (
        input == null
    ) {

        throw new IllegalStateException(
                "jdbc.properties 不存在"
        );
    }

    properties.load(input);
}

一百一十九、读取配置

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

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

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

一百二十、完整 JdbcUtil 思路

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
                );
    }
}

一百二十一、DAO 查询完整示例

public Student findById(
        Long id
) {

    String sql =
            """
            SELECT id,
                   name,
                   age,
                   score
            FROM student
            WHERE id = ?
            """;

    try (
        Connection connection =
                JdbcUtil
                        .getConnection();

        PreparedStatement ps =
                connection
                        .prepareStatement(
                                sql
                        )
    ) {

        ps.setLong(
                1,
                id
        );

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

            if (
                rs.next()
            ) {

                Student student =
                        new Student();

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

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

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

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

                return student;
            }
        }

        return null;

    } catch (
            SQLException e
    ) {

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

一百二十二、DAO 新增

public int insert(
        Student student
) {

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

    try (
        Connection connection =
                JdbcUtil
                        .getConnection();

        PreparedStatement ps =
                connection
                        .prepareStatement(
                                sql
                        )
    ) {

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

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

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

        return ps.executeUpdate();

    } catch (
            SQLException e
    ) {

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

一百二十三、DAO 修改

public int update(
        Student student
) {

    String sql =
            """
            UPDATE student
            SET name = ?,
                age = ?,
                score = ?
            WHERE id = ?
            """;

    try (
        Connection connection =
                JdbcUtil
                        .getConnection();

        PreparedStatement ps =
                connection
                        .prepareStatement(
                                sql
                        )
    ) {

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

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

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

        ps.setLong(
                4,
                student.getId()
        );

        return ps.executeUpdate();

    } catch (
            SQLException e
    ) {

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

一百二十四、DAO 删除

public int deleteById(
        Long id
) {

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

    try (
        Connection connection =
                JdbcUtil
                        .getConnection();

        PreparedStatement ps =
                connection
                        .prepareStatement(
                                sql
                        )
    ) {

        ps.setLong(
                1,
                id
        );

        return ps.executeUpdate();

    } catch (
            SQLException e
    ) {

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

一百二十五、DAO 查询全部

public List<Student>
findAll() {

    String sql =
            """
            SELECT id,
                   name,
                   age,
                   score
            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()
        ) {

            Student student =
                    new Student();

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

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

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

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

            list.add(student);
        }

        return list;

    } catch (
            SQLException e
    ) {

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

一百二十六、Service 层

DAO:

负责数据库

Service:

负责业务逻辑

例如:

public class StudentService {

    private final StudentDao
            studentDao =
            new StudentDaoImpl();

    public void register(
            Student student
    ) {

        if (
            student.getName()
                    == null
            || student.getName()
                    .isBlank()
        ) {

            throw new IllegalArgumentException(
                    "学生姓名不能为空"
            );
        }

        studentDao.insert(
                student
        );
    }
}

一百二十七、为什么业务校验不要全塞 DAO

例如:

用户名长度是否合法

余额是否足够

订单状态是否允许修改

这些属于:

业务规则

更适合:

Service

而不是 DAO。


一百二十八、控制台学生管理案例结构

Main
↓
StudentService
↓
StudentDao
↓
StudentDaoImpl
↓
JdbcUtil
↓
MySQL

这已经是:

基础分层架构

一百二十九、连接池

如果每次数据库操作都:

DriverManager
        .getConnection(...)

都新建连接,

成本较高。

真实项目通常使用:

数据库连接池

一百三十、连接池是什么

连接池:

提前维护一定数量数据库连接

需要时:

借一个连接

使用结束:

归还连接池

而不是物理关闭整个数据库连接。


一百三十一、常见连接池

例如:

HikariCP

Druid

DBCP

Spring Boot 默认常见:

HikariCP

后面框架阶段会继续学习。


一百三十二、DataSource

JDBC 中:

DataSource

是连接池和数据源常见抽象。

相比:

DriverManager

真实项目更常通过:

DataSource.getConnection()

获取连接。


一百三十三、批量操作

如果需要一次插入很多数据:

不要每条都独立执行一次网络请求

可以使用:

batch

一百三十四、addBatch()

例如:

String sql =
        """
        INSERT INTO student
        (name, age)
        VALUES (?, ?)
        """;

PreparedStatement ps =
        connection
                .prepareStatement(
                        sql
                );

for (
    Student student :
    students
) {

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

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

    ps.addBatch();
}

int[] results =
        ps.executeBatch();

一百三十五、批量操作注意事务

大量批量数据:

通常配合手动事务

否则频繁提交会影响性能。

但具体性能优化需要实际测试。


一百三十六、SQL NULL 和 Java null

数据库字段:

NULL

映射到 Java 引用类型:

null

比较自然。

但基本类型:

int

不能表示:

null

一百三十七、为什么实体类常用包装类型

例如数据库:

age
允许 NULL

Java 如果:

int age;

默认:

0

无法区分:

数据库 NULL

和:

真实值 0

因此实体字段经常使用:

Integer age;

一百三十八、ResultSet.wasNull()

如果使用:

int age =
        rs.getInt(
                "age"
        );

数据库 NULL 时可能得到:

0

可以继续:

if (
    rs.wasNull()
) {

    // 原数据库值是 NULL
}

一百三十九、JDBC 日期时间映射

数据库:

DATE
DATETIME
TIMESTAMP

Java 可以根据需求映射:

LocalDate
LocalDateTime

现代 JDBC 支持:

rs.getObject(
        "create_time",
        LocalDateTime.class
);

具体兼容性取决于驱动版本。


一百四十、不要把数据库密码提交到 Git

例如:

jdbc.password=123456

如果是真实生产密码:

不要提交公开仓库

应该使用:

环境变量
私有配置
密钥管理

一百四十一、SQL 日志中的敏感信息

不要把:

密码
Token
身份证
银行卡

直接完整打印在 SQL 日志中。

调试方便不能高于:

数据安全

一百四十二、常见错误:Statement 拼接用户输入

错误:

String sql =
        "SELECT * FROM user "
        + "WHERE username='"
        + username
        + "'";

正确:

PreparedStatement + ?

一百四十三、常见错误:忘记关闭 ResultSet

数据库资源包括:

ResultSet

Statement

Connection

都应该及时关闭。

推荐:

try-with-resources

一百四十四、常见错误:连接写死在 DAO 中

如果每个 DAO 都:

DriverManager
        .getConnection(...)

会大量重复。

应该抽取:

JdbcUtil

或者后续使用:

DataSource
MyBatis
Spring

统一管理。


一百四十五、常见错误:一个 DAO 方法里处理所有业务

例如 DAO 中:

判断用户是否成年

判断余额是否足够

发送短信

写数据库

职责混乱。

DAO 应主要负责:

数据访问

一百四十六、常见错误:SELECT *

学习阶段:

SELECT *

方便。

实际长期代码中更推荐:

SELECT id,
       name,
       age
FROM student;

优点:

字段清晰
减少无用数据
表结构变化影响更小

一百四十七、常见错误:UPDATE / DELETE 无 WHERE

这是非常危险的数据库事故来源。

执行前一定确认:

WHERE

尤其生产数据库。


一百四十八、常见错误:金额字段用 DOUBLE

金额推荐:

数据库 DECIMAL

Java BigDecimal

避免浮点误差。


一百四十九、常见错误:异常只 printStackTrace

基础学习可以:

e.printStackTrace();

真实项目应:

记录日志
包装异常
统一处理

一百五十、常见错误:事务只 commit 不 rollback

如果关闭自动提交:

setAutoCommit(false)

就必须考虑:

成功 commit

失败 rollback

一百五十一、JDBC 为什么后来会用 MyBatis

JDBC 虽然能完成所有数据库操作,

但会出现大量重复代码:

获取连接

创建 PreparedStatement

设置参数

执行

遍历 ResultSet

映射对象

关闭资源

所以后面会学习:

MyBatis

让框架帮助处理很多模板代码。


一百五十二、JDBC 和 MyBatis 的关系

不是:

学 MyBatis 就不用懂 JDBC

而是:

MyBatis 底层最终还是基于 JDBC

理解 JDBC 后,

才能真正理解:

Mapper
SqlSession
参数绑定
结果映射
事务
连接

这些概念。


一百五十三、综合案例:用户登录查询

SQL:

SELECT id,
       username
FROM user
WHERE username = ?
AND password = ?;

JDBC:

public User login(
        String username,
        String password
) {

    String sql =
            """
            SELECT id,
                   username
            FROM user
            WHERE username = ?
            AND password = ?
            """;

    try (
        Connection connection =
                JdbcUtil
                        .getConnection();

        PreparedStatement ps =
                connection
                        .prepareStatement(
                                sql
                        )
    ) {

        ps.setString(
                1,
                username
        );

        ps.setString(
                2,
                password
        );

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

            if (
                rs.next()
            ) {

                User user =
                        new User();

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

                user.setUsername(
                        rs.getString(
                                "username"
                        )
                );

                return user;
            }

            return null;
        }

    } catch (
            SQLException e
    ) {

        throw new RuntimeException(
                "登录查询失败",
                e
        );
    }
}

真实系统:

密码不能以明文形式存储或直接比对。

后面安全相关章节应使用:

密码哈希
加盐
BCrypt / Argon2 等合适算法

此处仅用于演示 JDBC 参数查询。


一百五十四、综合案例:学生分页查询

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

    int offset =
            (pageNum - 1)
                    * pageSize;

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

    // PreparedStatement 设置
    // offset 和 pageSize
    // 然后组装 List<Student>

    return new ArrayList<>();
}

一百五十五、分页参数校验

例如:

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

    throw new IllegalArgumentException(
            "分页参数不合法"
    );
}

避免:

负数
超大 pageSize

一百五十六、综合案例:转账事务

基本流程:

查询余额
↓
判断余额
↓
A 扣款
↓
B 加款
↓
提交事务

失败:

回滚事务

一百五十七、转账事务核心

connection
        .setAutoCommit(
                false
        );

try {

    // A 扣钱

    // B 加钱

    connection.commit();

} catch (
        Exception e
) {

    connection.rollback();

    throw e;
}

注意:

两条 SQL 必须使用同一个 Connection,才能处于同一个 JDBC 本地事务中。


一百五十八、为什么同一事务必须同一 Connection

如果:

A 扣款使用 Connection1

B 加款使用 Connection2

它们是:

两个不同数据库会话

无法直接作为同一个 JDBC 本地事务统一提交和回滚。


一百五十九、DAO 中事务的难点

如果:

DAO1 自己获取连接

DAO2 自己获取连接

Service 想把两个 DAO 方法放入同一个事务:

就比较麻烦

这也是后面:

Spring 事务管理

要解决的问题之一。


一百六十、基础 JDBC 项目目录建议

例如:

src
└─ main
   ├─ java
   │  └─ com.example
   │     ├─ entity
   │     │  └─ Student.java
   │     ├─ dao
   │     │  ├─ StudentDao.java
   │     │  └─ impl
   │     │     └─ StudentDaoImpl.java
   │     ├─ service
   │     │  └─ StudentService.java
   │     ├─ util
   │     │  └─ JdbcUtil.java
   │     └─ Main.java
   │
   └─ resources
      └─ jdbc.properties

这个结构已经可以练习:

分层
实体
DAO
Service
工具类
配置文件

一百六十一、练习题 1:建库建表

创建:

school_db

创建表:

student

字段:

id
name
age
gender
score
create_time

一百六十二、练习题 2:基础 SQL

完成:

插入 5 名学生

查询全部

按成绩降序

查询及格学生

修改某学生成绩

删除指定学生

一百六十三、练习题 3:JDBC 查询

使用:

PreparedStatement

根据 id 查询学生。

要求返回:

Student

对象。


一百六十四、练习题 4:JDBC 新增

编写:

insertStudent(
        Student student
)

返回:

受影响行数

一百六十五、练习题 5:DAO

创建:

StudentDao
StudentDaoImpl

实现:

新增
删除
修改
按 id 查询
查询全部

一百六十六、练习题 6:JdbcUtil

把:

URL
用户名
密码

放到:

jdbc.properties

然后封装:

JdbcUtil.getConnection()

一百六十七、练习题 7:登录查询

创建:

user

表。

要求:

用户名和密码从参数传入
必须使用 PreparedStatement
不能字符串拼接 SQL

一百六十八、练习题 8:分页查询

实现:

findPage(
        int pageNum,
        int pageSize
)

使用:

LIMIT ?, ?

一百六十九、练习题 9:统计

SQL 完成:

学生总数

平均分

最高分

不同性别人数

一百七十、练习题 10:事务

建立:

account

表。

实现:

A 给 B 转账

要求:

关闭自动提交

成功 commit

失败 rollback

一百七十一、练习题 11:批量插入

准备:

List<Student>

使用:

addBatch
executeBatch

批量插入。


一百七十二、练习题 12:控制台学生管理系统

功能:

1. 新增学生

2. 删除学生

3. 修改学生

4. 查询单个学生

5. 查询全部学生

6. 分页查询

0. 退出

结构:

Main
↓
StudentService
↓
StudentDao
↓
JdbcUtil
↓
MySQL

这是进入 Web 开发之前非常适合做的综合练习。


一百七十三、本章核心知识结构

数据库基础
│
├─ 数据库
├─ 表
├─ 字段
├─ 主键
└─ 约束

SQL
│
├─ DDL
│   ├─ CREATE
│   ├─ ALTER
│   └─ DROP
│
├─ DML
│   ├─ INSERT
│   ├─ UPDATE
│   └─ DELETE
│
└─ DQL
    └─ SELECT

JDBC
│
├─ DriverManager
├─ Connection
├─ PreparedStatement
├─ ResultSet
└─ Transaction

项目结构
│
├─ Entity
├─ DAO
├─ Service
└─ JdbcUtil

一百七十四、JDBC 核心 API 必须记住

DriverManager.getConnection()

Connection.prepareStatement()

PreparedStatement.setString()

PreparedStatement.setInt()

PreparedStatement.setLong()

PreparedStatement.executeQuery()

PreparedStatement.executeUpdate()

ResultSet.next()

ResultSet.getString()

ResultSet.getInt()

Connection.setAutoCommit()

Connection.commit()

Connection.rollback()

一百七十五、必须掌握 PreparedStatement

需要理解:

为什么不推荐 Statement 拼接用户输入

什么是 SQL 注入

? 占位符如何设置参数

参数下标为什么从 1 开始

SELECT 使用 executeQuery

增删改使用 executeUpdate

一百七十六、必须掌握 DAO

至少能回答:

DAO 是什么?

为什么要有 DAO?

DAO 和 Service 有什么区别?

JdbcUtil 为什么要封装?

为什么实体类和数据库表经常对应?

一百七十七、本章总结

这一章真正完成了:

Java 程序

和:

数据库

之间的连接。

前面我们写:

List<Student>

数据只存在:

内存

现在通过 JDBC:

Java
↓
PreparedStatement
↓
MySQL

数据可以真正持久化。

学习路线可以总结:

数据库概念
↓
SQL
↓
MySQL 表设计
↓
JDBC
↓
Connection
↓
PreparedStatement
↓
ResultSet
↓
CRUD
↓
事务
↓
DAO
↓
JdbcUtil
↓
Service 分层

如果这一章能够独立完成一个:

控制台学生管理系统
+
MySQL
+
JDBC
+
DAO

那么后面学习:

MyBatis
Servlet
SpringBoot

时就会非常顺。

因为这些框架本质上是在进一步解决:

JDBC 模板代码太多

对象映射繁琐

连接事务管理复杂

项目分层需要规范化

的问题。