23

I have function checking mandatory participation as follows:

CREATE FUNCTION member_in_has_address()
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (SELECT *
       FROM address a, member_details b
       WHERE b.member_id = a.member_id);
END;
$$  LANGUAGE plpgsql;

Then called from CHECK constraint

ALTER TABLE member_details
 ADD CONSTRAINT member_in_has_address_check
  CHECK (member_in_has_address());

To create deferable constraint in Standard SQL it would be:

ALTER TABLE member_details
 ADD CONSTRAINT member_in_has_address_check
  INITIALLY DEFERRED
  CHECK (member_in_has_address()); 

How can I do the same in PostgreSQL?

Sam
  • 7,252
  • 16
  • 46
  • 65
Radovan Luptak
  • 281
  • 1
  • 2
  • 8
  • Your current `member_in_has_address()` will return `true` when **any** of the members has address. It wont check if a particular member has a address. – Ihor Romanchenko May 01 '13 at 18:05
  • Thank you Igor, but my main question is how to deffer such constraint until the child (`address`) is updated. The insert is as follows: insert into `member_details` which is parent and then to `address` which has foreigh key. `member_details` has mandatory participation with `address`. – Radovan Luptak May 01 '13 at 18:51
  • See my answer below. The answer - create a defferred foreign key. – Ihor Romanchenko May 01 '13 at 18:53
  • Yes, you are right I've just joined the two tables. – Radovan Luptak May 05 '13 at 18:57

4 Answers4

27

You can defer constraints in PostgreSQL in the same way as in other RDBMSs, but for current version (9.2) you can only defer UNIQUE, PRIMARY KEY, EXCLUDE, and REFERENCES. Extract from this page of the manual:

DEFERRABLE
NOT DEFERRABLE

This controls whether the constraint can be deferred. A constraint that is not deferrable will be checked immediately after every command. Checking of constraints that are deferrable can be postponed until the end of the transaction (using the SET CONSTRAINTS command). NOT DEFERRABLE is the default. Currently, only UNIQUE, PRIMARY KEY, EXCLUDE, and REFERENCES (foreign key) constraints accept this clause. NOT NULL and CHECK constraints are not deferrable.

INITIALLY IMMEDIATE
INITIALLY DEFERRED

If a constraint is deferrable, this clause specifies the default time to check the constraint. If the constraint is INITIALLY IMMEDIATE, it is checked after each statement. This is the default. If the constraint is INITIALLY DEFERRED, it is checked only at the end of the transaction. The constraint check time can be altered with the SET CONSTRAINTS command.

You can create a simple deferred foreign key from member_details to address instead of your current constraint to check, if every member has an address.

UPDATE: You need to create 2 foreign keys. One regular one from address(member_id) to member_details(member_id). The other one - deferred from member_details(member_id) to address(member_id).

With this two foreign keys you will be able to:

  1. Create a member in member_details.
  2. Create an address in address for member from step 1
  3. Commit (with no errors)

OR

  1. Create a member in member_details.
  2. Commit (and get error from deferred foreign key).
Patrick Brinich-Langlois
  • 1,381
  • 1
  • 15
  • 29
Ihor Romanchenko
  • 26,995
  • 8
  • 48
  • 44
  • Thanks. So it means I can deffer a foreign key in `address` and then insert first to `address` and then to `member_details`. – Radovan Luptak May 01 '13 at 18:56
  • But there is another problem, `member_details` has autoincremtned primary key so I first insert into `member_details` then retriev its primary key and then insert into `address`. – Radovan Luptak May 01 '13 at 19:05
  • @RadovanLuptak Just create 2 foreign keys, one `member_details(member_id)` -> `address(member_id)`, and second `address(member_id)` -> `member_details(member_id)`. – Ihor Romanchenko May 01 '13 at 19:10
  • I am quite new to this can you tell me what is the benefit? I only suspect that when I enforce both foreign keys as `NOT NULL` I can achieve mandatory participation on both sides and by making foreign key referencing `address` `address_id UNIQUE` I can achieve 1 to n participation. – Radovan Luptak May 01 '13 at 19:21
  • @RadovanLuptak Thats correct. Pair of FK can be used to enforce `1..many` to `1..many` relation. Further limiting one side with `UNIQUE` you can enforce `1` to `1..many` relation. And there is no need to reference `address_id`. Both keys can be on `member_id` – Ihor Romanchenko May 01 '13 at 19:28
  • That's great. Thanx. I am also thinking about this: `CREATE CONSTRAINT TRIGGER manatory_participation_member_details AFTER INSERT ON member_details DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN (NOT member_in_has_address()) EXECUTE PROCEDURE ...;` And procedure raising exception. Could it work? – Radovan Luptak May 01 '13 at 19:33
  • @RadovanLuptak It will work (probably. It must be written correctly to work). But it will be much slower and harder to understand. – Ihor Romanchenko May 01 '13 at 19:41
  • The problem with deferred foreigh keys in Igor's example is that both tables has generated primary keys and I don't know how to set them after records are created. – Radovan Luptak May 02 '13 at 18:21
  • @RadovanLuptak As I mentioned before - both FK can be on `member_id`. This will allow you to generate `member_id` when you create the record in `member_details` and use the same `member_id` for creating record in `address`. – Ihor Romanchenko May 02 '13 at 19:03
  • I'll try this as well. Thanx Igor. – Radovan Luptak May 02 '13 at 20:36
