Course / Writing data / Lesson 21

Lesson 21 · Writing data

The order of the write cycle

Know every step that `insert`, `update` and `delete` run, in their order, and how a hook refuses a write or starts another one.

about 5 minutes · screenplay estimate9 scenesPrerequisites: lesson 20

Development course · Reading edition · API compatibility

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.

  1. checkPkey(record)
  2. protect_validate(record)
  3. field triggers onInserting
  4. trigger_onInserting(record)
  5. trigger_assignCounters(record=record)
  6. protect_draft(record) — only with a draft field
  7. INSERT ... RETURNING
  8. field triggers onInserted
  9. 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)".

  1. protect_update(record, old_record=...)
  2. protect_validate(record, old_record=...)
  3. field triggers onUpdating
  4. trigger_onUpdating(record, old_record=...)
  5. protect_draft(record) — only with a draft field
  6. trigger_assignCounters(record=record, old_record=...)
  7. UPDATE ... RETURNING
  8. updateRelated(record, old_record=...)
  9. field triggers onUpdated
  10. 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)".

  1. deletable=False → refused
  2. protect_delete(record)
  3. field triggers onDeleting
  4. trigger_onDeleting(record)
  5. deleteRelated(record)
  6. DELETE — exactly one row
  7. field triggers onDeleted
  8. trigger_onDeleted(record)
  9. 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".