views:

219

answers:

2

For this question, consider an application using a multi-tennant database with modeling of manufacturers and models. If we're talking about cars then the manufacturers would be Ford, Chevrolet, BMW, etc. and the models would be F-150, Camaro, and M3.

Relationship of model to manufacturer is many to one. Data for each tenant is separated using a customer_id.

Requirements for the data model:

  • Manufacturers and models can be defined at the master level to make them available to all customers
  • Customer select which of the master entities they would like to use
  • Customers may override attributes of a master model or manufacturer
  • Customers may create their own manufacturers
  • Customers may create their own models for their own or a master manufacturer
  • Other entities in the model will be related to these entities so it is desirable that there be one main table for each that a foreign key can be made to. The manufacturers and models tables fill that role in the example.

In this example:

  • Customer 1 uses Ford as-is, overrides Chevrolet, and adds two custom manufacturers
  • Customer 1 uses Chevrolet and BMW as-is and adds one custom manufacturer
  • Models are created as per the comments in the script

Below is an annotated sample implementation that meets all of the requirements.

  • How can this be improved?
  • In what other ways could these relationships be modeled?

Manufacturer Tables

/*
 * Master manufacturers shared between all customers
 */
CREATE TABLE master_manufacturers (
    master_manufacturer_id INTEGER NOT NULL,
    name VARCHAR(100) NOT NULL,
    attribute_1 VARCHAR(50),
    /* ... */
    attribute_n VARCHAR(50),
    PRIMARY KEY (master_manufacturer_id)
);

INSERT INTO
    master_manufacturers (master_manufacturer_id, name)
VALUES
    (1, 'Ford'),
    (2, 'Chevrolet'),
    (3, 'BMW');

/*
 * A Customer's manufacturer.  
 *   If master_manufacturer_id IS NULL, then it is a custom manufacturer and manufacturer_custom contains the data
 *   If master_manufacturer_id IS NOT NULL and manufacturer_custom does not exist, then the master is used without modification
 *   If master_manufacturer_id IS NOT NULL and manufacturer_custom exists, then the master is overridden
 */
CREATE TABLE manufacturers (
    manufacturer_id INTEGER NOT NULL,
    customer_id INTEGER NOT NULL,
    master_manufacturer_id INTEGER,
    PRIMARY KEY (manufacturer_id),
    FOREIGN KEY (master_manufacturer_id) REFERENCES master_manufacturers (master_manufacturer_id),
    UNIQUE (customer_id, master_manufacturer_id)
);

INSERT INTO
    manufacturers (manufacturer_id, customer_id, master_manufacturer_id)
VALUES
    (1, 1, 1),
    (2, 1, 2),
    (3, 1, NULL),
    (4, 1, NULL),
    (5, 2, 2),
    (6, 2, 3),    
    (7, 2, NULL);    

CREATE TABLE manufacturer_custom (
    manufacturer_id INTEGER NOT NULL,
    name VARCHAR(100) NOT NULL,
    attribute_1 VARCHAR(50),
    /* ... */
    attribute_n VARCHAR(50),
    PRIMARY KEY (manufacturer_id),
    FOREIGN KEY (manufacturer_id) REFERENCES manufacturers (manufacturer_id)
);

INSERT INTO
    manufacturer_custom (manufacturer_id, name)
VALUES
    (2, 'Chevy'),
    (3, 'Cust 1 Custom 1'),
    (4, 'Cust 1 Custom 2'),
    (7, 'Cust 2 Custom 1');

Model Tables

/*
 * Master models shared between all customers
 */
CREATE TABLE master_models (
    master_model_id INTEGER NOT NULL,
    master_manufacturer_id INTEGER NOT NULL,
    name VARCHAR(100) NOT NULL,
    attribute_1 VARCHAR(50),
    /* ... */
    attribute_n VARCHAR(50),
    PRIMARY KEY (master_model_id),
    FOREIGN KEY (master_manufacturer_id) REFERENCES master_manufacturers (master_manufacturer_id)
);

INSERT INTO
    master_models (master_model_id, master_manufacturer_id, name)
VALUES
    (1, 1, 'F-150'),
    (2, 1, 'F-250'),
    (3, 1, 'Falcon'),
    (4, 2, 'Camaro'),
    (5, 2, 'Corvette'),
    (6, 3, 'M3'),
    (7, 3, '135i');

/*
 * A Customer''s model.  
 *   If master_model_id IS NULL, then it is a custom model and model_custom contains the data
 *   If master_model_id IS NOT NULL and model_custom does not exist, then the master is used without modification
 *   If master_model_id IS NOT NULL and model_custom exists, then the master is overridden
 */
