DB2 for i Triggers in 2026: SQL and External Triggers, BEFORE and AFTER Timing, Audit Logging, and Calling RPG from a Trigger

A trigger is logic that DB2 for i runs automatically when data in a table changes. Because the database runs it, a trigger fires no matter which program, user, or tool made the change, so it can enforce rules that application code cannot bypass. This guide covers SQL triggers and external program triggers: BEFORE, AFTER, and INSTEAD OF timing, row-level and statement-level triggers, transition variables and transition tables, audit logging, how triggers interact with transactions, external RPG trigger programs registered with ADDPFTRG, and managing triggers in production.

What Triggers Are and Why They Matter

A trigger fires on an INSERT, UPDATE, or DELETE against its table, whether the change comes from an RPG program using record-level access, an SQL statement in ACS, a Java application over JDBC, or a utility such as CPYF. That server-side enforcement is what makes triggers valuable.

Common uses on IBM i include:

  • Audit logging — record who changed what, with the old and new values and a timestamp.
  • Defaulting and normalising values — set a creation date, a created-by user, or a sequence number, or clean up input, without relying on every application to do it.
  • Validation — reject a change that breaks a business rule by raising an SQL error with SIGNAL.
  • Cross-table maintenance — keep a summary row in step with its detail rows.
  • Updatable views — route inserts, updates, and deletes against a view to the underlying tables.

Without triggers, every program that touches the table must remember to do these things, and one forgotten code path leaves the audit trail incomplete or the summary wrong.

SQL Triggers and External Triggers

IBM i supports two kinds of trigger, and both are still useful:

  • SQL triggers are created with CREATE TRIGGER. The trigger body is written in SQL, the trigger is recorded in the SQL catalog, and the database manages it. They are the usual choice for new development.
  • External triggers are registered with the CL command ADDPFTRG. DB2 for i calls a program — RPG, COBOL, C, or CL — and passes it a buffer containing the before and after images of the record. They suit logic that needs ILE capabilities, existing programs, or system APIs.

A table can have both kinds at the same time.

Trigger Concepts

Four choices define how an SQL trigger behaves.

Activation time

  • BEFORE — runs before the row is changed. It can inspect and modify the new values, and it can reject the change, but its body cannot contain data-change statements: a BEFORE trigger cannot insert, update, or delete rows.
  • AFTER — runs after the change. Use it to write audit rows, maintain other tables, or call procedures.
  • INSTEAD OF — defined on a view; runs in place of the INSERT, UPDATE, or DELETE against the view.

Granularity

  • FOR EACH ROW — the body runs once for every affected row.
  • FOR EACH STATEMENT — the body runs once per statement, even if it changes many rows (or none). Statement-level triggers must be AFTER or INSTEAD OF triggers.

Transition variables and transition tables

  • Transition variables (OLD ROW AS O, NEW ROW AS N) give a row-level trigger the before and after values of the current row.
  • Transition tables (OLD TABLE AS OT, NEW TABLE AS NT) give an AFTER trigger the complete set of affected rows as a table it can query.

Mode

  • MODE DB2ROW activates the trigger on each row operation. It supports row-level triggers with transition variables, but not transition tables or statement-level triggers.
  • MODE DB2SQL activates an AFTER trigger after all of the row operations of the statement have completed. Statement-level triggers and transition tables require MODE DB2SQL.

The valid combinations are:

GranularityActivationMODETransition variablesTransition tables
FOR EACH ROWBEFOREDB2ROW (DB2SQL only without a REFERENCING clause)OLD, NEWnone
FOR EACH ROWAFTER or INSTEAD OFDB2ROWOLD, NEWnone
FOR EACH ROWAFTER or INSTEAD OFDB2SQLOLD, NEWOLD TABLE, NEW TABLE
FOR EACH STATEMENTAFTERDB2SQLnoneOLD TABLE, NEW TABLE

INSERT triggers have only new values, DELETE triggers have only old values, and UPDATE triggers have both.

