Modeling a three-way association with optional relation
-
28-02-2021 - |
Question
Business rules
I have three tables (Parties, Categories and Products) which representing the following relationships:
- A product is classified by zero-one-to-many categories
- A category classifies zero-one-or-many products
Then, I have the party relationships:
- A product is classified by one-to-one party
- A party classifies one-to-many products
In other words, a product doesn't have to be assigned a category, but they have to be assigned a party respectively.
The party_id
for a Category must match the party_id
for a Product in order to relate.
EDIT
Following is a correction to the business rules above, based on @damir-sudarevic's solution proposal:
Category
is defined by aparty
.- Each
category
is defined by exactly oneparty
. - Each
party
may define more than onecategory
.
- Each
Product
is classified by aparty
.- Each
product
is classified by exactly oneparty
. - Each
party
may classify more than oneproduct
.
- Each
Product
is classified in acategory
by aparty
.- Each
product
may be classified in more than onecategory
. - More than one
product
may be classified in the samecategory
. - A
product
is classified by aparty
in acategory
, then thatcategory
is defined by thatparty
.
- Each
Design proposals
I have based my first design on the proposal found here, but it's not entirely applicable since I want to enforce party_id
for both Products and Categories respectively and in the relation.
I have made a second proposal that simplifies the design somewhat, but I'm not sure how to enforce the party_id
to the Product-Category relation.
SQL based on latest design
Based on the comments and solution proposals, I have added a simplified SQL to create the tables and their relations.
CREATE TABLE IF NOT EXISTS parties (
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS categories (
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
name_key VARCHAR(255) NOT NULL,
party_id INT(10) UNSIGNED NOT NULL,
parent_id INT(10) UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (id, party_id),
INDEX fk_categories_parent_category_idx (parent_id ASC),
UNIQUE INDEX name_key_UNIQUE (name_key ASC, party_id ASC),
INDEX fk_categories_party_idx (party_id ASC),
CONSTRAINT fk_categories_parent_category
FOREIGN KEY (parent_id)
REFERENCES categories (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT fk_categories_party
FOREIGN KEY (party_id)
REFERENCES parties (id)
ON DELETE CASCADE
ON UPDATE CASCADE);
CREATE TABLE IF NOT EXISTS products (
id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
party_id INT(10) UNSIGNED NOT NULL,
product_code VARCHAR(50) NOT NULL,
PRIMARY KEY (id, party_id),
UNIQUE INDEX product_code_UNIQUE (product_code ASC, party_id ASC),
INDEX fk_products_party_idx (party_id ASC),
CONSTRAINT fk_products_party
FOREIGN KEY (party_id)
REFERENCES parties (id)
ON DELETE CASCADE
ON UPDATE CASCADE);
CREATE TABLE IF NOT EXISTS product_category (
product_id INT(10) UNSIGNED NOT NULL,
category_id INT(10) UNSIGNED NOT NULL,
party_id INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (product_id, category_id),
INDEX fk_product_category_product_idx (product_id ASC, party_id ASC),
INDEX fk_product_category_category_idx (category_id ASC, party_id ASC),
CONSTRAINT fk_product_category_product
FOREIGN KEY (product_id , party_id)
REFERENCES products (id , party_id)
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT fk_product_category_category
FOREIGN KEY (category_id , party_id)
REFERENCES categories (id , party_id)
ON DELETE CASCADE
ON UPDATE CASCADE);
Question
How can I setup the three-way association table correctly to avoid the risk of having an application layer assigning a product to a category without enforcing the party_id
?
Solution
The
party_id
for aCategory
must match theparty_id
for aProduct
in order to relate.
Well, according to your wording of the problem, there is no need for party_id
in Category
.
The confusion likely stems from imprecise wording, you are blending predicate and constraints in one sentence.
For example:
- A product is classified by one-to-one party.
- A party classifies one-to-many products.
Can be worded more clearly:
Product
is classified byparty
.- Each
product
is classified by exactly oneparty
. - Each
party
may classify more than oneproduct
.
This wording then directly leads to a usable model (predicate, constraints).
Option 1
-- Party PTY exists.
--
party {PTY}
PK {PTY}
Each product is classified by exactly one party; for each party, that party may classify more than one product.
-- Product PRO, classified by party PTY exists.
--
product {PRO, PTY}
PK {PRO}
FK {PTY} REFERENCES party {PTY}
-- Category CAT exists.
--
category {CAT}
PK {CAT}
For each product, that product may be classified in more than one category. For each category, more than one product may be classified as belonging to that category.
-- Product PRO is classified in category CAT.
--
product_category {PRO, CAT}
PK {PRO, CAT}
FK1 {PRO} REFERENCES product {PRO}
FK2 {CAT} REFERENCES category {CAT}
Option 2
It may be that a product is known before the matching party is known. Then a variation:
-- Party PTY exists.
--
party {PTY}
PK {PTY}
-- Product PRO exists.
--
product {PRO}
PK {PRO}
-- Product PRO is classified by party PTY.
--
product_party {PRO, PTY}
PK {PRO}
FK1 {PRO} REFERENCES product {PRO}
FK2 {PTY} REFERENCES party {PTY}
-- Category CAT exists.
--
category {CAT}
PK {CAT}
-- Product PRO is classified in category CAT.
--
product_category {PRO, CAT}
PK {PRO, CAT}
FK1 {PRO} REFERENCES product_party {PRO}
FK2 {CAT} REFERENCES category {CAT}
EDIT
After few comments:
Category
is defined by aparty
.- Each
category
is defined by exactly oneparty
. - Each
party
may define more than onecategory
.
- Each
Product
is classified by aparty
.- Each
product
is classified by exactly oneparty
. - Each
party
may classify more than oneproduct
.
- Each
Product
is classified in acategory
by aparty
.- Each
product
may be classified in more than onecategory
. - More than one
product
may be classified in the samecategory
. - If a
product
is classified by aparty
in acategory
, then thatcategory
is defined by thatparty
.
- Each
Option 3
Party must exists before category and product.
-- Party PTY exists.
--
party {PTY}
PK {PTY}
-- Product PRO, classified by party PTY exists.
--
product {PRO, PTY}
PK {PRO}
SK {PRO, PTY}
FK {PTY} REFERENCES party {PTY}
-- Category CAT, defined by party PTY exists.
--
category {CAT, PTY}
PK {CAT}
SK {CAT, PTY}
FK {PTY} REFERENCES party {PTY}
-- Product PRO is classified in category CAT
-- by party PTY.
--
product_category {PRO, CAT, PTY}
PK {PRO, CAT}
FK1 {PRO, PTY} REFERENCES product {PRO, PTY}
FK2 {CAT, PTY} REFERENCES category {CAT, PTY}
Option 4
If product and category can exist independently of party.
-- Party PTY exists.
--
party {PTY}
PK {PTY}
-- Product PRO exists.
--
product {PRO}
PK {PRO}
-- Category CAT exists.
--
category {CAT}
PK {CAT}
-- Product PRO is classified by party PTY.
--
product_party {PRO, PTY}
PK {PRO}
SK {PRO, PTY}
FK1 {PRO} REFERENCES product {PRO}
FK2 {PTY} REFERENCES party {PTY}
-- Category CAT is defined by party PTY.
--
category_party {CAT, PTY}
PK {CAT}
SK {CAT, PTY}
FK1 {CAT} REFERENCES category {CAT}
FK2 {PTY} REFERENCES party {PTY}
-- Product PRO is classified in category CAT
-- by party PTY.
--
product_category {PRO, CAT, PTY}
PK {PRO, CAT}
FK1 {PRO, PTY} REFERENCES
product_party {PRO, PTY}
FK2 {CAT, PTY} REFERENCES
category_party {CAT, PTY}
Note:
All attributes (columns) NOT NULL
PK = Primary Key
AK = Alternate Key (Unique)
SK = Proper Superkey (Unique)
FK = Foreign Key
OTHER TIPS
Using your Model #1, I believe the database itself can be made to enforce the stated constraints (i.e., a category and a product may only be related when they are both related to the same party) using the following constraints.
- The primary key for category_party consists of category_id and party_id. This creates an optional many-to-many relationship between categories and parties. This implies the following.
- A category is classified by zero-one-to-many parties.
- A party classifies zero-one-to-many categories.
- The primary key for product_party consists of product_id and party_id. This creates an optional many-to-many relationship between categories and parties. This implies the following.
- A product is classified by zero-one-to-many parties. This will be overridden below to meet your stated requirements.
- A party is classified by zero-one-to-many products.
There is unique constraint on product_id in the product_party. This makes the relationship between party and product a one-to-many instead of many-to-many. This implies the following.
- A product is classified by one-to-one party, overriding the previously-stated cardinality of this relationship.
The primary key of product_category_assignment consists of category_id, product_id and party_id.
- The foreign key from product_category_assignment to category_party consists of category_id and party_id
- The foreign key from product_category_assignment to product_party consists of product_id and party_id
- The constraint that a category and a product can only be related when they are related to a common party is enforced because product_category_assignment only contains one party_id which is used in both foreign key relationships.
I believe that the only requirement not covered here is that a product is required to have a related party (i.e., party is not optional).
Using Model #2 would require much more code (i.e., an ugly stored procedure?) to enforce the constraint because the structure of the database will allow the following.
- The category can be have one party_id (e.g., 1).
- The product can have a different party_id (e.g., 2).
- The product_category can have yet a third party_id (e.g., 3).
I do want to point out that there appear to be some unstated requirements here that could have some minor bearing on my answer above. For example...
- What is the correct cardinality of the relationship between category and party? The two models you presented are different in this respect.
- Model #1 allows a category to be related to zero, one or many parties.
- Model #2 only allows for zero or one party for each category.
- Is a category required to have (at least) one party or is that relationship optional? This is not clear from your description or either of your models.
I have always said that there are three things that are important in a data model: details, DeTaiLs and DETAILS. Sometimes nailing down the details of every relationship around the complexity can help you discover the best way to design the data model. Sometimes, complex relationships like this may point to missing entities (tables). Sometimes, complex requirements defy enforcement using only database structural constraints and require a bit of code (e.g., a stored procedure?) to finish the job.
Hope that helps.