2. Write the Model Code

The JS class model can be directly coded for getting the code of the model layer of our bidirectional association app.

2.1. New issues

Compared to the unidirectional association app, we have to deal with a number of new technical issues:

  1. We define the derived inverse reference properties, like Publisher::/publishedBooks, without a check operation and without a set operation.

  2. 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

    1. 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;

    2. 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;

    3. 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);

    4. 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;

    5. 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.

2.2. Coding Summary

Code each class of the JS class model as an ES6 class with implicit getters and setters:

  1. 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.

  2. For each single-valued property, code the specified getter and setter:

    1. 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.

    2. 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.

  3. For each multi-valued property, code its add and remove operations, as well as the specified get/set operations:

    1. Code the add/remove operations as (instance-level) methods that invoke the corresponding property checks.

    2. Code the setter such that it invokes the add operation for each item of the collection to be assigned.

    3. 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.

  4. 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).

  5. 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.

2.3. Code each class of the JS class model

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.

2.4. Code the set methods of single-valued properties

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;
    }
  }
}

2.5. Code the add and remove operations

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;
  }
}

2.6. Suppress the storage of the values of derived properties

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;
}

2.7. Take care of deletion dependencies

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!`);
  }
};