The JS class model can be directly coded for getting the code of the model layer of our JavaScript frontend app.
Compared to the unidirectional association app, we have to deal with a number of new technical issues:
We define the derived inverse reference properties, like Publisher
::/publishedBooks
, without a check operation and without a set operation.
We also have to take care of maintaining the derived inverse reference properties by maintaining the derived (sets of) inverse references that form the collection value of a derived inverse reference property. This requires in particular that
whenever the value of a single-valued master reference property is initialized or updated with the help of a setter (such as assigning a reference to a Publisher
instance p
to b.publisher
for a Book
instance b
), an inverse reference has to be assigned or added to the corresponding value of the derived inverse reference property (such as adding b
to p.publishedBooks
); when the value of the master reference property is updated and the derived inverse reference property is multi-valued, then the obsolete inverse reference to the previous value of the single-valued master reference property has to be deleted;
whenever the value of an optional single-valued master reference property is unset (e.g. by assigning null
to b.publisher
for a Book
instance b
), the inverse reference has to be removed from the corresponding value of the derived inverse reference property (such as removing b
from p.publishedBooks
), if the derived inverse reference property is multi-valued, otherwise the corresponding value of the derived inverse reference property has to be unset or updated;
whenever a reference is added to the value of a multi-valued master reference property with the help of an add method (such as adding an Author
reference a
to b.authors
for a Book
instance b
), an inverse reference has to be assigned or added to the corresponding value of the derived inverse reference property (such as adding b
to a.authoredBooks
);
whenever a reference is removed from the value of a multi-valued master reference property with the help of a remove
method (such as removing a reference to an Author
instance a
from b.authors
for a Book
instance b
), the inverse reference has to be removed from the corresponding value of the derived inverse reference property (such as removing b
from a.authoredBooks
), if the derived inverse reference property is multi-valued, otherwise the corresponding value of the derived inverse reference property has to be unset or updated;
whenever an object with a single reference or with multiple references as the value of a master reference property is destroyed (e.g., when a Book
instance b
with a single reference b.publisher
to a Publisher
instance p
is destroyed), the derived inverse references have to be removed first (e.g., by removing b
from p.publishedBooks
).
Notice that when a new object is created with a single reference or with multiple references as the value of a master reference property (e.g., a new Book
instance b
with a single reference b.publisher
), its setter or add method will be invoked and will take care of creating the derived inverse references.
Code each class of the JS class model as an ES2015 class with implicit getters and setters (or, alternatively, as an ES5 constructor function with explicit 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:
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.
If the concerned property is the inverse of a derived reference property (representing a bidirectional association), make sure that the setter also assigns/unsets (or adds/removes) the corresponding inverse reference to/from (the collection value of) the inverse property.
For each multi-valued property, code its add and remove operations, as well as the specified get/set operations:
Code the add/remove operations as (instance-level) methods that invoke the corresponding property checks.
Code the setter such that it invokes the add operation for each item of the collection to be assigned.
If the concerned property is the inverse of a derived reference property (representing a bidirectional association), make sure that the add/remove methods also assign/unset (or add/remove) the corresponding inverse reference to/from (the collection value of) the inverse property.
Write the code of the serialization function toString()
and the object-to-storage conversion function toRecord()
. In the object-to-storage conversion of a publisher or author object with toRecord()
, the derived properties publishedBooks
and authoredBooks
are not included since their information is redundant (it is derived from the publisher
and authors
properties of books).
Take care of deletion dependencies in the destroy
method. Make sure that when an object with a single reference or with multiple references as the value of a master reference property is destroyed, the derived inverse references are unset (or removed) first.
These steps are discussed in more detail in the following sections.
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;
// derived inverse reference property (inverse of Book::publisher)
this._publishedBooks = {}; // initialize as an empty map
}
get name() {...}
static checkName( n) {...}
static checkNameAsId( n) {...}
static checkNameAsIdRef( n) {...}
set name( n) {...}
get address() {...}
static checkAddress( a) {...}
set address( a) {...}
toString() {...}
toRecord() {...}
}
Notice that we have added the (derived) multi-valued reference property publishedBooks
, but we do not assign it in the constructor function because it is a read-only property that is assigned implicitly when its inverse master reference property Book::publisher
is assigned.
Any setter for a reference property that is coupled to a derived inverse reference property (implementing a bidirectional association), now also needs to assign/add/remove inverse references to/from the corresponding collection value of the inverse reference property. An example of such a setter is publisher
in the Book
class:
set publisher( p) { if (!p) { // the publisher reference is to be deleted // delete the inverse reference in Publisher::publishedBooks delete this._publisher.publishedBooks[ this._isbn]; // unset the publisher property delete this._publisher; } else { // p can be an ID reference or an object reference const publisher_id = (typeof p !== "object") ? p : p.name; const constraintViolation = Book.checkPublisher( publisher_id); if (constraintViolation instanceof NoConstraintViolation) { if (this._publisher) { // delete the inverse reference in Publisher::publishedBooks delete this._publisher.publishedBooks[ this._isbn]; } // create the new publisher reference this._publisher = Publisher.instances[ publisher_id]; // add the new inverse reference to Publisher::publishedBooks this._publisher.publishedBooks[ this._isbn] = this; } else { throw constraintViolation; } } }
For any multi-valued reference property that is coupled to a derived inverse reference property, both the add and the remove method also have to assign/add/remove corresponding references to/from (the value set of) the inverse property.
For instance, for the multi-valued reference property Book::authors
that is coupled to the derived inverse reference property Author:authoredBooks
for implementing the bidirectional authorship association between Book
and Author
, the Book::addAuthor
method is coded in the following way:
addAuthor( a) { // a can be an ID reference or an object reference const author_id = (typeof a !== "object") ? parseInt(a) : a.authorId; const validationResult = Book.checkAuthor( author_id); if (author_id && validationResult instanceof NoConstraintViolation) { // add the new author reference this._authors[author_id] = Author.instances[author_id]; // automatically add the derived inverse reference this._authors[author_id].authoredBooks[this._isbn] = this; } else { throw validationResult; } }
For the remove operation removeAuthor
we obtain the following code:
removeAuthor( a) { // a can be an ID reference or an object reference const author_id = (typeof a !== "object") ? parseInt(a) : a.authorId; const validationResult = Book.checkAuthor( author_id); if (validationResult instanceof NoConstraintViolation) { // automatically delete the derived inverse reference delete this._authors[author_id].authoredBooks[this._isbn]; // delete the author reference delete this._authors[author_id]; } else { throw validationResult; } }
In the object-to-storage conversion of a publisher or author object with toRecord()
, the derived properties publishedBooks
and authoredBooks
are not included since their information is redundant (derived from the publisher
and authors
properties of books). For instance, the Author::toRecord
function is coded in the following way:
toRecord() { var rec = {}; // loop over all Author properties for (let p of Object.keys( this)) { // keep underscore-prefixed properties except "_authoredBooks" if (p.charAt(0) === "_" && p !== "_authoredBooks") { // remove underscore prefix rec[p.substr(1)] = this[p]; } }; return rec; }
When a Book
instance b
, with a single reference b.publisher
to a Publisher
instance p
and multiple references b.authors
to Author
instances, is destroyed, the derived inverse references have to be removed first (e.g., by removing b
from p.publishedBooks)
:
Book.destroy = function (isbn) { const book = Book.instances[isbn]; if (book) { console.log( book.toString() + " deleted!"); if (book.publisher) { // remove inverse reference from book.publisher delete book.publisher.publishedBooks[isbn]; } // remove inverse references from all book.authors for (let authorID of Object.keys( book.authors)) { delete book.authors[authorID].authoredBooks[isbn]; } // finally, delete book from Book.instances delete Book.instances[isbn]; } else { console.log(`There is no book with ISBN ${isbn} in the database!`); } };