数据库系统原理 · 数据库应用开发 · 自学总结

本章核心:应用程序怎么连数据库?体系结构怎么选?用什么技术访问数据库?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. 关注点分离 ------ 每层只解决一类问题,代码清晰。

  2. 可扩展性 ------ 哪层压力大就扩展哪层。

  3. 可维护性 ------ 改界面不影响数据库,改数据库不影响界面。

  4. 容错性 ------ 分布式避免单点故障。


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 批量操作,一次发送

核心价值

  1. 标准化 ------ 一套 API 通吃各种数据库。

  2. 效率 ------ 连接池、预编译、批量操作大幅提升性能。

  3. 安全 ------ 预编译语句根治 SQL 注入。

  4. 可控 ------ 程序里精确控制事务边界。


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 自动翻译方言

核心价值

  1. 开发效率 ------ 不写 SQL,专注业务逻辑。

  2. 可维护性 ------ 数据库结构变了,改映射配置即可。

  3. 数据库无关 ------ 从 MySQL 切 Oracle,理论上只改配置。

  4. 面向对象 ------ 用 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 = EAGERJOIN 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,复杂场景需调优

总结基于《数据库系统概论》数据库应用开发相关章节知识体系整理

相关推荐
No8g攻城狮2 小时前
【人大金仓】wsl2+ubuntu22.04安装人大金仓数据库V9
java·数据库·spring boot·非关系型数据库
山峰哥2 小时前
SQL慢查询调优实战:从全表扫描到索引覆盖的完整复盘
前端·数据库·sql·性能优化
代码中介商2 小时前
Redis入门:5大数据类型全解析
数据库·redis·缓存
渣渣盟3 小时前
数据库设计范式详解(纯小白版)
数据库·oracle·软考·数据库工程师
夜雪闻竹4 小时前
Cursor 对话导入:解析 SQLite 里的宝藏
数据库·sqlite·ai编程
hhb_6185 小时前
PL/SQL核心技术难点梳理与实战应用案例解析
数据库·sql
m0_470857645 小时前
PHP怎么实现工厂模式_Factory模式编写指南【指南】
jvm·数据库·python
用户434309241696 小时前
Day29:图片上传 + 存数据库(Multer + MySQL)
数据库·后端
lolo大魔王6 小时前
MongoDB 索引机制详解:单字段索引、复合索引、唯一索引与性能优化
数据库·mongodb