有没有办法创建更好的约束,使其操作类似于外键,其中引用的外键表和列是可变的?

1sbrub3j  于 2022-10-15  发布在  PostgreSQL
关注(0)|答案(1)|浏览(127)

我有一个例子,我想要进行一个复杂的约束检查,其中我的表中引用的外键会根据该表中的另一个值而变化。这可能不是最好的实现,但由于科技债务太多,我无法改变它。目前我们没有约束检查,这可能会导致问题,我的目的是解决这个问题,而不是重做整个数据库,即使这是理想的。
以下是我的案例的摘要:

CREATE TABLE VehicleType (
    vehicletypeid integer primary key,
    vehicletypename varchar not null,
    pktablename varchar,
    pkcolumnname varchar
);
CREATE TABLE Car (carid integer primary key, carname varchar);
CREATE TABLE Truck (truckid integer primary key, truckname varchar);
CREATE TABLE VehicleColourOptions (
    vehicletypeid integer REFERENCES VehicleType(vehicletypeid),
    vehicleid integer,
    colourid integer
); /* colourid would have an fk reference in my use case */
INSERT INTO VehicleType (vehicletypeid, vehicletypename, pktablename, pkcolumnname) VALUES
(1, 'Car', 'Car', 'carid'), (2, 'Truck', 'Truck', 'truckid');

也可能有两种以上的类型。
之后,我当前的解决方案是创建一个函数和触发器,该函数和触发器将检查引用的vehicleid是否作为主键存在于CarTruck中,具体取决于提供的vehicletypeid。这些可能有些类似:

CREATE OR REPLACE FUNCTION check_vehicle_has_pk()
    RETURNS trigger LANGUAGE 'plpgsql' AS $$
DECLARE
    tablename varchar;
    columnname varchar;
    typename varchar;
    key_count integer;
    vehicleid integer;
