Oracle的约束

约束简介

约束是数据库用来确保数据满足业务规则的手段,不过在真正的企业开发中,除了主键约束这类具有强需求的约束,像外键约束,检查约束更多时候仅仅出现在数据库设计阶段,真实环境却很少应用,更多是放到程序逻辑中去进行处理。这也比较容易理解,约束会一定程度上较低数据库性能,有些规则直接在程序逻辑中处理就可以了,同时,也有可能在面对业务变更或是系统扩展时,数据库约束会使得处理不够方便。但用与不要,还是要根据自己的需求、项目现状等实际情况综合决定。

约束种类

oracle数据库的约束有五种:

  • 主键约束(primary key)
  • 唯一约束(unique)
  • 非空约束(not null)
  • 外键约束(foreign key)
  • 检查约束(check)

主键约束(primary key)

主键是定位表中单个行的方式,可唯一确定表中的某一行,关系型数据库要求所有表都应该有主键,不过oracle数据库没有遵循这个规范,oracle中的表可以没有主键(这种情况常见于一些中间表的情况),关于主键有几个需要注意的点:

  • 键列必须具有唯一性,且不能为空,其实主键约束相当于 unique + not null。
  • 一个表中只允许有一个主键。
  • 主键所在列必须具有索引(主键的唯一约束通过索引来实现),如果不存在,将会在索引添加的时候自动创建。

唯一约束(unique)

唯一约束可作用在单列或者多列上,对于这些列或者列组合,唯一性约束保证每一行的唯一性。
唯一性约束有以下几个注意点:

  • 对于unique约束来说,索引是必须的。如果不存在,就自动创建一个索引(unique的唯一性本质上是通过索引来保证的)。
  • unique允许存在null值,unique约束的列可以存在多个null,这是因为,unique唯一性通过btree索引来实现,而btree索引中不包含null。当然,这也造成了在where语句中使用null值进行过滤会造成全表扫描。

注意
通过对主键约束和唯一约束的介绍,需要我们注意一个概念问题:约束和索引是两个不同的概念。比如我们只在某列上创建一个唯一索引,但这个列上是并没有约束的,即我们在PS/SQL的表编辑界面中能看到 Indexs 中有数据,但 Keys 中不一定有数据,如下图:

对于唯一约束,创建唯一约束时,会强制创建或者重用列上的索引。如果之前列上没有索引,那么强制创建的索引是unique index,如果列上已经存在索引,就重用之前的索引。

非空约束(not null)

非空约束作用的列也叫强制列。顾名思义,非空列中必须要有值,当然建表的时候如果使用default关键字指定了默认值,则可以不显示输入。

外键约束(foreign key)

外键约束定义在具有父子关系的子表中,外键约束使得子表中的列对应父表的主键列,用以维持数据库的完整性。不过出于性能和后期的业务系统扩展的考虑,很多时候,外键约束仅仅出现在数据库的设计中,实际会放在业务程序中进行处理。外键约束要注意以下几点:

  • 外键约束的子表中的列和对应父表中的列数据类型必须相同,列名可以不同。
  • 对应的父表列必须存在主键约束(primary key)或者唯一约束(unique)。
  • 外键约束允许列存在null值,对应的行就成了孤行。

其实很多时候不使用外键,很多人认为会让删除操作变得比较麻烦,比如要删除父表中的某条数据,但是某个子表中又有对该条数据的引用,这时就会导致删除失败。这种情况下有几种方式来优化这个场景:

  • 第一种方式简单粗暴,删除的时候,级联删除掉子表中的所有匹配行,在创建的时候,通过on delete cascade 子句指定该外键列可以级联删除。
  • 第二种方式,删除父表中的对应行,会将对应子表中的所有匹配行的外键约束列置为null,通过on delete set null子句实现。
  • 第三种,默认,强制不让删。

检查约束(check)

检查约束可以用来实施一些简单的规则,比如列值必须在某个范围内,检查的规则必须是一个结果为true或false的表达式。

创建约束

这里以创建下面两张比表为例子。
1.用户表(w_user)

