前言

这是关于 MySQL 系列文章的第三篇,本篇在 MySQL 使用的基础上做一个小小的升华,来简单聊一聊数据库的设计,还有一句话不得不再次赘述,数据库博大精深,本系列文章内容较浅,适合于前端的同学们对 MySQL 的入门,这也是我的学习笔记,希望可以帮助大家。

系列文章:

为什么设计数据库

说到为什么要设计数据库,就要说到数据的完整性,我们要在设计数据库时保证域的完整性和实体的完整性,同时从性能出发,我们要保证最大限度的节省存储空间,比如一张成绩表,上面没必要存储学生的姓名、年龄等信息,只需要存储成绩,如果一个数据库设计的合理,最后的结果就是方便我们对数据库的开发和扩展。

如果是一个 “糟糕” 的数据库设计会造成一系列的不良反应,比如数据冗余,存储空间浪费,内存浪费,有时甚至会造成数据插入和更新的异常,比如学生表存了学生信息,而成绩表也存了,这样在修改时没有全部修改就会出现错误。

软件项目开发中数据库设计的生命周期

软件项目开发中数据库设计的生命周期可大概分为以下几个阶段:

  • 需求分析阶段,分析客户的业务和数据处理需求;
  • 概要设计阶段,设计数据库 E-R 模型图,确认需求的正确和完整性;
  • 详细设计阶段,应用三大范式审核数据库;
  • 代码编写阶段,物理实现数据库,编码实现应用;
  • 软件测试阶段;
  • 安装部署阶段。

上面数据库的设计经历了从 “现实世界” 到 “信息世界” 到 “数据库模型” 再到 “数据库” 产生的一个完整过程。

设计数据库的步骤

  • 收集信息:收集信息其实就是与相关人员进行交流、访谈、调研,充分了解用户需求,理解整个项目的完整流程,并理解数据库需要完成的任务,这部分工作大部分由需求人员完成,并根技术人员进行对接。
  • 标识实体和实体属性:开发人员在明确需求和流程之后,标识数据库的实体,比如学生信息表,每一条实体中应该由哪些字段组成,成绩表中实体由哪些字段组成等等。
  • 标识实体之间的关系:其实就是通过表之间的某字段对表进行关联,对表的实体之间建立对应关系,如学生表的 id 字段会关联成绩表的 student_id 字段,用来查找某个学生的成绩。

数据库 E-R 图

E-R 图基本概念

E-R 图也叫做实体关系图,是指用实体、关系、属性三个基本概念概括数据的基本结构,从而描述静态数据的概念模型。

  • E-R 图的实体:即数据模型中的数据对象,每一张表就是一个 E-R 图的实体。
  • E-R 图的属性:即数据对象中所具有的属性,例如学生表的学生、姓名、年龄等,属性又分为唯一属性和非唯一属性,唯一属性如经过唯一约束和主键约束的属性,不可重复,其他的都是非唯一属性。
  • E-R 图的关系:用来表示每一个数据对象与数据对象之间的联系,即每一个实体之间的联系,例如学生表和成绩表之间的联系,因为每个学生都有自己的成绩。

E-R 图的关联关系

1 对 1 (1 : 1)

11 关系是指对于实体集 A 和 实体集 BA 中的每一个实体最多与 B 中的一个实体有关系,反之在实体集 B 中的每一个实体之多与实体集 A 中的一个实体有关系。


1 对 1 关系图
1 对 1 关系图


1 对多(1 : N)

1 对多关系是指实体集 A 与实体集 B 中至少有 N (N > 0) 个实体有关系,并且实体集 B 中最多与实体集 A 中的一个实体有关系。


1 对多关系图
1 对多关系图


多对多(M : N)

多对多关系是指实体集 A 中的每一个实体与实体集 B 中至少有 M (M > 0) 个实体有关系,并且实体集 B 中的每一个实体与实体集 A 中至少有 N (N > 0) 个实体有关系。


多对多关系图
多对多关系图


数据库设计的三大范式

确保每列的原子性

如果每列都是不可再分的最小单元信息,则满足第一范式,比如下图中,地址是由国家和城市组成的,显然可以继续在拆分成两个列,国家和城市,是不满足第一范式的,需要将地址列差分成国家和城市两个列。


原子性事例
原子性事例


举一个简单的例子,我们平时在淘宝购物的时候需要添加地址,在填写新地址时,都是让我们选择国家、省、城市、区、街道、小区这样的方式,而不是让我们自己将这些地址写在一起,其原因就是因为淘宝的数据库设计严格遵循每列的原子性,这样的提交可以方便后端获取每一个列的信息在数据库中进行存储。

每张表只能描述一件事情

如下图中所示,在左侧的表中,描述了学生信息和课程信息,这明显是两件事情,假设再有一张成绩表,也要描述学生信息,课程信息和成绩等多件事情,就会造成数据的重复、冗余,也可能会导致更新、插入、删除数据异常的现象。


每个表描述一件
每个表描述一件


所以正确的做法是应该将左侧表差分成两张表分别为学生表和课程表,并使用学生编号与课程编号进行关联。

其他列都不传递依赖于主键列

其他列都不传递依赖于主键列的意思是表中各列必须都与主键直接相关,不能间接相关,从下图左表可以看出,学生编号为主键,年级 ID 也应该为主键,正常应该通过学生编号找到年级 ID,再找到年级名称,这样年级名称与学生编号之间就形成了一个传递并且依赖于主键年级 ID,即年级 ID 做为主键在中间隔了一层,这样就使年级名称与主键学生编号间接相关,如果在同一张表中,所有的字段都是应该直接依赖于主键,而不是再通过其他的主键传递。


