1

我可以动态插入表列名问题是当我想在我的日志表中插入新值或旧值时,我得到一个字符串'old.Colname'或'new.Colname'而不是旧值或新值。

DECLARE C_USER VARCHAR(255);
DECLARE VARIABLE OPERATION_EVENT CHAR(8);
DECLARE GROUPIDNO INT;
DECLARE LOGDATAID_NO INT;
DECLARE VARIABLE FN CHAR(31);
DECLARE VARIABLE NEWCOL CHAR(31);
DECLARE VARIABLE OLDCOL CHAR(31);

BEGIN

  SELECT CURRENT_USER FROM RDB$DATABASE INTO :C_USER;

  IF (DELETING) THEN
  BEGIN
    OPERATION_EVENT = 'DELETE';
  END
  ELSE
  BEGIN
    IF (UPDATING) THEN
      OPERATION_EVENT = 'UPDATE';
    ELSE
      OPERATION_EVENT = 'INSERT';
  END

  SELECT MAX(GROUPID) FROM LOG_DATA INTO :GROUPIDNO;

  IF(GROUPIDNO IS NULL) THEN    
    GROUPIDNO = 1;
  ELSE
    GROUPIDNO = GROUPIDNO + 1;

  IF(INSERTING) THEN
  BEGIN
FOR SELECT RDB$FIELD_NAME FROM RDB$RELATION_FIELDS WHERE RDB$RELATION_NAME = 'ARAC' INTO :FN       DO
  BEGIN
        
        OLDCOL = 'old.'||:FN;
        NEWCOL = 'new.'||:FN;

        INSERT INTO LOG_DATA (OLD_VALUE,NEW_VALUE, COLUMN_NAME, TABLE_NAME, OPERATION, 
         CREATEDAT,USERS,GROUPID,LOGDATAID)                                      
        VALUES     (:OLDCOL,:NEWCOL,:FN,'ARAC',trim(:OPERATION_EVENT),
           current_timestamp,:C_USER,:GROUPIDNO,:LOGDATAID_NO + 1);
  END
END

这是我的日志表的屏幕截图,我想插入旧值和新值,但列名被插入为字符串 在此处输入图像描述

4

1 回答 1

1

问题是您试图将旧上下文和新上下文引用为字符串,这是不可能的。具体问题是:

OLDCOL = 'old.'||:FN;
NEWCOL = 'new.'||:FN;

这会产生一个带有值的字符串'old.<whatever the value of FN is>'(对于 和 相同new)。它不会FNOLDorNEW上下文中生成名称为 in 的列的值。

不幸的是,不可能按名称动态引用OLDNEW上下文中的列。您将明确需要在代码中使用OLD.columnnameNEW.columnname,这意味着您将需要编写(或生成)一个单独插入每一列的触发器。

或者,您可以升级到 Firebird 3,并使用 UDR 在本机代码、C# 或 Java(或其他支持的语言)中定义触发器。这些 UDR 引擎允许您动态引用旧上下文和新上下文中的列。

例如,使用FB/Java 外部引擎(查看存储库中关于如何安装 FB/Java 的自述文件):

创建CHANGELOG表:

create table changelog (
  id bigint generated by default as identity constraint pk_changelog primary key,
  tablename varchar(31) character set unicode_fss not null,
  row_id varchar(30) character set utf8,
  columnname varchar(31) character set unicode_fss not null,
  new_value varchar(2000) character set utf8,
  old_value varchar(2000) character set utf8,
  operation char(6) character set ascii not null,
  modification_datetime timestamp default localtimestamp not null 
)

还有一个 FB/Java 触发器:

package nl.lawinegevaar.fbjava.experiment;

import org.firebirdsql.fbjava.TriggerContext;
import org.firebirdsql.fbjava.Values;
import org.firebirdsql.fbjava.ValuesMetadata;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiPredicate;

import static java.util.Collections.unmodifiableSet;
import static org.firebirdsql.fbjava.TriggerContext.Action.*;

public class ChangelogTrigger {

