Course / Writing data / Lesson 22

Lesson 22 · Writing data

Field triggers

Attach write logic to a column with the six field-trigger attributes and `trigger_table`.

about 4 minutes · screenplay estimate8 scenesPrerequisites: lessons 3, 21

Development course · Reading edition · API compatibility

Scene 01

Six column attributes

A table hook sees the whole record. A field trigger is attached to one column. The column element has six attributes for it. On inserting, on updating and on deleting run before the SQL statement. On inserted, on updated and on deleted run after it. The value of the attribute is a method name or a Python callable. A seventh attribute, trigger table, says on which table to look for the method.

Example & screen notes

slide "Field triggers". Two rows of three boxes.

Row "before the SQL": onInserting, onUpdating, onDeleting Row "after the SQL": onInserted, onUpdated, onDeleted

Bullets: - value: a method name, or a callable - trigger_table: take the method from another table

Scene 02

A method name

When the value is a string, the cycle calls a method named trigger, underscore, and that string. Here the string is product price, so the method is trigger product price. The method receives the record and the column name as field name. It also receives the full name of the table as the keyword table name. During an update it also receives the old record. Accept extra keywords with double-star kwargs, so one method serves every event. In this example, a new invoice row without a unit price takes the price of its product. The method writes into the record, and the value reaches the INSERT.

Example & screen notes

code. The onInserting='productPrice' argument and the method name trigger_productPrice are highlighted with the same colour.

class InvoiceRow(SqlTable):
    def trigger_productPrice(self, record, fldname, **kwargs):
        if record.get(fldname) is None:
            row = self.db.table('invc.product').query(
                columns='$unit_price', where='$id=:p', p=record['product_id']).fetchRecord()
            record[fldname] = row['unit_price']

def invoice_row(pkg):
    tbl = pkg.table('invoice_row', pkey='id', name_long='Invoice row',
                    x_table_class=InvoiceRow)
    ...
    tbl.column('unit_price', dtype='money', onInserting='productPrice')

Box: "called as trigger_<name>(record, fldname=..., tblname=...); update adds old_record=..."

Scene 03

A callable

The value can also be a callable. A callable is called with two positional arguments, the record and the column name. It receives no table name and no old record. Use a method name when you need the table object or the old values. Use a callable for a small function that only looks at the record.

Example & screen notes

code.

def stamp(record, fldname):
    print('callable', fldname)

tbl.column('quantity', dtype='I', onInserting=stamp)

Scene 04

trigger_table

Trigger table moves the method to another table. The vat rate column of an invoice row declares on inserting row vat rate, with trigger table invoicing dot vat type. The cycle then calls the method on the vat type table object, not on the invoice row. Inside the method, self is the vat type table. The table name keyword still says invoice row, the table being written. This keeps the VAT rule in the table that owns VAT data.

Example & screen notes

code. trigger_table='invc.vat_type' highlighted.

class VatType(SqlTable):
    def trigger_rowVatRate(self, record, fldname, tblname=None, **kwargs):
        if record.get(fldname) is None:
            product = self.db.table('invc.product').query(
                columns='$vat_type_code', where='$id=:p', p=record['product_id']).fetchRecord()
            vat = self.query(columns='$vat_rate', where='$code=:c',
                             c=product['vat_type_code']).fetchRecord()
            record[fldname] = vat['vat_rate']

def vat_type(pkg):
    tbl = pkg.table('vat_type', pkey='code', name_long='VAT type', x_table_class=VatType)
    ...

# in invoice_row:
tbl.column('vat_rate', dtype='perc', onInserting='rowVatRate',
           trigger_table='invc.vat_type')

Scene 05

When they run

Field triggers run at a fixed place in the cycle of lesson 21. The before-triggers run after the protections and before the table hook. The after-triggers run right after the SQL statement and before the table hook. Among themselves, field triggers run in the order the columns are declared. Only physical columns carry them.

Example & screen notes

two lists side by side.

Left, "position in the cycle": - onInserting → after protect_validate, before trigger_onInserting - onUpdating → after protect_validate, before trigger_onUpdating - onDeleting → after protect_delete, before trigger_onDeleting - on...ed → right after the SQL, before the table hook

Right, "order among columns": column declaration order.

Scene 06

The result

We insert a row with only the invoice, the product and the quantity. The callable on quantity runs first, because quantity is declared first. Then the product price trigger fills the unit price. Then the VAT trigger, on the vat type table, fills the rate. The record returned by insert holds both values. A query on the table shows the stored row, and the line total formula uses the price. The timestamp keys in the record come from field triggers too. The next scene shows where.

Example & screen notes

code and output.

row = invoice_row.insert({'invoice_id': inv['id'], 'product_id': p['id'], 'quantity': 3})
callable quantity
{'invoice_id': '...', 'product_id': '...', 'quantity': 3, 'id': '...',
 '__ins_ts': ..., '__mod_ts': ..., '__ins_user': None, '__mod_user': None,
 'unit_price': Decimal('10.00'), 'vat_rate': Decimal('20.00')}
[[quantity=3, unit_price=10.00, vat_rate=20.00, line_total=30.00]]

Scene 07

System fields use the same mechanism

The table options of asqueel use field triggers too. The system fields option declares its timestamp and user columns with on inserting and on updating set to set TS now and set current user. The hierarchical option declares its triggers on the parent id column. The methods exist on the base table class. Lesson 25 covers these options.

Example & screen notes

the field-trigger attributes of invc.product_group, printed from the model.

__ins_ts    onInserting='setTSNow'
__mod_ts    onInserting='setTSNow', onUpdating='setTSNow'
__ins_user  onInserting='setCurrentUser'
parent_id   onInserting='hierarchical_before', onUpdating='hierarchical_before',
            onUpdated='hierarchical_after'

Scene 08

Recap

A field trigger is a column attribute. Its value names a method trigger underscore name, or it is a callable with the record and the column name. Trigger table takes the method from another table. Field triggers run in column order, after the protections and before the table hook. The next lesson covers what happens to related rows when you delete or update a record.

Example & screen notes

three bullets. - onInserting ... onDeleted on a column: a method name trigger_<name> or a callable f(record, fldname) - trigger_table takes the method from another table - They run in column order, after the protections and before the table hook

Next: "Lesson 23 — Relation actions: Python and SQL".