Overriding Events

The the section called Data Objects Tutorial walked you through creating and accessing Data Objects using the Persistence system. The tutorial assumes that the SQL generated by the persistence system using the provided metadata will both be general enough to handle all situations and perform fast enough to be efficient. This, however, is not a valid assumption, as often there are special situations in which developers need control over how information is stored or accessed within the database.

In order to provide developers will the flexibility that is needed when developing large systems, the Persistence layer provides the ability to override the default events generated for object types. This document discusses how to override events for standard object types and and associations.

Overriding Object Type Insert, Retrieve, Update, and Delete Events

The Data Object Tutorial briefly mentioned that each object type has five predefined events that are typically generated by the MDSQL generator. These events are retrieve, retrieve all, insert, update, and delete.

The following example shows how to define these events for the Publication object type. The SQL that is used is identical to the SQL that would be generated by the Persistence system. Therefore, defining it as follows will cause the same behavior as defining it as it was defined within the tutorial.

model tutorial;

object type Publication {
    BigDecimal id = publications.id;
    String name = publications.name;

    object key (id);

    retrieve {
       do {
          select publication_id, name
          from publications
          where publication_id = :id
       } map {
          id = publications.publication_id;
          name = publications.name;
       }
    }

    retrieve all {
       do {
          select publication_id, name
          from publications
       } map {
          id = publications.publication_id;
          name = publications.name;
       }
    }

    insert {
       do {
         insert into publications (publication_id, name)
         values
         (:id, :name)
       }
    }

    update
       do {
         update publications
         set name = :name
         where publication_id = :id
       }
    }

    delete {
       do {
          delete from publications where publication_id = :id
       }
    }
}

The retrieve event definition contains a SQL select statement for retrieving a Publication's state from the publications table. Any attribute can be used in the SQL statement by prefacing it with a colon. After the SQL select statement, there is a list of "<attribute> = <column>" pairings. This is used to associate a column in the ResultSet to an attribute. Any pairing defined here causes the value to be copied from the ResultSet into the object's state.

Also note the use of the colon in the event blocks. You can access an object type's attribute using the colon followed by the attribute name. This is useful when inserting or updating values in a row, or when using an attribute value as part of a where clause.

Each event block has a set of do blocks. These are used to allow you to perform multiple statements within a single event, for instance, if there is a table named all_publications that requires a row to be inserted whenever a publication is created. To accommodate this, the publication insert event will be changed to the following, with each piece of SQL inside the do block being executed in order:

insert {
   do {
     insert into publications (publication_id, name)
     values
     (:id, :name)
   }

   do {
     insert into all_publications (publication_id) values (:id)
   }
}

NoteNote
 

Note that the do blocks are executed in the same order that they are defined. If they were not then the above example would fail if the insert into all_publications was executed first and the table all_publications referenced publications.

Based on your needs, you may deliberately choose to define certain event blocks to override the default for a particular object type. In most situations, you will probably not override all 5 events. Rather, you will only override the events for which you need special behavior. For example, if you want to define a read-only object type, you can simply override the update and delete event block. This will allow objects to be inserted and retrieved from the database but not updated or deleted. The PDL file then appears as follows:

model tutorial;

object type Publication {
    BigDecimal id = publications.id;
    String name = publications.name;

    object key (id);

    // by leaving the next two events empty, the system does
    // not do anything when code tries to execute them.
    update
    }

    delete {
    }

    // MDSQL will generate the retrieve, retrieve all, and insert
    // events
}

The final key to overriding events is the use of the super keyword when dealing with object types that extend other object types. For instance, suppose you have an all_magazines as well as an all_publications table. In this case, you want to override the insert event for the magazine, without having to know how publication is executing its insert. The following PDL can be used to achieve this goal:

model tutorial;

object type Magazine extends Publication {
    String issueNumber = magazines.issue_number VARCHAR(30);

    reference key (magazines.magazine_id);

    insert {
       // the use of the super keyword means that
       // this calls the insert event for 
       // Publication before continuing
       super;

       do {
         insert into magazines (magazine_id, issue_number)
         values
         (:id, :issueNumber)
       }

       do {
         insert into all_magazines (magazine_id) values (:id)
       }
    }
}

