3

On the project I am working on I have a table that needs to be defined as having a one to nine relationship and I was wondering what the best way of creating that in the database was? I am working in PostgreSQL.

My original idea was to create a table and just explicitly create the links (note that actual_id is because of the system I have to virtualize the id because I need unique tables but I also need to know what the actual id of the template is)

CREATE TABLE template (
    id int,
    actual_id int,
    foreign_key0 int references other_table(id),
    foreign_key1 int references other_table(id),
    foreign_key2 int references other_table(id),
    foreign_key3 int references other_table(id),
    foreign_key4 int references other_table(id),
    foreign_key5 int references other_table(id),
    foreign_key6 int references other_table(id),
    foreign_key7 int references other_table(id),
    foreign_key8 int references other_table(id)
);

However this is creating an issue when I want to clean the data from the referenced table when nothing is being referenced anymore. Also I was pretty positive that this was bad db design from the beginning.

My other idea is that I would just make the table with one constraint

CREATE TABLE template (
    id int,
    actual_id int,
    foreign_key0 int references other_table(id) );

However the problem here is how do I constrain this to only have 9 references to the other table? Stored procedures? Programmatically?

Ultimately if I stick with the first way I am pretty sure I am just going to have to select all of the different foreign_key's into another table that just has a single column and compare that against other_table's id. I don't want to do this. It seems really dumb. I really want to do it the second way but I have no idea how to best go about this.

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Harageth
  • 47
  • 1
  • 8
  • 1
    I think we need a better description of your problem domain. However, at minimum, you're (probably) massively violating good nomalization practices with the repeated foreign key columns (especially all listing the same column, although they might not be the same rows). – Clockwork-Muse Aug 10 '12 at 23:44
  • 2
    Other than being ugly, the 1st approach with 9 foreign key refs is wrong too. I can `INSERT INTO template (foreign_key0, foreign_key1, ...) VALUES (1,1,...)` ie insert the *same* child for each fk ref. You'd also have to add a `CHECK` constraint that checks that each fk `IS DISTINCT FROM` each other fk. Ick. – Craig Ringer Aug 11 '12 at 09:52
  • @AdamMartinek Any thoughts on the suggestions offered here? – Craig Ringer Aug 13 '12 at 03:54
  • Wow I was not expecting such a swift response. (First time posting) @CraigRinger I rather thought that I had a bad design and I was leaning towards using triggers/stored procedures but I haven't ever done them before (and our more experienced database guy has said he hasn't had a database complex enough for them before) and so before I made the time investment into figuring that out I wanted to corroborate my idea. – Harageth Aug 13 '12 at 14:14
  • @AdamMartinek My opinion is that triggers are right for pretty much *any* database. I use triggers heavily for audit logging, sanity checks, enforcing integrity constraints that can't be expressed as simple FK refs or `CHECK` constraints, etc. Sounds like that guy probably just prefers to do all the logic in the application, which is valid enough if only the one app ever accesses the DB. – Craig Ringer Aug 13 '12 at 23:41

5 Answers5

4

A 1:n relationship can always be reversed to be seen as n:1 . In other words, instead of:

parent:field1 -> child1:id
parent:field2 -> child2:id
parent:field3 -> child3:id
....
parent:field9 -> child9

you can always write:

child1:parent_id -> parent:id
child2:parent_id -> parent:id
child3:parent_id -> parent:id
....
child9:parent_id -> parent:id

... and constrain the number of children per parent via a trigger or in the application. That's the approach I would strongly recommend. You'll need a deferrable constraint trigger to allow you to insert anything.

If you want to enforce it in the database, use a constraint trigger. Given the dummy schema:

CREATE TABLE parent (id serial primary key);
CREATE TABLE child( id serial primary key, parent_id integer references parent(id) );
INSERT INTO parent (id) values ( DEFAULT );
INSERT INTO child ( parent_id ) 
SELECT p.id FROM parent p CROSS JOIN generate_series(1,9) x;

You could write:

CREATE OR REPLACE FUNCTION children_per_parent() RETURNS TRIGGER AS $$
DECLARE
    n integer;
BEGIN
    IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
        SELECT INTO n count(id) FROM child WHERE parent_id = NEW.parent_id;
        IF n <> 9 THEN
            RAISE EXCEPTION 'During % of child: Parent id=% must have exactly 9 children, not %',tg_op,NEW.parent_id,n;
        END IF;
    END IF;

    IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
        SELECT INTO n count(id) FROM child WHERE parent_id = OLD.parent_id;
        IF n <> 9 THEN
            RAISE EXCEPTION 'During % of child: Parent id=% must have exactly 9 children, not %',tg_op,NEW.parent_id,n;
        END IF;
    END IF;

    RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';