其他列都不传递依赖于主键列
其他列都不传递依赖于主键列


如果一个表中表述了多件事情并有多个作为主键的列,与上一条的处理方式相同,应该拆成多张表,并且每张表只有一个主键列。

RBAC 基于角色的访问控制

RBAC 的含义

RBACRole-Based Access Control)基于角色的访问控制,就是用户通过角色与权限进行关联,简单的说,一个用户拥若干个角色,每个角色拥有若干个权限,这样就构造成了 “用户 → 角色 → 权限 → 资源” 的授权模型,在这个模型中,用户与角色之间,角色与权限之间,权限与资源之间,一般都是多对多的关系,在 RBAC 中最重要的概念主要有四部分,就是用户(User)、角色(Role)、权限(Permission)和资源(Resource)。

RBAC 的安全原则

  • 最小权限原则:最小权限原则之所以被 RBAC 所支持,是因为 RBAC 可以将其角色配置成完成任务所需要的最小的权限集;
  • 责任分离原则:可以通过调用相互独立互斥的角色来共同完成敏感的任务而体现,比如要求一个计帐员和财务管理员共参与同一个帐目;
  • 数据抽象原则:数据抽象可以通过权限的抽象来体现,如财务操作用借款、存款等抽象权限,而不用操作系统提供的典型的读、写、执行权限。

RBAC 的 E-R 图

之前说 RBAC 最重要的概念由四部分,其实体现在数据库的表中有主要三部分,因为角色和用户是重叠的,那么主要有三张表分别为用户表、权限表和资源表,其中用户表与权限表之间有一张关联表,权限表与资源表之间有一张关联表,E-R 图如下。


RBAC 的 E-R 图
RBAC 的 E-R 图


事务

为什么需要事务?

在生活中我们经常使用银行转账或者支付宝和微信支付,这种操作每一次至少影响两个用户的数据信息,比如一方给另一方转钱,如果成功则转钱方余额减去转出金额,而收钱方余额增加收到的金额,这应该是一个请求操作了数据表中的俩个实体,如果在两个操作数据的环节任意一个失败了,都会影响两个人数据的正确性,这种时候需要两个操作同时失败或同时成功,就是说有一个操作出现失败的情况,即使另一个成功了也需要进行回滚操作,这就是事务的由来。

什么是事务

事务是作为单个逻辑工作单元执行的一系列操作,多个操作作为一个整体向系统提交,要么都执行,要么都不执行,是一个不可分割的工作逻辑单元。

转账过程就是一个整体,它需要两条 UPDATE 语句,如果任何一个出错,则整个转账业务取消,两个账户的余额都恢复到原来的数据(回滚),确保总余额不变。

这里再举一个例子,有一个上传文件的功能,后端接收到文件流时是需要先写入的,当写入成功后,会将上传成功的结果返回给客户端,如果文件很大,写入的时间就会长,如果在此期间突然写入失败,则会删除之前写入的内容,将整个操作回滚到写入之前,这里面主要两步操作,创建一个新文件并写入,写入成功删除旧文件,如果写入失败,两个操作将会同时失败,即不会删除旧文件,这也是一个事务的例子,只是没有转账那么明显。

事务的特性 ACID

事务具有以下特性,被简称为 ACID

  • 原子性(Atomicity):事务是一个完整的操作,事务各个部分是不可分的,要么都执行,要么都不执行;
  • 一致性(Consistency):当事务完成后,数据必须处理完整的状态;
  • 隔离性(Isolation):并发事务彼此隔离、独立,它不应该以任何方式依赖于其它事务;
  • 持久性(Durability):事务完成后,它对数据库的修改被永久保持。

如何创建事务

创建表

创建表 accountid 列为主键列,name 列为姓名,balance 为余额。

CREATE TABLE `account` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(64) NOT NULL,
  `balance` INT(11) DEFAULT 0
  PRIMARY KEY (`id`)
);

添加数据

将表 account 添加两条数据,分别为 “张三” 和 “李四”,余额都为 100

INSERT INTO `student` (`name`, `balance`) VALUES ("张三", 100);
INSERT INTO `student` (`name`, `balance`) VALUES ("李四", 100);

使用 Node.js 实现事务

/* 一个非常简单的事务 */
const mysql = require('mysql');

// 创建数据库连接
const connection = mysql.createConnection({
  host: 'localhost', // 主机名
  port: '3306', // 数据库服务端口号
  username: 'root', // 数据库名称
  pwd: '123456', // 数据库密码
  database: 'school'  // 连接的数据库名称
});

connection.connect();

// 开启事务
connection.beginTransaction(err => {
  // 回调参数为错误对象,返回结果,返回字段描述
  connection.query('UPDATE account SET balance - 50 WHERE id = 1', (err, result, fields) => {
    if (err) {
      connection.rollback(); // 如果失败直接回归
    } else {
      connection.query('UPDATE account SET balance + 50 WHERE id = 1', (err, result, fields) => {
        err ? connection.rollback() : connection.commit();
      });
    }
  });
});

总结

到此关于 MySQL 的系列文章就告一段落了,希望前端的同学们在看了这几篇文章后对你们入门 MySQL 有一些帮助,那这几篇的文章就达到目的了,也欢迎后端的小伙伴来指出文章中的错误和不足。