3

Wrap your queries in a transaction, and then use a deferred foreign key and deferred constraint triggers if at least one address is needed:

CREATE CONSTRAINT TRIGGER member_details_address_check_ins
  AFTER INSERT ON member_details
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE member_details_address_check_ins();

ALTER TABLE address
ADD CONSTRAINT address_member_details_member_id_fkey
FOREIGN KEY (member_id) REFERENCES member_details(member_id)
ON UPDATE NO ACTION ON DELETE NO ACTION
DEFERRABLE INITIALLY DEFERRED;

CREATE CONSTRAINT TRIGGER address_member_details_check_del
  AFTER DELETE ON address
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE address_member_details_check_del();

-- also consider the update cases for the inevitable merge of duplicate members.

On a separate note, normalized and pretty, but putting addresses and contact details such as emails in a separate address table occasionally introduces very colorful UI/UX issues. E.g. an untrained secretary changing the company and address of all of her boss' contacts at company A when one of them switched to company B. Yeah, seen it happen for real when the UI behaved differently from Outlook...

Anyway, and fwiw, I've found that it's usually more convenient to store this stuff in the same table as the contact, i.e. address1, address2, email1, email2, etc. It makes other things simpler for a variety of other reasons -- namely running checks like the one you're looking into. The extremely rare case where you'd want to store more than two such pieces of information are, in practice, simply not worth the hassle.

