The JS class model can be directly coded for getting the code of the model layer of our bidirectional association 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 (set) 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 ES6 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:
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 functions
toString()
and toJSON()
. In the
object-to-storage conversion of a publisher or author object with
toJSON()
, the derived properties
publishedBooks
and authoredBooks
are not
included since their information is redundant (they are 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, all referenced objects are
destroyed as well or their (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) {...} get publishedBooks() {...} toString() {...} toJSON() {...} }
Notice that the (derived) multi-valued reference property
publishedBooks
has no setter method and is not assigned 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 (or add/remove) inverse references to (or 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/unset // 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 toJSON()
, the derived properties
Publisher
::publishedBooks
and
Author
::authoredBooks
are not included since
their information is redundant (derived from the
Book
::publisher
and
Book
::authors
properties). For instance, the
Author
::toJSON
function is coded in the
following way:
toJSON() { var rec = {}; // loop over all Author properties for (const 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, depending on the chosen
deletion policy (1) CASCADE or (2) DROP-REFERENCES, (1) the dependent
Publisher
instance and Author
instances have
to be deleted first or (2) the derived inverse references have to be
removed first (e.g., by removing b
from
p.publishedBooks)
. We assume Existential Independence for
both associated object types and, consequently, implement the
DROP-REFERENCES policy:
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 (const 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!`); } };