Annotation Interface OnChange


Annotates methods to be invoked whenever some target field in some target object changes during a transaction.

Overview

Annotated methods accept a single change event parameter whose type must be a sub-type of FieldChange. When a matching change in a matching object occurs, an event of the appropriate type is created and delivered to the annotated method.

A matching change is one that originates from one of the fields specified by value() (or every event-generating field if value() is empty), and that has a type compatible with the method's parameter type. This type compatibility check is based on the parameter's generic type, not just its raw type.

For instance methods, a matching object is one that is found at the end of the reference path specified by path(), starting from the object to be notified. By default, path() is empty, which means changes in the object itself are monitored. See ReferencePath for more information about reference paths.

For static methods, path() must be empty and every object is a matching object, so any change event compatible with the method's parameter type will be delivered.

A class may have multiple @OnChange methods, each with a specific purpose.

Method Parameter Types

In all cases the annotated method must return void and take one parameter whose type is compatible with least one of the FieldChange sub-types appropriate for the field(s) being monitored. The parameter type can be narrowed to restrict which notifications are delivered. For example, a method with a SetFieldChange parameter will receive notifications about all changes to a set field, but a method with a SetFieldAdd parameter will receive notification only when an element is added to the set. Similarly, a SetFieldClear<Animal> will receive notifications when a set associated with any Animal is cleared, but a SetFieldClear<Cat> will only receive notifications when a set associated with a Cat is cleared.

The method may have any level of access, including private.

Multiple fields may be specified in value(); if so, all of the fields are monitored together, and they all must emit FieldChanges compatible with the method's parameter type. When multiple fields are monitored by the same method, the method's parameter type may need to be widened to accomodate them all.

If value() is empty (the default), then every event-generating field in the target object is monitored, though again only changes compatible with the method's parameter type will be delivered.

Instance vs. Static Methods

An instance method method will be invoked on each object for which the changed field is found at the end of the specified reference path, starting from that object. For example, if there are three child Node's pointing to the same parent Node, and the Node class has an instance method annotated with @OnChange(path = "parent", value = "name"), then all three child Node's will be notified when the parent's name changes.

A static method is invoked once for any matching change event; the path() is ignored and must be empty.

Examples

This example shows how the annotation and the method parameter work together to determine which events are delivered:


   @PermazenType
   public abstract class Account implements PermazenObject {

   // Database fields

       public abstract boolean isEnabled();
       public abstract void setEnabled(boolean enabled);

       @NotNull
       public abstract String getName();
       public abstract void setName(String name);

       public abstract NavigableSet<AccessLevel> getAccessLevels();

   // @OnChange instance methods

       @OnChange
       private void handleAnyChange1(FieldChange<Account> change) {
           // Sees any change to ANY field of THIS account
       }

       @OnChange("accessLevels")
       private void handleAccessLevelsChange(SetFieldAdd<Account, AccessLevel> change) {
           // Sees any addition to THIS account's access levels
       }

       @OnChange
       private void handleSimpleChange(SimpleFieldChange<Account, ?> change) {
           // Sees any change to any SIMPLE field of THIS account (i.e., "enabled", "name")
       }

   // @OnChange static methods

       @OnChange
       private static void handleAnyChange2(FieldChange<Account> change) {
           // Sees any change to ANY field of ANY account
       }
   }
 

This example shows how to use the path() property to track changes in other objects:


   @PermazenType
   public abstract class User implements PermazenObject {

   // Database fields

       @NotNull
       @PermazenField(indexed = true, unique = true)
       public abstract String getUsername();
       public abstract void setUsername(String username);

       @NotNull
       public abstract Account getAccount();
       public abstract void setAccount(Account account);

       public abstract NavigableSet<User> getFriends();

   // @OnChange instance methods

       @OnChange("username")
       private void handleUsernameChange(SimpleFieldChange<User, String> change) {
           // Sees any change to THIS user's username
       }

       @OnChange(path = "account", value = "enabled")
       private void handleUsernameChange(SimpleFieldChange<Account, Boolean> change) {
           // Sees any change to THIS user's account's enabled status
       }

       @OnChange(path = "->friends->friends->account")
       private void handleFOFAccountNameChange(SimpleFieldChange<Account, ?> change) {
           // Sees any change to ANY simple field in ANY friend-of-a-friend's Account
       }

       @OnChange(path = "->account<-User.account", value = "username")
       private void handleSameAccountUserUsernameChange(SimpleFieldChange<User, String> change) {
           // Sees changes to the username of any User having the same Account as this User.
           // Note the use of the inverse step "<-User.account" from Account back to User
       }

   // @OnChange static methods

       @OnChange("account")
       private static void handleAccountChange(SimpleFieldChange<User, Account> change) {
           // Sees any change to ANY user's account
       }

       @OnChange("name")
       private static void handleAccountNameChange(SimpleFieldChange<Account, String> change) {
           // Sees any change to ANY account's name
       }
   }
 

