我需要在 firebird 2.5 的触发器中动态插入字段名称和新旧值
I need to insert dynamically the field names and values old and new in a trigger, in firebird 2.5
我可以动态插入 table 列名问题是当我想在我的日志中插入新值或旧值时 table 我得到一个字符串 '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
这是我的日志的屏幕截图 table,我想插入旧值和新值,但列名称被插入为字符串
问题是您试图将旧上下文和新上下文作为字符串引用,这是不可能的。具体问题是:
OLDCOL = 'old.'||:FN;
NEWCOL = 'new.'||:FN;
这会生成一个值为 'old.<whatever the value of FN is>'
的字符串(new
也是如此)。它不会从 OLD
或 NEW
上下文中生成名称为 FN
的列的值。
遗憾的是,无法按名称动态引用 OLD
和 NEW
上下文中的列。您将明确需要在代码中使用 OLD.columnname
和 NEW.columnname
,这意味着您将需要编写(或生成)单独插入每一列的触发器。
或者,您可以升级到 Firebird 3,并使用 UDR 以本机代码、C# 或 Java(或其他支持的语言)定义触发器。这些 UDR 引擎允许您动态引用旧上下文和新上下文中的列。
例如,使用 FB/Java external engine(查看存储库中关于如何安装 FB/Java 的自述文件):
创建 CHANGELOG
table:
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触发器非常通用,可以用于多个table。我还没有用所有数据类型测试这个触发器。例如,为了能够使触发器正确处理 blob 类型或其他二进制类型的列,将需要额外的工作。
构建触发器并使用 FB/Java.
的 fbjava-deployer 实用程序将其加载到数据库中
然后在你想要的table上定义触发器(在本例中,我在TEST_CHANGELOG
table上定义它):
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()
) 和 table (ID
) 的(单个)主键列的名称,用于在 ROW_ID
列中记录标识符。
我可以动态插入 table 列名问题是当我想在我的日志中插入新值或旧值时 table 我得到一个字符串 '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
这是我的日志的屏幕截图 table,我想插入旧值和新值,但列名称被插入为字符串
问题是您试图将旧上下文和新上下文作为字符串引用,这是不可能的。具体问题是:
OLDCOL = 'old.'||:FN;
NEWCOL = 'new.'||:FN;
这会生成一个值为 'old.<whatever the value of FN is>'
的字符串(new
也是如此)。它不会从 OLD
或 NEW
上下文中生成名称为 FN
的列的值。
遗憾的是,无法按名称动态引用 OLD
和 NEW
上下文中的列。您将明确需要在代码中使用 OLD.columnname
和 NEW.columnname
,这意味着您将需要编写(或生成)单独插入每一列的触发器。
或者,您可以升级到 Firebird 3,并使用 UDR 以本机代码、C# 或 Java(或其他支持的语言)定义触发器。这些 UDR 引擎允许您动态引用旧上下文和新上下文中的列。
例如,使用 FB/Java external engine(查看存储库中关于如何安装 FB/Java 的自述文件):
创建 CHANGELOG
table:
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触发器非常通用,可以用于多个table。我还没有用所有数据类型测试这个触发器。例如,为了能够使触发器正确处理 blob 类型或其他二进制类型的列,将需要额外的工作。
构建触发器并使用 FB/Java.
的 fbjava-deployer 实用程序将其加载到数据库中然后在你想要的table上定义触发器(在本例中,我在TEST_CHANGELOG
table上定义它):
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()
) 和 table (ID
) 的(单个)主键列的名称,用于在 ROW_ID
列中记录标识符。