CREATE TRIGGER Syntax

CREATE OR REPLACE TRIGGER schema.trigger_name
    { BEFORE | AFTER | INSTEAD OF }
    { INSERT | DELETE | UPDATE [ OF column, ... ] }
    ON schema.table_or_view
    [ REFERENCING
        [ OLD ROW AS old_name ]
        [ NEW ROW AS new_name ]
        [ OLD TABLE AS old_table_name ]
        [ NEW TABLE AS new_table_name ] ]
    { FOR EACH ROW | FOR EACH STATEMENT }
    [ MODE { DB2ROW | DB2SQL } ]
    [ WHEN ( search_condition ) ]
BEGIN
    -- trigger body
END;

UPDATE OF column limits an update trigger to UPDATE statements that set one of the listed columns. The optional WHEN clause skips the body unless the condition is true; it cannot be used on an INSTEAD OF trigger.

BEFORE Triggers: Defaults, Normalisation, and Validation

A BEFORE trigger is the place to set or correct values before they are stored. This trigger fills in three columns that applications no longer need to supply:

CREATE SEQUENCE ORDLIB.ORDHDR_SEQ
    AS INTEGER
    START WITH 1
    INCREMENT BY 1
    NO CYCLE;

CREATE OR REPLACE TRIGGER ORDLIB.ORDHDR_BI
    BEFORE INSERT ON ORDLIB.ORDHDR
    REFERENCING NEW ROW AS N
    FOR EACH ROW
    MODE DB2ROW
BEGIN
    SET N.ORDCREDT = CURRENT DATE;
    SET N.ORDCREBY = SESSION_USER;
    SET N.ORDSEQ   = NEXT VALUE FOR ORDLIB.ORDHDR_SEQ;
    SET N.ORDSTS   = COALESCE(NULLIF(UPPER(TRIM(N.ORDSTS)), ''), 'OP');
END;

NEXT VALUE FOR returns a unique value, but sequence values are not guaranteed to be consecutive: a rolled-back insert, a failed statement, or cached values lost in a system failure all leave gaps. Use a sequence for unique identifiers, not for numbering that must have no gaps.

A BEFORE trigger can also reject a change. SIGNAL raises an error, the triggering statement fails, and the caller receives SQL0438 with your SQLSTATE and message:

CREATE OR REPLACE TRIGGER ORDLIB.ORDHDR_BI_VALIDATE
    BEFORE INSERT ON ORDLIB.ORDHDR
    REFERENCING NEW ROW AS N
    FOR EACH ROW
    MODE DB2ROW
BEGIN
    IF NOT EXISTS (SELECT 1 FROM ORDLIB.CUSTMST WHERE CUSTNO = N.ORDCUST) THEN
        SIGNAL SQLSTATE '75001'
            SET MESSAGE_TEXT = 'Order rejected: customer does not exist';
    END IF;

    IF EXISTS (SELECT 1 FROM ORDLIB.CUSTMST
                WHERE CUSTNO = N.ORDCUST AND CUSTSUSP = 'Y') THEN
        SIGNAL SQLSTATE '75002'
            SET MESSAGE_TEXT = 'Order rejected: customer account is suspended';
    END IF;
END;

DB2 does not use SQLSTATE classes beginning with 7 through 9 or I through Z for its own conditions, so values such as 75001 are a safe choice for application-defined errors. Document the values you use so that calling programs can recognise them.

The WHEN Clause: Firing Only When It Matters

A WHEN clause keeps the trigger body from running unless a condition is true. This trigger stops a shipped order from being reopened:

CREATE OR REPLACE TRIGGER ORDLIB.ORDHDR_BU_NO_REOPEN
    BEFORE UPDATE OF ORDSTS ON ORDLIB.ORDHDR
    REFERENCING OLD ROW AS O
                NEW ROW AS N
    FOR EACH ROW
    MODE DB2ROW
    WHEN (O.ORDSTS = 'SH' AND N.ORDSTS = 'OP')
