为什么创建自定义数据类型?
自定义类型首先帮助保证数据完整性,使数据库保存的数据始终符合预期,同时还会带来维护上的好处。
本教程通过简单例子说明这些优势,分为两部分:第一部分介绍 DOMAIN,第二部分介绍自定义 TYPE。
DOMAIN 的用途
定义自己的 domain,可以阻止不合格式或不符合要求的数据插入。
CREATE DOMAIN 用于定义新的域,语法如下:
Command: CREATE DOMAIN
Description: define a new domain
Syntax:
CREATE DOMAIN name [ AS ] data_type
[ COLLATE collation ]
[ DEFAULT expression ]
[ constraint [ ... ] ]
其中 constraint 为:
[ CONSTRAINT constraint_name ]
{ NOT NULL | NULL | CHECK (expression) }
完整说明见 CREATE DOMAIN 文档。
用例子说明
假设数据库中有如下 person 表:
test=# \d person
Table "public.person"
Column | Type | Collation | Nullable | Default
------------+---------+-----------+----------+------------------------------------
id | integer | | not null | nextval('person_id_seq'::regclass)
firstname | text | | not null |
lastname | text | | not null |
birth_date | date | | |
email | text | | not null |
Indexes:
"person_pkey" PRIMARY KEY, btree (id)create table person
birth_date 可以为空,也可以是任意有效日期,甚至公元 1 世纪。email 只是普通文本,任何文本都能存入。
生产系统中经常如此,因为数据检查放在应用内,数据库只能寄希望于应用处理正确。但 PostgreSQL 本身具备约束检查等完整能力,为什么不利用它?这样首先便于把应用从一种语言迁移到另一种语言,其次允许绕过应用直接使用数据库,同时仍由约束阻止不合要求的数据。
假设应用只希望插入或更新 1920 年后出生、可能仍在世的人,并希望邮箱看起来有效。以下示例用 1930 年 1 月 1 日作为实际检查边界。
可以在建表时添加检查。虽然也能 ALTER 现有表,为了让教程更清楚,这里创建新表,后面还会再创建另一个:
create table person_using_checks (
id bigint generated always as identity primary key
, firstname text not null
, lastname text not null
, birth_date date
, email text not null
, check (birth_date>'1930-01-01'::date)
, check (email ~ '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$')
);
示例通过 GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY 创建代理键,详情见 CREATE TABLE 文档。新表的 identity 键优先使用 BIGINT。
建表后,尝试插入以下数据:
insert into person_using_checks (firstname,lastname,birth_date,email)
values ('Jhon','Doe','1970-01-01'::date,'john@doe.org');
insert into person_using_checks (firstname,lastname,birth_date,email)
values ('Georges','Washington','1732-02-22'::date,'georges@whitehouse.gov.us');
insert into person_using_checks (firstname,lastname,birth_date,email)
values ('Starman','Sky','1972-04-28','unknown');
后两条插入中,PostgreSQL 会指出数据问题。
如果只有一张表使用这些数据规则,这样已经足够。但如果其他表也需要相同检查,更好的方式是定义 PostgreSQL domain,再把同一数据定义应用于任意需要的表。
创建两个 domain:
create domain date_of_birth as date
check (value > '1930-01-01'::date)
;
create domain valid_email as text
not null
check (value ~* '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$')
;
注意,date_of_birth 仍允许 null,而 valid_email 不允许。
在 psql 中列出 domain:
\dD
然后创建使用 domain 的表,无需额外检查,因为检查已经由 domain 执行:
create table person_using_domains (
id bigint generated always as identity primary key
, firstname text not null
, lastname text not null
, birth_date date_of_birth
, email valid_email
);
在新表中尝试插入与之前相同的数据:
insert into person_using_domains (firstname,lastname,birth_date,email)
values ('Jhon','Doe','1970-01-01'::date,'john@doe.org');
insert into person_using_domains (firstname,lastname,birth_date,email)
values ('Georges','Washington','1732-02-22'::date,'georges@whitehouse.gov.us');
insert into person_using_domains (firstname,lastname,birth_date,email)
values ('Starman','Sky','1972-04-28','unknown');
PostgreSQL 仍然拒绝不合要求的数据,只是错误消息现在引用 domain 约束。
假设应用演进后,只希望保存 1980 年后出生的人。必须先删除 domain 的约束,再新建约束,因为 PostgreSQL 不能直接原位修改 domain 的 CHECK 条件。完整语法见 ALTER DOMAIN。
alter domain date_of_birth drop constraint date_of_birth_check ;
在 psql 查看定义,可以看到 Check 已消失:
\dD date_of_birth
重新创建约束:
alter domain date_of_birth add check (value > '1980-01-01'::date);
PostgreSQL 会拒绝,因为先前插入的 John Doe 出生于 Unix epoch 起点,即 1970 年 1 月 1 日,不符合新要求。
可以加入 NOT VALID,表示不检查已有存储数据是否满足约束,从而暂时允许不符合新规则的数据继续存在:
alter domain date_of_birth add check (value > '1980-01-01'::date) not valid;
随后,可以要求 PostgreSQL 验证约束:
alter domain date_of_birth validate constraint date_of_birth_check ;
该操作不会直接指出哪条数据违反新约束,需要自行查询不匹配的数据并清理。例如创建相同列结构的 person_history,把历史数据移入;或根据实际需求删除。
应该在设计 schema 时就考虑 domain。逐列思考:属于哪种数据域,需要哪些 CHECK,是否允许 null,默认值是否合理。
domain 经常被忽视,但这样会增加维护风险。以邮政编码为例,如果数据规则变化,可能必须逐表逐列修改,很容易遗漏。如果每张表还各自定义了邮编 CHECK,所有检查也都需要修改。
如今多数 schema 创建与维护工具都支持定义和管理 domain,并生成包含 domain、表、索引等的完整 SQL。善用它,可以显著改善 schema 的可读性和可维护性。
原文:Custom data types: DOMAINS。作者/维护方:Crunchy Data 教程团队。本文为中文翻译,代码及命令保留原文。
原站版权声明:© 2018–2026 Crunchy Data Solutions, Inc.











暂无评论内容