Annotation Interface OnChange
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 FieldChange
s 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
-
Element Details
-
path
String pathSpecify the reference path to the target object(s) that should be monitored for changes. SeeReferencePath
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[] valueSpecify 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
FieldChange
s 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:
- {}
-