PostgreSQL 自定义数据类型:DOMAIN

为什么创建自定义数据类型?

自定义类型首先帮助保证数据完整性,使数据库保存的数据始终符合预期,同时还会带来维护上的好处。

本教程通过简单例子说明这些优势,分为两部分:第一部分介绍 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.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容