Formula

The Formula Business Logic Rule declares a Domain Objects attribute to be maintained as an expression comprised of local and Parent (possibly derived) attributes, with optional conditions.


Context

A typical Formula is show in the screen shot below:

  • Note the Javadoc, which communicates the essence of the derivation: a simple expression
  • The actual logic is largely similar, but typically includes some null checking
  • As suggested by this example,
    • Formulas are usually very short methods (akin to spreadsheet formulas)
    • They typically include null checking
    • It is common they may entail if/else logic
    • It is uncommon they require temporary variables, although these are allowed and may in fact simplify your expressions

Such Formulas typically utilize Forward Chaining to achieve a Requirements Objective. The example below is Step 4 of the Check Credit Requirement of Place Order, explained in the context of the Sum Rule:

  • The inputs to this formula (AmountTotal, AmountPaid) are both derivation results (sums) - see Step 3
  • The result of this is referenced by other rules (Customer.Balance Sum rule) - see Step 5


Usage

The following screen shot declares the AmountUnPaid as AmountTotal-AmountPaid (with provisions for null values). The detailed procedure is described in the subsections below.


Define a Business Logic Component

If you have not already done so, create a Business Logic Component, shown as CustomerLogic, above.


Add a Business Logic Method derive<AttributeName>

By convention, we recommend the method name shown in the title. This designates the attribute being derived, ?? in the example above. (You can optionally specify the attributeName value in the annotation.)


Precede the method with the @Formula annotation

Insert the @Formula annotation before the method, as follows:

@Formula [attributeName="LocalAttributeName"), lazy==[True | False], persistent=[True | False], ]

attributeName

Use this to specify the Parent Attribute Name when your Logic Method Name does not encode this.

persistent

Indicates whether the attribute is persistently stored in the database.

lazy

Whether invocation of the formula should be delayed until its value is actually read.



value expression

You have 2 ways to define the expression for the formula:
  1. You can specify code in the Business Logic Method, as explained below.  This option supports more complicated formulas, including if/else logic, reference to Java/Groovy methods, etc.

  2. You can specify simple formulas in the annotation itself
Such annotations use the value form  of annotations:
@Formula ['expression' ]
For example:

@Formula("productPrice * qtyOrdered")

public BigDecimal deriveAmount() { }

If you use this form, you can still employ the debugger - just provide some method code to stop on.  Any method return value is ignored if you use this form.

Specify the Business Logic Method

Build the method in your IDE, utilizing its services for code completion, syntax highlighting, error indications, javadoc display and so forth.

The sections below describe the key elements of specifying the Formula Logic.


Returns a Value

This method is a function: it returns a single value. The returned type must correspond to the type of the attribute.


May contain if statements

As with Spreadsheet formulas, you can declare conditional logic.


May not set other attributes

Logic simplicity and the Business Logic Engine's dependency analysis both require that the method is not allowed to set other attributes.


Can refer to local/parent attributes

Recall the context of logic execution: the Business Logic Engine is responding to Hibernate events, and is invoking your logic per dependencies. Prior to the invocation, it sets several instance variables. In the example above:

  • purchaseorder - this is an instance of your Domain Object representing the current values, identified with the @CurrentBean annotation. This enables your code to refer to (using standard Groovy/Java syntax):
    • Any local attribute
    • Any Parent value
    • It may not refer to Parent.GrandParent values
  • purchaseorder_old - a Domain Object instance representing prior values, identified with the @OldBean annotation. You can use this for state transition logic
    • For example, Inventory onHand increases by the change in the @Sum(Shipment.quantityReceived)
  • transactionContext - you can use this for more complex coding as described below
You can also refer to parent attributes.  For example, the Bill of Materials Price Rollup example utilizes a formula ProductBillofmaterials.value as kitNumberRequired * product.price, 
so that Component Price changes are reflected in the kits price.
Important: Parent Reference is one-level only.  You cannot reference "grandparent" attributes.

Can invoke Groovy/Java methods

Importantly, your code can invoke Groovy/Java methods, provided they do not alter bean attributes. These can be any methods addressable, whether local to the Business Logic Component, inherited, or external classes. The latter enables Systems Developers to build re-usable libraries of logic services as part of Extensibility.


System Operation

As further explained below, the Business Logic Engine processes your Formulas like this:

 In response to events from client changes, or Forward Chaining
   Instantiate Business Logic Component with old/new Domain Object Instances
   for each Formula in dependence order
      if not prunable
        execute Formula method
   for each child
      if referenced attributes changed
         Forward Chain altered Parent References to Children


Responds to client / Forward Chained Updates

