Scene 01
Hooks live on a table subclass
The write cycle calls methods of the table object. The base class is SQL table, and each of its hook methods does nothing. To add behaviour, write a subclass and override the hooks you need. Then name the subclass in the table declaration, with the x table class attribute. Here the invoice table gets an invoice number before the INSERT, when the caller did not give one. The hook changes the record it receives. That change reaches the SQL statement.
Example & screen notes
code. The x_table_class=Invoice argument is highlighted.
from asqueel import SqlTable
class Invoice(SqlTable):
def trigger_onInserting(self, record):
record['inv_number'] = record.get('inv_number') or 'INV-' + record['id'][-4:]
def invoice(pkg):
tbl = pkg.table('invoice', pkey='id', name_long='Invoice',
caption_field='inv_number', x_table_class=Invoice)
...
Bullet: "Every hook of the base class does nothing. Override the ones you need."
Scene 02
The insert cycle
This is the insert cycle, in the order the code runs it. First, check pkey assigns a missing key. So every later step already sees the key. Second, protect validate can refuse the values. Third come the field triggers declared on the columns for the inserting event. Lesson 22 covers them. Fourth, the table hook trigger on inserting. Fifth, trigger assign counters, a hook for counter values. Sixth, protect draft. It runs only when the table has a draft field and your subclass defines protect draft. Its return value is written into the draft field. Then the INSERT statement runs, with RETURNING for the values the database decides. After the statement come the inserted field triggers, then the table hook trigger on inserted. Both after-steps run in the same transaction as the INSERT.
Example & screen notes
slide "insert(record)". A numbered vertical list. Step 7, the SQL statement, is drawn in a different colour.
checkPkey(record)protect_validate(record)- field triggers
onInserting trigger_onInserting(record)trigger_assignCounters(record=record)protect_draft(record)— only with a draft fieldINSERT ... RETURNING- field triggers
onInserted trigger_onInserted(record)
Scene 03
The update cycle
The update cycle has no key check. It starts with protect update, which can refuse the update as a whole. Then protect validate checks the values. Then the updating field triggers and the table hook trigger on updating. Every hook of the update receives the old record as a keyword argument. Remember from lesson 20 that the old record is the one you passed, or None. Note one difference from insert. In update, protect draft runs before trigger assign counters. After the UPDATE statement, update related applies the on update actions of the relations that point to this table. Lesson 23 covers them. Last come the updated field triggers and trigger on updated.
Example & screen notes
slide "update(record, old_record=None, pkey=None)".
protect_update(record, old_record=...)protect_validate(record, old_record=...)- field triggers
onUpdating trigger_onUpdating(record, old_record=...)protect_draft(record)— only with a draft fieldtrigger_assignCounters(record=record, old_record=...)UPDATE ... RETURNINGupdateRelated(record, old_record=...)- field triggers
onUpdated trigger_onUpdated(record, old_record=...)
Scene 04
The delete cycle
The delete cycle starts with the deletable attribute of the table. Then protect delete, the deleting field triggers and trigger on deleting. Then delete related applies the on delete actions of the relations that point to this table. This happens before the DELETE statement, so dependent rows are handled first. The DELETE must remove exactly one row. After it come the deleted field triggers, trigger on deleted, and finally trigger release counters.
Example & screen notes
slide "delete(record)".
deletable=False→ refusedprotect_delete(record)- field triggers
onDeleting trigger_onDeleting(record)deleteRelated(record)DELETE— exactly one row- field triggers
onDeleted trigger_onDeleted(record)trigger_releaseCounters(record)
Scene 05
The order, observed
This output comes from an invoice subclass that records each call. The date column declares a field trigger for all six events. The invoice table has no draft field, so protect draft does not appear. The three blocks match the three lists of the previous slides.
Example & screen notes
terminal output of a run where every hook appends its name to a list. Three blocks, one per command.
INSERT
protect_validate
field trigger date
trigger_onInserting
trigger_assignCounters
field trigger date
trigger_onInserted
UPDATE
protect_update
protect_validate
field trigger date
trigger_onUpdating
trigger_assignCounters
field trigger date
trigger_onUpdated
DELETE
protect_delete
field trigger date
trigger_onDeleting
field trigger date
trigger_onDeleted
trigger_releaseCounters
Scene 06
Refusing a write
A hook refuses a write by raising an exception. The table method exception builds it for you. You give it a name from this table, the record and a message. The exception text names the table and the key of the record. Any Python error raised during a write has one more effect. The connection becomes rollback-only. A commit then raises transaction state error, until you call rollback. This prevents a commit of a write that stopped halfway.
Example & screen notes
code and its error.
class Invoice(SqlTable):
def protect_validate(self, record, old_record=None):
if record.get('total') is not None and record['total'] < 0:
raise self.exception('protect_validate', record=record,
msg='total must not be negative')
ProtectValidateException: The record Z0G7qbMe803uUhTCtzMh0F in table invc.invoice
contains invalid data: total must not be negative
Table of names for self.exception(name, ...):
| name | exception |
|---|---|
save |
SaveException |
delete |
DeleteException |
protect_update |
ProtectUpdateException |
protect_delete |
ProtectDeleteException |
protect_validate |
ProtectValidateException |
business_logic |
BusinessLogicException |
standard |
RecordException |
Footer: "After the error: commit() raises TransactionStateError until rollback()."
Scene 07
currentTrigger and nested writes
A hook may write other tables. The nested write runs its own full cycle, on the same connection and in the same transaction. Here, inserting an invoice inserts a note. Inside a write, current trigger describes the operation in progress. It is available on the table and on the database. It has five fields: event, table, record, old record and parent. Event is insert, update or delete. Parent is the operation that caused this one, or None at the top level. So the note hook can see that it runs inside the insert of an invoice. Outside any write, current trigger is None. The record a hook receives is the caller's object. A change made by a before-hook is visible to the caller after the command returns.
Example & screen notes
code on top, output below.
class Invoice(SqlTable):
def trigger_onInserted(self, record):
self.db.table('invc.invoice_note').insert(
{'invoice_id': record['id'], 'note_type': 'log', 'note_text': 'created'})
class Note(SqlTable):
def trigger_onInserting(self, record):
t = self.currentTrigger
print(t.event, t.table, t.parent.event, t.parent.table)
insert invc.invoice_note insert invc.invoice
Box on the right, "currentTrigger fields": event, table, record, old_record, parent.
Scene 08
No commit inside a hook
A hook cannot finish the transaction it runs in. Commit and rollback on the connection in use raise transaction state error while a write is in progress. The caller of the outermost command decides when to commit.
Example & screen notes
code and error.
def trigger_onInserted(self, record):
self.db.commit()
TransactionStateError: Cannot finish the transaction of a table write or hook in progress
Scene 09
Recap
Before the SQL statement run the key check, the protections, the field triggers, the table hook, the counters and the draft check. After the statement run the field triggers and the table hook, with the relation actions of update in between. Delete runs its relation actions before the statement. Hooks refuse with self dot exception, they can write other tables, and they never commit. The next lesson looks at the field triggers in detail.
Example & screen notes
three bullets.
- Before the SQL: key, protections, field triggers, table hook, counters, draft
- After the SQL: related actions on update, field triggers, table hook; on delete the related actions run before the SQL
- Hooks refuse with self.exception(...), may write other tables, never commit
Next: "Lesson 22 — Field triggers".