BEGIN
    SIGNAL SQLSTATE '75003'
        SET MESSAGE_TEXT = 'A shipped order cannot be reopened';
END;

Because the condition is checked before the body runs, the trigger costs almost nothing for the updates it does not apply to.

AFTER Triggers: Audit Logging

An AFTER trigger is the standard way to keep an audit trail. The audit table:

CREATE TABLE ORDLIB.ORDAUDIT (
    AUDID     BIGINT       GENERATED ALWAYS AS IDENTITY,
    AUDACTION CHAR(1)      NOT NULL,      -- I insert, U update, D delete
    AUDORDNO  CHAR(10)     NOT NULL,
    AUDOLDSTS CHAR(2),
    AUDNEWSTS CHAR(2),
    AUDOLDAMT DECIMAL(11,2),
    AUDNEWAMT DECIMAL(11,2),
    AUDTS     TIMESTAMP    NOT NULL DEFAULT CURRENT TIMESTAMP,
    AUDUSER   VARCHAR(128) NOT NULL,
    AUDAPPL   VARCHAR(255),
    PRIMARY KEY (AUDID)
);

One trigger per event records inserts, status or amount changes, and deletes:

CREATE OR REPLACE TRIGGER ORDLIB.ORDHDR_AI_AUDIT
    AFTER INSERT ON ORDLIB.ORDHDR
    REFERENCING NEW ROW AS N
    FOR EACH ROW
    MODE DB2ROW
BEGIN
    INSERT INTO ORDLIB.ORDAUDIT
           (AUDACTION, AUDORDNO, AUDNEWSTS, AUDNEWAMT, AUDUSER, AUDAPPL)
    VALUES ('I', N.ORDNO, N.ORDSTS, N.ORDAMT, SESSION_USER, CURRENT CLIENT_APPLNAME);
END;

CREATE OR REPLACE TRIGGER ORDLIB.ORDHDR_AU_AUDIT
    AFTER UPDATE OF ORDSTS, ORDAMT ON ORDLIB.ORDHDR
    REFERENCING OLD ROW AS O
                NEW ROW AS N
    FOR EACH ROW
    MODE DB2ROW
    WHEN (O.ORDSTS <> N.ORDSTS OR O.ORDAMT <> N.ORDAMT)
BEGIN
    INSERT INTO ORDLIB.ORDAUDIT
           (AUDACTION, AUDORDNO, AUDOLDSTS, AUDNEWSTS, AUDOLDAMT, AUDNEWAMT,
            AUDUSER, AUDAPPL)
    VALUES ('U', N.ORDNO, O.ORDSTS, N.ORDSTS, O.ORDAMT, N.ORDAMT,
            SESSION_USER, CURRENT CLIENT_APPLNAME);
END;

CREATE OR REPLACE TRIGGER ORDLIB.ORDHDR_AD_AUDIT
    AFTER DELETE ON ORDLIB.ORDHDR
    REFERENCING OLD ROW AS O
    FOR EACH ROW
    MODE DB2ROW
BEGIN
    INSERT INTO ORDLIB.ORDAUDIT
           (AUDACTION, AUDORDNO, AUDOLDSTS, AUDOLDAMT, AUDUSER, AUDAPPL)
    VALUES ('D', O.ORDNO, O.ORDSTS, O.ORDAMT, SESSION_USER, CURRENT CLIENT_APPLNAME);
END;

UPDATE OF ORDSTS, ORDAMT means the update trigger fires only when an UPDATE sets one of those columns. Setting a column does not prove its value changed, so the WHEN clause compares old and new values before writing an audit row. If either column can be null, compare with IS DISTINCT FROM instead of <>.

Keeping a summary row in step

An AFTER trigger can also maintain another table. When an order line amount changes, this trigger recalculates the order total:

