简体   繁体   中英

PL/pgSQL query in PostgreSQL returns result for new, empty table

I am learning to use triggers in PostgreSQL but run into an issue with this code:

CREATE OR REPLACE FUNCTION checkAdressen() RETURNS  TRIGGER AS $$
DECLARE
  adrCnt int = 0;
BEGIN
  SELECT INTO adrCnt count(*) FROM Adresse
  WHERE gehoert_zu = NEW.kundenId;

  IF adrCnt < 1 OR adrCnt > 3 THEN
    RAISE EXCEPTION 'Customer must have 1 to 3 addresses.';
  ELSE
    RAISE EXCEPTION 'No exception';
  END IF;
END;
$$ LANGUAGE plpgsql;

I create a trigger with this procedure after freshly creating all my tables so they are all empty. However the count(*) function in the above code returns 1. When I run SELECT count(*) FROM adresse; outside of PL/pgSQL, I get 0. I tried using the FOUND variable but it is always true.

Even more strangely, when I insert some values into my tables and then delete them again so that they are empty again, the code works as intended and count(*) returns 0. Also if I leave out the WHERE gehoert_zu = NEW.kundenId , count(*) returns 0 which means I get more results with the WHERE clause than without.

--Edit:

Here is an example of how I use the procedure:

CREATE TABLE kunde (
kundenId    int PRIMARY KEY
);

CREATE TABLE adresse (
id      int PRIMARY KEY,
gehoert_zu  int REFERENCES kunde
);

CREATE CONSTRAINT TRIGGER adressenKonsistenzTrigger AFTER INSERT ON Kunde
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE checkAdressen();

INSERT INTO kunde VALUES (1);
INSERT INTO adresse VALUES (1,1);

It looks like I am getting the DEFERRABLE INITIALLY DEFERRED part wrong. I assumed the trigger would be executed after the first INSERT statement but it happens after the second one, although the inserts are not inside a BEGIN; - COMMIT; - Block.

According to the PostgreSQL Documentation inserts are commited automatically every time if not inside such a block and thus there shouldn't be an entry in adresse when the first INSERT statement is commited.

Can anyone point out my mistake?

--Edit:

The trigger and DEFERRABLE INITIALLY DEFERRED seem to be working all right. My mistake was to assume that since I am not using a BEGIN - COMMIT -Block each insert would be executed in an own transaction with the trigger being executed afterwards every time.

However even without the BEGIN - COMMIT all inserts get bundled into one transaction and the trigger is executed afterwards. Given this behaviour, what is the point in using BEGIN - COMMIT ?

You need a transaction plus the "DEFERRABLE INITIALLY DEFERRED" because of the chicken and egg problem.


starting with two empty tables:

  • you cannot insert a single row into the person table, because the it needs at least one address.
  • you cannot insert a single row into the address table, because the FK constraint needs a corresponding row on the person table to exist

This is why you need to bundle the two inserts into one operation: the transaction. You need the BEGIN+ COMMIT, and the DEFERRABLE allows transient forbidden database states to exists: it causes the check to be evaluated at commit time.

This may seem a bit silly, but the answer is you need to stop deferring the trigger and run it BEFORE the insert. If you run it after the insert, of course there is data in the table.

As far as I can tell this is working as expected.

One further note, you probably dont mean:

RAISE EXCEPTION 'No Exception';

You probably want

RAISE INFO 'No Exception';

Then you can change your settings and run queries in transactions to test that the trigger does what you want it to do. As it is, every insert is going to fail and you have no way to move this into production without editing your procedure.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM