2. Write the Model Code

How to Encode a JavaScript Data Model

The JavaScript data model can be directly encoded for getting the code of the model layer of our JavaScript frontend app.

2.1. New issues

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

  1. In the model code you now have to take care of maintaining the derived inverse reference properties by maintaining the derived (sets of) inverse references that form the values 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 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 refences 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. In the UI code we can now exploit the inverse reference properties for more efficiently creating a list of inversely associated objects in the list objects use case. For instance, we can more efficiently create a list of all published books for each publisher. However, we do not allow updating the set of inversely associated objects in the update object use case (e.g. updating the set of published books in the update publisher use case). Rather, such an update has to be done via updating the master objects (in our example, the books) concerned.

2.2. Summary

  1. Encode each model class as a JavaScript constructor function.

  2. Encode the property checks in the form of class-level ('static') methods. Take care that all constraints of a property as specified in the JavaScript data model are properly encoded in the property checks.

  3. Encode the property setters as (instance-level) methods. In each setter, the corresponding property check is invoked and the property is only set, if the check does not detect any constraint violation. If the property is the inverse of a derived reference property (representing a bidirectional association), make sure that the setter also assigns (or adds) corresponding references to (the value set of) the inverse property.

  4. Encode the add/remove operations as (instance-level) methods that invoke the corresponding property checks. If the multi-valued reference property is the inverse of a derived reference property (representing a bidirectional association), make sure that both the add and the remove operation also assign/add/remove corresponding references to/from (the value set of) the inverse property.

  5. Encode any other operation.

  6. Take care of the deletion dependencies in deleteRow methods.

These six steps are discussed in more detail in the following sections.

2.3. Encode each class of the JavaScript data model as a constructor function

For instance, the Publisher class from the JavaScript data model is encoded in the following way:

function Publisher( slots) {
  // set the default values for the parameter-free default constructor
  this.name = "";            // String
  this.address = "";         // String, optional
  // derived inverse reference property (map of Book objects)
  this.publishedBooks = {};  // inverse of Book::publisher

  // constructor invocation with arguments
  if (arguments.length > 0) {
    this.setName( slots.name);
    this.setAddress( slots.address);
  }
};

Notice that we have added the (derived) multi-valued reference property publishedBooks, but we do not assign it in the constructor function because it will be assigned when the inverse reference property Book::publisher will be assigned.

2.4. Encode the property checks

The property checks from the unidirectional association app do not need to be changed in any way.

2.5. Encode the setter operations

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 value (set) of the inverse property. An example of such a setter is the following setPublisher method:

Book.prototype.setPublisher = function (p) {
  var constraintViolation = null;
  var publisherIdRef = "";
  // a publisher can be given as ...
  if (typeof(p) !== "object") {  // an ID reference or 
    publisherIdRef = p;
  } else {                       // an object reference
    publisherIdRef = p.name;
  }
  constraintViolation = Book.checkPublisher( publisherIdRef);
  if (constraintViolation instanceof NoConstraintViolation) {
    if (this.publisher) {  // update existing book record
      // delete the obsolete inverse reference in Publisher::publishedBooks
      delete this.publisher.publishedBooks[ this.isbn];  
    }
    // assign the new publisher reference 
    this.publisher = Publisher.instances[ publisherIdRef];
    // add the inverse reference to publisher.publishedBooks
    this.publisher.publishedBooks[ this.isbn] = this;
  } else {
    throw constraintViolation;
  }
};

2.6. Encode the add and remove operations

For any multi-valued reference property that is coupled to a derived inverse reference propertiy, 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 propertiy Author:authoredBooks for implementing the bidirectional authorship association between Book and Author, the addAuthor method is encoded in the following way:

Book.prototype.addAuthor = function( a) {
  var constraintViolation=null, authorIdRef=0, authorIdRefStr="";
  // an author can be given as ...
  if (typeof( a) !== "object") {  // an ID reference or
    authorIdRef = parseInt( a);
  } else {                       // an object reference
    authorIdRef = a.authorId;
  }
  constraintViolation = Book.checkAuthor( authorIdRef);
  if (authorIdRef && constraintViolation instanceof NoConstraintViolation) {
    authorIdRefStr = String( authorIdRef);
    // add the new author reference
    this.authors[ authorIdRefStr] = Author.instances[ authorIdRefStr];
    // automatically add the derived inverse reference
    this.authors[ authorIdRefStr].authoredBooks[ this.isbn] = this;
  }
};

For the remove operation removeAuthor we obtain the following code:

Book.prototype.removeAuthor = function( a) {
  var constraintViolation=null, authorIdRef=0, authorIdRefStr="";
  // an author can be given as an ID reference or an object reference
  if (typeof(a) !== "object") {
    authorIdRef = parseInt( a);
  } else {
    authorIdRef = a.authorId;
  }
  constraintViolation = Book.checkAuthor( authorIdRef);
  if (authorIdRef && constraintViolation instanceof NoConstraintViolation) {
    authorIdRefStr = String( authorIdRef);
    // automatically delete the derived inverse reference 
    delete this.authors[ authorIdRefStr].authoredBooks[ this.isbn];
    // delete the author reference
    delete this.authors[ authorIdRefStr];
  }
};

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, the derived inverse references have to be removed first (e.g., by removing b from p.publishedBooks).

Book.deleteRow = function (isbn) {
  var book = Book.instances[isbn], keys=[], i=0;
  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
    keys = Object.keys( book.authors);
    for (i=0; i < keys.length; i++) {
      delete book.authors[keys[i]].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!");
  }
};