Your Formula rules execute when the domain object is inserted, updated or deleted. This can occur because:

  1. Client Changes: trigger the following sequence:
    1. client updates are submitted to Hibernate.
    2. Hibernate raises events handled by the Business Logic Engine
    3. The Business Logic Engine invokes your Formulas in an order dictated by their dependencies
  2. Forward Chained: updates can also be triggered as follows:
    1. Child adjustments
      Changes in aggregated child data will Forward Chain updates and evaluate formulas as explained in Business Logic Execution triggered by Child changes
    2. Changes to Referenced Parent Attributes
      Such changes are automatically propagated by Parents to all children (the propagation does not occur if the referenced Parent attributes are not changed).
      There are important cases when this is not desirable - see Parent Reference vs. Parent Copy, below.

Logic Ordering based on dependence

When your Business Logic Component is first loaded, the Business Logic Engine invokes the Dependency Analyzer to determine a proper order of execution. Your Formula rules can be specified in any order within your Business Logic Component. In the following example:

 A = B+C
 B = X+Y

The Dependency Analyzer will determine that B must be executed before A.


Pruning based on Domain Object changes

As noted above, Formulas can contain parent references, such as:

 A  =  B + C + Parent.Attribute

Parent References required (possibly cached) SQL to obtain the parent data. These are expensive, so are pruned if possible.

The Business Logic Engine will detect that this SQL is not required if

  • B and C (referenced local attributes) are not altered
  • The system can determine that Parent.Attribute has not been altered

Forward Chain Cascade altered Parent References to Children

The Dependency Analyzer detects all parent references from child Domain Objects. In the example above, the Parent Business Logic Dependency Analysis detects the reference from the current (child) Domain Object Formula for A

If (and only if) your Formula alters a child Parent Referenced attributes, the Business Logic Engine will Forward Chain to each related Child instance so that it can re-evaluate its Business Logic in light of the new data. This is called Cascade processing, and is indicated in the Console Log:

  [Purchaseorder USER] .. cascading to child ...Purchaseorder.lineitems from parent Purchaseorder ...

For example, consider the Use Case Make Order Ready.


Notes

Be aware of the following.

LogicContext

The following is excerpted from the javadoc - consult that for the latest information.

An object providing state/services for Business Logic Components. This object is optionally made available to your Business Logic Components through a variable marked with the LogicContext annotation, like this:
   @LogicContextObject
   LogicContext logicContext = null
 

It provides state information your Business Logic may require, such as

  1. Nest Level - distinguish changes from clients (vs. Forward Chain
  2. Verb - distinguish Insert, Update and Delete
  3. Session - for Hibernate retrieval/meta data access
  4. Current/Old State - convenient for passing this object to Logic Extensions

Services are provided for insert, update and delete of objects from your Business Logic Components.

Important: use these (rather than native Hibernate services), to assure that the business logic is executed for updated objects.

Domain Object proxies

Bean instances are proxies (used in the Hibernate architecture, and by Business Logic). They inherit from your Domain Objects.

Use caution in testing class name - use LogicContext#getEntityNameForObject.

Parent Reference vs. Parent Copy

When referring to parent data, the default case (described above) is that subsequent changes to parent data propagate to the child, re-executing any referencing formulas. While this is often the desired result, it is not always the case.

Consider a Lineitem reference to a Product Price. The business may reasonably stipulate that Purchaseorders retain the quoted price. In other words, subsequent changes should not propagate to the child. Unlike a (propagated) reference, you want a non-progagated copy.

To address this, TransactionContext provides the following service, which you can use like this:

 /**
  * @return Derivation copy (part.price) only on connecting to new Product
  */
  @Formula
  BigDecimal derivePartPrice() {
     if (logicContext.isAttachingToParent("product"))
        return lineitem.getProduct().getPrice();
     else
        return lineitem.getPartPrice();		
  }


Null Handling

Null handling in Java is governed by Java rules:

  1. Derivation methods must return a value
  2. Return null to nullify the attribute
  3. Return the current value to make no change, for example:

Groovy provides flexibility used to simplify null handling:

  1. Derivation methods need not return a value; this is interpreted as "no change"
    • Note: Groovy returns "nothing" as null, so returning null is interpreted as "no change"
  2. To return null, use the special value LogicContext.nullValue

For example, CustomerLogic in the sample database includes:

	/**
	 * test for elementary rule execution (technical test - logic is nonsense)
	 */
	@Formula (attributeName = "notes")
	String setNotes() { 
		int set0_for_exception = 1; // 0 to force exception
		int i = 1/set0_for_exception; 
		if ( "Basic Rule Test" == customer.notes )
			return  "1 Elm";
		else if ("Basic Rule Test Insert" == customer.notes) 
			return "1 Spring";
		else if ("Set To Null" == customer.notes)
			return LogicContext.nullValue  

                // return null/nothing means no change; use this for explicit null
	}
	

@OldBean Values are from prior nest level (not before transaction)

Note that @OldBean contents reflect state changes from the prior nest level of this instance, not the beginning of the transaction.



Comments