CREATE OR REPLACE TRIGGER ORDLIB.ORDLIN_AU_TOTAL
    AFTER UPDATE OF LINAMT ON ORDLIB.ORDLIN
    REFERENCING NEW ROW AS N
    FOR EACH ROW
    MODE DB2ROW
BEGIN
    UPDATE ORDLIB.ORDHDR
       SET ORDAMT = (SELECT COALESCE(SUM(LINAMT), 0)
                       FROM ORDLIB.ORDLIN
                      WHERE LINORDNO = N.LINORDNO)
     WHERE ORDNO = N.LINORDNO;
END;

Remember that this update fires any triggers defined on ORDHDR, including the audit trigger above. Trace the chain of triggers before adding one that changes another table.

Statement-Level Triggers and Transition Tables

A row-level trigger on a table that is updated in bulk runs once per row. When you only need to know what a statement did as a whole, a statement-level trigger runs once and reads the affected rows from a transition table. Statement-level triggers require MODE DB2SQL:

CREATE TABLE ORDLIB.BULKLOG (
    LOGTS      TIMESTAMP    NOT NULL DEFAULT CURRENT TIMESTAMP,
    OPERATION  CHAR(1)      NOT NULL,
    TABLENAME  VARCHAR(128) NOT NULL,
    ROWCOUNT   INTEGER      NOT NULL,
    TOTALAMT   DECIMAL(15,2),
    CHANGEDBY  VARCHAR(128) NOT NULL
);

CREATE OR REPLACE TRIGGER ORDLIB.ORDHDR_AU_BULKLOG
    AFTER UPDATE ON ORDLIB.ORDHDR
    REFERENCING NEW TABLE AS NT
    FOR EACH STATEMENT
    MODE DB2SQL
BEGIN
    INSERT INTO ORDLIB.BULKLOG (OPERATION, TABLENAME, ROWCOUNT, TOTALAMT, CHANGEDBY)
    SELECT 'U', 'ORDLIB.ORDHDR', COUNT(*), SUM(ORDAMT), SESSION_USER
      FROM NT;
END;

A statement-level trigger also fires for a statement that changes no rows, in which case the transition table is empty. Row-level AFTER triggers in MODE DB2SQL can use transition tables too, which is useful when the logic for one row needs to look at the other rows changed by the same statement.

INSTEAD OF Triggers on Views

A view that joins tables is normally read-only. An INSTEAD OF trigger makes it writable by defining what an INSERT, UPDATE, or DELETE against the view should do:

CREATE OR REPLACE VIEW ORDLIB.V_ORDER_SUMMARY AS
    SELECT O.ORDNO, O.ORDCUST, C.CUSTNAME, O.ORDSTS, O.ORDAMT
      FROM ORDLIB.ORDHDR O
      JOIN ORDLIB.CUSTMST C ON C.CUSTNO = O.ORDCUST;

CREATE OR REPLACE TRIGGER ORDLIB.V_ORDER_SUMMARY_II
    INSTEAD OF INSERT ON ORDLIB.V_ORDER_SUMMARY
    REFERENCING NEW ROW AS N
    FOR EACH ROW
    MODE DB2ROW
BEGIN
    -- CUSTNAME belongs to CUSTMST and is ignored here
    INSERT INTO ORDLIB.ORDHDR (ORDNO, ORDCUST, ORDSTS, ORDAMT)
    VALUES (N.ORDNO, N.ORDCUST, N.ORDSTS, N.ORDAMT);
END;

INSTEAD OF triggers are row-level and cannot have a WHEN clause. The insert into ORDHDR fires that table’s own triggers, so the defaulting, validation, and audit triggers above still apply. This lets applications work with one business view while the base tables keep their rules.

Triggers and Transactions

A trigger runs as part of the statement that fired it. Its changes belong to the same unit of work:

  • If the triggering statement fails — for example because a BEFORE trigger signals an error, or a later row in the same statement fails — the trigger’s changes are undone together with the statement’s.
  • Under commitment control, audit rows written by an AFTER trigger are committed or rolled back together with the change they describe. If the application rolls back, the audit rows disappear with it.