Use Case: Custom Indexes

@OnChange annotations are useful for creating custom database indexes. In the most general sense, an "index" is a secondary data structure that (a) is entirely derived from some primary data structure, (b) can be efficiently updated when the primary data changes, and (c) provides a quick answer to a question that would otherwise require an extensive calculation if computed from scratch.

The code below shows an example index that tracks the average and variance in House prices. See Welford's Algorithm for an explanation of how HouseStats.adjust() works.


  @PermazenType
  public abstract class House implements PermazenObject {

      public abstract String getAddress();
      public abstract void setAddress(String address);

      public abstract double getPrice();
      public abstract void setPrice(double price);
  }

  @PermazenType(singleton = true)
  public abstract class HouseStats implements PermazenObject {

  // Public Methods

      public static HouseStats getInstance() {
          return PermazenTransaction.getCurrent().getSingleton(HouseStats.class);
      }

      // The total number of houses
      public abstract long getCount();

      // The average house price
      public abstract double getAverage();

      // The variance in house price
      public double getVariance() {
          return this.getCount() > 1 ? this.getM2() / this.getCount() : 0.0;
      }

  // Listener Methods

      @OnCreate
      private static void handleAddition(House house) {
          getInstance().adjust(true, house.getPrice());   // price is always zero here
      }

      @OnDelete
      private static void handleRemoval(House house) {
          getInstance().adjust(false, house.getPrice());
      }

      @OnChange("price")
      private static void handlePriceChange(SimpleFieldChange<House, Double> change) {
          HouseStats stats = getInstance();
          stats.adjust(false, change.getOldValue());
          stats.adjust(true, change.getNewValue());
      }

  // Non-public Methods

      protected abstract void setCount(long count);

      protected abstract void setAverage(double average);

      protected abstract double getM2();
      protected abstract void setM2(double m2);

      private void adjust(boolean add, double price) {
          long count = this.getCount();
          double mean = this.getAverage();
          double m2 = this.getM2();
          double delta;
          double delta2;
          if (add) {
              count++;
              delta = price - mean;
              mean += delta / count;
              delta2 = price - mean;
              m2 += delta * delta2;
          } else if (count == 1) {
              count = 0;
              mean = 0;
              m2 = 0;
          } else {                    // reverse the above steps
              delta2 = price - mean;
              mean = (count * mean - price) / (count - 1);
              delta = price - mean;
              m2 -= delta * delta2;
              count--;
          }
          this.setCount(count);
          this.setAverage(mean);
          this.setM2(m2);
      }
  }
 

Use Case: Dependent Objects

@OnChange annotations can be used to automatically garbage collect dependent objects. A dependent object is one that is only useful or meaningful in the context of some other object(s) that reference it. You can combine @OnChange with @OnDelete and @ReferencePath to keep track of these references.

For example, suppose multiple Person's can share a common Address. You only want Address objects in your database when they are referred to by at least one Person; as soon as an Address is no longer referenced, you want it to be automaticaly garbage collected.

You can use @OnChange annotations to implement a simple reference counting scheme. Actually, you don't need to count references, you just need to check whether any references still exist at the appropriate times.