NoteNote
 

  • The super keyword is not valid for the retrieve or retrieve all events. For these events, it is better to directly join to the tables in the super type.

  • Do not use semicolons in your SQL statements within the event blocks. The SQL statements are passed directly to the database as they are defined. Any error in your SQL statement is caught at run time, not at initialization time.

Overriding Association Add, Retrieve, Update, and Delete Events

As with Object Types, one of the nice flexibilities provided by the Persistence system is the ability to override the default SQL that will be run for any given event. As was briefly mentioned in the section called Defining Associations, each associations contains four events: add, retrieve, remove, and clear. This section discusses how these events can be manually defined for Role References as well as Association blocks.

Overriding Association Block Events

Block associations are typically two-way associations that contain a total of 9 events (4 for each object type) plus an update event. The update event is run when the association is saved but the mapping has not changed. This is only useful for updating values of link attributes. For instance, if there is a last_modified link attribute then the update would probably set the column to the current date.

Associations are defined within association blocks, which are similar to "object type" blocks. That is, they begin with the Attribute definitions and are followed by the event blocks. Note that the Attribute Type is actually an Object Type, rather than simply a Java Type. Also, the Attribute Type is followed by a Multiplicity (an "empty" multiplicity defaults to [0..1]).

The following example depicts a full association block with all events overridden. It defines the same association between the Article and the Magazine that was defined in the section called Association Blocks and uses the same SQL that is generated by the MDSQL generator so in practice none of the events would be defined.

association {  
   // since we are defining all 8 events in this block, 
   // the join paths after the "=" are not necessary but are good
   // to have if they are possible to define.
   Article[0..n] articles = join magazines.magazine_id
                              to magazine_article_map.magazine_id,
                            join magazine_article_map.article_id
                              to articles.article_id;
   Magazine[0..n] magazines = join articles.article_id
                                to magazine_article_map.article_id,  
                              join magazine_article_map.magazine_id
                                to magazines.magazine_id;

    // the retrieve event that can be called from a magazine DataObject
    // to retrieve all of the articles in the magazine.  Note that we
    // use the object key for Magazine (id) for the bind variable
    // here instead of the object key for Article (articleID).
    retrieve articles {
       do {
          select articles.article_id, title 
          from articles, magazine_article_map m
          where articles.article_id = m.article_id
          and m.magazine_id = :id
       } map {
          articleID = articles.article_id;
          title = articles.title;        
       }
    }

    // the retrieve event that can be called from an Article DataObject
    // to retrieve all of the magazines for an article.  Note that we 
    // use the object key for Article (articleID) for the bind variable
    // here instead of the object key for Magazine (id).
    retrieve magazines {
       do {
          select magazines.magazine_id, issue_number, name
          from magazines, publications, magazine_article_map m
          where magazines.article_id = :articleID
          and publication_id = magazines.magazine_id
          and magazines.magazine_id = m.magazine.id
       } map {
          id = magazines.magazine_id;
          issueNumber = magazines.issue_number;
          name = publications.name;
       }
    }


    // for the other 6 events, we switch between magazines and articles
    add articles {
       do {
          insert into magazine_article_map (article_id, magazine_id)
          values (:articles.articleID, :id)
       }
    }

    add magazines {
       do {
          insert into magazine_article_map (article_id, magazine_id)
          values (:articleID, :magazines.id)
       }
    }

    remove articles {
       do {
          delete from magazine_article_map 
          where magazine_id = :id
          and article_id = :articles.articleID
       }
    }  

    remove magazines {
       do {
          delete from magazine_article_map 
          where magazine_id = :magazines.id
          and article_id = :articleID
       }
    }  

    clear articles {
       do {
          delete from magazine_article_map 
          where magazine_id = :id
       }
    }

    clear magazines {
       do {
          delete from magazine_article_map 
          where article_id = :articleID
       }
    }

    // there are no link attributes so there is nothing for the 
    // update event to do.  Therefore, we do not define it.
}

When defining the association events, it is very important to recognize with which end of the association you are dealing. Using the association just defined, when the retrieve magazines event is called, the implied "this" object is the Article object. Thus, when you reference attributes, they are in terms of the article. For example, if the object key of Article was id instead of articleID, :id would refer to the Article object's id property, where :magazines.id referred to the Magazine's id property. In the retrieve articles, the :id refers to the Magazine object's id property.