Do not issue COMMIT or ROLLBACK in a trigger. The trigger cannot know whether the triggering statement is one step in a larger unit of work, and ending the transaction from inside it would commit or discard changes the application has not finished with. To stop a change, use SIGNAL.

Sometimes a record must survive even if the application rolls back — a log of rejected attempts, for example. DB2 for i supports AUTONOMOUS procedures (IBM i 7.2 and later), which run as a separate unit of work that is committed when the procedure returns, independent of the caller. A trigger can call such a procedure to write that record; see DB2 for i stored procedures for writing procedures. Use this deliberately: an autonomous log can contain entries for changes that were later rolled back, so its consumers must expect that.

External Triggers with ADDPFTRG

An external trigger is a program that DB2 for i calls when the file changes. Register it with ADDPFTRG:

ADDPFTRG FILE(ORDLIB/ORDHDR) +
         TRGTIME(*AFTER) +
         TRGEVENT(*UPDATE) +
         PGM(ORDLIB/ORDTRGPGM) +
         TRG(ORDHDR_AU_PGM) +
         RPLTRG(*NO)
  • TRGTIME — *BEFORE or *AFTER.
  • TRGEVENT — *INSERT, *UPDATE, *DELETE, or *READ.
  • PGM — the trigger program. It must be a program (*PGM), not a service program.
  • TRG — a name for the trigger, which makes it easier to identify, disable, and remove later.
  • RPLTRG — *YES replaces an existing trigger for the same time and event; the default *NO protects against replacing one by accident.

For update triggers, the default TRGUPDCND(*ALWAYS) calls the program for every update, whether or not a value changes; TRGUPDCND(*CHANGE) calls it only when a value actually changes. A BEFORE trigger program can change the new record image only if the trigger is added with ALWREPCHG(*YES); the default is *NO.

The trigger buffer

DB2 for i calls the trigger program with two parameters: the trigger buffer and the length of that buffer. The buffer begins with a fixed header (offsets 0 to 95) followed by the record images:

OffsetTypeField
0CHAR(10)Physical file name
10CHAR(10)Physical file library name
20CHAR(10)Physical file member name
30CHAR(1)Trigger event: 1 insert, 2 delete, 3 update, 4 read
31CHAR(1)Trigger time: 1 after, 2 before
32CHAR(1)Commit lock level
33CHAR(3)Reserved
36BINARY(4)CCSID of the data
40BINARY(4)Relative record number
44CHAR(4)Reserved
48BINARY(4)Original record offset
52BINARY(4)Original record length
56BINARY(4)Original record null byte map offset
60BINARY(4)Original record null byte map length
64BINARY(4)New record offset
68BINARY(4)New record length
72BINARY(4)New record null byte map offset
76BINARY(4)New record null byte map length

The record images and null byte maps follow the header. Always locate them through the offsets in the header rather than assuming fixed positions: an insert has no original record, a delete has no new record, and the record length depends on the file.

An RPG trigger program

This ILE RPG program sends a message to the system operator when an order’s status changes. It maps the record images with externally described data structures, so the layout always matches the file:

**FREE
// ORDTRGPGM - external AFTER UPDATE trigger for ORDLIB/ORDHDR
Ctl-Opt DftActGrp(*No) ActGrp(*Caller);

Dcl-DS TrgHdr_t Qualified Template;
  FileName   Char(10);
  LibName    Char(10);
  MbrName    Char(10);
  Event      Char(1);     // 1 insert, 2 delete, 3 update, 4 read
  Time       Char(1);     // 1 after, 2 before
  CmtLckLvl  Char(1);
  *N         Char(3);
  Ccsid      Int(10);
  Rrn        Int(10);
  *N         Char(4);
  OldOff     Int(10);
  OldLen     Int(10);
  OldNullOff Int(10);
  OldNullLen Int(10);
  NewOff     Int(10);
  NewLen     Int(10);
  NewNullOff Int(10);
  NewNullLen Int(10);
