Skip to content

by code

Val Huber edited this page Sep 13, 2021 · 26 revisions

Contrast Rules vs. Code

Backend transaction logic is a significant component of a system - as much as half the code.

This page contrasts the same requirements implemented by:

  • declarative logic (5 rules)

  • a conventional procedural approach using Python code (200 lines of legacy code - a factor of 40x)

The specification addresses around a dozen transactions. Let's focus on just two:

  • Add Order (Check Credit) - enter an order/orderdetails, and rollup to AmountTotal / Balance to check CreditLimit

  • Ship / Unship an Order (Adjust Balance) - when an Order's DateShippped is changed, adjust the Customers balance

For more information, including the data model, see Check Credit.

By Rules

The logic "cocktail napkin" requirements are implemented with the following 5 rules (which note exactly match the requirements):

Legacy code

The same "cocktail napkin spec" requires over 200 lines of legacy code (not counting shareable logic in utils):

You can review the code here:

  • 45 in examples/nw/logic/legacy/setup.py to set up listeners (code here)
  • 90 in examples/nw/logic/legacy/order_code.py for Order code (code here)
  • 90 in examples/nw/logic/legacy/order_detail_code.py for OrderDetail code (code here)
  • 27 in examples/nw/logic/legacy/customer_code.py for OrderDetail code (code here)

To facilitate comparison, the legacy code is strictly focused on the 5 requirements above:

  • The by-hand code does not address the cascade of ShippedDate into OrderDetails, nor its adjustment to Products.

  • test asserts fail since counts are not implemented in the legacy code

Example1 - Add Order: Check Credit

Here we focus on placing an order, and checking credit. The focus here is on multi-level roll-ups, to compute the balance and check it against the credit.

Execution begins in examples/nw/tests/test_add_order.py.

The import statement from nw.nw_logic import session runs examples/nw/logic/__init__, which opens the database and registers the listeners.

When add_order.py issues commit, sqlalchemy invokes the listeners. These forward before_flush events to examples/nw/logic/legacy/order_details_code.py and examples/nw/logic/legacy/order_code.py.

Adjustment Logic

Adjustments require we access/update rows that were not in the dirty list. Observations

  • for attached parents (like customer, arising from the upd_order_shipped test), updates are automatic. They do not, however, trigger flush logic.
  • for non-attached parents (like customer, arising from the add_order test)
    • you need to read them
    • add them to the session
    • and explicitly invoke their update logic (customer_update(customer, old_customer, a_session))

Example 2 - Ship / Unship an Order: Adjust Balance

Execution begins in examples/nw/tests/test_upd_order_shipped.py, and follows the same paths described above.

Here we explore old values - we need to see what the ShippedDate was, and what it is changed to:

  • If it changed from empty to shipped, we need to decrease the balance.
  • If it changed from shipped to empty, we decrease the balance.

Observe the service to get an old_row, so you can code things like if a_row.ShippedDate != an_old_row.ShippedDate: Flow is as described above, reaching nw_logic/order_logic.py Note the call to logic_engine/util.get_old_row.

Why is the Difference So Large?

In a declarative approach, the following are automated:

  • Re-use - like a spreadsheet, the rules are automatically applied to different operations (insert, update, delete)

  • Dependency Management - it requires a good deal of manual code to check for what attributes are changed, to prune the paths that require SQL commands

  • Persistence - manual code is required to issue sql commands to read and iterate through data

The declarative approach is not only more concise, it ensures higher quality, and is easier to maintain

For more information, see Declarative.