CREATE TABLE models (
    model_id INTEGER NOT NULL,
    master_model_id INTEGER,
    manufacturer_id INTEGER NOT NULL,
    attribute_1 VARCHAR(50),
    /* ... */
    attribute_n VARCHAR(50),
    PRIMARY KEY (model_id),
    FOREIGN KEY (master_model_id) REFERENCES master_models (master_model_id)
);

INSERT INTO
    models (model_id, master_model_id, manufacturer_id)
VALUES
    (1, 1, 1),    /* F-150 for customer_1's Ford */
    (2, 2, 1),    /* F-250 for customer_1's Ford */
    (3, 4, 2),    /* Camaro for customer_1's Chevy */
    (4, 4, 5),    /* Camaro for customer_2's Chevrolet */
    (5, 5, 5),    /* Corvette for customer_2's Chevrolet */
    (6, 6, 6),    /* M3 for customer_2's BMW */
    (7, NULL, 1), /* F-350 (custom) for customer_1's Ford */
    (8, NULL, 6), /* M7 (custom) for customer_2's BMW */
    (9, NULL, 7); /* Custom Model (custom) for customer_2's Custom Mfg */

CREATE TABLE model_custom (
    model_id INTEGER NOT NULL,
    name VARCHAR(100) NOT NULL,
    attribute_1 VARCHAR(50),
    /* ... */
    attribute_n VARCHAR(50),
    PRIMARY KEY (model_id),
    FOREIGN KEY (model_id) REFERENCES models (model_id)
);

INSERT INTO
    model_custom (model_id, name)
VALUES
    (7, 'F-350'),        /* F-350 for customer_1's Ford */
    (8, 'M7'),           /* M7 for customer_2's BMW */
    (9, 'Custom Model'); /* Custom Model for customer_2's Custom Mfg */

Views to simplify using these tables

/*
 * View for a customer''s manufacturers
 */
CREATE VIEW vw_manufacturers AS
    SELECT
        m.customer_id,
        m.manufacturer_id, 
        COALESCE(cm.name, mm.name) AS name,
        COALESCE(cm.attribute_1, mm.attribute_1) AS attribute_1,
        /* ... */
        COALESCE(cm.attribute_n, mm.attribute_n) AS attribute_n
    FROM
        manufacturers m
    LEFT JOIN
        master_manufacturers mm
    USING
        (master_manufacturer_id)
    LEFT JOIN
        manufacturer_custom cm
    USING
        (manufacturer_id);

/*
 * View for a customer's models
 */
CREATE VIEW vw_models AS
    SELECT
        mfg.customer_id,
        mfg.manufacturer_id,
        mfg.name AS manufacturers_name,
        m.model_id,
        COALESCE(cm.name, mm.name) AS name,
        COALESCE(cm.attribute_1, mm.attribute_1) AS attribute_1,
        /* ... */
        COALESCE(cm.attribute_n, mm.attribute_n) AS attribute_n
    FROM
        vw_manufacturers mfg,
        models m
    LEFT JOIN
        master_models mm
    USING
        (master_model_id)
    LEFT JOIN
        model_custom cm
    USING
        (model_id)
    WHERE
        mfg.manufacturer_id = m.manufacturer_id;

Manufacturers for customer_id 1

SELECT manufacturer_id, name FROM vw_manufacturers WHERE customer_id = 1;

 manufacturer_id |      name       
-----------------+-----------------
           1 | Ford
           2 | Chevy
           3 | Cust 1 Custom 1
           4 | Cust 1 Custom 2

Manufacturers for customer_id 2

SELECT manufacturer_id, name FROM vw_manufacturers WHERE customer_id = 2;

 manufacturer_id |      name       
-----------------+-----------------
           5 | Chevrolet
           6 | BMW
           7 | Cust 2 Custom 1

Models for customer_id 1

SELECT * FROM vw_models WHERE customer_id = 1;

 customer_id | manufacturer_id | manufacturers_name | model_id |  name  
-------------+-----------------+--------------------+----------+--------
       1 |               1 | Ford               |        1 | F-150
       1 |               1 | Ford               |        2 | F-250
       1 |               2 | Chevy              |        3 | Camaro
       1 |               1 | Ford               |        7 | F-350

Models for customer_id 2

SELECT * FROM vw_models WHERE customer_id = 2;

 customer_id | manufacturer_id | manufacturers_name | model_id |     name     