Denis de Bernardy
  • 75,850
  • 13
  • 131
  • 154
  • Thanks, I completly forgot about insert and delete trigger to enforce mandatory participation. I've done it in Sybase. – Radovan Luptak May 02 '13 at 18:04
  • Btw, there was a related question today, where I ended up expanding on my concluding point: http://dba.stackexchange.com/a/41430/1860 – Denis de Bernardy May 02 '13 at 18:08
  • can't aggree with your view to "join" the 1:n contact type infos more generally (like address, mail, tel, ...) with the contact itself. in applications that have no focus on this, it may be ok, but as soon as it is more relevant to manage contacts often or for a long period of time reliably and flexibly, handling this with 1:n relationships and separate tables may be more easily designed and maintained in my experience. Outlook does IMO not offer a best practices approach in this case, since it has only a quite basic data model that does not fit well for more complex contact management. – Andreas Covidiot Sep 21 '15 at 08:27
  • @AndreasDietrich: Yeah, not using 1-n relationships bugs me too. But in practice it also introduces very colorful UI/UX issues that lead to incorrect data finding its way all over the place. I've literally see a secretary rename a contact's org, with the change getting incorrectly applied to a few dozen other contacts as a result of that - plus a dup org entry. It also introduces engineering (and performance) problems that, in my opinion, are simply not worth spending any time on in smaller companies. – Denis de Bernardy Sep 21 '15 at 08:54
  • So "smaller companies" in combination with "and no bigger focus on CRM etc. and rather lousy programmers ;)" sounds reasonable to me. otherwise it should be no performance deal really with straight forward indexing. The problems you describe seem to stem from application bugs that do not seem to be per se related to this problem. Lousy programming or buggy software (which is not?) should not be an excuse to not apply reasonable models, good technologies or best practices (I don't mean "golden cup handles"). It's the philosophy of our throw-away + cheap society that often makes things difficult – Andreas Covidiot Sep 21 '15 at 09:34
2

This is what I come up with.

ALTER TABLE address
ADD CONSTRAINT address_member_in_has_address
FOREIGN KEY (member_id) REFERENCES member_details(member_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED;

CREATE FUNCTION member_in_has_address() RETURNS trigger AS $BODY$
    BEGIN
    IF NOT EXISTS(SELECT * 
                   FROM member_details
                   WHERE member_id IN (SELECT member_id 
                                        FROM address)) 
    THEN
            RAISE EXCEPTION 'Error: member does not have address';
        END IF;
    RETURN NEW;
    END;
$BODY$ LANGUAGE plpgsql;

CREATE CONSTRAINT TRIGGER manatory_participation_member_details_ins
 AFTER INSERT ON member_details 
 DEFERRABLE INITIALLY DEFERRED 
 FOR EACH ROW  
 EXECUTE PROCEDURE member_in_has_address();

CREATE CONSTRAINT TRIGGER manatory_participation_member_details_del
 AFTER INSERT ON member_details 
 DEFERRABLE INITIALLY DEFERRED 
 FOR EACH ROW 
 EXECUTE PROCEDURE member_in_has_address();

I tried Igor's version using foreign keys in both tables without the triggers. In this case this constraint is not deffered.

ALTER TABLE member_details
ADD CONSTRAINT member_details_in_has_address
FOREIGN KEY (address_id) REFERENCES address
ON UPDATE NO ACTION ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED;

I get this: ERROR: null value in column "address_id" violates not-null constraint

When inserting using this annonymous block:

DO $$ 
DECLARE 
 mem BIGINT;
BEGIN
INSERT INTO member_details (member_first_name, member_last_name, member_dob, member_phone_no, 
member_email, member_gender, industry_position, account_type, music_interests)
VALUES ('Rado','Luptak','07/09/80','07540962233','truba@azet.sk','M','DJ','basic','hard core');

SELECT member_id 
 INTO mem
FROM member_details
WHERE member_first_name = 'Rado' AND member_last_name = 'Luptak'
AND member_dob = '07/09/76';

INSERT INTO address (address_id, house_name_no, post_code, street_name, town, country, member_id)
VALUES (mem, '243', 'E17 3TT','Wood Road','London', 'UK', mem);

UPDATE member_details
 SET  address_id = mem WHERE member_id = mem;
END
$$;

Another problem with enforcing mandatory participation in member_details using address_id of address table (Igor's version) is that this allows me to insert row into member_details and reference an existing address row, but the existing address row references different member_details row. When the latter member_details row is deleted it cascades and deletes the address row, which can or cannot delete (depends on settings) the new inserted member_details row. It would also return different details when joining on member_id and on address_id. Therefore, it requires another constraint, so I stayed with trigger and dropping it before insert and recreating it after insert, due to the trigger is not deferred.

Radovan Luptak
  • 281
  • 1
  • 2
  • 8
  • As I said before, the function `member_in_has_address()` will return true when **any** of the members has address. It wont check if a particular member has a address. – Ihor Romanchenko May 02 '13 at 19:12
0

2 tested approaches.

1.Alter CONSTRAINTS INITIALLY DEFERRED.

begin;

alter TABLE t1 alter CONSTRAINT t1_fkey deferrable INITIALLY DEFERRED;

delete from t1;

-- insert into t1 (...)

alter TABLE t1 alter CONSTRAINT t1_fkey not deferrable;

-- commit;
rollback;

2.SET CONSTRAINTS ALL DEFERRED.

begin;

alter TABLE t1 alter CONSTRAINT t1_fkey deferrable initially immediate;
SET CONSTRAINTS t1_fkey DEFERRED
-- SET CONSTRAINTS ALL DEFERRED;  -- or, do this.

delete from t1;

-- insert into t1 (...)

alter TABLE t1 alter CONSTRAINT t1_fkey not deferrable;

-- commit;
rollback;
Charlie 木匠
  • 2,234
  • 19
  • 19
  • If you actually want to have all rows deleted from `t1` at the end, note that you need to swap the commented lines on `--commit` and `rollback` (ie, you **can't** just use `rollback` to try to undo the `SET CONTRAINTS` OR `alter CONSTRAINT`.) As the answer is, it lets you safely just run through the commands, confirming that each one is valid and does not cause an error (eg, on a constraint somewhere) and then puts _everything_ back to the way it was before the BEGIN . – Randall Apr 11 '23 at 21:03