本章核心:应用程序怎么连数据库?体系结构怎么选?用什么技术访问数据库?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 │
- └─────────┘ └─────────┘ └─────────┘
复制代码
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(); // 归还连接池,不是真关闭
复制代码
连接池为什么快?
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,复杂场景需调优 |
总结基于《数据库系统概论》数据库应用开发相关章节知识体系整理
|