BEGIN  
    SELECT pktablename, pkcolumnname, vehicletypename
    INTO tablename, columnname, typename
    FROM VehicleType
    WHERE NEW.vehicletypeid = vehicletypeid;

    IF tablename IS NOT NULL AND columnname IS NOT NULL THEN
        vehicleid := NEW.vehicleid;
        EXECUTE format('
            SELECT count(*)
            FROM %s
            WHERE %s.%s = %s;
        ', tablename, tablename, columnname, vehicleid)
        INTO key_count;

        IF key_count = 0 THEN
            RAISE EXCEPTION 'referenced vehicle type % does not have corresponding id %', typename, NEW.vehicleid;
            IF TG_OP = 'INSERT' THEN
                RETURN NULL;
            ELSE
                RETURN OLD;
            END IF;
        ELSE
            RETURN NEW;
        END IF;
    END IF;
END;$$;

CREATE CONSTRAINT TRIGGER VehicleColourOptionsInsertOrUpdate
    AFTER INSERT OR UPDATE
    ON VehicleColourOptions
    FOR EACH ROW
    EXECUTE PROCEDURE check_vehicle_has_pk();

因此,我们在VehicleColourOptions中的INSERT或UPDATE期间进行条件检查,以确定事务是否会继续。
但是逆约束不存在,也就是说,如果引用的VehicleType的表中不存在对应的pk,则不能在VehicleColourOptions中创建或更新值,但是,如果VehicleColourOptions引用的值在其对应表中被删除,则默认情况下不会进行相应的检查。例如,以下删除应该是不可能的,但可以:

INSERT INTO Car
(carid, carname)
VALUES
(1, 'big car');

INSERT INTO VehicleColourOptions
(vehicletypeid, vehicleid, colourid)
VALUES
(1, 1, 1);

DELETE FROM Car WHERE carid = 1;

我想弄清楚的是如何实现反向约束,或者是否存在比我所做的更好的解决方案(假设我不能更改表或列)。我想到的解决方案是在作为车辆类型表的每个表上创建反向删除CONSTRAINT TRIGGER。问题是,我有大约20个表引用了“VehicleType”和~50“VehicleType s”,每个表大约有1000行可以被引用(提醒一下,这是我的实际数据库的抽象)。这意味着,如果我对每个表都这样做,我将不得不对INSERT和UPDATE进行大约20次检查(我对此很满意),但然后我将需要创建大约50个删除触发器,并且每个触发器在删除期间都需要检查大约20个表。这看起来是这样的:

CREATE OR REPLACE FUNCTION check_pk_not_referenced_as_vehicleid()
    RETURNS trigger
    LANGUAGE 'plpgsql'
    AS $$
DECLARE
    oldvehicletypeid integer := TG_ARGV[0]::integer;
    oldvehicleid integer;
    columnname varchar;
    checktables varchar[] := ARRAY['VehicleColourOptions']::varchar[];
    tablewithmatch varchar[] := ARRAY[]::varchar[];
    t_iter varchar;
    ref_count integer;
BEGIN
    IF TG_OP = 'INSERT' THEN RETURN NEW;
    END IF;

    SELECT pkcolumnname
    INTO columnname
    FROM VehicleType
    WHERE VehicleType.vehicletypeid = oldvehicletypeid;

    EXECUTE format('SELECT ($1).%s;', columnname)
    USING OLD
    INTO oldvehicleid;

    FOREACH t_iter IN ARRAY checktables LOOP
        EXECUTE format('
            SELECT count(*)
            FROM %s
            WHERE vehicletypeid = %s
            AND vehicleid = %s;
        ', t_iter, oldvehicletypeid, oldvehicleid)
        INTO ref_count;
        IF ref_count > 0 THEN
            tablewithmatch := ARRAY_APPEND(tablewithmatch, t_iter);
        END IF;
    END LOOP;

    IF (SELECT count(*) FROM UNNEST(tablewithmatch)) > 0 THEN
        RAISE EXCEPTION 'vehicletypeid % with vehicleid % cannot be deleted because it is referenced in tables: %', oldvehicletypeid, oldvehicleid, array_to_string(tablewithmatch, ',');
        RETURN OLD;
    ELSE
        RETURN NULL;
    END IF;
END;$$;

CREATE CONSTRAINT TRIGGER CarUpdateOrDelete
    AFTER UPDATE OR DELETE
    ON Car
    FOR EACH ROW
    EXECUTE PROCEDURE check_pk_not_referenced_as_vehicleid(1);

CREATE CONSTRAINT TRIGGER TruckUpdateOrDelete
    AFTER UPDATE OR DELETE
    ON Truck
    FOR EACH ROW
    EXECUTE PROCEDURE check_pk_not_referenced_as_vehicleid(2);

是否有其他方法可以更好地为变量FK关系复制FK约束?

附带问题

除了不能使用FK约束之外,表不应该以这种方式构建的好理由是什么?我可以想到优点,主要的优点是可扩展性,因此用户可以将任何类型与任何类型相关联。我上面的抽象没有实现这个用例,因为人们可能没有用例来关联轿车和卡车,但在我的工作中,类型概念用于许多不同的事情,从词汇表到位置再到对象,我们有许多不同的关联表。
还有一个好处是,后端代码可以非常简单。如果我想要一个用户界面来将车辆与颜色选项相关联,我只需要提供一个后端服务,该服务将从VehicleColourOptions表中删除。如果我有一个CarColourOptions和TruckColourOptions表,这就变得更复杂了。并不是我不愿意投入这项工作,但当我在写后台时,这是很好的。

jtw3ybtb

jtw3ybtb1#

我完全不确定您要做什么,但我猜您正在尝试实现某种“子类型”?如果是这种情况,典型的模式是:

CREATE TABLE vehicle_type
( vehicletype_id INT NOT NULL PRIMARY KEY
, vehicletype_name VARCHAR(?) NOT NULL 
);

CREATE TABLE vehicle
( vehicle_id INT NOT NULL PRIMARY KEY -- could you use a natural identifier such as VIN or licence plate?
, vehicletype_id INT NOT NULL
      REFERENCES vehicle_type (vehicletype_id)
, ...
, CONSTRAINT ak_vehicle UNIQUE (vehicletype_id, vehicle_id)
);

CREATE TABLE car
( vehicle_id INT NOT NULL PRIMARY KEY
, vehicletype_id INT NOT NULL
, ...
, CONSTRAINT fk_vehicle_type FOREIGN KEY (vehicletype_id, vehicle_id)
, CONSTRAINT ck_car CHECK (vehicletype_id = 1) -- assuming car is type 1
);

现在,FK FK_Vehicle_type可确保汽车具有与父车辆相同的Vehicle_type,而检查约束ck_car可确保只有汽车才能具有类型Car。

相关问题