Overriding Role Reference Events

Now that you have seen how association block events can be overridden, it is easy to understand how Role Reference events can also be overridden. Working off the example from the associations document, suppose you want to alter the way that screen names are handled. That is, suppose you want to enforce a unique constraint at the PDL level. If any author is given a screen name ID that is already used by another author, the author that currently owns the screen name will no longer have one and the new author will have the existing screen name.

In order to make this work, you must override the add screenName event using code such as the following:

object type Author {
    BigInteger[1..1] id = authors.author_id INTEGER;
    String[1..1] firstName = author.first_name VARCHAR(700);
    String[1..1] lastName = author.last_name VARCHAR(700);
    Blob[0..1] portrait = authors.portrait BLOB;

    // the following line is the role reference.  Notice that it 
    // appears in the definition just like an Attribute.  The only
    // difference is that instead of pointing to a column in the   
    ScreenName[0..1] screenName = 
               join authors.screen_name_id to screen_names.name_id;

    object key (id);

    add screenName {
        // we include this extra do block so that
        // we null out the screen name of anyone that has the screen
        // name that is about to be taken by the author with id = :id
        do { 
            update authors set screen_name_id = null
            where screen_name_id = :screenName.id
        }

        // This do block is the block that would
        // have been created by standard MDSQL
        do {
            update authors set screen_name_id = :screenName.id
            where author_id = :id
        }
    }

    // we do not define any more Role Reference events or object type
    // events because the Persistence layer automatically generates 
    // the rest
}

NoteNote
 

This is a contrived example and enforcing a unique constraint this way is not recommended. Unique constraints should be enforced at the database level and by declaring the attribute as unique.

Overriding Composite Events

As is the case with standard MDSQL the section called Composite Associations, overriding composite events is almost identical to overriding any other association event (whether it is declared as part of a role reference or an association block). Again, the only difference is that the composite keyword is used. To continue with the example of a composite association from the Associations document, assume you want to overload the Paragraph insert event and the corresponding add paragraph event within the association. You would overload these events because it does not make sense for a Paragraph to belong outside of an Article. Thus, you can make it impossible to insert a Paragraph outside of the Association add paragraph event. This means that creating a new Paragraph and calling save() will do nothing, which may initially be confusing.

Another approach to implementing this would be for the insert event to contain an insert SQL statement and the add event of the association to be left empty. This solution would be preferable, except that it forces the Paragraph to know to which Article it belongs, which is not always the case. If the paragraph does know which Article it belongs to, leaving the add event empty is the recommended approach. However, since more often than not the dependent object does not know of the other object, the empty insert approach is usually taken.

The final possible approach to take in the PDL without forcing the Java to include extra information is for the Paragraph insert to contain a normal insert statement setting the article_id to null. The problem with this approach is that there cannot be an is not null constraint on the database, which could potentially cause an invalid state.

object type Paragraph {
    BigDecimal id = paragraphs.paragraph_id;
    String text = paragraphs.text;
    
    object key (paragraphs.paragraph_id);

    insert {
       // notice that the insert block is empty.  This is because
       // it does not make sense for a Paragraph to belong
       // outside of an Article so there is no way to insert it
       // outside of the Association "add" event.  Note that this
       // means that creating a new Paragraph and calling save() will
       // not actually do anything which may be confusing at first
    }
}

association {
   Article[1..1] articles = join paragraphs.article_id 
                              to articles.article_id;
   // notice the composite keyword indicates that if the article does
   // not exist then the paragraph also does not exist
   composite Paragraph[0..n] paragraphs = join articles.article_id
                                            to paragraphs.article_id;

    add paragraphs {
       do {
          insert into paragraphs (paragraph_id, text, article_id)
          values (:paragraphs.id, :text, :articleID)
       }
    }
}                                

As a frame of reference for saving associations, if the code adds the paragraph to an article and then calls article.save(), the events are fired in the following order:

  • Article.insert() (or Article.update())

  • Paragraph.insert() (or Paragraph.update())

  • Article.add() paragraph event

This is useful to know when determining the information that will be in the database when items are saved.

NoteNote
 

The recommended approach for this is to allow MDSQL to generate your events for you so that you do not need to worry about the order of items being executed.