如何创建验证另一个表中的列的 PL/SQL 行触发器

Posted

技术标签:

【中文标题】如何创建验证另一个表中的列的 PL/SQL 行触发器【英文标题】:How to create a PL/SQL row trigger that validates a column from another table 【发布时间】:2021-09-10 17:34:21 【问题描述】:

我有两张桌子:

CREATE TABLE PRODUCT
(
PRODUCT_NAME    VARCHAR(40)     NOT NULL,
SUPPLIER_NAME   VARCHAR(40)     NOT NULL,
CATEGORY_NAME   VARCHAR(30) NOT NULL,
QUANTITY_PER_UNIT   VARCHAR(20)         NULL,
UNIT_PRICE      NUMBER(10,2)    DEFAULT 0,
UNITS_IN_STOCK  NUMBER(9)   DEFAULT 0,
UNITS_ON_ORDER  NUMBER(9)   DEFAULT 0,
REORDER_LEVEL   NUMBER(9)   DEFAULT 0,
DISCONTINUED    CHAR(1)     DEFAULT 'N',
CONSTRAINT PK_PRODUCT PRIMARY KEY (PRODUCT_NAME),
CONSTRAINT FK_CATEGORY_NAME FOREIGN KEY (CATEGORY_NAME) REFERENCES CATEGORY(CATEGORY_NAME),
CONSTRAINT FK_SUPPLIER_NAME FOREIGN KEY (SUPPLIER_NAME) REFERENCES SUPPLIER(COMPANY_NAME),
CONSTRAINT CK_PRODUCT_UNIT_PRICE CHECK (UNIT_PRICE >= 0),
CONSTRAINT CK_PRODUCT_UNITS_IN_STOCK CHECK (UNITS_IN_STOCK >= 0),
CONSTRAINT CK_PRODUCT_UNITS_ON_ORDER CHECK (UNITS_ON_ORDER >= 0),
CONSTRAINT CK_PRODUCT_REORDER_LEVEL CHECK (REORDER_LEVEL >= 0),
CONSTRAINT CK_PRODUCT_DISCONTINUED CHECK (DISCONTINUED in ('Y','N'))
);

CREATE TABLE SUPPLIER
(
COMPANY_NAME    VARCHAR(40) NOT NULL,
CONTACT_NAME    VARCHAR(30)     NOT NULL,
CONTACT_TITLE   VARCHAR(30)     NOT NULL,
ADDRESS         VARCHAR(60)     NOT NULL,
CITY        VARCHAR(15)     NOT NULL,
REGION      VARCHAR(15)         NULL,
POSTAL_CODE     VARCHAR(10)     NOT NULL,
COUNTRY         VARCHAR(15)     NOT NULL,
PHONE       VARCHAR(24)     NOT NULL,
FAX         VARCHAR(24)         NULL,
HOME_PAGE       VARCHAR(500)        NULL,
TRUSTED_SUPPLIER    VARCHAR(3)    NULL,
CONSTRAINT PK_SUPPLIER PRIMARY KEY (COMPANY_NAME)  
);

我需要创建一个 ROW TRIGGER,它可以自动验证有关要插入数据库的新产品的信息是否由受信任的供应商提供。如果供应商不受信任,则插入必须失败并且必须显示错误消息。

到目前为止,这是我的行触发器:

CREATE OR REPLACE TRIGGER VERIFY_SUPPLIER_TRUST
BEFORE INSERT ON PRODUCT
FOR EACH ROW 
DECLARE TRUST VARCHAR(3);

    BEGIN
    SELECT SUPPLIER.TRUSTED_SUPPLIER
    INTO TRUST
    FROM SUPPLIER
    INNER JOIN PRODUCT ON PRODUCT.SUPPLIER_NAME = SUPPLIER.COMPANY_NAME
    WHERE SUPPLIER.TRUSTED_SUPPLIER = 'YES';

