iSeries
Trigger Techniques Re-Visited
by Alex Jayasundara
and Thibault Dambrine
Think of a hypothetical situation where a system has been
running for many years, with some "known bugs, but nothing too
critical". Consequences? Minor - One of the senior programmers simply runs
a quick scan program and fixes a few records once in a while. It is a problem,
a "known bug", but the company can survive without too much of a
hiccup. Sounds familiar?
What if the consequences were MAJOR? What if individual bank
accounts were at stake? Actual people's paychecks could be affected? How can
one make an application bug-proof? Bug-proofing any application, especially
complex and sensitive programs such as ones affecting payrolls, medical records
or banking records is not a trivial task. One giant step towards increasing
reliability by killing obvious bugs can be achieved with triggers.
Trigger programs typically are simple and automatically
activated. They enforce simple rules, such as "don't allow creation of a
detail record without an existing header" or "don't allow a money transfer for an amount
greater than what is available in the source account". They can also be more sophisticated, look at
related records in a number of other files before deciding to take action,
which itself can be a number of things, from writing a log record to preventing
an action or sending a message.
The point here is that simply said, bigger bugs tend to
occur less often if the more obvious ones are kept in check.
While the general principle of triggers has not changed
much, over time the AS/400 has become the iSeries, languages used on this
machine have evolved, and so have trigger techniques. So, get a cup of coffee, have a seat, and we
will show you with a few examples how you too can use triggers to make your
systems more reliable.
Trigger Basics
The official, IBM definition of a database trigger is as
follows:
Triggers are user-written programs that are associated with
database tables. You can define a trigger for update, delete, and insert
operations. Whenever the operation takes place, regardless of the interface
that is changing the data, the trigger program is automatically activated by
DB2 database and executes its logic. In this way, complex rules can be
implemented at the database level, totally independent from the application
layer.
Triggers are mainly intended for monitoring database changes
and taking appropriate actions. The main advantage of using triggers, instead
of calling the program from within an application, is that triggers are
activated automatically, regardless of the process at the origin of the data
change.
In addition, once a trigger is in place, application
programmers and end users cannot circumvent it. When a trigger is activated,
the control shifts from the application program to the database manager. The
operating system executes the trigger program to perform the actions you
designed. The application has no choice but to wait until the trigger program
has finished its job and only then gets control again.
Triggers may cause other triggers to be called. This is a
consideration one has to understand when designing triggers, which affect files
that are themselves equipped with triggers.
There are two types of triggers available in iSeries
database tables. They are SQL triggers and External triggers.
SQL triggers can be created using the SQL CREATE TRIGGER
statement. Part of the task when using SQL to create a trigger involves
specifying the name of the trigger, what table it is attached to, when it
should be activated (BEFORE INSERT, AFTER UPDATE etc), and finally what actions
it should perform. Using the SQL statement provided, the operating system will
create a C program with your SQL statements embedded within. In effect, the
system will do a combination of CRTSQLCI and CRTPGM commands to produce the
final trigger program. What will be visible to the user will be an ILE C *PGM
object.
External triggers are more conventional user written
programs. They may or may not contain SQL statements. External triggers can be
created using any high level language capable of generating *PGM objects. Each
programming language has its own strengths and weaknesses. It is up to the
programmer to select the optimal programming language to begin with. Since
external triggers start life as regular programs, they have to be manually
attached to a file to be implemented as triggers. This can be done with either
ADDPFTRG command or with the iSeries Navigator.
On the topic of iSeries languages in particular, SQL is emerging
as the more universal database access language. The iSeries has seen its SQL
abilities grow in the last few years. C is emerging as the language of choice
for applications that demand more portability. Finally, RPG did evolve in the
last 8 years. In this article we will
discuss triggers using pure SQL (SQL Triggers) and External triggers using RPG
and C. On that particular point, we will
use both SQL and conventional OS/400 terminology interchangeably in this
article. Appendix 1 gives the equivalent terminology for both.
Benefit of Triggers
Typical use of triggers include:
Ø
Provide
consistent auditing
Ø
Prevent
invalid transactions
Ø
Enforce
complex business rules
Ø
Enforce
complex security authorizations
Ø
Provide
automatic event logging
Ø Automatically generate derived
column values Preserving data consistency
across different database tables
Ø Data
validation and audit trail
Regardless of the application and the use one can make of the trigger,
the principles remain the same. The trigger fires on a given condition, say, a
deletion or the addition of a new record. When this happens, the contents of
the old and the new record are passed to the trigger program and the same
decision, the same rules and the same actions are taken every time, regardless
of what process modified the content of the file. Note that because trigger
programs will be fired for a number of circumstances, they should be very well
tested, as buggy trigger programs can create giant size messes, depending on
what they do.
To illustrate triggers in the real world, we will use an
every-day life example: depositing or withdrawing money through ATM. Using that
example, we will explore the following scenarios:
The iSeries is writing Automated Teller Machine transactions
by writing add/change/delete row(s) to a transaction file named ATMTXN. The
table ATMTXN is equipped with a trigger. For each operation on ATMTXN the
trigger will log the transaction to a file named ATMTXNLOG.
While our example will only do logging, this solution can be
easily enhanced to update account balances, maintain ATM totals or perform
other outside operations.
The base of our example resides in two files: The
transaction file, ATMTXN, and the logging file, ATMTXNLOG. Here are the DDS
maps for these two files:
|
* ATM Transactions
UNIQUE R
ATMTRANR
ATMID 5A COLHDG('ATM ID')
ACCTID 5A COLHDG('Account #')
TCODE 1A COLHDG('Txn Code')
AMOUNT 7S 2 COLHDG('Txn Amount') DESC 10A COLHDG('Txn Description')
ALWNULL K
ATMID
K ACCTID |
The fields
in ATMTXN are self-explanatory. Note the ALWNULL keyword on field DESC, which
will allow this field to contain NULL's. We made DESC field as null-capable to
be able to explore the topic of NULL maps in relationship with their purpose
when using triggers.
ATMTXNLOG
|
* ATM
Transactions log R TXNLOGR
LOPER 1A COLHDG('Operation') LDATE L COLHDG('Date') LTIME T COLHDG('Time') LIMGB 28A COLHDG('Before Image') LNMAPB 5A COLHDG('Null Map - Before') LIMGA 28A COLHDG('After Image') LNMAPA 5A COLHDG('Null Map - After') |
DDS for ATMTXNLOG
IN ATMTXNLOG,
-
Field
LOPER will contain a transaction code (I
– insert, U – update, D – delete) to identify the type of operation.
-
LDATE
and LTIME will hold the date and time of the transaction.
-
LIMGB
and LNMAPB will hold the record before the operation and null map before the
operation respectively
-
LIMGA
and LNMAPA are the corresponding fields for after the operation.
-
LNMAPB
and LNMAPA fields will contain the NULL MAPS for each of the record images.
NULL maps are sequences of ones and zeros used to indicate the presence of null
values in the field they represent. There are as many bytes in the NULL map as
there are fields. Encountering a NULL value in a field can make a high level
program crash. The trigger program can use the NULL Map to know in advance if a
field contains NULLs. The idea is to enable program logic based on the value of
the NULL indicator rather than attempting to use a NULL-filled field which
would make it crash.

