Introduction to PostgreSQL PL/Java, part 3: Triggers

By thebearinboulderNo Comments

In the past two entries I discussed the basics of PL/Java. Here I will describe one of the most powerful uses of stored procedures – triggers.

Triggers

A database trigger is stored procedure that is automatically run during one of the three of the four CRUD (create-read-update-delete) operations.

  • insertion - the trigger is provided the new value and is able to modify the values or prohibit the operation outright.
  • update - the trigger is provided both old and new values. Again it is able to modify the values or prohibit the operation.
  • deletion - the trigger is provided the old value. It is not able to modify the value but can prohibit the operation.

A trigger can be run before or after the operation. You would execute a trigger before an operation if you want to modify the values; you would execute it after an operation if you want to log the results.

Typical Usage

Insertion and Update: Data Validation

A pre-trigger on insert and update operations can be used to enforce data integrity and consistency. In this case the results are either accepted or the operation is prohibited.

Insertion and Update: Data Normalization and Sanitization

Sometimes values can have multiple representations or potentially be dangerous. A pre-trigger is a chance to clean up the data, e.g., to tidy up XML or replace < with &lt; and > with &gt;.

All Operations: Audit Logging

A post-trigger on all operations can be used to enforce audit logging. Applications can log their own actions but can’t log direct access to the database. This is a solution to this problem.

A trigger can be run for each row or after completion of an entire statement. Update triggers can also be conditional.

Triggers can be used to create ‘updateable views’.

PL/Java Implementation

Any java method can be a used in a trigger provided it is a public static method returning void that takes a single argument, a TriggerData object. Triggers can be called “ON EACH ROW” or “ON STATEMENT”.

TriggerDatas that are “ON EACH ROW” contain a single-row, read-only, ResultSet as the ‘old’ value on updates and deletions, and a single-row, updatable ResultSet as the ‘new’ value on insertions and updates. This can be used to modify content, log actions, etc.

  1. public class AuditTrigger {
  2.  
  3.     public static void auditFoobar(TriggerData td) throws SQLException {
  4.  
  5.         Connection conn = DriverManager
  6.                 .getConnection("jdbc:default:connection");
  7.         PreparedStatement ps = conn
  8.                 .prepareStatement("insert into javatest.foobar_audit(what, whenn, data) values (?, ?, ?::xml)");
  9.  
  10.         if (td.isFiredByInsert()) {
  11.             ps.setString(1, "INSERT");
  12.         } else if (td.isFiredByUpdate()) {
  13.             ps.setString(1, "UPDATE");
  14.         } else if (td.isFiredByDelete()) {
  15.             ps.setString(1, "DELETE");
  16.         }
  17.         ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
  18.  
  19.         ResultSet rs = td.getNew();
  20.         if (rs != null) {
  21.             ps.setString(3, toXml(rs));
  22.         } else {
  23.             ps.setNull(3, Types.VARCHAR);
  24.         }
  25.  
  26.         ps.execute();
  27.         ps.close();
  28.     }
  29.  
  30.     // simple marshaler. We could use jaxb or similar library
  31.     static String toXml(ResultSet rs) throws SQLException {
  32.         String foo = rs.getString(1);
  33.         if (rs.wasNull()) {
  34.             foo = "";
  35.         }
  36.         String bar = rs.getString(2);
  37.         if (rs.wasNull()) {
  38.             bar = "";
  39.         }
  40.         return String.format("<my-class><foo>%s</foo><bar>%s</bar></my-class>", foo, bar);
  41.     }
  42. }
public class AuditTrigger {

    public static void auditFoobar(TriggerData td) throws SQLException {

        Connection conn = DriverManager
                .getConnection("jdbc:default:connection");
        PreparedStatement ps = conn
                .prepareStatement("insert into javatest.foobar_audit(what, whenn, data) values (?, ?, ?::xml)");

        if (td.isFiredByInsert()) {
            ps.setString(1, "INSERT");
        } else if (td.isFiredByUpdate()) {
            ps.setString(1, "UPDATE");
        } else if (td.isFiredByDelete()) {
            ps.setString(1, "DELETE");
        }
        ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));

        ResultSet rs = td.getNew();
        if (rs != null) {
            ps.setString(3, toXml(rs));
        } else {
            ps.setNull(3, Types.VARCHAR);
        }

        ps.execute();
        ps.close();
    }

    // simple marshaler. We could use jaxb or similar library
    static String toXml(ResultSet rs) throws SQLException {
        String foo = rs.getString(1);
        if (rs.wasNull()) {
            foo = "";
        }
        String bar = rs.getString(2);
        if (rs.wasNull()) {
            bar = "";
        }
        return String.format("<my-class><foo>%s</foo><bar>%s</bar></my-class>", foo, bar);
    }
}