IF TRUST = 'NO' THEN 
RAISE_APPLICATION_ERROR(20001, 'SUPPLIER NOT TRUSTED');
END IF;
END;
/

但是,当我尝试使用有效和无效的插入语句测试触发器时:

//this should insert without errors
INSERT INTO PRODUCT
(PRODUCT_NAME, SUPPLIER_NAME, CATEGORY_NAME, QUANTITY_PER_UNIT, UNIT_PRICE, 
UNITS_IN_STOCK, UNITS_ON_ORDER, REORDER_LEVEL, DISCONTINUED)
 VALUES
   ('Backlestan','Exotic Liquids', 'Beverages', '10 boxes x 30 bags', 
    11, 20, 0, 27, 'N');

// this should display the trigger's error
INSERT INTO PRODUCT
(PRODUCT_NAME, SUPPLIER_NAME, CATEGORY_NAME, QUANTITY_PER_UNIT, UNIT_PRICE, 
UNITS_IN_STOCK, UNITS_ON_ORDER, REORDER_LEVEL, DISCONTINUED)
 VALUES
   ('Backlestan','Another Company', 'Beverages', '10 boxes x 30 bags', 
    11, 20, 0, 27, 'N');

我收到以下错误:

Error report -
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "RK721.VERIFY_SUPPLIER_TRUST", line 4
ORA-04088: error during execution of trigger 'BH576.VERIFY_SUPPLIER_TRUST'

Error report -
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at "RK721.VERIFY_SUPPLIER_TRUST", line 4
ORA-04088: error during execution of trigger 'BH576.VERIFY_SUPPLIER_TRUST'

有人知道如何解决这个错误吗?任何帮助将不胜感激!

【问题讨论】:

【参考方案1】:

您的触发器存在多个问题。让我们从 select 语句和剩余代码之间的“关系”开始。在这种特殊情况下,select..if...end_if (目前假设您的选择确实有效,它没有但只是假设)。现在专注于 WHERE 子句。

SELECT SUPPLIER.TRUSTED_SUPPLIER
    INTO TRUST
    ...
    WHERE SUPPLIER.TRUSTED_SUPPLIER = 'YES';

IF TRUST = 'NO' THEN ...

由于您的选择仅返回 YES,因此 if 语句永远不会为 True。因此永远不会引发应用程序异常。现在,select 有什么问题。 首先,您正在访问触发触发器的表。虽然在某些情况下您可以侥幸逃脱,但通常会导致ORA-04091: table <table_name> is mutating, trigger/function may not see it。总是避免完全引用触发表是失败的。您使用 :NEW 和/或 :OLD 伪记录引用表数据。其次,您的查询没有按照您的想法进行。它说

为供应商中的每一行选择trusted_supplier列 在 Product 表中至少有 1 行的表和 trusted_supplier 列是“YES”。

但是,INTO 子句要求语句返回正好 1 行。多于 1 行会导致异常,0 行会导致no data found 异常。 最后,raise_application_error statement 存在问题。如果它被执行,它会引发 number 参数...超出范围 异常。第一个参数必须介于 -20999 到 -20000(负数)之间。 那么结果是什么样子的:

create or replace trigger verify_supplier_trust
before insert or update on product
for each row 
declare 
    trust varchar2(3);

begin
    select supplier.trusted_supplier
      into trust
      from supplier 
     where supplier.company_name = :new.supplier_name
       and supplier.trusted_supplier = 'YES';
exception
   when no_data_found then 
        raise_application_error(-20001, 'supplier not trusted');
end;
/

注意事项: 不要使用数据类型 VARCHAR。这是允许的,但 Oracle 建议不要这样做。意味着他们保留随时更改其功能的权利。请改用推荐的 VARCHAR2。 我将触发器更改为在插入或更新时触发。如果在 Insert 上被解雇,只有某人可以更改供应商名称以引用不受信任的供应商,一切都会好起来的。

【讨论】:

