|
Triggers |
|
|
A trigger is an optional user written function associated with a hashed file and configured to be executed when certain file operations are performed. Executed before a write or delete, the trigger can be used to apply data validation. Executed after a record is written or deleted, the function can trigger other events such as related file updates. Trigger functions can also be executed after a read and before or after a clear file operation.
The trigger function is simply a catalogued QMBasic subroutine which is automatically executed as part of the file operation. The subroutine is passed a mode flag to indicate the action being performed, the record id, the record data (read or write operations) and a flag indicating whether the QMBasic ON ERROR clause is present. The subroutine may do whatever processing the application designer wishes. If the write or delete is to be disallowed, the pre-write or pre-delete trigger function should set the @TRIGGER.RETURN.CODE variable to a non-zero value such as an error number or an error message text to cause the write or delete to take its ON ERROR clause if present or to abort if omitted. The STATUS() function will return ER$TRIGGER when executed in the program that initiated the file operation. Programs should test STATUS() rather than testing for @TRIGGER.RETURN.CODE being non-zero to determine whether the trigger function has disallowed the write or delete as @TRIGGER.RETURN.CODE is only updated when the error status is set.
The trigger function is set up using the SET.TRIGGER command. After it has been set up, the trigger function is loaded into memory when the file is opened and is called for all operations defined by the mode settings in the SET.TRIGGER command. Modifying and recataloguing the trigger function will have no effect on processes that have the file open until they close and reopen it.
If the trigger function is not in the catalogue or has the incorrect number of arguments, no error occurs until the first action that would call the function. Note that the trigger function must be visible to all accounts that may reference the file. Where a file is used by multiple accounts, this can be achieved by using global cataloguing, sharing a private catalogue, or ensuring that the VOC entry for a locally catalogued trigger function is present in each account. Although it would be possible for a shared file to use a different trigger function depending on the account from which it is referenced, this is not recommended.
When using QMNet, the server process runs in the QMSYS account. The implications of this are:
The interface into a trigger function is:
SUBROUTINE name(mode, id, data, on.error, fvar)
where
Other values may be used in the future. Trigger functions should be written to ignore unrecognised values.
When writing trigger functions, the original data of the record to be written or deleted can be examined by reading it in the usual way. Trigger functions should not attempt to write the record for which they are called. Neither should they release the update lock on this record as this could cause concurrent update of the record.
If the value of data is changed by a pre-write trigger function, the modified data is written to the file. Similarly, a read trigger can modify the data that will be returned to the application that requested the read. Changes to the value of id will not affect the database update in any way.
Trigger functions may perform all of the actions available to other QMBasic subroutines including performing updates that may themselves cause trigger functions to be executed.
The mode values correspond to bit positions in a binary value and hence a condition such as IF MODE = 4 OR MODE = 8 THEN ... is equivalent to IF BITAND(MODE, 12) THEN ... which can simplify some trigger functions.
Example
The following simple trigger function could be used to capture all writes and deletes, logging the new record data in a "replication log" file which can then be used to maintain a copy of the data on a separate server. The same trigger function can be applied to many files.
SUBROUTINE REPTRIGGER(MODE, ID, DATA, ON.ERROR, FVAR) $CATALOGUE GLOBAL
$INCLUDE KEYS.H
COMMON /REPLOG/RLG.F ;* REPLOG file variable, persistent across calls
FN = FILEINFO(FVAR, FL$VOCNAME)
IF NOT(FILEINFO(RLG.F, FL$OPEN)) THEN OPEN REPLOG TO RLG.F ELSE LOGMSG 'Unable to open REPLOG' RETURN END END
BEGIN CASE CASE MODE = FL$TRG.PRE.WRITE RECORDLOCKU RLG.F, FN:' ':ID WRITE DATA TO RLG.F, FN:' ':ID
CASE MODE = FL$TRG.PRE.DELETE RECORDLOCKU RLG.F, FN:' ':ID WRITE '' TO RLG.F, FN:' ':ID END CASE END
On first call, this trigger routine will open a file named REPLOG which is used for logging. Because the file variable is stored in a common block, the file will remain open for subsequent calls to the trigger function.
For write operations (mode FL$TRG.PRE.WRITE), the trigger routine writes a copy of the data to a record in REPLOG with its id constructed from the file name and record id of the write that is being replicated.
For delete operations (mode FL$TRG.PRE.DELETE), the trigger routine writes a null record to REPLOG with its id constructed in the same way.
A separate program, perhaps running via QMNet from another server, could periodically read all records from the REPLOG file and apply the changes to a copy of the original data, deleting the REPLOG entry. Note that the REPLOG is not a sequential audit trail but stores only the last update to any record. Thus a long series of updates will only ever produce a single REPLOG record.
There are two assumptions made by this example. Firstly, the data files being replicated never contain null records. We can, therefore, recognise a delete operation from the presence of a null record in the REPLOG. If this were not a safe assumption, we would need to add an extra field to the REPLOG data to say whether this was a write or a delete.
Secondly, there is an assumption that a file only has a single VOC entry defining it. If this were not the case, a combination of the filename (FN variable) and record id (ID argument) would not be a unique reference to the record. If this assumption was not valid, it would be possible to use FILEINFO() to get the pathname of the file and use this instead.
|