问题
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