数据库的“契约” —— 约束(Constrains)

文章目录

数据库的"契约" ------ 约束(Constrains)

一、为什么需要约束??

正所谓"垃圾进,垃圾出(Garbage In,Garbage Out)",翻译成人话就是"输入的数据质量决定着输出的结果质量",也就是说,如果数据库里的数据都是脏数据,那么就算这个代码再高效,再精美,再优雅,但是也改变不了它会吐出垃圾的事实。

举一个具体的例子:假设现在用Java写一个用户注册的代码

java 复制代码
public class UserRegister {
    
    public static void register(String username, String password) {
        // 检查用户名
        if (username == null || username.isEmpty()) {
            System.out.println("错误:用户名不能为空");
            return;
        }
        
        // 检查用户名长度
        if (username.length() < 3 || username.length() > 20) {
            System.out.println("错误:用户名长度必须在3-20个字符之间");
            return;
        }
        
        // 检查密码
        if (password == null || password.length() < 6) {
            System.out.println("错误:密码不能少于6位");
            return;
        }
        
        // 注册成功
        System.out.println("注册成功!欢迎 " + username);
    }
   
}
java 复制代码
    public static void main(String[] args) {
        register("ab", "123456");      // 用户名太短
        register("zhangsan", "123");   // 密码太短
        register("zhangsan", "123456");// 成功
    }

看起来完全没有问题,保护的很周道,但是在具体的生产环境上,总会出现一些令人难以想象的场景,例如,如果有人绕过后端直接向数据库插数据怎么办?

sql 复制代码
insert into users(username,password) values('ab','123');
insert into users(username,password) values(null,null);

直接无视了后端的防线,向我们的数据库插入了非法数据,也就是脏数据,你的校验逻辑再严密,也只能保护走你这条路的数据,一旦有人抄小道,那么防线就穿了。

所以,这就是数据库约束存在的意义。

让数据库自己守住大门,无论数据从何处来(应用层,SQL客户端,还是不走寻常路等乱七八糟的数据),都必须过安检,安检过了,那么才能过进入数据库作为数据被存起来,确保数据的完整性和有效性。

数据完整性 ≠ 数据有效性

需要区分一下这两个概念:

  • 完整性:数据完整,关联关系正确,比如订单一定要有用户,学生表要有学生,由外键约束负责;
  • 有效性 :数据格式,范围合理,比如年龄不能是负数,邮箱必须带@,由check约束管一部分,重点看应用层。
    约束主要解决的是完整性问题

好的数据库设计,应该让错误的数据进不来,而不是指望写代码的人永远不犯错。

二、五大核心约束

sql 复制代码
create table student (
    id int primary key auto_increment,        -- 主键:唯一且非空,搭配自增
    sn varchar(20) unique,                   -- 唯一约束:学号不能重复,但允许为 null
    name varchar(20) not null,               -- 非空约束:名字必须填
    class_id int,                            -- 准备作为外键的列
    enrollment_status int default 1,         -- 默认值:不填的话默认状态为 1(在读)
    foreign key (class_id) references class(id) -- 外键约束:关联 class 表的主键
);

1)primary key(主键)

唯一标识每一行,不能重复,不能为null

  • auto_increment:不用你手动填,mysql 自动从1 开始递增
sql 复制代码
id int primary key auto_increment

表示给每一个学生发一个唯一编号,类似身份证

sql 复制代码
-- 插入时不写 id,数据库自动分配 1, 2, 3...
insert into student (sn, name, class_id) values ('2026001', '张三', 1);
insert into student (sn, name, class_id) values ('2026002', '李四', 1);
-- 结果:id 分别是 1 和 2

注意:一张表只能有一个主键,但主键可以是组合键(多个字段联合做主键)

2)unique

唯一性,值不能重复

sql 复制代码
sn varchar(20) unique
primary key unique
能不能有多个 一张表只能一个 可以有多个
能不能为 null 绝对不能 可以(除非加 not null)
典型用途 系统内部编号 业务唯一标识(学号、邮箱、手机号)
sql 复制代码
-- 第一次插入成功
insert into student (sn, name, class_id) values ('2026001', '张三', 1);

-- 第二次用相同学号,报错!
insert into student (sn, name, class_id) values ('2026001', '李四', 1);
-- error: duplicate entry '2026001' for key 'sn'

3) not null

不能为空!!

java 复制代码
name varchar(20) not null

名字是必填项,不能不填,也不能填 null

java 复制代码
-- 不填 name,报错!
insert into student (sn, class_id) values ('2026003', 1);
-- error: field 'name' doesn't have a default value

-- 填 null,也报错!
insert into student (sn, name, class_id) values ('2026003', null, 1);
-- error: column 'name' cannot be null

经常与unique一起用

java 复制代码
sn varchar(20) unique not null,  -- 学号必须填,且不能重复

4) default

设置默认值