编号 字段名 字段类型 说明
1 userid number(5) 用户id,主键约束
2 username varchaer2(30) 用户名,非空约束,4~20个字符
3 userpwd varchar(20) 密码,非空约束,4~18个字符
4 age number(3) 年龄,默认18,值大于等于18
5 gender char(2) 性别,默认‘男’,只能是‘男’或者‘女’
6 email varchar2(30) 邮箱,唯一约束
7 regtime date 注册日期,默认当前日期

2.文章表(w_txt)

编号 字段名 字段类型 说明
1 txtid number(5) 文章编号,主键约束
2 title varchar2(32) 文章标题,非空约束,长度4~20字符
3 txt varchar2(1024) 内容,最大长度1024
4 pubtime date 发布日期,默认当前日期
5 userid number(5) 作者,外键约束,参考用户表的用户id,删除时,自设为null

建表时创建约束

创建w_user表:

1
2
3
4
5
6
7
8
9
10
--建表时就加上约束,但不设置约束名称
create table w_user (
userid number(5) primary key,--主键,唯一且非空
username varchar2(30) not null check(length(username ) between 4 and 20),--检查约束和非空约束
userpwd varchar2(20) check(length(userpwd) between 4 and 18) not null,--检查约束和非空约束
age number(3) default(18) check(age>=18),--设置默认值和检查约束
gender char(4) default('男') check(gender in('男','女')),--默认值和检查约束
email varchar2(30) unique,--唯一约束
regtime date default(sysdate) --默认系统当前时间
);

创建完成后再PL/SQL中查看表的字段如下:

查看检查约束:

创建w_txt表:

1
2
3
4
5
6
7
create table w_txt (
txtid number(5) primary key,
title varchar2(32) not null check(length(title)>=4 and length(title)<=30),
txt varchar2(1024),
pubtime date default(sysdate),
userid number(5) references w_user(userid) on delete set null
);

最后一个字段是外键,这里on delete set null 意思是当w_user表里面的某个userid删除之后,这个表里面对应的设置为null。
然后在PL/SQL中查看Keys,主要观察一下外键。

在建表时创建带名字的约束

在上面创建两个表的时,并没有指定对应的约束名称,这样当我们后期对表进行一些操作的时候,如果违反了某个约束,报错的时候提示的是系统默认的约束名称,不方便开发人员排查错误,因此我们一般要给约束指定名称。
下面我们先把已建好的这两张表删除,再重新创建一下。

1
2
3
--删除表
drop table w_txt;
drop table w_user;

重新建w_user表:

1
2
3
4
5
6
7
8
9
10
11
--创建约束带有名字的w_txt表
create table w_txt(
txtid number(5),
title varchar2(32) constraint nn_txt_title not null,
txt varchar2(1024),
pubtime date default(sysdate),
userid number(5),
constraint pk_txt_id primary key(txtid),
constraint ck_txt_title check(length(title)>=4 and length(title)<=30),
constraint fk_txt_user_id foreign key (userid) references w_user(userid) on delete set null
);

重新建w_txt表:

1
2
3
4
5
6
7
8
9
10
11
--创建约束带有名字的w_txt表
create table w_txt(
txtid number(5),
title varchar2(32) constraint nn_txt_title not null,
txt varchar2(1024),
pubtime date default(sysdate),
userid number(5),
constraint pk_txt_id primary key(txtid),
constraint ck_txt_title check(length(title)>=4 and length(title)<=30),
constraint fk_txt_user_id foreign key (userid) references w_user(userid) on delete set null
);

完成后,再在Keys里查看就能看到键的名称变成我们自己定义的了。

建表之后追加约束

有时候我们创建表的时候没考虑太多,创建好之后才发现某些字段需要加上约束,这时候可以使用追加约束的sql语句实现功能。
还是接上面的表,先把之前的表删除(因为创建的表内容都一样)。

1
2
3
--删除表
drop table w_txt;
drop table w_user;

建w_user表:

1
2
3
4
5
6
7
8
9
10
--创建w_user表 不带约束
create table w_user(
userid number(5),
username varchar2(30),
userpwd varchar2(20),
age number(3),
gender char(4),
email varchar2(30),
regtime date
);

PL/SQL中查看表结构如下:

