The JS class model can be directly coded for getting the JS classes of our app.
Code each class of the JS class model as an ES2015 class with implicit getters and setters:
Code the property checks in the form of class-level ('static') methods. Take care that all constraints of a property as specified in the JS class model are properly coded in the property checks.
For each single-valued property, code the specified getter and setter such that in each setter, the corresponding property check is invoked and the property is only set/unset, if the check does not detect any constraint violation.
Write the code of the serialization function toString()
and the storage conversion function toRecord()
.
Take care of deletion dependencies in the destroy
method.
These steps are discussed in more detail in the following sections.
Each class C
of the JS class model is coded as an ES2015 class with the same name C
and a constructor having a single record parameter, which specifies a field for each (non-derived) property of the class. The range of these properties should be indicated in a comment. In the case of a reference property the range is another model class.
In the constructor body, we assign the fields of the record parameter to corresponding properties. These property assignments invoke the corresponding setter methods.
For instance, the Publisher
class from the JS class model is coded in the following way:
class Publisher {
constructor ({name, address}) {
this.name = name;
this.address = address;
}
...
};
Since the setters may throw constraint violation exceptions, the constructor function, and any setter, should be called in a try-catch block where the catch clause takes care of logging suitable error messages.
For each model class C
, we define a class-level property C.instances
representing the collection of all C
instances managed by the application in the form of an entity table (a map of records). This property is initially set to {}
. For instance, in the case of the model class Publisher
, we define:
Publisher.instances = {};
The Book
class from the JS class model is coded in a similar way:
class Book {
constructor ({isbn, title, year, authors, authorIdRefs,
publisher, publisher_id}) {
this.isbn = isbn;
this.title = title;
this.year = year;
this.authors = authors || authorIdRefs;
if (publisher || publisher_id) {
this.publisher = publisher || publisher_id;
}
}
...
}
Notice that the Book
constructor can be invoked either with object references authors
and publisher
or with ID references authorIdRefs
and publisher_id
. This approach makes using the Book
constructor more flexible and more robust.
Take care that all constraints of a property as specified in the JS class model are properly coded in its check function, as explained in Chapter 8 of Volume 1. Recall that constraint violation (or validation error) classes are defined in the file lib/errorTypes.js
.
For instance, for the Publisher.checkName
function we obtain the following code:
class Publisher { ... static checkName(n) { if (n === undefined) { return new NoConstraintViolation(); // not mandatory } else { if (typeof n !== "string" || n.trim() === "") { return new RangeConstraintViolation( "The name must be a non-empty string!"); } else { return new NoConstraintViolation(); } } } static checkNameAsId(n) {...} ... }
Notice that, since the name
attribute is the standard ID attribute of Publisher
, we only check syntactic constraints in checkName
, and check the mandatory value and uniqueness constraints in checkNameAsId
, which invokes checkName
:
static checkNameAsId( n) {
var validationResult = Publisher.checkName(n);
if ((validationResult instanceof NoConstraintViolation)) {
if (n === undefined) {
return new MandatoryValueConstraintViolation(
"A publisher name is required!");
} else if (Publisher.instances[n]) {
validationResult = new UniquenessConstraintViolation(
"There is already a publisher record with this name!");
} else {
validationResult = new NoConstraintViolation();
}
}
}
If we have to deal with ID references (foreign keys) in other classes, we need to provide a further check function, called checkNameAsIdRef
, for checking the referential integrity constraint:
static checkNameAsIdRef(n) {
var validationResult = Publisher.checkName(n);
if ((validationResult instanceof NoConstraintViolation) &&
n !== undefined) {
if (!Publisher.instances[n]) {
validationResult = new ReferentialIntegrityConstraintViolation(
"There is no publisher record with this name!");
}
}
return validationResult;
}
The condition !Publisher.instances[n]
checks if there is no publisher object with the given name n
, and then creates a validationResult
object as an instance of the exception class ReferentialIntegrityConstraintViolation. The Book.checkNameAsIdRef
function is called by the Book.checkPublisher
function:
class Book { ... static checkPublisher( publisher_id) { var validationResult = null; if (publisher_id === undefined || publisher_id === "") { validationResult = new NoConstraintViolation(); // optional } else { // invoke foreign key constraint check validationResult = Publisher.checkNameAsIdRef( publisher_id); } return validationResult; } ... }
In the setters, the corresponding check function is called and the property is only set, if the check does not detect any constraint violation. In the case of a reference property, we allow invoking the setter either with an object reference or with an ID reference. The resulting ambiguity is resolved by testing if the argument provided in the invocation of the setter is an object or not. For instance, the publisher
setter is coded in the following way:
class Book { ... set publisher( p) { if (!p) { // unset publisher delete this._publisher; } else { // p can be an ID reference or an object reference const publisher_id = (typeof p !== "object") ? p : p.name; const validationResult = Book.checkPublisher( publisher_id); if (validationResult instanceof NoConstraintViolation) { // create the new publisher reference this._publisher = Publisher.instances[ publisher_id]; } else { throw validationResult; } } } ... }
For any reference property, we have to choose and implement one of the two possible deletion policies discussed in Section 2 for managing the corresponding object destruction dependency in the destroy
method of the property's range class. In our case, when deleting a publisher record, we have to choose between
deleting all records of books published by the deleted publisher (Existential Dependency);
dropping from all books published by the deleted publisher the reference to the deleted publisher (Existential Independence).
Assuming that books do not existentially depend on their publishers, we choose the second option. This is shown in the following code of the Publisher.destroy
method where for all concerned book
objects the property book.publisher
is cleared:
Publisher.destroy = function (name) {
const keys = Object.keys( Book.instances);
var publisher = Publisher.instances[name];
// delete all references to this publisher in book objects
for (let i=0; i < keys.length; i++) {
let book = Book.instances[keys[i]];
if (book.publisher === publisher) delete book.publisher;
}
// delete the publisher record
delete Publisher.instances[name];
console.log("Publisher " + name + " deleted.");
};
Notice that the deletion of all references to the deleted publisher is performed in a sequential scan through all book objects, which may be inefficient when there are many of them. It would be much more efficient when each publisher object would hold a list of references to all books published by this publisher. Creating and maintaining such a list would make the association between books and their publisher bidirectional.
In the case of a reference property, like Book::publisher
, the serialization function Book::toString()
has to show a human-readable identifier of the referenced object, like this.publisher.name
:
toString() { var bookStr = `Book{ ISBN: ${this.isbn}, title: ${this.title},` + `year: ${this.year}`; if (this.publisher) bookStr += `, publisher: ${this.publisher.name}`; return bookStr + "}"; }
The object-to-storage conversion function Book::toRecord()
converts typed JS objects with object references to corresponding (untyped) record objects with ID references:
toRecord() { var rec = {}; for (let p of Object.keys( this)) { // copy only property slots with underscore prefix if (p.charAt(0) === "_") { switch (p) { case "_publisher": // convert object reference to ID reference if (this._publisher) rec.publisher_id = this._publisher.name; break; default: // remove underscore prefix rec[p.substr(1)] = this[p]; } } }; return rec; }
The inverse conversion, from untyped record objects with ID references to corresponding typed objects with object references, is performed by the Book
constructor, which tolerates both ID references and object references as arguments for setting reference properties.