PostgreSQL: How to implement minimum cardinality?

给你一囗甜甜゛ 提交于 2019-12-21 03:55:13

问题


As answered in this question: Cardinality in PostgreSQL, cardinality is enfforced using constraints.

Cardinality rules define the allowable counts of the relationships – one-to-many, many-to-many, etc. Many-to-many is achieved using join-tables and one-to-many using FOREIGN KEY.

But how can one implement one-to-one_or_many (one-to-1+) relationship. Which is same as to ask: How can I enforce minimum cardinality in PostgreSQL?

A practical situation would be where one needs to store say address (or telephone number) which MUST be provided (but can be more that one) by the person (say user or customer).

Edit:

The above mentioned situation is a special case (with cardinality one) of a general problem. The general problem being: How to enforce cardinality of arbitrary number?

As answered by jug a non-null FOREIGN KEY reference can be used as a work-around, if minimum-cardinality is one. It will also provide an additional feature to select default among many.

But consider another situation of relationship between team of Cricket and its players. Every team MUST have a MINIMUM of 11 players to qualify as a team. Here the minimum cardinality is eleven (11).

Similary, a relation between a course and a student in a school, where every student MUST enroll in AT-LEAST 5 courses and every course MUST have a MINIMUM of 10 students.


回答1:


There's no way to specify this using a CHECK constraint, so I think the best approach is a trigger:

http://www.postgresql.org/docs/9.1/static/sql-createtrigger.html
http://www.postgresql.org/docs/9.1/static/plpgsql-trigger.html

You'd end up with something like (I haven't tested it or anything):

CREATE TRIGGER at_least_one before INSERT, UPDATE, DELETE ON the_one_table  FOR EACH ROW EXECUTE PROCEDURE check_at_least_one();

CREATE OR REPLACE FUNCTION check_at_least_one() RETURNS trigger AS $$
    BEGIN
    nmany := select count(*) from the_many_table where the_many_table.the_one_id=NEW.id;   
    IF nmany > 0 THEN 
        RETURN NEW;
    END IF;
    RETURN NULL;
END;



回答2:


There is no way to enforce such rules using only FOREIGN KEY constraints.

1) One way is by allowing circular references between tables (the "default" column, advised by jug). This results in chicken-and-egg problems that are difficult to manage, you'll have to use deferrable constraints. Plus, this option is simply not available in some DBMS. Another disadvantage is that for a football team, you'll have to add 11 "default" columns (and you'll have to deal with a chicken-and-11-eggs problem)!

2) Another option is to use triggers.

3) Another option is to use a database-level constraint between the 2 tables. Not sure if there is any DBMS that has such functionality. Besides the typical UNIQUE, PRIMARY and FOREIGN KEY constraints, most DBMS have just row level constraints and with limitations (no subqueries, etc).

4) Another option is to enforce such rules by creating appropriate INSERT, UPDATE and DELETE procedures that can only access the two related tables and enforce the integrity according to these rules. This is the better approach (in my opinion).

5) One more option, simpler to implement is to use standard Foreign Key constraints, enforcing the 1-to-many relationship and have a View that shows those Teams that actually have 11 or more players. This off course mean sthat you don't actually enforce the rules you ask for. But it's possible (and may I say probable) that you can afford not too. If the football players get killed in an accident for example, the team can not longer play in tournaments but it's still a team. So, you may define two entities, the Team (the base Table) and the ProperTeam (View), that can play games. Example:

CREATE VIEW AS ProperTeam
( SELECT *
  FROM Team
  WHERE ( SELECT COUNT(*)
          FROM Player
          WHERE Player.TeamId = Team.TeamId
        ) >= 11
) 

Options 1 and 2 look rather "messy" but that's only a personal opinion, many people like triggers.

I would choose Option 4, unless I can ("cheat" and) actually not enforce the constraint with Option 5.




回答3:


If you have the addresses in one table adresses, you can define a column "default_address" (in the table customers) that is a non-null foreign key reference to the address that must be provided.

If you have a table shippings that provides an optional many-to-many relationship by referencing a person, an address and maybe an order(-item), then you could use coalesce to overwrite the NULLs for addresses you get in an outer join between (customers inner join orders) and shipping_addresses (a view joining shippings with addresses). But to prevent problems with maybe different numbers of non-null components of addresses, Stephane Faroult recommends in his (strongly recommended!) book The Art of SQL to use the "hidden sort key" technique (assuming that customers_with_default_address is a view joining customers with addresses using "default_address":

select *
  from (select 1 as sortkey,
               line_1,
               line_2,
               city,
               state,
               postal_code,
               country
        from shipping_addresses
          where customer_id = ?
        union
        select 2 as sortkey,
               line_1,
               line_2,
               city,
               state,
               postal_code,
               country
        from customers_with_default_address
          where customer_id = ?
        order by 1) actual_shipping_address
      limit 1



回答4:


The approach that worked for me and required a reasonable amount of coding was (translated to your team/player question):

  • create a deferrable foreign key constraint to enforce the "each player has one team" relationship.
  • create just one trigger on table team, that checks that at least n players are attached to the team. The trigger should throw an exception if a cardinality is not respected, as pointed out by AdamKG.

This will enforce the constraint, but as a side-effect this will also allow only one way of encoding a new team of players (that is, without rejecting it as a key violation)

start transaction;
set constraints all deferred;
insert player_1 with teamPK --teamPK is not yet in table team, will be below
...
insert player_n with teamPK
insert teamPK --this fires the trigger which is successful.
commit transaction; --this fires the foreign key deferred check, successful.

Note that I do that by using self-defined primary keys (for teamPK), e.g. a unique team name, so that I can know the teamPK before actually inserting the line in table team (as opposed to using the auto-incremented id).



来源:https://stackoverflow.com/questions/9249660/postgresql-how-to-implement-minimum-cardinality

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!