and

  1.   CREATE TABLE javatest.foobar (
  2.        foo   varchar(10),
  3.        bar   varchar(10)
  4.   );
  5.  
  6.   CREATE TABLE javatest.foobar_audit (
  7.        what  varchar(10) not null,
  8.        whenn timestamp not null,
  9.        data  xml
  10.   );
  11.  
  12.   CREATE FUNCTION javatest.audit_foobar()
  13.       RETURNS trigger
  14.       AS 'sandbox.AuditTrigger.auditFoobar'
  15.       LANGUAGE 'java';
  16.  
  17.   CREATE TRIGGER foobar_audit
  18.       AFTER INSERT OR UPDATE OR DELETE ON javatest.foobar
  19.       FOR EACH ROW
  20.       EXECUTE PROCEDURE javatest.audit_foobar();
  CREATE TABLE javatest.foobar (
       foo   varchar(10),
       bar   varchar(10)
  );

  CREATE TABLE javatest.foobar_audit (
       what  varchar(10) not null,
       whenn timestamp not null,
       data  xml
  );

  CREATE FUNCTION javatest.audit_foobar()
      RETURNS trigger
      AS 'sandbox.AuditTrigger.auditFoobar'
      LANGUAGE 'java';

  CREATE TRIGGER foobar_audit
      AFTER INSERT OR UPDATE OR DELETE ON javatest.foobar
      FOR EACH ROW
      EXECUTE PROCEDURE javatest.audit_foobar();

Rules

A PostgreSQL extension is Rules. They are similar to triggers but a bit more flexible. One important difference is that Rules can be triggered on a SELECT statement, not just INSERT, UPDATE and DELETE.

Rules, unlike triggers, use standard functions.

The Interface

As before I have not been able to find a maven repository of a recent version and am including the files for your convenience.