End-DS;

Dcl-PI *N;
  TrgBuf    LikeDS(TrgHdr_t);   // trigger buffer, passed by reference
  TrgBufLen Int(10);            // length of the trigger buffer
End-PI;

Dcl-DS OldRow ExtName('ORDHDR') Qualified Based(pOldRow) End-DS;
Dcl-DS NewRow ExtName('ORDHDR') Qualified Based(pNewRow) End-DS;

Dcl-DS ErrCode_t Qualified Template;
  BytesProv  Int(10);
  BytesAvail Int(10);
  ExcpId     Char(7);
  *N         Char(1);
  ExcpData   Char(256);
End-DS;

Dcl-PR SndNonPgmMsg ExtPgm('QMHSNDM');
  MsgId      Char(7)   Const;
  MsgFile    Char(20)  Const;
  MsgText    Char(256) Const;
  MsgTextLen Int(10)   Const;
  MsgType    Char(10)  Const;
  MsgQueues  Char(20)  Const;
  NbrQueues  Int(10)   Const;
  ReplyQueue Char(20)  Const;
  MsgKey     Char(4);
  ErrorCode  LikeDS(ErrCode_t);
End-PR;

Dcl-DS ErrCode LikeDS(ErrCode_t);
Dcl-S  MsgText Varchar(256);
Dcl-S  MsgKey  Char(4);

If TrgBuf.Event = '3' And TrgBuf.Time = '1';      // after update
  pOldRow = %Addr(TrgBuf) + TrgBuf.OldOff;
  pNewRow = %Addr(TrgBuf) + TrgBuf.NewOff;

  If OldRow.ORDSTS <> NewRow.ORDSTS;
    MsgText = 'Order ' + %Trim(NewRow.ORDNO) + ' status changed from '
            + OldRow.ORDSTS + ' to ' + NewRow.ORDSTS;
    ErrCode.BytesProv = %Size(ErrCode);              // return errors, do not signal them
    SndNonPgmMsg(' '                                 // immediate message: blank ID
               : ' '                                 // no message file
               : MsgText
               : %Len(MsgText)
               : '*INFO'
               : '*SYSOPR'                           // special value, library blank
               : 1
               : ' '                                 // reply queue: blank unless *INQ
               : MsgKey
               : ErrCode);
  EndIf;
EndIf;

Return;
CRTBNDRPG PGM(ORDLIB/ORDTRGPGM) SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(ORDTRGPGM)

A few points about trigger programs:

  • Activation group. ActGrp(*Caller) runs the program in the activation group of the job that changed the file, so it uses the same commitment definition and avoids the cost of starting a new activation group on every call.
  • Return, do not end. The program ends with Return without setting on *INLR, so it stays ready for the next call instead of being reinitialised for every row.
  • Errors. If a trigger program ends with an unhandled error, the change that fired it fails. Handle expected errors deliberately and send an escape message only when you mean to reject the change.
  • Recompile after file changes. The externally described structures take their layout from the file when the program is compiled.

Managing Triggers

Listing triggers

QSYS2.SYSTRIGGERS lists both SQL triggers and triggers added with ADDPFTRG:

SELECT TRIGGER_SCHEMA, TRIGGER_NAME, ACTION_TIMING, EVENT_MANIPULATION,
       ACTION_ORIENTATION, TRIGGER_MODE, ENABLED, OPERATIVE, ACTION_ORDER,
       TRIGGER_PROGRAM_LIBRARY, TRIGGER_PROGRAM_NAME, CREATED
  FROM QSYS2.SYSTRIGGERS
 WHERE EVENT_OBJECT_SCHEMA = 'ORDLIB'
   AND EVENT_OBJECT_TABLE  = 'ORDHDR'
 ORDER BY ACTION_TIMING, EVENT_MANIPULATION, ACTION_ORDER;

