本章核心:应用程序怎么连数据库?体系结构怎么选?用什么技术访问数据库?ORM 框架解决了什么问题?
一、数据库系统的体系结构
1.1 是什么?
数据库系统的体系结构 = 应用程序与数据库之间的组织方式和交互模式,决定了系统如何分层、如何部署、如何扩展。
下辖知识点
| 知识点 | 是什么 |
|---|---|
| 主机-终端结构(集中式) | 所有计算和数据库都在一台大型机上,终端只是显示器和键盘 |
| 客户机/服务器结构(C/S) | 客户端负责界面和部分逻辑,服务器负责数据库,通过网络交互 |
| 浏览器/服务器结构(B/S) | 浏览器做展示,Web 服务器做业务逻辑,数据库服务器存数据,三层架构 |
| 两层 C/S | 客户端直接连数据库(胖客户端) |
| 三层 C/S | 客户端 → 应用服务器 → 数据库服务器(瘦客户端) |
| 多层架构 / N-tier | 表现层 → 业务逻辑层 → 数据访问层 → 数据库层,每层可独立部署 |
| 分布式数据库 | 数据物理分布在多个节点上,逻辑上是一个整体 |
| 并行数据库 | 利用多 CPU/多磁盘并行处理查询 |
| 云数据库架构 | 数据库部署在云端,支持弹性伸缩 |
| 微服务架构 | 每个服务有自己的数据库,服务间通过 API 通信 |
| CQRS / 读写分离 | 读操作和写操作分离到不同数据库实例 |
| 分库分表 / Sharding | 数据水平拆分到多个数据库实例 |
三种经典架构对比
| 架构 | 结构 | 优点 | 缺点 | 典型应用 |
|---|---|---|---|---|
| 主机-终端 | 大型机 + 哑终端 | 集中管理、安全 | 昂贵、扩展难、单点故障 | 银行核心系统(历史) |
| C/S 两层 | 客户端 ↔ 数据库 | 交互响应快、功能丰富 | 客户端维护麻烦、升级困难 | 财务软件、ERP 客户端 |
| B/S 三层 | 浏览器 → Web 服务器 → 数据库 | 零客户端维护、跨平台、易扩展 | 交互体验受限、网络依赖 | 电商网站、管理系统 |
| N-tier / 微服务 | 浏览器 → 网关 → 服务集群 → 数据库集群 | 高可用、弹性伸缩、技术异构 | 复杂度爆炸、分布式事务难 | 互联网大厂、电商平台 |
三层架构详解
┌─────────────────────────────────────────┐
│ 表现层(Presentation) │ ← 浏览器/APP,负责界面展示
│ HTML/CSS/JS / Android / iOS │
├─────────────────────────────────────────┤
│ 业务逻辑层(Business Logic) │ ← 后端服务,处理业务规则
│ Java / Python / Node.js / Go │
├─────────────────────────────────────────┤
│ 数据访问层(Data Access) │ ← 数据库交互,CRUD + 事务
│ JDBC / ADO.NET / ORM / SQL │
├─────────────────────────────────────────┤
│ 数据库层(Database) │ ← MySQL / PostgreSQL / Oracle
│ 数据存储 + 查询处理 + 事务管理 │
└─────────────────────────────────────────┘
分布式数据库架构
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 应用服务器 │───→│ 数据库中间件 │←──│ 应用服务器 │
└─────────────┘ └──────┬──────┘ └─────────────┘
│
┌──────────────┼──────────────┐
↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 分片1 │ │ 分片2 │ │ 分片3 │
│ 节点A │ │ 节点B │ │ 节点C │
└─────────┘ └─────────┘ └─────────┘
-
分片(Sharding):按某种规则(如用户ID哈希、地理区域)把数据分布到不同节点。
-
副本(Replication):每个分片有主副本和从副本,保证高可用和读扩展。
-
协调节点:负责路由查询到正确的分片,汇总结果。
1.2 为什么要有体系结构?
没有清晰的体系结构,系统就是 spaghetti:
| 没有体系结构的问题 | 体系结构解决 |
|---|---|
| 界面代码和 SQL 混在一起 | 分层后各层只关心自己的事 |
| 换个数据库要重写整个程序 | 数据访问层抽象隔离了数据库差异 |
| 用户量涨了系统直接崩 | 三层可独立扩展(加 Web 服务器、加数据库从库) |
| 前端团队和后端团队互相踩脚 | 接口层定义好后,两边独立开发 |
| 数据库挂了整个业务停摆 | 分布式架构 + 读写分离 + 副本保证高可用 |
核心价值:
-
关注点分离 ------ 每层只解决一类问题,代码清晰。
-
可扩展性 ------ 哪层压力大就扩展哪层。
-
可维护性 ------ 改界面不影响数据库,改数据库不影响界面。
-
容错性 ------ 分布式避免单点故障。
1.3 怎么用?
选型决策
| 场景 | 推荐架构 |
|---|---|
| 内部管理系统、用户少 | B/S 三层 |
| 需要复杂交互、实时响应(游戏、交易终端) | C/S 或 B/S + WebSocket |
| 互联网 ToC 应用、海量用户 | N-tier + 微服务 + 分库分表 |
| 金融核心系统、强一致性要求 | 集中式 / 分布式事务 + 两地三中心 |
| 全球化部署 | 分布式数据库 + 多活架构 |
分层开发实践
数据访问层(DAO/Repository)设计:
┌─────────────────────────────────────────┐
│ interface StudentRepository │
│ + findById(id): Student │
│ + findByDept(dept): List<Student> │
│ + save(student): void │
│ + delete(id): void │
├─────────────────────────────────────────┤
│ class JdbcStudentRepository │
│ - 用 JDBC 实现接口方法 │
│ class MyBatisStudentRepository │
│ - 用 MyBatis 实现 │
│ class JpaStudentRepository │
│ - 用 JPA/Hibernate 实现 │
└─────────────────────────────────────────┘
好处:今天用 MySQL,明天换 PostgreSQL,只需换实现类,上层业务代码不动。
读写分离实践
写操作 → 主库(Master)
读操作 → 从库(Slave1, Slave2, ...)
主库异步同步到从库(有短暂延迟,通常毫秒级)
应用层实现:
- 框架自动路由(如 ShardingSphere)
- 或手动在 DAO 层指定数据源
二、数据库访问技术
2.1 是什么?
数据库访问技术 = 应用程序连接和操作数据库的编程接口和标准,是应用层与数据库层之间的"桥梁"。
下辖知识点
| 知识点 | 是什么 |
|---|---|
| ODBC(Open Database Connectivity) | 微软推出的数据库访问标准 API,C 语言接口,跨数据库 |
| JDBC(Java Database Connectivity) | Java 版数据库访问标准 API |
| ADO.NET | .NET 平台的数据访问技术 |
| 数据库驱动(Driver) | 连接特定数据库的适配器(如 MySQL Connector/J) |
| 数据源(DataSource)/ 连接池 | 预先创建并管理的数据库连接集合,复用连接减少开销 |
| SQL 注入防护 | 预编译语句(PreparedStatement)防止拼接攻击 |
| 事务 API | 编程控制事务的提交和回滚 |
| 批量操作(Batch) | 一次性发送多条 SQL,减少网络往返 |
| 存储过程调用 | 通过 API 调用数据库中的存储过程 |
| 结果集处理 | 遍历查询返回的数据行 |
| 连接字符串 | 包含服务器地址、端口、数据库名、用户名、密码的配置串 |
JDBC 核心接口
| 接口/类 | 作用 |
|---|---|
| DriverManager | 管理数据库驱动,获取连接 |
| Connection | 代表一个数据库连接 |
| Statement | 执行静态 SQL |
| PreparedStatement | 执行预编译 SQL(防注入,性能更好) |
| CallableStatement | 调用存储过程 |
| ResultSet | 封装查询结果,支持逐行遍历 |
| ResultSetMetaData | 获取结果集的列信息(列名、类型等) |
2.2 为什么要有数据库访问技术?
没有统一访问技术,每个数据库都要写一套代码:
| 没有访问技术的问题 | 访问技术解决 |
|---|---|
| Java 程序连 MySQL 要调一套 API,连 Oracle 又调另一套 | JDBC 统一接口,换数据库只需换驱动 jar 包 |
| 每次操作都新建连接,慢且耗资源 | 连接池预先创建,用完归还复用 |
| SQL 拼接导致注入攻击 | PreparedStatement 参数化,安全 |
| 程序崩溃时数据库操作只执行了一半 | 事务 API 保证原子性 |
| 逐条插入 10000 条,网络往返 10000 次 | Batch 批量操作,一次发送 |
核心价值:
-
标准化 ------ 一套 API 通吃各种数据库。
-
效率 ------ 连接池、预编译、批量操作大幅提升性能。
-
安全 ------ 预编译语句根治 SQL 注入。
-
可控 ------ 程序里精确控制事务边界。
2.3 怎么用?
JDBC 完整示例
// 1. 加载驱动(现代 JDBC 可省略)
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取连接(实际项目用连接池,不用 DriverManager)
String url = "jdbc:mysql://localhost:3306/教学管理?useSSL=false";
Connection conn = DriverManager.getConnection(url, "user", "password");
// 3. 开启事务
conn.setAutoCommit(false);
try {
// 4. 预编译语句(防注入 + 性能优化)
String sql = "INSERT INTO 学生 (学号, 姓名, 系号) VALUES (?, ?, ?)";
PreparedStatement stmt = conn.prepareStatement(sql);
// 5. 批量插入
for (Student s : studentList) {
stmt.setString(1, s.getId());
stmt.setString(2, s.getName());
stmt.setString(3, s.getDept());
stmt.addBatch(); // 加入批量
}
stmt.executeBatch(); // 一次性执行
// 6. 查询
String query = "SELECT * FROM 学生 WHERE 系号 = ?";
PreparedStatement qstmt = conn.prepareStatement(query);
qstmt.setString(1, "CS");
ResultSet rs = qstmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("姓名"));
}
// 7. 提交事务
conn.commit();
} catch (Exception e) {
conn.rollback(); // 出错回滚
} finally {
conn.close(); // 归还连接池(或关闭)
}
连接池配置(生产环境必备)
// HikariCP(Java 最快的连接池)
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/教学管理");
config.setUsername("user");
config.setPassword("password");
config.setMaximumPoolSize(20); // 最大连接数
config.setMinimumIdle(5); // 最小空闲连接
config.setConnectionTimeout(30000); // 获取连接超时
config.setIdleTimeout(600000); // 空闲连接超时
config.setMaxLifetime(1800000); // 连接最大生命周期
HikariDataSource dataSource = new HikariDataSource(config);
// 使用
Connection conn = dataSource.getConnection();
// ... 操作 ...
conn.close(); // 归还连接池,不是真关闭
连接池为什么快?
-
省去 TCP 握手、数据库认证的时间(连接已建好)。
-
控制并发连接数,避免压垮数据库。
-
连接复用,减少资源浪费。
ODBC / ADO.NET 简要
// C# ADO.NET 示例
string connStr = "Server=localhost;Database=教学管理;Trusted_Connection=true;";
using (SqlConnection conn = new SqlConnection(connStr))
{
conn.Open();
string sql = "SELECT * FROM 学生 WHERE 系号 = @dept";
using (SqlCommand cmd = new SqlCommand(sql, conn))
{
cmd.Parameters.AddWithValue("@dept", "CS"); // 参数化
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine(reader["姓名"]);
}
}
}
}
三、对象-关系映射框架(ORM)
3.1 是什么?
ORM(Object-Relational Mapping)= 把数据库的表和应用程序的对象自动映射起来的技术,让程序员用面向对象的方式操作数据库,不用手写 SQL。
下辖知识点
| 知识点 | 是什么 |
|---|---|
| 对象-表映射 | 一个类对应一张表,一个对象对应一行记录 |
| 属性-列映射 | 类的字段对应表的列 |
| 关联映射 | 对象间的引用关系对应表间的外键关系(一对一/一对多/多对多) |
| 继承映射 | 类的继承层次映射到数据库的三种策略(单表/类表/具体表) |
| CRUD 自动生成 | 增删改查不用写 SQL,调对象方法即可 |
| 懒加载(Lazy Loading) | 关联对象在真正访问时才从数据库加载 |
| 急加载(Eager Loading) | 查询主对象时顺便把关联对象一起查出来 |
| 一级缓存 / 会话缓存 | 同一个事务内,重复查同一对象直接返回内存中的实例 |
| 二级缓存 / 全局缓存 | 跨会话的对象缓存,减少数据库访问 |
| 脏检查(Dirty Checking) | 自动检测对象属性变化,事务提交时自动 UPDATE |
| 事务管理 | 声明式事务(@Transactional),自动 begin/commit/rollback |
| 查询语言 | JPQL(JPA)、HQL(Hibernate)、LINQ(.NET)等面向对象的查询语言 |
| 主要框架 | Hibernate(Java)、MyBatis(半 ORM)、Entity Framework(.NET)、Django ORM(Python)、SQLAlchemy(Python) |
ORM 映射关系速查
| 对象关系 | 数据库表示 | ORM 配置 |
|---|---|---|
| 一对一 | 两张表,一方含外键指向另一方 | @OneToOne + @JoinColumn |
| 一对多 | 一张"多"方表含外键指向"一"方 | @OneToMany + @ManyToOne |
| 多对多 | 中间关联表,存两方主码 | @ManyToMany + @JoinTable |
| 继承 | 单表/类表/具体表三种策略 | @Inheritance |
| 组件/嵌入 | 类的部分属性存到同一张表 | @Embeddable + @Embedded |
3.2 为什么要有 ORM?
直接用 JDBC 的困境:
| 问题 | JDBC 现状 | ORM 解决 |
|---|---|---|
| 大量重复 SQL(CRUD 占 80%) | 每个表都要手写 INSERT/SELECT/UPDATE/DELETE | 自动生成 |
| 结果集遍历转对象,代码冗长 | while(rs.next()) { s.setName(rs.getString(...)) } |
自动映射 |
| 表改了字段,程序里 SQL 全改 | 全文搜索替换,漏改就崩 | 改一处映射配置,代码不动 |
| 对象关系复杂时 SQL 极难写 | 查一个订单要 JOIN 5 张表 | order.getItems() 自动查 |
| 事务管理代码重复 | 每个方法都写 try-begin-commit-catch-rollback | @Transactional 声明式 |
| 数据库方言差异 | MySQL 分页用 LIMIT,SQL Server 用 TOP | ORM 自动翻译方言 |
核心价值:
-
开发效率 ------ 不写 SQL,专注业务逻辑。
-
可维护性 ------ 数据库结构变了,改映射配置即可。
-
数据库无关 ------ 从 MySQL 切 Oracle,理论上只改配置。
-
面向对象 ------ 用
student.getDepartment().getName()而不是手写 JOIN。
ORM 的代价:
-
性能开销 ------ 自动生成 SQL 不一定最优,复杂查询可能慢。
-
学习成本 ------ 要理解缓存、懒加载、N+1 问题等概念。
-
复杂查询受限 ------ 报表、统计分析等复杂 SQL 用 ORM 反而麻烦。
3.3 怎么用?
JPA / Hibernate 完整示例
// === 1. 定义实体 ===
@Entity
@Table(name = "学生")
public class Student {
@Id
@Column(name = "学号")
private String id;
@Column(name = "姓名", nullable = false)
private String name;
@ManyToOne // 多对一:多个学生属于一个系
@JoinColumn(name = "系号")
private Department department;
@OneToMany(mappedBy = "student", fetch = FetchType.LAZY) // 懒加载
private List<Enrollment> enrollments;
// getters / setters
}
@Entity
@Table(name = "系")
public class Department {
@Id
@Column(name = "系号")
private String id;
@Column(name = "系名")
private String name;
@OneToMany(mappedBy = "department")
private List<Student> students;
}
// === 2. 数据访问层(Repository)===
public interface StudentRepository extends JpaRepository<Student, String> {
// 方法名解析自动生成查询
List<Student> findByDepartmentName(String deptName);
// JPQL 自定义查询
@Query("SELECT s FROM Student s WHERE s.department.name = :dept")
List<Student> findByDept(@Param("dept") String deptName);
}
// === 3. 业务层(声明式事务)===
@Service
public class StudentService {
@Autowired
private StudentRepository repo;
@Transactional(readOnly = true)
public List<Student> getStudentsByDept(String dept) {
return repo.findByDepartmentName(dept);
}
@Transactional
public void transferStudent(String studentId, String newDeptId) {
Student s = repo.findById(studentId).orElseThrow();
s.setDepartment(new Department(newDeptId));
// 不用手动 save,脏检查自动 UPDATE
}
}
MyBatis(半 ORM)示例
MyBatis 不全自动映射,开发者写 SQL,框架负责参数绑定和结果映射。适合 SQL 复杂、需要精细控制的场景。
// 实体(纯 POJO,无注解)
public class Student {
private String id;
private String name;
private Department department;
// getters / setters
}
// Mapper 接口 + XML
public interface StudentMapper {
@Select("SELECT * FROM 学生 WHERE 系号 = #{deptId}")
@Results({
@Result(property = "id", column = "学号"),
@Result(property = "name", column = "姓名"),
@Result(property = "department",
column = "系号",
one = @One(select = "getDepartmentById"))
})
List<Student> findByDept(String deptId);
}
ORM 避坑指南
| 坑 | 现象 | 解决 |
|---|---|---|
| N+1 问题 | 查 100 个学生,触发 100 次额外的系查询 | 急加载 fetch = EAGER 或 JOIN FETCH |
| 懒加载异常 | 会话关闭后访问懒加载属性报错 | 在会话内访问,或改用急加载 |
| 大事务 | 事务里加载太多对象,内存溢出 | 分页处理,或改用手动控制 |
| 缓存不一致 | 二级缓存没更新,读到旧数据 | 合理设置缓存过期,或禁用缓存 |
| 批量插入慢 | 逐条 INSERT,每条都刷盘 | 配置批量大小,或改用 JDBC 批量 |
| 复杂查询性能差 | ORM 生成的 SQL 太啰嗦 | 手写原生 SQL 或存储过程 |
N+1 问题详解:
场景:查询所有学生,并显示他们的系名
ORM 做法:
1. SELECT * FROM 学生 → 查出 100 条
2. 遍历每个学生,调用 getDepartment()
→ SELECT * FROM 系 WHERE 系号 = ? 执行 100 次!
解决:
1. JOIN FETCH:SELECT s FROM Student s JOIN FETCH s.department
→ 一条 SQL 把学生和系都查回来
2. 急加载配置:@ManyToOne(fetch = EAGER)
四、三种访问方式对比
| 维度 | 纯 JDBC | MyBatis(半 ORM) | Hibernate/JPA(全 ORM) |
|---|---|---|---|
| SQL 控制权 | 完全手写 | 手写 SQL,框架辅助映射 | 自动生成,可自定义 |
| 开发效率 | 低(大量样板代码) | 中 | 高(CRUD 自动生成) |
| 灵活性 | 最高 | 高 | 中(复杂查询受限) |
| 性能调优 | 完全可控 | 可控 | 需理解内部机制 |
| 数据库可移植 | 低 | 中 | 高(方言自动适配) |
| 学习成本 | 低 | 中 | 高(缓存、懒加载等) |
| 适用场景 | 性能极致、简单项目 | SQL 复杂、需精细控制 | 业务复杂、快速开发 |
五、知识脉络图
数据库应用开发
│
├── 数据库系统的体系结构
│ ├── 是什么:应用与数据库的组织方式和交互模式
│ ├── 为什么:关注点分离、可扩展、可维护、高可用
│ └── 怎么用:
│ ├── 集中式 / C/S / B-S / N-tier / 微服务 选型
│ ├── 三层架构:表现层 → 业务层 → 数据访问层 → 数据库层
│ ├── 分布式:分片 + 副本 + 协调节点
│ └── 读写分离:写主库,读从库
│
├── 数据库访问技术
│ ├── 是什么:应用程序连接和操作数据库的编程接口
│ ├── 为什么:标准化访问、连接复用、防注入、事务控制
│ └── 怎么用:
│ ├── JDBC / ODBC / ADO.NET 标准 API
│ ├── 连接池(HikariCP / Druid)配置与使用
│ ├── PreparedStatement 参数化防注入
│ ├── 批量操作(Batch)提升写入性能
│ └── 事务 API 控制原子性
│
└── 对象-关系映射框架(ORM)
├── 是什么:对象与数据库表自动映射的技术
├── 为什么:消除样板代码、提升效率、面向对象操作数据库
└── 怎么用:
├── Hibernate / JPA 全自动映射
├── MyBatis 半自动映射(SQL 手写 + 结果自动映射)
├── 关联映射:@OneToOne / @OneToMany / @ManyToMany
├── 加载策略:懒加载 vs 急加载
├── 缓存:一级缓存(会话)vs 二级缓存(全局)
├── 脏检查 + 声明式事务 @Transactional
└── 避坑:N+1、懒加载异常、大事务、复杂查询性能
六、一句话记忆
| 概念 | 一句话 |
|---|---|
| C/S | 客户端胖,装软件,直连数据库 |
| B/S | 浏览器瘦,零安装,通过 Web 服务器连数据库 |
| 三层架构 | 界面、逻辑、数据各管一层,互不干扰 |
| 分库分表 | 数据太多一台撑不住,拆成多台一起扛 |
| 读写分离 | 写交给老大(主库),读交给小弟(从库) |
| JDBC | Java 连数据库的标准接口,一套 API 走天下 |
| 连接池 | 连接预先建好,用完归还,不用每次都握手 |
| PreparedStatement | SQL 先编译好,参数往里填,防注入神器 |
| ORM | 数据库表当成 Java 类来操作,不用写 SQL |
| 懒加载 | 用到的时候才去数据库查,省资源 |
| 急加载 | 一次性全查回来,省得跑第二趟 |
| N+1 | 查了 1 个列表,触发 N 次额外查询,性能杀手 |
| 脏检查 | 对象属性变了,提交时 ORM 自动帮你 UPDATE |
| 声明式事务 | 方法头贴个注解,事务自动管开管关 |
| MyBatis | SQL 自己写,映射交给它,灵活又省事 |
| Hibernate | 全自动,CRUD 零 SQL,复杂场景需调优 |
总结基于《数据库系统概论》数据库应用开发相关章节知识体系整理