Skip to content

Second Tutorial Part 10: Behavior Customization

Laurent Hasson edited this page Dec 6, 2019 · 19 revisions
Previous Main Next
<-- Part 9 Main Part 11 -->

An important part of the Tilda architecture is that it separates the generated classes from the application-level classes that you can customize. Not only can you add new methods in either the Factory or Data classes as we have highlighted in this tutorial, but there are also methods you can override. Based on the object's definition, a variety of methods with empty bodies will be defined in the generated application classes.

🎈 NOTE: Default implementations of required overridable methods will be generated the first time a class is introduced in the system. If an object's definition is changed during the course of development, then the application-class won't be re-generated and a compiler error might occur as a result. For example, you might change the OCC status of a class and suddenly your App class should define its own touch method. Most modern IDEs will flag this and even offer a quick way to add the method to our class which you can then complete with an implementation.

Example

For the purpose of this tutorial, we'll look at a Dummy table in our tutorial defined as such:

  • We'll turn off OCC
  • nameNorm is an AUTO column
  • charCount is a CALCULATED column
  • updated is a DATETIME column to keep track of our own life cycle timestamp
    ,{ "name":"Dummy"
      ,"description":"Dummy class for testing purpose"
      ,"occ":false
      ,"columns":[
          { "name":"nameFirst", "type":"STRING" , "size":255, "nullable":false
           ,"description":"A first name"
          }
         ,{ "name":"nameLast" , "type":"STRING" , "size":255, "nullable":false
           ,"description":"A last name"
          }
         ,{ "name":"nameNorm" , "type":"STRING" , "size":255, "nullable":false
           ,"mode":"AUTO"
           ,"description":"A normalized name as upper(nameLast+','+nameFirst)."
          }
         ,{ "name":"charCount", "type":"INTEGER"            , "nullable":true
           ,"mode":"CALCULATED"
           ,"description":"An app-only count of the character length of nameNorm."
          }
         ,{ "name":"updated"  , "type":"DATETIME"            , "nullable":true 
           ,"description":"A timestamp to track this row's life cycle"
          }
        ]
      ,"primary": { "autogen": true, "keyBatch": 500 }
      ,"indices": [ 
          { "name":"nameNorm", "columns": ["nameNorm"] }
        ]
     }

The generated Java application class will look like:

public class Dummy_Data extends tilda_tutorial.data._Tilda.TILDA__DUMMY
 {
   protected static final Logger LOG = LogManager.getLogger(Dummy_Data.class.getName());

   public Dummy_Data() { }

/////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////
//   Implement your customizations, if any, below.
/////////////////////////////////////////////////////////////////////

   @Override
   protected void setNameNorm()
   throws Exception
    {
      // Do something to set the value of the auto field.
      ...
    }

   @Override
   public int getCharCount()
    {
      // return some value
      ...
    }

   @Override
   protected boolean beforeWrite(Connection C) throws Exception
     {
       // Do things before writing the object to disk, for example, take care of AUTO fields.
       return true;
     }

   @Override
   protected boolean afterRead(Connection C) throws Exception
     {
       // Do things after an object has just been read form the data store, for example, take care of AUTO fields.
       return true;
     }

   @Override
   public boolean touch(Connection C) throws Exception
     {
       // Do things here to update your custom life-cycle tracker fields, like timestamp, if any.
       ... something like setLastUpdatedNow();

       // the write the object to complete the touch operation.
       return write(C);
     }
 }

🎈 NOTE: The methods that require an implementation are generated with code that explicitly induces compile errors so it's clear that immediate attention is needed. A body needs to be provided for an implementation that only you can write.

Getters And Setters

Getters and setters are declared as final in the generated Data base class. They are not overridable and this decision was not taken lightly: because the behavior of a setter or getter is complex, that introduced too many issues in earlier versions of the framework.

However, if a column is defined as "mode":"CALCULATED", the getter will be declared as abstract in the base class and therefore will need to be concretized in the application-level Data class. defining a column as CALCULATED delegates the column to only exist in the application-space: no column will actually be declared in the database. It is then your responsibility to provide a getter implementation. Of course, no setter will be generated.

A simple implementation for charCount would be as follows:

@Override
public int getCharCount()
  {
    String Str = getNameNorm();
    return TextUtil.isNullOrEmpty(Str) == true ? 0 : Str.length();
  }

Additionally, if a column is defined as "mode":"AUTO", the regular setter will be protected and a protected "auto" setter method without any parameter will be generated. That method will be called by the framework right before a database write (insert of update) and the beforeWrite() method is called.

A simple implementation for nameNorm would be as follows:

@Override
protected void setNameNorm()
throws Exception
  {
    if (hasChangedNameFirst() == true || hasChangedNameLast() == true)
      setNameNorm(getNameLast().toUpperCase() + ", " + TextUtil.normalCapitalization(getNameFirst()));
  }

🎈 NOTE: Here, we use the hasChangedXxx() methods to check whether our auto field should be updated at all. It's a simple optimization which becomes useful if the cost of the setter is more extensive.

🎈 NOTE: Your auto setter should eventually call the normal setter for the column. Auto columns exist in the database and so go through all the normal logic for regular columns: the only difference is that you have implemented logic to set the value programmatically, rather than let users of your object set the value themselves. This is useful for caching logic for example and mirrors SQL's computed columns, except that the logic is implemented app-side. A future feature of Tilda will implement support for native computed columns in the database, especially since Postgres 12 now supports the feature.

🎈 NOTE: Because auto setters are called before a Write operation, the columns will effectively not be set between a call to create() and write().

OCC (Optimistic Concurrency control)

By default, all objects are OCC-enabled and key life-cycle management columns such as created, lastpdated and deleted are automatically generated. However, if the object is declared with "occ":false, those fields won't be defined and an abstract touch() method will defined.

An implementation would be as follows:

@Override
public boolean touch(Connection C) throws Exception
 {
   // Do things here to update your custom life-cycle tracker fields, like timestamp, if any.
   setUpdatedNow();

   // the write the object to complete the touch operation.
   return write(C);
 }

🎈 NOTE: Here, we are mimicking the normal behavior of Tilda internally which uses a DateTime column called lastUpdated, but more complex logic such as some internal counter or a database sequence could be used.

Object Life-Cycle Events

Before an object is written to the database, the beforeWrite() method is called. After an object has been read from the database, the afterRead() method is called.

Typically, logic in there can perform additional validation, logging, keep track of cached values which may be used by AUTO or CALCULATED fields etc...

🎈 NOTE: Remember that auto fields are set prior calling the beforeWrite() method.

Previous Main Next
<-- Part 9 Main Part 11 -->
Clone this wiki locally