1

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 a party.

    • Each category is defined by exactly one party.
    • Each party may define more than one category.
  • Product is classified by a party.

    • Each product is classified by exactly one party.
    • Each party may classify more than one product.
  • Product is classified in a category by a party.

    • Each product may be classified in more than one category.
    • More than one product may be classified in the same category.
    • A product is classified by a party in a category, then that category is defined by that party.

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.

Three-way association design proposal

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.

enter image description here


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?

2 Answers 2

1

The party_id for a Category must match the party_id for a Product 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 by party.
  • Each product is classified by exactly one party.
  • Each party may classify more than one product.

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 a party.

    • Each category is defined by exactly one party.
    • Each party may define more than one category.
  • Product is classified by a party.

    • Each product is classified by exactly one party.
    • Each party may classify more than one product.
  • Product is classified in a category by a party.

    • Each product may be classified in more than one category.
    • More than one product may be classified in the same category.
    • If a product is classified by a party in a category, then that category is defined by that party.

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
8
  • Thank you @damir-sudarevic. As mentioned in the comment to @rickc, I have missed out some details in my question which will be added. Both answers have been very helpful and made me realize there are details to be considered which are not yet mentioned in my original question. One example is that the party_id is scoped in the Categories table, meaning that the nested sets (left and right) are calculated using party_id.
    – Jonah
    Apr 25, 2020 at 13:56
  • 1
    @Jonah, added options 3 and 4 deal with category and party. Apr 25, 2020 at 18:07
  • Original question updated to reflect a more correct definition of the business rules as well as a SQL simplified code to test out the solution proposal above. It seems to work fine, but I just need to figure out how to select Products both with and without a Category assigned. Possibly through a LEFT JOIN of product_categoryand products.
    – Jonah
    Apr 25, 2020 at 23:42
  • do you mind providing an example and/or link to the definition of "proper superkey"? I want to ensure that I have understood your Option 3 correctly in terms of e.g. SK {PRO, PTY}. Thank you
    – Jonah
    Apr 27, 2020 at 12:18
  • @Jonah (1/3) super-set of a KEY, having at least one element more. Logically not needed, but SQL needs it to allow for FK target. KEY is a short for a candidate key a minimal, irreducible superkey. Apr 27, 2020 at 12:38
1

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.

1
  • Thank you! The model is still in design phase, so I realize that I have missed out a few details in my question and the mistakes in Model #2. Your suggestions to Model #1 is definitely the way to go. To answer your questions on the unstated requirements, a category should only relate to one party. This is valid for Model #2 too of course and should be applied to party as well. I will amend my initial question and test above changes shortly.
    – Jonah
    Apr 25, 2020 at 13:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.