接下来给这个表添加之前的那些约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
--给表添加约束
--userid追加主键
alter table w_user add constraint pk_user_id primary key(userid);
--username追加非空和check约束
alter table w_user modify(username constraint nn_user_name not null);--非空约束
alter table w_user add constraint ck_user_name check(length(username) between 4 and 20);--检查约束
--userpwd追加非空约束和check约束
alter table w_user modify(userpwd constraint nn_user_pwd not null);--非空约束
alter table w_user add constraint ck_user_pwd check(length(userpwd) between 4 and 18);--检查约束
--age追加check约束和默认值
alter table w_user add constraint ck_user_age check(age>=18);--check约束
alter table w_user modify(age default(18));--追加默认约束
--gender追加check和default
alter table w_user add check(gender in('男','女'));--check约束
alter table w_user modify(gender default('男'));--追加默认约束
--email追加唯一约束
alter table w_user add constraint uq_uaer_email unique(email);--唯一约束
--regtime追加默认值
alter table w_user modify(regtime default(sysdate));--追加默认约束

执行之后查看表的结构:

检查约束界面:

接下来是w_txt表的操作

1
2
3
4
5
6
7
8
--创建w_txt表,不添加约束
create table w_txt(
txtid number(5),
title varchar2(32),
txt varchar2(1024),
pubtime date,
userid number(5)
);

追加w_txt表的约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--给w_txt追加约束
--txtid添加主键约束
alter table w_txt add constraint pk_txt_id primary key(txtid);--主键约束
--title添加非空约束以及check约束
alter table w_txt modify(title constraint nn_txt_title not null);--非空约束
alter table w_txt add constraint ck_txt_title check(length(title)>=4 and length(title)<=30);--检查约束
--pubtime设置默认值
alter table w_txt modify(pubtime default(sysdate));--追加默认约束
--userui设置外键约束
--方法1:强制不让删 【文章表的内容没删除干净之前,用户表的内容不让删】
alter table w_txt add constraint fk_txt_user_id foreign key (userid) references w_user(userid);--强制不让删
--方法2:自动设置为null 【如果文章没有删除,然后用户id删除了,那么文章的关联id自动设置为null】
alter table w_txt add constraint fk_txt_user_id foreign key (userid) references w_user(userid) on delete set null;--自动设置为null
--方法3:级联删除 【当用户表的id删除时,文章表对应的内容一起删除】
alter table w_txt add constraint fk_txt_user_id foreign key (userid) references w_user(userid) on delete cascade;--级联删除

禁用和启用约束

很多时候由于业务需要,比如我们有大量的历史数据,需要和现有数据合并,当前表存在数据库约束(如非空约束),而这些历史数据又包含违背非空约束的数据行,为了避免导入时由于违反约束而导入失败,我们通过调整约束状态来达到目的。

数据库约束有两类状态

  • 启用/禁用(enable/disable):是否对新变更的数据启用约束验证
  • 验证/非验证(validate/novalidate):是否对表中已客观存在的数据进行约束验证

这两类四种状态从语法角度讲可以随意组合,默认是enable validate。
四类组合效果说明:

  • enable validate:默认的约束组合状态,无法添加违反约束的数据行,数据表中也不能存在违反约束的数据行;
  • enable novalidate:无法添加违反约束的数据行,但对已存在的违反约束的数据行不做验证;
  • disable validate:可以添加违反约束的数据行,但对已存在的违反约束的数据行会做约束验证(从描述中可以看出来,这本来就是一种相互矛盾的约束组合,只不过是语法上支持这种组合罢了,造成的结果就是会导致DML失败)
  • disable novalidate:可以添加违法约束的数据行,对已存在的违反约束的数据行也不做验证。
    拿上面的例子来说,我们需要上传大量违反非空约束的历史数据(从业务角度讲这些数据不会造成系统功能异常),可以临时将约束状态转为disable novalidate,以保证这些不合要求的数据导入表中。

举个例子:

1
2
alter table w_user add primary key(userid) disable;--禁用主键
alter table w_user modify primary key(userid) enable novalidate;--启用主键并验证

删除约束

语法格式:

1
alter table 表名 drop constraint 约束名 cascade

从上面的语法结构中可知,要删除约束就要知道对应约束的名称,所以给约束起名字是一个很好的习惯。

1
alter table w_user drop constraint uq_user_email cascade

这个sql语句最后的cascade是级联删除,意思是把跟这个约束相关的约束一起删除。

------ 本文完 ------