sql 复制代码
enrollment_status int default 1

作用:如果不填这个字段,数据库自动帮你填上 1。

含义:假设 1=在读,2=休学,3=毕业。大部分新生都是在读状态,不用每次都手动写。

sql 复制代码
-- 不填 enrollment_status,自动得到 1
insert into student (sn, name, class_id) values ('2026002', '李四', 1);

-- 该同学休学了?手动修改
insert into student (sn, name, class_id, enrollment_status) 
values ('2026003', '王五', 1, 2);  -- 手动设为休学

select * from student;

-- 结果:李四的 enrollment_status 是 1,王五的是 2

5) foreign key(外键)

这个值一定存在于另一个表,如果不存在,则无法插入数据库

拿学生表和班级表举例,一定要先有班级表

sql 复制代码
create table class (
    id int primary key auto_increment,
    name varchar(20) not null
);

保证学生挂靠的是真实存在的班级,不存在假班级

sql 复制代码
-- 先往 class 表插一条记录
insert into class (name) values ('高一24班');  -- 假设得到 id=24

-- 正常:class_id=24 存在,插入成功
insert into student (sn, name, class_id) values ('2026004', '赵六', 24);

-- 异常:class_id=999 不存在,报错!
insert into student (sn, name, class_id) values ('2026005', '钱七', 999);
-- error: cannot add or update a child row: 
--       a foreign key constraint fails

当发生级联操作(指对一个表的数据进行操作时,自动触发对关联表的相关操作,保持数据一致性)时,

sql 复制代码
-- 如果班级被删除,自动把学生的 class_id 设为 null
foreign key (class_id) references class(id) on delete set null

-- 当班级有学生时,禁止删除班级(保护数据)
foreign key (class_id) references class(id) on delete restrict

三、面试重点:外键的取舍与逻辑删除

上面我们提到了用foreign key 来保证数据的完整性,这是数据库层面被称作"物理外键",但如果你去阅读大厂的Java客服手册,会看到一句话------"不得使用外键与级联,一切外键概念必须在应用层解决"

这是为什么呢???

因为物理外键有一个致命伤,就是当insert 或者update 时,数据库都要去查另一张表来校验一下外键是否存在,当出于高并发的场景下,就会极度地消耗资源与效率;同时当系统数据量变大,不得不进行"分库分表"时(例如学生表在A服务器,班级表在B服务器),跨数据库是无法建立物理外键的。

因此在实际开发中,大厂采用了一个新方法------逻辑外键

什么是逻辑外键??

就是在建表时,仍然保留一个class_id这个字段,但是不使用foreign key这句约束代码,把对数据的关联关系的校验全部交给Java后端代码来解决,也就是说在Java后端代码进行插入数据前,先查询一下班级是否存在,这就是逻辑外键。

既然我们不用物理外键了,那要怎么删除数据?我们要明白一个道理------数据是无价的,尤其是对于这些企业来说。所以要避免或者很少地去使用delete 语句去真正意义上抹除一行数据(物理删除),而更多的使用逻辑删除

所以在设计表的时候,统一加上一个状态字段---------"is_deleted"

sql 复制代码
alter table student add colunm is_deleted tinyint default 0;
-- 0 代表未删除,1 代表已删除

当我们想要开除一个学生时,就不再使用**"delete"** 而是使用**"update"**

sql 复制代码
-- 所谓的删除,只是把状态标记为 1
update student set is_deleted = 1 while id = 开除学生id;

并且在之后的查询中,sql语句都要永久地加上一个默认条件

sql 复制代码
select * from student where is_deleted = 0;

总结: 约束的理念是完美的,但在极限的工程环境下,为了性能和扩展性,不得不将部分"校验"和"删除"的工作从数据库层上移到代码业务层。

相关推荐
m0_678485452 小时前
如何在Bootstrap中自定义Modal的弹出动画效果
jvm·数据库·python
与衫2 小时前
[特殊字符] 解决 DataHub 无法解析复杂 SQL 血缘的问题(gsp-datahub-sidecar 实测)
数据库·sql
m0_493934532 小时前
CSS如何禁止子元素浮动影响父级_设置父容器BFC属性
jvm·数据库·python
weixin_586061462 小时前
Golang怎么安装和配置开发环境_Golang环境搭建完整教程【总结】
jvm·数据库·python
m0_493934532 小时前
html标签怎么避免标签嵌套错误_div不能放在p内原因【详解】
jvm·数据库·python
独自破碎E2 小时前
面试官:你有用过Java的流式吗?比如说一个列表.stream这种,然后以流式去处理数据。
java·开发语言
2301_782659182 小时前
Go语言goroutine调度原理_Go语言GMP调度模型教程【高效】
jvm·数据库·python
qq_334563552 小时前
Layui layer弹窗如何实现居中显示
jvm·数据库·python
2601_949818092 小时前
头歌答案--爬虫实战
java·前端·爬虫