-- The body of an SQL trigger (null for triggers added with ADDPFTRG)
SELECT ACTION_STATEMENT
  FROM QSYS2.SYSTRIGGERS
 WHERE TRIGGER_SCHEMA = 'ORDLIB'
   AND TRIGGER_NAME   = 'ORDHDR_AU_AUDIT';

When several triggers share the same table, time, and event, they fire in the order shown by ACTION_ORDER. From CL, DSPFD FILE(ORDLIB/ORDHDR) TYPE(*TRG) shows the same information for one file.

Disabling and enabling

A disabled trigger stays defined but does not fire:

ALTER TRIGGER ORDLIB.ORDHDR_AU_AUDIT DISABLE;
ALTER TRIGGER ORDLIB.ORDHDR_AU_AUDIT ENABLE;
CHGPFTRG FILE(ORDLIB/ORDHDR) TRG(ORDHDR_AU_PGM) STATE(*DISABLED)
CHGPFTRG FILE(ORDLIB/ORDHDR) TRG(ORDHDR_AU_PGM) STATE(*ENABLED)

Removing triggers

DROP TRIGGER ORDLIB.ORDHDR_AU_AUDIT;
RMVPFTRG FILE(ORDLIB/ORDHDR) TRGTIME(*AFTER) TRGEVENT(*UPDATE) TRG(ORDHDR_AU_PGM)

Treat trigger definitions as source code: keep the CREATE TRIGGER statements and ADDPFTRG commands under version control and deploy them with the tables they belong to.

Troubleshooting Triggers

  • The caller receives SQL0438. A trigger raised an error with SIGNAL. The SQLSTATE and message text identify which rule rejected the change.
  • A trigger stopped firing. Check ENABLED and OPERATIVE in QSYS2.SYSTRIGGERS. A trigger can become inoperative, for example when an object it depends on is dropped; re-create it to make it operative again.
  • An external trigger program fails. Look in the job log of the job that changed the file. The trigger runs in that job, not in a separate one.
  • Updates are slower than expected. Count the triggers on the table and the tables they change in turn; each row change can cascade into several more. Narrow update triggers with UPDATE OF and WHEN, and keep trigger bodies short.
  • Unexpected values after an insert. A BEFORE trigger may be overriding the application’s values. Check the BEFORE triggers on the table before debugging the application.

Trigger Best Practices

  1. Keep trigger bodies short. They run for every affected row; put complex logic in a procedure and test with realistic volumes.
  2. Never COMMIT or ROLLBACK inside a trigger. Let the application control its unit of work, and use SIGNAL to reject a change.
  3. Fire only when needed. Use UPDATE OF column lists and WHEN conditions so triggers run only for the changes they care about.
  4. Plan bulk loads. For a one-off load of historical data, consider disabling audit triggers, loading, re-enabling them, and recording the load separately. Remember that the triggers do not fire while disabled, so any rule they enforce is not applied.
  5. Use clear application SQLSTATEs. Choose values starting with 7–9 or I–Z, give each rule its own value, and document them.
  6. Know your transaction semantics. Trigger changes commit and roll back with the triggering statement. Use an autonomous procedure only for records that must survive a rollback.
  7. Consider journaling for change capture. For high-volume tables where you only need a history of changes, reading the journal can capture changes without adding work to every update.

Summary

DB2 for i triggers put rules in the database where no program can bypass them. Use BEFORE triggers to set, correct, and validate values; AFTER triggers for audit trails and related tables; INSTEAD OF triggers to make views writable; and statement-level triggers with transition tables, in MODE DB2SQL, when you care about a statement as a whole. External triggers registered with ADDPFTRG bring RPG and other ILE programs into the same model through the trigger buffer. Keep triggers short, let them share the application’s transaction, and manage them with QSYS2.SYSTRIGGERS, ALTER TRIGGER, and CHGPFTRG.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top