CREATE CONSTRAINT TRIGGER children_per_parent_tg
AFTER INSERT OR UPDATE OR DELETE ON child
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE children_per_parent();

CREATE OR REPLACE parent_constrain_children() RETURNS trigger AS $$
DECLARE 
    n integer;
BEGIN
    IF TG_OP = 'INSERT' THEN
        SELECT INTO n count(id) FROM child WHERE parent_id = NEW.id;
        IF n <> 9 THEN
            RAISE EXCEPTION 'During INSERT of parent id=%: Must have 9 children, found %',NEW.id,n;
        END IF;
    END IF;
    -- No need for an UPDATE or DELETE check, as regular referential integrity constraints
    -- and the trigger on `child' will do the job.
    RETURN NULL;
END;
$$ LANGUAGE 'plpgsql';


CREATE CONSTRAINT TRIGGER parent_limit_children_tg
AFTER INSERT ON parent
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE PROCEDURE parent_constrain_children();

Note that there are two triggers above. The trigger on child is obvious. The trigger on parent is needed to prevent insertion of a parent without any children.

Now observe a test:

regress=# delete from child;
ERROR:  During DELETE: Parent id 1 must have exactly 9 children, not 0
regress=# insert into child( parent_id) SELECT id FROM parent;
ERROR:  During INSERT: Parent id 1 must have exactly 9 children, not 10

Because the deferred constraint trigger is checked when the transaction commits, not immediately or at the end of the statement, you can still do this:

regress# BEGIN;
BEGIN
regress# INSERT INTO parent (id) values ( DEFAULT ) RETURNING id;
 id 
----
  2
INSERT 0 1
regress# insert into child ( parent_id ) SELECT p.id FROM parent p CROSS JOIN generate_series(1,9) x WHERE p.id = 4;
INSERT 0 9
regress# COMMIT;
COMMIT

... but if you change the "generate_series" max to 8 or 10, or leave off inserting any children entirely, COMMIT will fail like, eg:

regress=# commit;
ERROR:  During INSERT: Parent id 5 must have exactly 9 children, not 8

If you only require each parent to have a maximum of 9 children, not exactly 9 children as implemented in the above trigger, you can remove the DEFERRABLE INITIALLY DEFERRED, change the <> 9 to <= 9, and chop out the DELETE handler in the child trigger.


BTW, if I were working with JPA in Java or some other reasonably clever ORM I'd just constrain the size of the collection of children on the parent:

@Entity
public Parent {

    @Column
    @Size(min=9,max=9)
    private Collection<Child> collectionOfChildren;

}

Way simpler, albeit not enforced at the database level.

Craig Ringer
  • 307,061
  • 76
  • 688
  • 778
  • Technically I am just an intern.. however I am the only person working on this project. So I would like to do things so that when they need to work on it again in the future (and if I am not here) then they have the data integrity maintained in the db as opposed to the messy php that is being written. – Harageth Aug 13 '12 at 15:33
  • Also this works like a charm. I modified it slightly for my purposes (only need less than 9) but this works great and will majorly simplify my code and help a ton in maintaining clean data. Thank you very much. – Harageth Aug 13 '12 at 16:01
  • @AdamMartinek No worries. You might want to consider making the FK refs `ON DELETE CASCADE` too, so deleting a parent object deletes all its children automatically. That's typically the right thing to do when the child objects are "owned" by the parent object and not referenced from elsewhere in the DB. – Craig Ringer Aug 13 '12 at 23:43
  • I have changed them to be ON DELETE CASCADE as well and that is working nicely as well. Thanks for the tip. – Harageth Aug 14 '12 at 18:07
1

However this is creating an issue when I want to clean the data from the referenced table when nothing is being referenced anymore.

If I understand this correctly, you'd like dangling pointers to be automatically removed. Would ... REFERENCES other_table(id) ON DELETE CASCADE help?

solidsnack
  • 1,631
  • 16
  • 26
  • `ON DELETE CASCADE` is dangerous here. It means that if one of the 9 referenced rows is deleted, the template will be deleted too. I don't think that this is the wanted behaviour. – ypercubeᵀᴹ Aug 11 '12 at 13:12
  • Ultimately I am storing schedule data for remote integrated systems in this part of the db. Because of how the database is currently designed and the nature of these systems I need to clean the db every time I update the schedule of a device. So I need to remove the schedule data per system but there may be multiple systems with the same configuration so currently they are all referencing through the same tables so I am not guaranteed that when I remove the schedule data the template data or the other_table data is getting cleaned (currently the problem is with the other_table data). – Harageth Aug 13 '12 at 14:21
0

I don't think this can be done with constraints, see How to write a constraint concerning a max number of rows in postgresql? for several ideas.

Below I have written an example where a foo keeps a count of relations to a bar with the following assumptions:

  • foos and bars are independent entities whose lifetime doesn't depend on each other
  • the relationship is stored in a separate foo2bar mapping table
  • the mapping has to be deleted first from the mapping table
  • the deletion of foos and bars without relationships is ignored

\pset pager off

begin;

create table foo(id serial primary key, data text not null,
                 bar_count integer check(bar_count >= 0 and bar_count <= 3));

create table bar(id serial primary key, data text not null);

create table foo2bar(id serial primary key,
                     foo_id integer not null references foo(id),
                     bar_id integer not null references bar(id));

create or replace function trigger_update_bar_count() returns trigger
as $$
declare
  v_bar_count integer := 0;
begin
  if TG_OP = 'INSERT' then
    select count(*) into v_bar_count from foo2bar where foo_id = new.foo_id;

    update foo
       set bar_count = v_bar_count + 1
     where id = new.foo_id;

    return new;
   elsif TG_OP = 'DELETE' then
    select count(*) into v_bar_count from foo2bar where foo_id = old.foo_id;

    update foo
       set bar_count = v_bar_count - 1
     where id = old.foo_id;

    return old;
  end if;

end;
$$ language plpgsql;

create trigger trigger_foo2bar_1
before insert or delete on foo2bar
for each row execute procedure trigger_update_bar_count();

insert into foo(data) values('foo 1');

insert into bar(data) values('bar 1');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));

insert into bar(data) values('bar 2');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 3');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));

insert into foo(data) values('foo 2');

insert into bar(data) values('bar 4');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 5');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 6');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));

insert into foo(data) values('foo 3');

insert into bar(data) values('bar 7');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 8');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));
insert into bar(data) values('bar 9');
insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
                                           currval('bar_id_seq'));

-- deletes only mappings
delete from foo2bar where foo_id = 1;
delete from foo2bar where bar_id = 6;

-- This will raise because the check constraint will be violated
-- insert into bar(data) values('bar 10');
-- insert into foo2bar(foo_id, bar_id) values(currval('foo_id_seq'),
--                                            currval('bar_id_seq'));

select * from foo order by id;
select * from bar order by id;
select * from foo2bar order by id;

select foo.data as foo, bar.data as bar
  from foo2bar
 inner join foo on foo2bar.foo_id = foo.id
 inner join bar on foo2bar.bar_id = bar.id
 order by foo2bar.id
;

rollback;
Community
  • 1
  • 1
user272735
  • 10,473
  • 9
  • 65
  • 96
  • It can be done with dependent entities using a deferred [constraint trigger](http://www.postgresql.org/docs/9.0/static/sql-createconstraint.html), see added answer. – Craig Ringer Aug 11 '12 at 09:35
0

A different idea. This is a 1-to-many relationship (only that n is restricted to 9) and in 1-to-many relationships, the foreign key reference is the opposite way of what you have.

So, make the FOREIGN KEY constraints the other way around (bonus: you need only one this way) and add a counter column and a CHECK constraint to restrict the number of related rows to the maximum of 9:

CREATE TABLE template (
    template_id int,
    actual_id int,
    PRIMARY KEY (template_id)
);

CREATE TABLE other_table (
    other_table_id int,
    template_id,
    counter smallint NOT NULL,
    --- other columns,
    PRIMARY KEY (other_table_id),
    UNIQUE KEY (template_id, counter),
    CHECK (counter BETWEEN 1 AND 9),
    FOREIGN KEY (template_id)
       REFERENCES template (template_id) 
);
ypercubeᵀᴹ
  • 113,259
  • 19
  • 174
  • 235
0

The maintainable and flexible approach is to normalize. Instead of just doing this:

CREATE TABLE template (
    id int,
    actual_id int,
    foreign_key0 int references other_table(id),
    foreign_key1 int references other_table(id),
    foreign_key2 int references other_table(id),
    foreign_key3 int references other_table(id),
    foreign_key4 int references other_table(id),
    foreign_key5 int references other_table(id),
    foreign_key6 int references other_table(id),
    foreign_key7 int references other_table(id),
    foreign_key8 int references other_table(id)
);

Do it the normalized way, introduce a third table(template_assoc_other_table):

CREATE TABLE template (
    id int not null primary key,
    actual_id int -- I don't what is this
    -- ...other fields here
);


create table template__assoc__other_table
(
    template_id int not null references template(id),
    other_table_id int not null references other_table(id),
    constraint pk_template__assoc__other_table 
        primary key (template_id, other_table_id)
);
Michael Buen
  • 38,643
  • 9
  • 94
  • 118
  • I would prefer not adding more tables. I already have an entity set to a relationship set to a relationship set to an entity set and I would prefer not adding another relationship set in there (actually I could probably add two more relationship sets in there if I was normalizing better but I don't feel like this project needs that much flexibility). – Harageth Aug 13 '12 at 15:13