Designing group with types and items with types where item type relate to group type

Viewed 283

We have performers in our translation company DB and they work remotely. At this time performers are translators and DTP professionals they can have many services groups and each service group can have many service items. When they apply to our company they need to fill a form and choose services they have. Each service group have a type, for example translation or DTP. Translation services group needs source and target languages while DTP service group doesn't. The problem is to show on select list correct service items for service groups.

enter image description here

At this moment I have following design:

enter image description here

I have performer_service_group_type and performer_service_item_type which relates to performer_service_group_type for showing proper service item type for chosen service group type in UI. But I don't know the way to validate by constraints it for db integrity because wrong relation of service item types to service group types can be saved without problem. The second problem is nullable source_language_id and target_language_id for some service groups (like DTP)

The second way I think it could be presented is to have 2 separate tables for group services - performer_language_service_groups' andperformer_misc_service_groupswith 2 tables for each for service items and service items types. Here pros that misc service group wouldn't hassource_language_idandtarget_language_id` but the cons is necessity to create 3 tables for each new service group if we will have some (delivery services and couriers on the way).

Any advice or suggestion would be helpful, appreciation in advance.

4 Answers

The problem here is that this seems to be a piece of a larger project and the wording is fairly confusing. I have tried to untangle this a bit, but I have way more questions than answers. However, your main concern seem to be constraints, so this example should help. The main problem is reliance on single-column keys -- not necessarily bad in itself, but hard to get constraints right.

Keep in mind that this is a logical design, that will not directly solve your problem as stated, but if you "convert" it to PostgreSQL, it will work. This way you can experiment with constraints and adjust your project.

Note:
All attributes (columns) NOT NULL

PK = Primary Key
AK = Alternate Key   (Unique)
SK = Proper Superkey (Unique)
FK = Foreign Key

Custom type for service type ID (discriminator); use this type for every SVC_TYP_ID column.

TYPE svc_typ ENUM (T, D)

Define basics: services, service types, languages, people.

-- Service type identified by SVC_TYP_ID,
-- named SVC_TYP_NAME exists.
--
service_typ {SVC_TYP_ID::svc_typ, SVC_TYP_NAME}
         PK {SVC_TYP_ID}
         AK {SVC_TYP_ID}

(T, translation)
(D, dtp)


-- Service named SVC_NAME of type SVC_TYP_ID
-- is identified by SVC_ID.
--
 service_ {SVC_ID, SVC_TYP_ID, SVC_NAME}
      PK {SVC_ID}
      AK {SVC_NAME}
      SK {SVC_ID, SVC_TYP_ID}
      FK {SVC_TYP_ID} REFERENCES service_typ

(WTR, T, written translation)
(EDT, T, editing)
(PRF, T, proof reading)
(IND, D, in-design markup)
(PHT, D, photoshop markup)
(ACD, D, autocad markup)


-- Language name LANG_NAME,
-- identified by LANG_ID exists.
--
lang {LANG_ID, LANG_NAME}
  PK {LANG_ID}
  AK {LANG_NAME}

(EN, English)
(FR, French)
(RU, Russian)


-- Perfomer (person) identified by PERF_ID
-- named FNAME, LNAME exists.
--
perfomer {PERF_ID, FNAME, LNAME}
      PK {PERF_ID}

(1, Lev,  Tolstoy)
(2, Jim,  Blah)
(3, Joe,  Doe)
(4, Jane, Doe)

People provide services, each person may provide more than one service type. In this example general term for a person providing a service is "performer". Translator and dtp-professional are subtypes of the performer supertype, descriminator is service type. It is possible for the same performer to provide more than one service type.

-- Perfomer PERF_ID registered for service type SVC_TYP_ID.
--
perf_svc_typ {PERF_ID, SVC_TYP_ID, cols_common_to_all}
          PK {PERF_ID, SVC_TYP_ID}
         FK1 {PERF_ID}    REFERENCES perfomer
         FK2 {SVC_TYP_ID} REFERENCES service_typ

(1, T, ... )
(2, T, ... )
(3, D, ... )
(4, T, ... )
(4, D, ... ) -- PERF_ID = 4 does translations and dtp


-- Performer PERF_ID is registered as a translator.
--
-- note: (SVC_TYP_ID = T)
--
translator {PERF_ID, SVC_TYP_ID, cols_specific_to_translators}
        PK {PERF_ID}
     CHECK (SVC_TYP_ID = T::svc_typ)

        FK {PERF_ID, SVC_TYP_ID} REFERENCES perf_svc_typ

(1, T, ...)
(2, T, ...)
(4, T, ...)


-- Performer PERF_ID is registered as a DTP professional.
--
-- note: (SVC_TYP_ID = D)
--
dtp_prof {PERF_ID, SVC_TYP_ID, cols_specific_to_dtp_prof}
      PK {PERF_ID}
   CHECK (SVC_TYP_ID = D::svc_typ)

          FK {PERF_ID, SVC_TYP_ID} REFERENCES
perf_svc_typ {PERF_ID, SVC_TYP_ID}

(3, D, ...)
(4, D, ...)

A translator may be able to provide services in more than one language.

-- Performer PERF_ID, who registered as a translator,
-- speaks language LANG_ID.
--
perf_lang {PERF_ID, LANG_ID}
       PK {PERF_ID, LANG_ID}

      FK1 {PERF_ID} REFERENCES translator
      FK2 {LANG_ID} REFERENCES lang

(1, EN)
(1, FR)
(1, RU)
(2, EN)
(2, FR)
(4, FR)
(4, RU)

Each person (performer) may offer multiple groups of services. Each service in a group must be of the same service type. Performer must be registered as a provider of this service type.

-- Performer PERF_ID offers a group of services,
-- identified by (PERF_ID, PERF_GROUP_NO);
-- each service in the group is of type SVC_TYP_ID.
--
svc_group {PERF_ID, PERF_GROUP_NO, SVC_TYP_ID}
       PK {PERF_ID, PERF_GROUP_NO}
       SK {PERF_ID, PERF_GROUP_NO, SVC_TYP_ID}
       FK {PERF_ID, SVC_TYP_ID} REFERENCES perf_svc_typ

(1, 1, T)
(1, 2, T)
(2, 1, T)
(3, 1, D)
(4, 1, T)
(4, 2, D)

Each performer's service group, lists services from that group's service-type, offered by that performer.

-- Performer PERF_ID offers translation (SVC_TYP_ID = T)
-- service SVC_ID from FROM_LANG to TO_LANG,
-- in that performer's service group
-- identified by (PERF_ID, PERF_GROUP_NO)
--
trans_svc {
         PERF_ID
       , PERF_GROUP_NO
       , SVC_ID
       , SVC_TYP_ID
       , FROM_LANG
       , TO_LANG
       }

PK {PERF_ID, PERF_GROUP_NO, SVC_ID, FROM_LANG, TO_LANG}

CHECK (SVC_TYP_ID = T::svc_typ)

FK1 {PERF_ID, PERF_GROUP_NO, SVC_TYP_ID} REFERENCES svc_group
FK2 {SVC_ID, SVC_TYP_ID} REFERENCES service_
FK3 {PERF_ID} REFERENCES  translator

      FK4 {PERF_ID, FROM_LANG} REFERENCES
perf_lang {PERF_ID, LANG_ID}

      FK5 {PERF_ID, TO_LANG} REFERENCES
perf_lang {PERF_ID, LANG_ID}


(1, 1, WTR, T, EN, RU) -- for translation from <> to
(1, 1, WTR, T, FR, RU)
(1, 2, PRF, T, RU, RU) -- for edit and proof from = to
(1, 2, EDT, T, RU, RU)
(1, 2, PRF, T, EN, EN)
(2, 1, WTR, T, EN, FR)
(2, 1, WTR, T, FR, EN)
(2, 1, EDT, T, EN, EN)
(2, 1, PRF, T, EN, EN)
(2, 1, PRF, T, FR, FR)
(4, 1, WTR, T, FR, RU)
(4, 1, PRF, T, FR, FR)
-- Performer PERF_ID offers DTP (SVC_TYP_ID = D) service_ SVC_ID
-- in group identified by (PERF_ID, PERF_GROUP_NO).
--
dtp_svc {PERF_ID, PERF_GROUP_NO, SVC_ID, SVC_TYP_ID}
     PK {PERF_ID, PERF_GROUP_NO, SVC_ID}

  CHECK (SVC_TYP_ID = D::svc_typ)

    FK1 {PERF_ID, PERF_GROUP_NO, SVC_TYP_ID} REFERENCES svc_group 

    FK2 {SVC_ID, SVC_TYP_ID} REFERENCES service_ 

    FK3 {PERF_ID} REFERENCES  dtp_prof

(3, 1, PHT, D)
(3, 1, ACD, D)
(4, 2, IND, D)
(4, 2, ACD, D)

Nullability can be addressed partly by creating 2 tables for SOURCE and TARGET languages (e.g. source_languages, target_languages. There you'll add a default value with say ID = 1, VALUE = "DEFAULT" that will correspond to your NULL field at performer_service_groups, that ought not be nullable anymore (i.e. remove null flag on performer_service_groups).

This way you will retain innate SQL foreign key constraint and on DML it will trigger if you modify source_language_id/target_language_id (respectively) at performer_service_groups (i.e. if you want NULL you will be forced to add ID = 1 to satisfy your DML, if not, you will add an appropriate one and during any language joins to performer_service_groups you will always have a value returned, so no need to check for null at backend side if you do; for UI just hide language <select> or dropdown if ID = 1 or VALUE = "DEFAULT").

About key validation I'm still thinking, I will probably update.

Had a thought about key validation. What if you create a table for your 2 states, it will basically reflect your UI. This will require performer_service_groups to have another ID field, state_id that will point to your state table. This will force you to include that state_id field in your queries and hence filter your resultset by states. Basically a logical division of data without actually separating data physically (by just adding another dimension; because every ID is a new dimension one can pivot or group by results in xyz table).

If you want to go full strict you can in theory validate DML of your nullable fields with BEFORE TRIGGERS or check constraint validations but these will add overhead and inevitable brittleness and complexity. Same in theory can be applied to key validation but I don't know to what extend pgSQL triggers are flexible to code around it (coming from Oracle/PLSQL and MYSQL here).

Hope it makes sense.

If you have a hundred of service groups, then I believe you also have many fields that only applies some groups. I think you have to use a table to store the "allowed fields" and whether they are required.

<table name="Service Group" comment="for example, DTL">
  <column name="ID" pk="true" />
  <column name="Name" />
</table>

<table name="Service Group Type" comment="for example, Photoshop Markup">
  <column name="ID" pk="true" />
  <column name="Service Group" fk="[Service Group].[ID]" />
  <column name="Name" />
</table>

<table name="List Of Values" comment="for example, Language">
  <column name="ID" pk="true" />
  <column name="Name" />
</table>

<table name="Choice Of List Of Values" comment="for example, Japanese">
  <column name="ID" pk="true" />
  <column name="List Of Values" fk="[List Of Values].[ID]" />
  <column name="Description" />
</table>

<table name="Field" comment="for example, From Language">
  <column name="ID" pk="true" />
  <column name="Name" />
  <column name="List Of Values" fk="[List Of Values].[ID]" />
</table>

<table name="Service Group Field">
  <column name="Service Group" pk="true" fk="[Service Group].[ID]" />
  <column name="Field" pk="true" fk="[Field].[ID]" />
  <column name="Nullable" comment="N means optional, Y means mandatory" />
</table>

<table name="Service Record" comment="only retain core fields here, e.g. Performer, Currency, etc">
  <column name="ID" pk="true" />
  <column name="Service Group Type" fk="[Service Group Type].[ID]" />
  <column name="Performer" fk="[Performer].[ID]" />
  <column name="Currency" fk="[Currency].[ID]" />
  ...
</table>

<table name="Service Record Detail" comment="to organize all conditional fields">
  <column name="ID" pk="true" />
  <column name="Service Record" fk="[Service Record].[ID]" />
  <column name="Field" fk="[Field].[ID]" />
  <column name="Choice" fk="[Choice Of List Of Values].[ID]" />
  <column name="Free Text" />
</table>

Issue 1 is resolved by only storing the Service Type in each Service Record. You should still do the validation on screen logic and pre-saving logic. To easy group the Service Records by Service Group, you can create views which joins data back to Service Group from the Service Group Type.

Issue 2 is resolved by the list of Optional / Mandatory fields. You must still add the validations needed:
1 - if a value is provided but pattern cannot be found from [Service Group Field], this is excess input of a field
2 - if a value is not provided but from [Service Group Field] it is mandatory, this is missing input of a field

At now I will stick with following solution.

enter image description here

If I will have new group with special fields for particular service group I would make new table with 1-1 relation and add boolean field to service_group as a flag for necessity to fetch something. Maybe later I will go for more dynamic thing. If someone still have an idea how to improve this schema - I would like to know :)

Related