    private static final Set<TriggerContext.Action> SUPPORTED_ACTIONS =
            unmodifiableSet(EnumSet.of(INSERT, UPDATE, DELETE));

    private static final int ROW_ID_LENGTH = 30;
    private static final int VALUE_LENGTH = 2000;

    private static final String INSERT_CHANGELOG =
            "insert into changelog (tablename, row_id, columnname, new_value, old_value, operation) " 
                    + "values (?, ?, ?, ?, ?, ?)";

    public static void logChanges() throws SQLException {
        TriggerContext ctx = TriggerContext.get();
        TriggerContext.Action action = ctx.getAction();
        if (!SUPPORTED_ACTIONS.contains(action)) {
            return;
        }
        String tableName = ctx.getTableName();
        if (tableName.equals("CHANGELOG")) {
            throw new IllegalStateException("Cannot apply ChangelogTrigger to table " + tableName);
        }
        String identifierColumn = ctx.getNameInfo();
        ValuesMetadata fieldsMetadata = ctx.getFieldsMetadata();
        int identifierIdx = identifierColumn != null ? fieldsMetadata.getIndex(identifierColumn) : -1;
        Values oldValues = ctx.getOldValues();
        Values newValues = ctx.getNewValues();
        Values primaryValueSet = action == INSERT ? newValues : oldValues;
        String identifierValue = identifierIdx != -1
                ? truncate(asString(primaryValueSet.getObject(identifierIdx)), ROW_ID_LENGTH)
                : null;

        try (PreparedStatement pstmt = ctx.getConnection().prepareStatement(INSERT_CHANGELOG)) {
            pstmt.setString(1, tableName);
            pstmt.setString(2, identifierValue);

            BiPredicate<Object, Object> logColumn = action == UPDATE
                    ? ChangelogTrigger::acceptIfModified
                    : ChangelogTrigger::acceptAlways;

            boolean batchUsed = false;
            for (int idx = 1; idx <= fieldsMetadata.getParameterCount(); idx++) {
                Object oldValue = oldValues != null ? oldValues.getObject(idx) : null;
                Object newValue = newValues != null ? newValues.getObject(idx) : null;
                if (logColumn.test(oldValue, newValue)) {
                    String columnName = fieldsMetadata.getName(idx);
                    pstmt.setString(3, columnName);
                    pstmt.setString(4, truncate(asString(newValue), VALUE_LENGTH));
                    pstmt.setString(5, truncate(asString(oldValue), VALUE_LENGTH));
                    pstmt.setString(6, action.name());

                    pstmt.addBatch();
                    batchUsed = true;
                }
            }

            if (batchUsed) {
                pstmt.executeBatch();
            }
        }
    }

    private static boolean acceptAlways(Object oldValue, Object newValue) {
        return true;
    }

    private static boolean acceptIfModified(Object oldValue, Object newValue) {
        return !Objects.equals(oldValue, newValue);
    }

    private static String asString(Object value) {
        return value != null ? String.valueOf(value) : null;
    }

    private static String truncate(String value, int maxLength) {
        if (value == null || value.length() <= maxLength) {
            return value;
        }

        return value.substring(0, maxLength - 3) + "...";
    }
}

这个 FB/Java 触发器非常通用,可以用于多个表。我没有用所有数据类型测试过这个触发器。例如,要使触发器能够正确处理 blob 类型或其他二进制类型的列,将需要额外的工作。

使用FB/Java的fbjava-deployer实用程序构建触发器并将其加载到数据库中。

然后在你想要的表上定义触发器(本例中我是在TEST_CHANGELOG表上定义的):

create trigger log_test_changelog
  before insert or update or delete
  on test_changelog
  external name 'nl.lawinegevaar.fbjava.experiment.ChangelogTrigger.logChanges()!ID' 
  engine JAVA

外部名称定义要调用的例程 ( nl.lawinegevaar.fbjava.experiment.ChangelogTrigger.logChanges()) 和表 ( ) 的(单个)主键列的名称,ID用于记录ROW_ID列中的标识符。

于 2021-02-01T11:03:46.457 回答