TriggerData

  1. /*
  2.  * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
  3.  * Distributed under the terms shown in the file COPYRIGHT
  4.  * found in the root folder of this project or at
  5.  * http://eng.tada.se/osprojects/COPYRIGHT.html
  6.  */
  7. package org.postgresql.pljava;
  8.  
  9. import java.sql.ResultSet;
  10. import java.sql.SQLException;
  11.  
  12. /**
  13.  * The SQL 2003 spec. does not stipulate a standard way of mapping
  14.  * triggers to functions. The PLJava mapping use this interface. All
  15.  * functions that are intended to be triggers must be public, static,
  16.  * return void, and take a <code>TriggerData</code> as their argument.
  17.  *
  18.  * @author Thomas Hallgren
  19.  */
  20. public interface TriggerData
  21. {
  22.     /**
  23.      * Returns the ResultSet that represents the new row. This ResultSet will
  24.      * be null for delete triggers and for triggers that was fired for
  25.      * statement.
  26. The returned set will be updateable and positioned on a
  27.      * valid row. When the trigger call returns, the trigger manager will see
  28.      * the changes that has been made to this row and construct a new tuple
  29.      * which will become the new or updated row.
  30.      *
  31.      * @return An updateable <code>ResultSet</code> containing one row or
  32.      *         <code>null</code>.
  33.      * @throws SQLException
  34.      *             if the contained native buffer has gone stale.
  35.      */
  36.     ResultSet getNew() throws SQLException;
  37.  
  38.     /**
  39.      * Returns the ResultSet that represents the old row. This ResultSet will
  40.      * be null for insert triggers and for triggers that was fired for
  41.      * statement.
  42. The returned set will be read-only and positioned on a
  43.      * valid row.
  44.      *
  45.      * @return A read-only <code>ResultSet</code> containing one row or
  46.      *         <code>null</code>.
  47.      * @throws SQLException
  48.      *             if the contained native buffer has gone stale.
  49.      */
  50.     ResultSet getOld() throws SQLException;
  51.  
  52.     /**
  53.      * Returns the arguments for this trigger (as declared in the <code>CREATE TRIGGER</code>
  54.      * statement. If the trigger has no arguments, this method will return an
  55.      * array with size 0.
  56.      *
  57.      * @throws SQLException
  58.      *             if the contained native buffer has gone stale.
  59.      */
  60.     String[] getArguments() throws SQLException;
  61.  
  62.     /**
  63.      * Returns the name of the trigger (as declared in the <code>CREATE TRIGGER</code>
  64.      * statement).
  65.      *
  66.      * @throws SQLException
  67.      *             if the contained native buffer has gone stale.
  68.      */
  69.     String getName() throws SQLException;
  70.  
  71.     /**
  72.      * Returns the name of the table for which this trigger was created (as
  73.      * declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException * if the contained native buffer has gone stale. */ String getTableName() throws SQLException; /** * Returns the name of the schema of the table for which this trigger was created (as * declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException * if the contained native buffer has gone stale. */ String getSchemaName() throws SQLException; /** * Returns <code>true</code> if the trigger was fired after the statement * or row action that it is associated with. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredAfter() throws SQLException; /** * Returns <code>true</code> if the trigger was fired before the * statement or row action that it is associated with. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredBefore() throws SQLException; /** * Returns <code>true</code> if this trigger is fired once for each row * (as opposed to once for the entire statement). * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredForEachRow() throws SQLException; /** * Returns <code>true</code> if this trigger is fired once for the entire * statement (as opposed to once for each row). * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredForStatement() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by a <code>DELETE</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByDelete() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by an <code>INSERT</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByInsert() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by an <code>UPDATE</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByUpdate() throws SQLException; } </code></code>
/*
 * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 * Distributed under the terms shown in the file COPYRIGHT
 * found in the root folder of this project or at
 * http://eng.tada.se/osprojects/COPYRIGHT.html
 */
package org.postgresql.pljava;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * The SQL 2003 spec. does not stipulate a standard way of mapping
 * triggers to functions. The PLJava mapping use this interface. All
 * functions that are intended to be triggers must be public, static,
 * return void, and take a <code>TriggerData</code> as their argument.
 *
 * @author Thomas Hallgren
 */
public interface TriggerData
{
	/**
	 * Returns the ResultSet that represents the new row. This ResultSet will
	 * be null for delete triggers and for triggers that was fired for
	 * statement.
The returned set will be updateable and positioned on a
	 * valid row. When the trigger call returns, the trigger manager will see
	 * the changes that has been made to this row and construct a new tuple
	 * which will become the new or updated row.
	 *
	 * @return An updateable <code>ResultSet</code> containing one row or
	 *         <code>null</code>.
	 * @throws SQLException
	 *             if the contained native buffer has gone stale.
	 */
	ResultSet getNew() throws SQLException;

	/**
	 * Returns the ResultSet that represents the old row. This ResultSet will
	 * be null for insert triggers and for triggers that was fired for
	 * statement.
The returned set will be read-only and positioned on a
	 * valid row.
	 *
	 * @return A read-only <code>ResultSet</code> containing one row or
	 *         <code>null</code>.
	 * @throws SQLException
	 *             if the contained native buffer has gone stale.
	 */
	ResultSet getOld() throws SQLException;

	/**
	 * Returns the arguments for this trigger (as declared in the <code>CREATE TRIGGER</code>
	 * statement. If the trigger has no arguments, this method will return an
	 * array with size 0.
	 *
	 * @throws SQLException
	 *             if the contained native buffer has gone stale.
	 */
	String[] getArguments() throws SQLException;

	/**
	 * Returns the name of the trigger (as declared in the <code>CREATE TRIGGER</code>
	 * statement).
	 *
	 * @throws SQLException
	 *             if the contained native buffer has gone stale.
	 */
	String getName() throws SQLException;

	/**
	 * Returns the name of the table for which this trigger was created (as
	 * declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException * if the contained native buffer has gone stale. */ String getTableName() throws SQLException; /** * Returns the name of the schema of the table for which this trigger was created (as * declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException * if the contained native buffer has gone stale. */ String getSchemaName() throws SQLException; /** * Returns <code>true</code> if the trigger was fired after the statement * or row action that it is associated with. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredAfter() throws SQLException; /** * Returns <code>true</code> if the trigger was fired before the * statement or row action that it is associated with. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredBefore() throws SQLException; /** * Returns <code>true</code> if this trigger is fired once for each row * (as opposed to once for the entire statement). * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredForEachRow() throws SQLException; /** * Returns <code>true</code> if this trigger is fired once for the entire * statement (as opposed to once for each row). * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredForStatement() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by a <code>DELETE</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByDelete() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by an <code>INSERT</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByInsert() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by an <code>UPDATE</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByUpdate() throws SQLException; } </code></code>

TriggerException

  1. /*
  2.  * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
  3.  * Distributed under the terms shown in the file COPYRIGHT
  4.  * found in the root folder of this project or at
  5.  * http://eng.tada.se/osprojects/COPYRIGHT.html
  6.  */
  7. package org.postgresql.pljava;
  8.  
  9. import java.sql.SQLException;
  10.  
  11. /**
  12.  * An exception specially suited to be thrown from within a method
  13.  * designated to be a trigger function. The message generated by
  14.  * this exception will contain information on what trigger and
  15.  * what relation it was that caused the exception
  16.  *
  17.  * @author Thomas Hallgren
  18.  */
  19. public class TriggerException extends SQLException
  20. {
  21.     private static final long serialVersionUID = 5543711707414329116L;
  22.  
  23.     private static boolean s_recursionLock = false;
  24.  
  25.     public static final String TRIGGER_ACTION_EXCEPTION = "09000";
  26.  
  27.     private static final String makeMessage(TriggerData td, String message)
  28.     {
  29.         StringBuffer bld = new StringBuffer();
  30.         bld.append("In Trigger ");
  31.         if(!s_recursionLock)
  32.         {
  33.             s_recursionLock = true;
  34.             try
  35.             {
  36.                 bld.append(td.getName());
  37.                 bld.append(" on relation ");
  38.                 bld.append(td.getTableName());
  39.             }
  40.             catch(SQLException e)
  41.             {
  42.                 bld.append("(exception while generating exception message)");
  43.             }
  44.             finally
  45.             {
  46.                 s_recursionLock = false;
  47.             }
  48.         }
  49.         if(message != null)
  50.         {
  51.             bld.append(": ");
  52.             bld.append(message);
  53.         }
  54.         return bld.toString();
  55.     }
  56.  
  57.     /**
  58.      * Create an exception based on the <code>TriggerData</code> that was
  59.      * passed to the trigger method.
  60.      * @param td The <code>TriggerData</code> that was passed to the trigger
  61.      * method.
  62.      */
  63.     public TriggerException(TriggerData td)
  64.     {
  65.         super(makeMessage(td, null), TRIGGER_ACTION_EXCEPTION);
  66.     }
  67.  
  68.     /**
  69.      * Create an exception based on the <code>TriggerData</code> that was
  70.      * passed to the trigger method and an additional message.
  71.      * @param td The <code>TriggerData</code> that was passed to the trigger
  72.      * method.
  73.      * @param reason An additional message with info about the exception.
  74.      */
  75.     public TriggerException(TriggerData td, String reason)
  76.     {
  77.         super(makeMessage(td, reason), TRIGGER_ACTION_EXCEPTION);
  78.     }
  79. }
/*
 * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 * Distributed under the terms shown in the file COPYRIGHT
 * found in the root folder of this project or at
 * http://eng.tada.se/osprojects/COPYRIGHT.html
 */
package org.postgresql.pljava;

import java.sql.SQLException;

/**
 * An exception specially suited to be thrown from within a method
 * designated to be a trigger function. The message generated by
 * this exception will contain information on what trigger and
 * what relation it was that caused the exception
 *
 * @author Thomas Hallgren
 */
public class TriggerException extends SQLException
{
    private static final long serialVersionUID = 5543711707414329116L;

    private static boolean s_recursionLock = false;

    public static final String TRIGGER_ACTION_EXCEPTION = "09000";

    private static final String makeMessage(TriggerData td, String message)
    {
        StringBuffer bld = new StringBuffer();
        bld.append("In Trigger ");
        if(!s_recursionLock)
        {
            s_recursionLock = true;
            try
            {
                bld.append(td.getName());
                bld.append(" on relation ");
                bld.append(td.getTableName());
            }
            catch(SQLException e)
            {
                bld.append("(exception while generating exception message)");
            }
            finally
            {
                s_recursionLock = false;
            }
        }
        if(message != null)
        {
            bld.append(": ");
            bld.append(message);
        }
        return bld.toString();
    }

    /**
     * Create an exception based on the <code>TriggerData</code> that was
     * passed to the trigger method.
     * @param td The <code>TriggerData</code> that was passed to the trigger
     * method.
     */
    public TriggerException(TriggerData td)
    {
        super(makeMessage(td, null), TRIGGER_ACTION_EXCEPTION);
    }

    /**
     * Create an exception based on the <code>TriggerData</code> that was
     * passed to the trigger method and an additional message.
     * @param td The <code>TriggerData</code> that was passed to the trigger
     * method.
     * @param reason An additional message with info about the exception.
     */
    public TriggerException(TriggerData td, String reason)
    {
        super(makeMessage(td, reason), TRIGGER_ACTION_EXCEPTION);
    }
}

More Information

PostgreSQL ‘create trigger’ documentation.

PostgreSQL ‘create rule’ documentation.

java

Leave your Comment

Blue Taste Theme created by Jabox