Whenever a class hierarchy is more complex, we cannot simply eliminate it, but have to implement it (1) in the app's model code, (2) in the underlying database and (3) in its user interface.
The starting point for our case study is the design model shown in Figure 12.8 above. In the following sections, we derive a JS class model and a JS entity table model from the design model. The entity table model is used as a design for the object-to-storage mapping that we need for storing the objects of our app with the browsers' Local Storage technology.
We design the model classes of our example app with the help of a JS class model that we derive from the design model by essentially leaving the generalization arrows as they are and just adding get/set methods and static check functions to each class. However, in the case of our example app, it is natural to apply the Class Hierarchy Merge design pattern (discussed in Section 6) to the single-subclass-segmentation of Employee
for simplifying the class model by eliminating the Manager
subclass. This leads to the model shown in Figure 13.2 below. Notice that a Person
may be an Employee
or an Author
or both.
Since we use the browsers' Local Storage as the persistent storage technology for our example app, we have to deal with simple key-value storage. For each design model class with a singular (capitalized) name Entity
, we use its pluralized lowercase name entities
as the corresponding table name and as a key such that its associated string value is obtained by serializing the object collection Entity.instances
with the help of the JSON.stringify
method.
We design a set of suitable JS entity tables in the form of a JS entity table model that we derive from the information design model. We have to make certain choices how to organize our data store and how to derive a corresponding entity table model.
The first choice to make concerns using either the Single Table Inheritance (STI), the Table per Class Inheritance (TCI) or the Joined Tables Inheritance (JTI) approach, which are introduced in Section 7.4. In the STI approach, a segmentation (or an entire class hierarchy) is represented with a single table, containing columns for all attributes of all classes involved, as shown in the following example.
Since the given segmentation is non-disjoint, a multi-valued enumeration attribute categories
is used for representing the information to which subclasses an instance belongs.
Using the STI approach is feasible for the given example, since the role hierarchy does not have many levels and the segment subclasses do not add many attributes. But, in a more realistic example, we would have a lot more attributes in the segment subclasses of the given role hierarchy. The STI approach is not really an option for representing a multi-level role hierarchy. However, we may choose it for representing the single-segment class hierarchy Manager
-is-subclass-of-Employee
.
For simplicity, and because the browsers' Local Storage does not support foreign keys as required by JTI, we choose the TCI approach, where we obtain a separate table for each class of the Person
segmentation, but without foreign keys. Our choices result in the model shown in Figure 13.4 below, which has been derived from the design model shown in Figure Figure 12.8 by
Merging the Manager
subclass into its superclass Employee
, according to the Class Hierarchy Merge design pattern described in Section 6.
Replacing the standard ID property modifier {id} of the personId
attribute of Person
, Author
and Employee
with {pkey} for indicating that the attribute is a primary key.
Replacing the singular (capitalized) class names (Person, Author and Employee) with pluralized lowercase table names (people, authors and employees).
Adding the «JS entity table» stereotype to all class rectangles (people
, authors
and employees
).
Replacing the platform-independent datatype names with JS datatype names.
Dropping all generalization/inheritance arrows and adding all attributes of supertables (personId
and name
) to their subtables (authors
and employees
).
In the case of using the JTI approach, in addition to the steps 1-5 above, we would
Copy the primary key column (personId
) of the root table (people
) to all subtables (authors
and employees
).
Replace the generalization arrows with «fkey»-stereotyped dependency arrows (representing foreign key dependencies) that are annotated at their source end with the name of the subtable's primary key (here: personId
).
Compared to the model of our first case study, shown in Figure 13.1 above, we have to deal with a number of new issues in the model code:
Defining the subclass relationships between Employee
and Person
, as well as between Author
and Person
, using the JS keyword extends
discussed in Section 1.
When loading the instances of the root class (Person.instances
) from persistent storage (in Person.retrieveAll
), we load (1) the records of the table representing the root class (people
) for creating its direct instances and (2) also the records of all other tables representing its subclasses (authors
and employees
) for creating their direct instances, while also adding their object references to the root class population (to Person.instances
). In this way, the root class population does not only contain direct instances, but all instances.
When saving the instances of Employee
and Author
as records of the JS entity tables employees
and authors
to persistent storage in Employee.saveAll
and Author.saveAll
(invoked in pl.v.employees.manage.exit
and pl.v.authors.manage.exit
), we also save the direct instances of Person
as records of the people
table.
The JS class model shown in Figure 13.2 above can be directly coded for getting the code of the model classes Person
, Employee
and Author
as well as for the enumeration type EmployeeCategoryEL
.
We define the subtype relationships between Employee
and Person
, as well as between Author
and Person
, with extends
. For instance, in m/Employee.js
we define:
EmployeeCategoryEL = new Enumeration(["Manager"]); class Employee extends Person { constructor ({personId, name, empNo, category, department}) { super({personId, name}); this.empNo = empNo; if (category) this.category = category; if (department) this.department = department; } ... }
When retrieving the instances of a class hierarchy's root class (in our example, Person
) from a persistent data store organized according to the TCI approach, we have to retrieve not only its direct instances from the table representing the root class (people
), but also all indirect instances from all tables representing its subclasses (employees
and authors
), as shown in the following code:
Person.retrieveAll = function () { var people={}, employees={}, authors={}; if (!localStorage["authors"]) localStorage["authors"] = "{}"; if (!localStorage["employees"]) localStorage["employees"] = "{}"; if (!localStorage["people"]) localStorage["people"] = "{}"; try { people = JSON.parse( localStorage["people"]); employees = JSON.parse( localStorage["employees"]); authors = JSON.parse( localStorage["authors"]); } catch (e) { console.log("Error when reading from Local Storage\n" + e); } for (let key of Object.keys( authors)) { try { // convert record to (typed) object Author.instances[key] = new Author( authors[key]); // create superclass extension Person.instances[key] = Author.instances[key]; } catch (e) { console.log(`${e.constructor.name} while deserializing` + `author ${key}: ${e.message}`); } } ... }
Each record of the authors
table is retrieved and converted to an Author
object, a reference to which is copied to Person.instances
. Also the records of the employees
table are processed in this way, while the records of the people
table are simply retrieved and converted to Person
objects:
Person.retrieveAll = function () { ... for (let key of Object.keys( employees)) { ... } for (let key of Object.keys( people)) { try { // convert record to (typed) object Person.instances[key] = new Person( people[key]); } catch (e) { console.log(`${e.constructor.name} while deserializing` + `author ${key}: ${e.message}`); } } }
Since the app's data is kept in main memory as long as the app is running (which is as long as the app's webpage is kept open in the browser), the data has to be saved to persistent storage when the app is exited (e.g., by closing its browser tab), When saving the instances of Employee
and Author
(as records of the JS entity tables employees
and authors
) to persistent storage in pl.v.employees.manage.exit
and pl.v.authors.manage.exit
, we also save the direct instances of Person
(as records of the people
table). This is necessary because changes to Employee
or Author
instances may imply changes of Person.instances
.
For instance, for Employee
data management, we define in v/employees.js
:
pl.v.employees.manage = { ... exit: function () { Employee.saveAll(); Person.saveAll(); }, ... }