You could do something like the example below. It defers the cleanup until validation time to avoid objects being deleted accidentally due to transient reference manipulation.


   @PermazenType
   public abstract class Person implements PermazenObject {

       @NotNull
       public abstract Address getAddress();
       public abstract void setAddress(Address address);
   }

   @PermazenType
   public abstract class Address implements PermazenObject {

       @NotNull
       public abstract String getNumberAndStreet();
       public abstract void setNumberAndStreet(String ns);

       @NotNull
       public abstract String getZip();
       public abstract void setZip(String zip);

   // Index queries

       @ReferencePath("<-Person.address")
       @Size(min = 1)
       public abstract NavigableSet<Person> getOccupants();

   // Dependent Object Checks

       @OnDelete(path = "<-Person.address")
       private void onOccupantDelete(Person person) {
           this.revalidate();
       }

       @OnChange(path = "<-Person.address", value = "address")
       private void onOccupantChange(SimpleFieldChange<Person, Address> change) {
           this.revalidate();
       }

       @OnValidate(early = true)
       private void deleteIfOrphan() {
           if (this.getOccupants().isEmpty())
               this.delete();
       }
   }
 

Notification Delivery

Notifications are delivered synchronously within the thread that made the change, after the change is made and just prior to returning to the original caller who invoked the method that changed the field. If an @OnChange method itself makes changes that generate additional change notifications, these new notifications are also handled prior to returning to the original caller. Put another way, the queue of outstanding notifications triggered by field changes is always emptied before the original method returns. Therefore, infinite loops are possible, e.g., if an @OnChange method modifies the field it's monitoring either directly, or indirectly via other @OnChange methods.

@OnChange operates within a single transaction; it does not notify about changes that occur in other transactions.

Fields of Sub-Types

The same field can appear in multiple sub-types, e.g., when implementing a Java interface containing a Permazen field. This can lead to some subtleties: for example, in some cases, a field may not exist in a Java object type, but it does exist in a some sub-type of that type. For example, consider this class:


 @PermazenType
 public abstract class Person {

     public abstract Set<Person> getFriends();

     @OnChange(path = "->friends", field="name")
     private void friendNameChanged(SimpleFieldChange<NamedPerson, String> change) {
         // ... do whatever
     }
 }

 @PermazenType
 public abstract class NamedPerson extends Person {

     public abstract String getName();
     public abstract void setName(String name);
 }
 
The path "->friends" implies type Person, but the field "name" is a field of NamedPerson, a narrower type than Person. However, this will still work as long as there is no ambiguity; in this example, "no ambiguity" would mean no other sub-types of Person exist that have a non-String "name" field.

Note that in the example above, the SimpleFieldChange parameter to the method friendNameChanged() necessarily has generic type NamedPerson, not Person.

Other Notes

Counter fields do not generate change notifications.

No notifications are delivered for "changes" that do not actually change anything (e.g., setting a simple field to the value already contained in that field, or adding an element to a set which is already contained in the set).

For any given field change and path, only one notification will be delivered per recipient object, even if the changed field is seen through the path in multiple ways (e.g., via reference path "->mylist" where the changed object appears multiple times in mylist).

Some notifications may need to be ignored by objects in detached transactions; you can use this.isDetached() to detect that situation.

When handing change events, any action that has effects visible to the outside world should be made contingent on successful transaction commit, for example, by wrapping it in Transaction.addCallback().

See Transaction.addSimpleFieldChangeListener() for further information on other special corner cases.

Meta-Annotations

This annotation may be configured indirectly as a Spring meta-annotation when spring-core is on the classpath.

See Also:
  • Optional Element Summary

    Optional Elements
    Modifier and Type
    Optional Element
    Description
    Specify the reference path to the target object(s) that should be monitored for changes.
    Specify the fields in the target object(s) that should be monitored for changes.
  • Element Details

    • path

      String path
      Specify the reference path to the target object(s) that should be monitored for changes. See ReferencePath for information on reference paths and their proper syntax.

      The default empty path means the monitored object and the notified object are the same.

      When annotating static methods, this property is unused and must be left unset.

      Returns:
      reference path leading to monitored objects
      See Also:
      Default:
      ""
    • value

      String[] value
      Specify the fields in the target object(s) that should be monitored for changes.

      Multiple fields may be specified; if so, each field is handled as a separate independent listener registration, and for each field, the method's parameter type must be compatible with at least one of the FieldChange event sub-types emitted by that field.

      If zero fields are specified (the default), every field in the target object(s) that emits FieldChanges compatible with the method's parameter type will be monitored for changes.

      Returns:
      the names of the fields to monitored in the target objects
      Default:
      {}