-------------+-----------------+--------------------+----------+--------------
           2 |               5 | Chevrolet          |        4 | Camaro
           2 |               5 | Chevrolet          |        5 | Corvette
           2 |               6 | BMW                |        6 | M3
           2 |               6 | BMW                |        8 | M7
           2 |               7 | Cust 2 Custom 1    |        9 | Custom Model
+1  A: 

You need the following tables:

  • MANUFACTURER-CODE
  • MANUFACTURERS-TYPE-CODE
  • MANUFACTURER-DETAILS
  • MODEL-CODE
  • MODELS-TYPE-CODE
  • MODEL-DETAILS

If you have tables with identical data - you need to consolidate them, and use a TYPE_CODE table to differentiate them.

Re: MANUFACTURERS & CUSTOMERS Currently, you'd need the PK to be MANUFACTURER-ID and CUSTOMER-ID. Better to split MANUFACTURERS into MANUFACTURERS-CODE and MANUFACTURER-DETAILS. MANUFACTURER-CODE would contain "BMW", "FORD", etc. plus custom. MANUFACTURER-DETAILS would allow you to keep details data on a per customer basis while allowing you to reuse codes for things like "BMW"/etc. The same goes for models.

The next step would be to define TYPE-CODE tables for things like engine, wheels, etc. I'd relate these to the MODEL-DETAILS using a XREF table called MODEL-ATTRIBUTES. The MODEL-ATTRIBUTES table would contain:

  • MODEL-DETAILS-ID (pk)
  • MODEL-ATTRIBUTE-TYPE-CODE (pk)
  • ATTRIBUTE-CODE (pk)

This would allow for optional model attributes to be associated with the applicable MODEL-DETAILS record, without constantly adding attributes to the MODEL-DETAILS table.

MANUFACTURER-CODE

  • MANUFACTURER-CODE VARCHAR(4) (pk)
  • DESCRIPTION
  • EFFECTIVE-DATE not null
  • EXPIRY-DATE not null

MANUFACTURER-CODE | DESCRIPTION | EFFECTIVE-DATE | EXPIRY-DATE
FORD | FORD | 01-01-1900 | 12-31-9999
BMW | BMW | 01-01-1900 | 12-31-9999
CHEV | Chevrolet | 01-01-1900 | 12-31-9999

MANUFACTURER-TYPE-CODE

  • MANUFACTURER-TYPE-CODE (pk)
  • DESCRIPTION not null

MANUFACTURER-TYPE-CODE | DESCRIPTION
MASTER | Master
CUSTOM | Custom

MANUFACTURER-DETAILS

  • MANUFACTURER-DETAILS-ID (pk)
  • MANUFACTURER-CODE (fk) not null
  • MANUFACTURER-TYPE-CODE (fk) not null
  • CUSTOMER-ID (fk) not null

MANUFACTURER-DETAILS-ID | MANUFACTURER-CODE | MANUFACTURER-TYPE-CODE | CUSTOMER-ID
1 | BMW | MASTER | 1
2 | BMW | CUSTOM | 1

MODEL

  • MODEL-ID (pk)
  • MANUFACTURER-DETAILS-ID (fk) not null
  • DESCRIPTION not null

MODEL-ID | MANUFACTURER-DETAILS-ID | DESCRIPTION
1 | 1 | M3
1 | 2 | M3 lowered

OMG Ponies
Can you clarify how this would work? I don't quite understand. A full example of the structure for manufacturers would be great. Trying to work it out I get the following, but I don't think it is what you're talking about.MANUFACTURER-CODE (mfg_id (PK), code) MANUFACTURER-DETAILS (mfg_id, (FK, PK), customer_id (PK), attr1, attr) MANUFACTURERS-TYPE-CODE (mfg_id (FK, PK), is_master)
cope360
How does customer_id work as a not null column of MANUFACTURER-DETAILS? In the example you have it set to 1 for a MASTER type, but MASTER types should be available to all customers.
cope360
Not null means the column can't have nulls, meaning in this case that you couldn't have a record in MANUFACTURER-DETAILS without a CUSTOMER-ID associated. The MASTER value is a foreign key (hence fk abbreviation) from the MANUFACTURER-TYPE-CODE table - it's a natural key, doesn't use a number to identify it. That said, there's no limit in the current data model to limit which customers see what MANUFACTURER-TYPE-CODE records. If you wanted this, you'd need to add a table between MANUFACTURER-TYPE-CODE and CUSTOMER using a combo of pk's from either table as the pk.
OMG Ponies
A: 

It depends on the use of this system. You would design this differently for OLAP vs OLTP.

Theoretically this could all be in 1 table....

DOOR3 James