Обсуждение: Each foo must have a bar
Folks,
I'm trying to figure out how to enforce the following. Table foo has
a primary key. Table bar has a foreign key to foo. So far so good.
I'd also like to say, "for each row in foo, there must be at least one
row in bar."
I've tried the following, but the check fails too soon. I also tried
an INITIALLY DEFERRED foreign key on bar to foo, trying the INSERT on
bar first, but that didn't work in transaction_isolation LEVEL
SERIALIZABLE.
Any clues?
Cheers,
D
CREATE TABLE foo (
id SERIAL PRIMARY KEY
);
CREATE TABLE bar (
foo_id INTEGER NOT NULL REFERENCES foo(id)
ON DELETE CASCADE
INITIALLY DEFERRED
);
CREATE FUNCTION foo_trg ()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
childless_foo_count INTEGER;
BEGIN
SELECT INTO
childless_foo_count
COUNT(*)
FROM
foo
LEFT JOIN
bar
ON (foo.id = bar.foo_id)
WHERE bar.foo_id IS NULL;
IF childless_foo_count > 0 THEN
RAISE EXCEPTION 'Each foo must have at least one bar.';
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER foo_after
AFTER INSERT OR UPDATE ON foo
FOR EACH STATEMENT
EXECUTE PROCEDURE foo_trg();
--
David Fetter david@fetter.org http://fetter.org/
phone: +1 415 235 3778
Remember to vote!
On Sat, Feb 11, 2006 at 12:56:36PM -0800, David Fetter wrote: > I'm trying to figure out how to enforce the following. Table foo has > a primary key. Table bar has a foreign key to foo. So far so good. > I'd also like to say, "for each row in foo, there must be at least one > row in bar." Possibly something involving CREATE CONSTRAINT TRIGGER, specifying DEFERRABLE INITIALLY DEFERRED? The documentation says it's not for general use; I've used it only in simple experiments so I'm not sure how problematic it could be. Anybody? -- Michael Fuhr
On Sat, Feb 11, 2006 at 02:59:48PM -0700, Michael Fuhr wrote:
> On Sat, Feb 11, 2006 at 12:56:36PM -0800, David Fetter wrote:
> > I'm trying to figure out how to enforce the following. Table foo
> > has a primary key. Table bar has a foreign key to foo. So far so
> > good. I'd also like to say, "for each row in foo, there must be
> > at least one row in bar."
>
> Possibly something involving CREATE CONSTRAINT TRIGGER, specifying
> DEFERRABLE INITIALLY DEFERRED? The documentation says it's not for
> general use; I've used it only in simple experiments so I'm not sure
> how problematic it could be. Anybody?
Kudos to Andrew at Supernews for helping me figure out what's below :)
Cheers,
D
CREATE TABLE foo (
id SERIAL PRIMARY KEY
);
CREATE TABLE bar (
id SERIAL PRIMARY KEY,
foo_id INTEGER NOT NULL REFERENCES foo(id)
ON DELETE CASCADE
DEFERRABLE
INITIALLY DEFERRED
);
CREATE FUNCTION foo_trg ()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
childless_foo_count INTEGER;
BEGIN
SELECT INTO
childless_foo_count
COUNT(*)
FROM
foo
LEFT JOIN
bar
ON (foo.id = bar.foo_id)
WHERE bar.foo_id IS NULL;
IF childless_foo_count > 0 THEN
RAISE EXCEPTION 'Each foo must have at least one bar.';
END IF;
RETURN NULL;
END;
$$;
CREATE TRIGGER foo_after
AFTER INSERT OR UPDATE ON foo
FOR EACH ROW
EXECUTE PROCEDURE foo_trg();
CREATE TRIGGER bar_after
AFTER UPDATE OR DELETE ON bar
FOR EACH ROW
EXECUTE PROCEDURE foo_trg();
--
David Fetter david@fetter.org http://fetter.org/
phone: +1 415 235 3778
Remember to vote!
On Feb 12, 2006, at 6:59 , Michael Fuhr wrote:
> On Sat, Feb 11, 2006 at 12:56:36PM -0800, David Fetter wrote:
>> I'm trying to figure out how to enforce the following. Table foo has
>> a primary key. Table bar has a foreign key to foo. So far so good.
>> I'd also like to say, "for each row in foo, there must be at least
>> one
>> row in bar."
>
> Possibly something involving CREATE CONSTRAINT TRIGGER, specifying
> DEFERRABLE INITIALLY DEFERRED? The documentation says it's not for
> general use; I've used it only in simple experiments so I'm not
> sure how problematic it could be. Anybody?
I've used constraint triggers to handle multi-statement updates for
temporal tables. I also haven't had any in production but am planning
to soon. I have tested it quite a bit and haven't seen any problems.
Caveat being that I tend to have small databases, so I'm not sure
about the performance of the PL/pgSQL function I use to enforce the
assertion. I suspect the except should be more performant than the
count, but that's speculation.
I don't know if the SQL spec allows for statements with multiple
updates, e.g. something like
insert into foo (foo) values ('blurfl'), -- note comma
insert into bar (bar, foo_id)
select 'bat', foo_id
from foo where id = 'blurfl';
(And I definitely don't know if that kind of recursive assignment
would even work; maybe it would have to be more like:
insert into foo (foo_id, foo) values (1, 'blurfl'),
insert into bar (bar, foo_id) values ('bat', 1);
)
That might help get rid of the need to use a constraint trigger
rather than a normal assertion. Not that PostgreSQL has assertions
yet anyway :)
Please find an example below. Hope this helps.
Michael Glaesemann
grzm myrealbox com
create table foo
(
foo_id serial primary key
, foo text not null unique
);
create table bar
(
bar_id serial primary key
, bar text not null unique
, foo_id integer not null
references foo (foo_id)
on update cascade on delete cascade
);
create function assert_bar_for_each_foo()
returns trigger
language plpgsql as $$
begin
if exists (
select foo_id
from foo
except
select foo_id
from bar
)
then raise exception 'Every foo must have a bar';
end if;
return null;
end;
$$;
create constraint trigger assert_bar_for_each_foo_tr
after insert on foo
-- With the on update on delete cascade I don't believe you
-- need to check on update or delete here.
initially deferred
for each row
execute procedure assert_bar_for_each_foo();
create constraint trigger assert_bar_for_each_foo_tr
after delete on bar
-- The foreign key on bar takes care of inserts and updates.
initially deferred
for each row
execute procedure assert_bar_for_each_foo();
insert into foo (foo) values ('blurfl');
ERROR: Every foo must have a bar
begin;
insert into foo (foo) values ('blurfl');
INSERT 0 1
insert into bar (bar, foo_id)
INSERT 0 1
select 'bat', foo_id
from foo
where foo = 'blurfl';
commit;
select *
from foo
natural join bar;
foo_id | foo | bar_id | bar
--------+--------+--------+-----
2 | blurfl | 1 | bat
(1 row)
update foo
set foo_id = 3
where foo = 'blurfl';
UPDATE 1
select *
from foo
natural join bar;
foo_id | foo | bar_id | bar
--------+--------+--------+-----
3 | blurfl | 1 | bat
(1 row)
update bar
set foo_id = 2
where bar = 'bat';
ERROR: insert or update on table "bar" violates foreign key
constraint "bar_foo_id_fkey"
DETAIL: Key (foo_id)=(2) is not present in table "foo".
delete from bar where bar = 'bat';
ERROR: Every foo must have a bar
delete from foo where foo = 'blurfl';
DELETE 1
select * from foo;
foo_id | foo
--------+-----
(0 rows)
select * from bar;
bar_id | bar | foo_id
--------+-----+--------
(0 rows)
select version();
version
------------------------------------------------------------------------
----------------------------------------------------------------------
PostgreSQL 8.1.0 on powerpc-apple-darwin8.3.0, compiled by GCC
powerpc-apple-darwin8-gcc-4.0.0 (GCC) 4.0.0 (Apple Computer, Inc.
build 5026)
(1 row)
I haven't seen this hit the lists yet, though I sent it nearly 12
hours ago. Resending for completeness.
On Feb 12, 2006, at 6:59 , Michael Fuhr wrote:
> On Sat, Feb 11, 2006 at 12:56:36PM -0800, David Fetter wrote:
>
>> I'm trying to figure out how to enforce the following. Table foo has
>> a primary key. Table bar has a foreign key to foo. So far so good.
>> I'd also like to say, "for each row in foo, there must be at least
>> one
>> row in bar."
>>
>
> Possibly something involving CREATE CONSTRAINT TRIGGER, specifying
> DEFERRABLE INITIALLY DEFERRED? The documentation says it's not for
> general use; I've used it only in simple experiments so I'm not
> sure how problematic it could be. Anybody?
>
I've used constraint triggers to handle multi-statement updates for
temporal tables. I also haven't had any in production but am planning
to soon. I have tested it quite a bit and haven't seen any problems.
Caveat being that I tend to have small databases, so I'm not sure
about the performance of the PL/pgSQL function I use to enforce the
assertion. I suspect the except should be more performant than the
count, but that's speculation.
I don't know if the SQL spec allows for statements with multiple
updates, e.g. something like
insert into foo (foo) values ('blurfl'), -- note comma
insert into bar (bar, foo_id)
select 'bat', foo_id
from foo where id = 'blurfl';
(And I definitely don't know if that kind of recursive assignment
would even work; maybe it would have to be more like:
insert into foo (foo_id, foo) values (1, 'blurfl'),
insert into bar (bar, foo_id) values ('bat', 1);
)
That might help get rid of the need to use a constraint trigger
rather than a normal assertion. Not that PostgreSQL has assertions
yet anyway :)
Please find an example below. Hope this helps.
Michael Glaesemann
grzm myrealbox com
create table foo
(
foo_id serial primary key
, foo text not null unique
);
create table bar
(
bar_id serial primary key
, bar text not null unique
, foo_id integer not null
references foo (foo_id)
on update cascade on delete cascade
);
create function assert_bar_for_each_foo()
returns trigger
language plpgsql as $$
begin
if exists (
select foo_id
from foo
except
select foo_id
from bar
)
then raise exception 'Every foo must have a bar';
end if;
return null;
end;
$$;
create constraint trigger assert_bar_for_each_foo_tr
after insert on foo
-- With the on update on delete cascade I don't believe you
-- need to check on update or delete here.
initially deferred
for each row
execute procedure assert_bar_for_each_foo();
create constraint trigger assert_bar_for_each_foo_tr
after delete on bar
-- The foreign key on bar takes care of inserts and updates.
initially deferred
for each row
execute procedure assert_bar_for_each_foo();
insert into foo (foo) values ('blurfl');
ERROR: Every foo must have a bar
begin;
insert into foo (foo) values ('blurfl');
INSERT 0 1
insert into bar (bar, foo_id)
INSERT 0 1
select 'bat', foo_id
from foo
where foo = 'blurfl';
commit;
select *
from foo
natural join bar;
foo_id | foo | bar_id | bar
--------+--------+--------+-----
2 | blurfl | 1 | bat
(1 row)
update foo
set foo_id = 3
where foo = 'blurfl';
UPDATE 1
select *
from foo
natural join bar;
foo_id | foo | bar_id | bar
--------+--------+--------+-----
3 | blurfl | 1 | bat
(1 row)
update bar
set foo_id = 2
where bar = 'bat';
ERROR: insert or update on table "bar" violates foreign key
constraint "bar_foo_id_fkey"
DETAIL: Key (foo_id)=(2) is not present in table "foo".
delete from bar where bar = 'bat';
ERROR: Every foo must have a bar
delete from foo where foo = 'blurfl';
DELETE 1
select * from foo;
foo_id | foo
--------+-----
(0 rows)
select * from bar;
bar_id | bar | foo_id
--------+-----+--------
(0 rows)
select version();
version
------------------------------------------------------------------------
----------------------------------------------------------------------
PostgreSQL 8.1.0 on powerpc-apple-darwin8.3.0, compiled by GCC
powerpc-apple-darwin8-gcc-4.0.0 (GCC) 4.0.0 (Apple Computer, Inc.
build 5026)
(1 row)