现在我得到一个错误:Error(7,36): PLS-00049: bad bind variable 'NEW.COMPANY_NAME' 不是新的,而是:新的。冒号 (:) 是必需的。 我已经包含了冒号。它只是没有出现在错误报告中 抱歉应该是where supplier.company_name = :new.supplier_name【参考方案2】:

您必须按插入行的 id 进行过滤!

而不是 SUPPLIER.TRUSTED_SUPPLIER = 'YES'

BEGIN
    SELECT SUPPLIER.TRUSTED_SUPPLIER
    INTO TRUST
    FROM SUPPLIER
    INNER JOIN PRODUCT ON PRODUCT.SUPPLIER_NAME = SUPPLIER.COMPANY_NAME
    WHERE PRODUCT.PRODUCT_NAME = :NEW.PRODUCT_NAME;

【讨论】:

【参考方案3】:

试试这个:

create or replace TRIGGER VERIFY_SUPPLIER_TRUST
BEFORE INSERT ON PRODUCT
REFERENCING NEW AS NEW_PRODUCT
FOR EACH ROW 
    DECLARE TRUST_COUNT NUMBER;

BEGIN
    SELECT COUNT(*) 
    INTO TRUST_COUNT
    FROM SUPPLIER    
    WHERE SUPPLIER.TRUSTED_SUPPLIER = 'YES'
    and SUPPLIER.COMPANY_NAME = :NEW_PRODUCT.SUPPLIER_NAME;

    IF TRUST_COUNT = 0 THEN 
      RAISE_APPLICATION_ERROR(20001, 'SUPPLIER NOT TRUSTED');
    END IF;
END;
/

对于这组数据:

INSERT INTO SUPPLIER(COMPANY_NAME,CONTACT_NAME,CONTACT_TITLE,ADDRESS,CITY,REGION,POSTAL_CODE,COUNTRY,PHONE,FAX,HOME_PAGE,TRUSTED_SUPPLIER) 
VALUES('C1','C1','CT1','A1','R1','PC1','C1','P1','F1','H1','H1','YES');

INSERT INTO SUPPLIER(COMPANY_NAME,CONTACT_NAME,CONTACT_TITLE,ADDRESS,CITY,REGION,POSTAL_CODE,COUNTRY,PHONE,FAX,HOME_PAGE,TRUSTED_SUPPLIER) 
VALUES('C2','C1','CT1','A1','R1','PC1','C1','P1','F1','H1','H1','NO');
COMMIT;

此插入有效:

INSERT INTO PRODUCT(PRODUCT_NAME, SUPPLIER_NAME, CATEGORY_NAME, QUANTITY_PER_UNIT, UNIT_PRICE, UNITS_IN_STOCK, UNITS_ON_ORDER, REORDER_LEVEL, DISCONTINUED)
VALUES('Backlestan111','C1', 'Beverages', '10 boxes x 30 bags',  11, 20, 0, 27, 'N'); /* it works*/

但是这个,没有:

INSERT INTO PRODUCT(PRODUCT_NAME, SUPPLIER_NAME, CATEGORY_NAME, QUANTITY_PER_UNIT, UNIT_PRICE, UNITS_IN_STOCK, UNITS_ON_ORDER, REORDER_LEVEL, DISCONTINUED)
VALUES('Backlestan222','C2', 'Beverages', '10 boxes x 30 bags',  11, 20, 0, 27, 'N'); /* it doesn't work*/

【讨论】:

以上是关于如何创建验证另一个表中的列的 PL/SQL 行触发器的主要内容,如果未能解决你的问题,请参考以下文章

“如何修复 oracle pl/sql 中的触发器?

Oracle PL / SQL触发器,在UPDATE之前/之后仅用于识别表中已修改的列

如何在 PL/SQL 中连接两个表而不创建新表

pl sql触发器如何比较触发器中的列值

创建一个触发器,当另一个表中的列更新时更新一个表上的列

PL/SQL 触发器插入下一个值