The diagram above shows that when any modification is made
to ATMTXN, the system will invoke the trigger program named ATMTXTRG. ATMTXTRG
will capture the old record, the old null map, the new record and the new null
map. It will then write these fields in the TXNLOGGER, along with the NULL
maps. This design was selected to facilitate the discussion of ATMTXTRG using
different languages (RPG and C), that have comparable features. Later, we will
discuss the same trigger example, using SQL this time, to do the job.
Basic information for trigger
programming
When a trigger is fired, the system provides two key
elements for trigger program. They are the Trigger Buffer and the Trigger
Buffer Length. An important point is that the Trigger buffer contains both a
static area and a dynamic area. More details on the trigger buffer and its
sub-fields can be found in the DB2 UDB for iSeries Database Programming V5R1.
Here is a graphic picture of what this buffer looks like:
10 20 30 31 32 33 36 40 44 48
|
Physical File
Name |
Library
Name |
Member
Name |
Trig. Event |
Trig. Time |
Cmt Level |
|
CCSID |
Relative Rec.
# |
|
64
80 96
|
Old
Record Information |
New
Record Information |
|
|
||||||
|
Offset |
Length |
|
Null Map Offset |
Null Map Length |
Offset |
Length |
Null Map Offset |
Null Map Length |
|
|
|
Old Record |
|
Old Record Null Map |
|
New Record |
|
New Record Null Map |
|
|
|
Trigger
Buffer – This buffer
will be passed to the external trigger program
|
|
Reserved by IBM for future use |
In the trigger buffer, position 1 to 44 contains physical file
information. This information may be useful in some circumstances, for example
if the trigger monitors for the size of the file or if the trigger program has
a decision to make based on the member being used. Position 49 to 80 however
tends to be the bread and butter information for most trigger programs.
Starting at position 49, you will find four fields, which can be better
qualified as pointers. They contain offsets and lengths for "old" or
before image of the record being modified and its corresponding null map. The
same pointers for new record can be found at position 65. This whole area of
the trigger buffer (position 0 thru 96) is called as static area.
Following the static area, in a variable position (you will
notice the diagram does not specify a position), is the dynamic area. This area
will contain the before and after records and their corresponding NULL maps.
This is most commonly the actual data that any trigger program will use to make
a decision.
Before V5R1 the before and after record images were placed
soon after the position 96. While this sounds ambiguous, there is a reason.
Here is how it used to work: . So if you had a file with F number of fields and
a record length of L, then the total length of the trigger buffer would be 96 +
2 * (L + F). In effect, the static and dynamic parts of the trigger buffer were
contiguous.
After V5R1, the dynamic portion of the trigger buffer may
not come immediately after the static portion of the buffer. Note that in
potentially, these fields can be change on every execution of the trigger for
the same file even if the field definitions did not change.
Based on above facts there is no way to access the dynamic
area correctly unless we use the information on position 49 thru 80 to find
where the dynamic portion of the data is located.
Because of this change, most trigger programs written prior
to V5R1 are not working well after V5R1. In this article we will assume that
all four dynamic data is placed in the different locations and will use the pointers
from position 65 thru 80 to determine the starting positions as follows.
Another notable V5R1 change is that IBM has removed the previous limit of 6
triggers per table since V5R1. Now one database table can have maximum of 300
triggers.
|
Starting position of old
record = Starting position of trigger buffer + Old
record offset
(value from position 49 – 52) Starting position of new
record = Starting position of trigger buffer + New
record offset
(value from position 64 – 67) Starting position of old null
map =
Starting position of trigger buffer + Old null map offset
(value from position 57 – 60) Starting position of new null
map =
Starting position of trigger buffer + new null map offset (value from position 73 – 76) |
When writing any trigger program, the first order of the day
is defining the trigger buffer. Here below is the definition of the trigger
buffer for RPG. Here is the definition for this example:
|
D* D* Data structures for Trigger Buffer D* and Trigger Buffer Length D* D* Trigger Buffer D* DTrgBuffer DS D TFileName 10 D TLibName 10 D TMemName 10 D TTrgEvent 1 D TTrgTime 1 D
TCommitLock 1 D TFiller1 3 D TCCSID 10I 0 D TRelRecNbr 10I 0 D TFiller2 10I 0 D TOldRecOff 10I 0 D TOldRecLen 10I 0
D
TOldNullOff 10I
0 D
TOldNullLen 10I
0 D TNewRecOff 10I 0 D TNewRecLen 10I 0 D
TNewNullOff 10I
0 D
TNewNullLen 10I 0 D* D* Trigger Buffer Length D*
DTrgBufferLen S 10I 0 |
Note the
use of I (integer) data type for all the binary data types. This is because
integer data type is more efficient and supports full range of values possible.
One useful technique is to define the trigger buffer data structure as a
separate source member. It can then be used with a COPY statement in any
trigger program. We saved the above as TRGBUFFER with the type DS (for Data
Structure) and it will be included in our ATMTXTRG RPG version.
Here is
the RPG portion of ATMTXTRG. For clearer understanding, we will break it down
in five parts:
Part 1 – Defining the trigger buffer
and other variables.
|
*.
1 ...+... 2 ...+... 3 ...+... 4 ...+... 5 ...+... 6 ... ***************
Beginning of data ************************* D*
D/COPY
QRPGLESRC,TRGBUFFER D*
|
The only
line in the code above will copy our trigger buffer to the program at time of
compilation.
Part 2 – Declaring data storage for
old and new records
To
declare structures describing the before and after images of the records
monitored by the trigger program, there is a good shortcut available in ILE
RPG: Use the file name to serve as an externally defined data structure. We are
going to use Extname keyword to
define the external file name and Prefix
keyword for global renaming of all fields in the database file. It is important
to distinguish fields from new and old records if you are using your driver
program to initiate specific tasks based on specific information (eg. Send a
notification for a withdrawal over certain amount).
|
*. 1 ...+... 2 ...+... 3 ...+... 4 ...+...
5 ...+... 6 ...+... 7 D*
D*
Record format for New and Old Records. D*
D OldRecord E DS ExtName(ATMTXN) D
Prefix(O_) D
Based(OldRecPtr) D*
D NewRecord E DS ExtName(ATMTXN) D
Prefix(N_) D
Based(NewRecPtr) |
By
combining Prefix and ExtName keywords what program does is copy the fields of
ATMTXN with prefix O_ to the data structure “OldRecord” and copy the fields of
ATMTXN with prefix N_ to the data structure “NewRecord”. You will see this
reflected in the compilation listing.
The
keywords "Based" are used
to identify which pointer is used to access the data for a given data
structure. As soon as the pointers are populated with the required data, the
record information will be made available in the data structure.
Part 3 – Declaring data storage for
old and new null maps
As a
reminder, the NULL maps contain a series of one-byte flags. There is one NULL
map byte per field and they are grouped in a data structure. Access to the NULL
map data structure will be done with the same method we used for the record
data. The length of the null map is always equal to number of fields in the
data base file (one byte per field). In this example, we declare an array with
the dimension of 5 one-byte elements since our file only has five data
fields.
|
DName+++++++++++ETDsFrom+++To/L+++IDc.Keywords+++++++++++++++++++++++++ D*
D* Record layout for New and Old Null
Maps. D*
D
OldNullMap DS Based(OldNullPtr) D ONullFld 1 DIM(5) D
NewNullMap DS Based(NewNullPtr) D NNullFld 1 DIM(5) D*
|
Here we
have defined two pointers named “OldNullPtr” and “NewNullPtr” to hold the
starting positions of the null maps and by setting the values for these
pointers data will be available on “OldNullMap”
and “NewNullMap”.
Part 4 – Declaring the parameter list
The
trigger buffer and trigger buffer length will be passed to the program by the
system as parameters each time the trigger is fired. These parameters are
written in the most standard RPG.
|
CL0N01Factor1+++++++Opcode&ExtFactor2+++++++Result++++++++Len++D+HiLoEq
C *ENTRY PLIST
C PARM TrgBuffer C PARM TrgBufferLen C*
|
Part 5
– Setting pointers
We will
use the RPG built in function %ADDR to supply the values to the pointers as
shown below. This will effectively populate the pointers that will tell the
program where to locate the data structures described in the sections above.
|
CL0N01Factor1+++++++Opcode&ExtExtended-factor2+++++++++++++++++++++++++++
C Eval OldRecPtr = %ADDR(TrgBuffer) + TOldRecOff C Eval NewRecPtr = %ADDR(TrgBuffer) + TNewRecOff C*
C Eval OldNullPtr = %ADDR(TrgBuffer) +
TOldNullOff C Eval NewNullPtr = %ADDR(TrgBuffer) +
TNewNullOff |
As soon
as the “OldRecPointer” is retrieved, data will be available in “OldRecord”. The same principle will apply to the other
combinations of pointers and data structures.
Having retrieved
the pointers and thus the associated data structures, the program now has
access to the before and after image of the record, broken up by field that can
be distinguished. By this, we mean that any of the old record fields can be
individually compared to its corresponding new version. The program is now
equipped with all the information necessary to write the logic within the
program.
The
example we have chosen to write here is a simple logging function. We could
have references to other, outside files, but for now, on with this example. The
trigger program will log the before and after images of the records modified,
along with their NULL maps and pass this information as parameters to a
separate program named TXNLOGGER.
|
C* C*
Call TXNLOGGER
C*
C CALL 'TXNLOGGER' C PARM TTrgEvent C PARM OldRecord C PARM OldNullMap C PARM NewRecord C PARM NewNullMap C*
C*
End the program
C* C Eval *InLR = *On C*
|
Using the parameters passed earlier on, TXNLOGGER will write
the actual log file record.
|
FATMTXNLOG
O A E DISK D* DInsert C CONST('1') DDelete C CONST('2') DUpdate C CONST('3') D* DTrgEvent S 1A DOldRecord S 28A
DOldNullMap S 5A DNewRecord S 28A
DNewNullMap S 5A D* DCurTime S T DCurDate S D D* D* C *ENTRY PLIST C PARM TrgEvent C PARM OldRecord C PARM OldNullMap C PARM NewRecord C PARM NewNullMap C* C* C TIME CurTime C TIME CurDate C* C MOVE CurDate LDATE C MOVE CurTime LTIME C* C Select C When TrgEvent = Insert C Move 'I' LOPER |