如何在 child 和 parent 行之间没有精确外键匹配的 PostgreSQL 中强制执行 one-to-many 关系?

How to enforce a one-to-many relationship in PostgreSQL where there is no exact foreign key match between child and parent rows?

我在对具有 parent table 且主键中包含开始和结束日期,以及 child table 包含其主键中的时间戳必须在 parent table 的开始和结束日期范围内。事实上,这个问题是嵌套的,因为 parent table 实际上是 child 到另一个 table - 一个“grandparent” table - 在其主键中也有开始和结束日期; parent table 的开始和结束日期也必​​须在 grandparent table 的开始和结束日期的范围内。

作为背景,我在一家水处理公司工作。作为处理合同的一部分,我们通过在各个地点部署水处理机来处理水。更具体地说:

因此,我们必须跟踪 sites、treatment_contracts、machine_deployments、machines 和 treatment_datapoints。一个site可以有多个treatment_contract,一个treatment_contract可以有多个machine_deployments和多个treatment_datapoint,一个machine可以有多个machine_deployments.

所以我要建模的数据的简化版本是这样的:

CREATE TABLE public.site
(
    id integer NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE public.treatment_contract
(
    site_id integer NOT NULL,
    start_date date NOT NULL,
    end_date date,
    PRIMARY KEY (site_id, start_date, end_date)
    CONSTRAINT fk_treatment_contract__site FOREIGN KEY (site_id)
        REFERENCES public.site (site_id) MATCH SIMPLE
);

CREATE TABLE public.machine_deployment
(
    site_id integer NOT NULL,
    machine_id integer NOT NULL,
    start_date date NOT NULL,
    end_date date,
    PRIMARY KEY (site_id, machine_id, start_date, end_date),
    CONSTRAINT fk_machine_deployment__machine FOREIGN KEY (machine_id)
        REFERENCES public.machine (id) MATCH SIMPLE,
    <some provision to require that machine_deployment.start_date and machine_deployment.end_date are between treatment_contract.start_date and treatment_contract.end_date, and that machine_deployment.site_id matches treatment_contract.site_id>
);

CREATE TABLE public.treatment_datapoint
(
    site_id integer NOT NULL,
    time_stamp timestamp NOT NULL,
    PRIMARY KEY (site_id, time_stamp),
    <some provision to require time_stamp is between treatment_contract.start_date and treatment_contract.end_date, and that treatment_datapoint.site_id matches treatment_contract.site_id>
);

CREATE TABLE public.machine
(
    id integer NOT NULL,
    PRIMARY KEY (id)
);

我不确定如何继续,因为 PostgreSQL 只能在所有外键字段之间完全匹配的情况下强制执行外键关系 - 外键约束中没有规定可以强制执行 child.timestamp BETWEEN parent.start AND parent.end. treatment_datapoint 应该有 treatment_contract 的外键,因为 treatment_datapoint 没有 treatment_contract 是没有意义的,但似乎没有办法强制执行这种外键关系。答案只是使用触发器吗?我总是被告知要避免使用触发器来定义 parent:child 关系,因为这就是外键的作用。

无论如何,都必须有一种方法来对此进行建模,因为我无法想象我是唯一一个需要在 child table 在 parent table.

中定义的范围内

简而言之:在没有外键的情况下加强关系 - 创建一个。

要使您的模型正常工作,您必须有指向 treatment_contract 的外键,因为 treatment_contract 的主键包含字段 site_idstart_dateend_date 您必须将 contract_start_datecontract_end_date 添加到您需要引用合同的表格中,即 machine_deploymenttreatment_datapoint.

为了让您的生活更轻松,我建议不要对合同和机器部署的未知结束日期使用 NULL。我会认为它是一个“神奇的数字”,意思是“无穷大”。这不是必需的,但可以使检查更简单。

我还要添加一个 check constraint 以确保合同在开始后结束。

最后,您可以使用检查约束来验证部署开始和结束以及数据点时间戳。

在下面的示例中,我在检查中使用 daterange and range operators。这是为了方便。您可以使用比较运算符 (<,<=...).

获得相同的结果

我建议的架构变体是:

CREATE TABLE public.site
(
    id integer NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE public.treatment_contract
(
    site_id integer NOT NULL,
    start_date date NOT NULL,
    end_date date NOT NULL,
    PRIMARY KEY (site_id, start_date, end_date),
    CONSTRAINT fk_treatment_contract__site FOREIGN KEY (site_id)
        REFERENCES public.site (id) MATCH SIMPLE
);

CREATE TABLE public.machine
(
    id integer NOT NULL,
    PRIMARY KEY (id)
);

CREATE TABLE public.machine_deployment
(
    site_id integer NOT NULL,
    machine_id integer NOT NULL,
    contract_start_date date NOT NULL,
    contract_end_date date NOT NULL,
    start_date date NOT NULL,
    end_date date NOT NULL,
    PRIMARY KEY (site_id, machine_id, start_date, end_date),
    CONSTRAINT fk_machine_deployment__machine FOREIGN KEY (machine_id)
        REFERENCES public.machine (id) MATCH SIMPLE,
    CONSTRAINT fk_machine_deployment__treatment_contract FOREIGN KEY (site_id, contract_start_date, contract_end_date)
        REFERENCES public.treatment_contract(site_id, start_date, end_date),
    CONSTRAINT chk_machine_deploiment_period CHECK (start_date <= end_date),    
    CONSTRAINT chk_machine_deploiment_in_contract CHECK (pg_catalog.daterange(start_date, end_date,'[]') <@ pg_catalog.daterange(contract_start_date, contract_end_date, '[]'))
);

CREATE TABLE public.treatment_datapoint
(
    site_id integer NOT NULL,
    contract_start_date date NOT NULL,
    contract_end_date date NOT NULL,
    time_stamp timestamp NOT NULL,
    PRIMARY KEY (site_id, time_stamp),
    CONSTRAINT fk_treatment_datapoint__treatment_contract FOREIGN KEY (site_id, contract_start_date, contract_end_date)
        REFERENCES public.treatment_contract(site_id, start_date, end_date),
    CONSTRAINT chk_datapoint_in_contract CHECK (time_stamp::date <@ pg_catalog.daterange(contract_start_date, contract_end_date, '[]'))
);