Java数据库基础与JDBC详解
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 模板代码太多
对象映射繁琐
连接事务管理复杂
项目分层需要规范化
的问题。