FirebirdSQL logo

Фабрика триггеров

Фабрика внешнего триггера должна реализовать интерфейсIUdrTriggerFactory. Для упрощения просто наследуем классIUdrTriggerFactoryImpl. Для каждого внешнего триггера нужна свояфабрика.

Метод dispose вызывается при уничтожении фабрики, в нём мы должныосвободить ранее выделенные ресурсы. В данном случае просто вызываемдеструктор.

Метод setup выполняется каждый раз при загрузке внешнего триггера в кешметаданных. В нём можно делать различные действия которые необходимыперед созданием экземпляра триггера, например для изменения форматасообщений для полей таблицы. Более подробно поговорим о нём позже.

Метод newItem вызывается для создания экземпляра внешнего триггера. Вэтот метод передаётся указатель на статус вектор, контекст внешнеготриггера и метаданные внешнего триггера. С помощью IRoutineMetadata выможете получить формат сообщения для новых и старых значений полей, теловнешнего триггера и другие метаданные. В этом методе вы можете создаватьразличные экземпляры внешнего триггера в зависимости от его объявления вPSQL. Метаданные можно передать в созданный экземпляр внешнего триггераесли это необходимо. В нашем случае мы просто создаём экземпляр внешнеготриггера TMyTrigger.

Фабрику триггера, а также сам триггер расположим в модуле TestTrigger.

unit TestTrigger;

{$IFDEF FPC}
{$MODE DELPHI}{$H+}
{$ENDIF}

interface

uses
  Firebird, SysUtils;

type
  { **********************************************************
    create table test (
      id int generated by default as identity,
      a int,
      b int,
      name varchar(100),
      constraint pk_test primary key(id)
    );

    create or alter trigger tr_test_biu for test
    active before insert or update position 0
    external name 'myudr!test_trigger'
    engine udr;
  }

  // структура для отображения сообщений NEW.* и OLD.*
  // должна соответствовать набору полей таблицы test
  TFieldsMessage = record
    Id: Integer;
    IdNull: WordBool;
    A: Integer;
    ANull: WordBool;
    B: Integer;
    BNull: WordBool;
    Name: record
      Length: Word;
      Value: array [0 .. 399] of AnsiChar;
    end;
    NameNull: WordBool;
  end;

  PFieldsMessage = ^TFieldsMessage;

  // Фабрика для создания экземпляра внешнего триггера TMyTrigger
  TMyTriggerFactory = class(IUdrTriggerFactoryImpl)
    // Вызывается при уничтожении фабрики
    procedure dispose(); override;

    { Выполняется каждый раз при загрузке внешнего триггера в кеш метаданных.
      Используется для изменения формата сообщений для полей.

      @param(AStatus Статус вектор)
      @param(AContext Контекст выполнения внешнего триггера)
      @param(AMetadata Метаданные внешнего триггера)
      @param(AFieldsBuilder Построитель сообщения для полей таблицы)
    }
    procedure setup(AStatus: IStatus; AContext: IExternalContext;
      AMetadata: IRoutineMetadata; AFieldsBuilder: IMetadataBuilder); override;

    { Создание нового экземпляра внешнего триггера TMyTrigger

      @param(AStatus Статус вектор)
      @param(AContext Контекст выполнения внешнего триггера)
      @param(AMetadata Метаданные внешнего триггера)
      @returns(Экземпляр внешнего триггера)
    }
    function newItem(AStatus: IStatus; AContext: IExternalContext;
      AMetadata: IRoutineMetadata): IExternalTrigger; override;
  end;

  TMyTrigger = class(IExternalTriggerImpl)
    // Вызывается при уничтожении триггера
    procedure dispose(); override;

    { Этот метод вызывается непосредственно перед execute и сообщает
      ядру наш запрошенный набор символов для обмена данными внутри
      этого метода. Во время этого вызова контекст использует набор символов,
      полученный из ExternalEngine::getCharSet.

      @param(AStatus Статус вектор)
      @param(AContext Контекст выполнения внешнего триггера)
      @param(AName Имя набора символов)
      @param(AName Длина имени набора символов)
    }
    procedure getCharSet(AStatus: IStatus; AContext: IExternalContext;

      AName: PAnsiChar; ANameSize: Cardinal); override;

    { выполнение триггера TMyTrigger

      @param(AStatus Статус вектор)
      @param(AContext Контекст выполнения внешнего триггера)
      @param(AAction Действие (текущее событие) триггера)
      @param(AOldMsg Сообщение для старых значение полей :OLD.*)
      @param(ANewMsg Сообщение для новых значение полей :NEW.*)
    }
    procedure execute(AStatus: IStatus; AContext: IExternalContext;
      AAction: Cardinal; AOldMsg: Pointer; ANewMsg: Pointer); override;
  end;

implementation

{ TMyTriggerFactory }

procedure TMyTriggerFactory.dispose;
begin
  Destroy;
end;

function TMyTriggerFactory.newItem(AStatus: IStatus; AContext: IExternalContext;
  AMetadata: IRoutineMetadata): IExternalTrigger;
begin
  Result := TMyTrigger.create;
end;

procedure TMyTriggerFactory.setup(AStatus: IStatus; AContext: IExternalContext;
  AMetadata: IRoutineMetadata; AFieldsBuilder: IMetadataBuilder);
begin

end;

{ TMyTrigger }

procedure TMyTrigger.dispose;
begin
  Destroy;
end;

procedure TMyTrigger.execute(AStatus: IStatus; AContext: IExternalContext;
  AAction: Cardinal; AOldMsg, ANewMsg: Pointer);
var
  xOld, xNew: PFieldsMessage;
begin
  // xOld := PFieldsMessage(AOldMsg);
  xNew := PFieldsMessage(ANewMsg);
  case AAction of
    IExternalTrigger.ACTION_INSERT:
      begin
        if xNew.BNull and not xNew.ANull then
        begin
          xNew.B := xNew.A + 1;
          xNew.BNull := False;
        end;
      end;

    IExternalTrigger.ACTION_UPDATE:
      begin
        if xNew.BNull and not xNew.ANull then
        begin
          xNew.B := xNew.A + 1;
          xNew.BNull := False;
        end;
      end;

    IExternalTrigger.ACTION_DELETE:
      begin

      end;
  end;
end;

procedure TMyTrigger.getCharSet(AStatus: IStatus; AContext: IExternalContext;
  AName: PAnsiChar; ANameSize: Cardinal);
begin

end;

end.

Сообщения

Под сообщением в UDR понимается область памяти фиксированного размерадля передачи в процедуру или функцию входных аргументов, или возвратавыходных аргументов. Для внешних триггеров на события записи таблицысообщения используются для получения и возврата данных в NEW и OLD.

Для доступа к отдельным переменным или полям таблицы, необходимо знатькак минимум тип этой переменной, и смещение от начала буфера сообщений.Как уже упоминалось ранее для этого существует два способа:

  • преобразование указателя на буфер сообщения к указателю на статическуюструктуру (в Delphi это запись, т.е. record);

  • получение смещений с помощью экземпляра класса реализующего интерфейсIMessageMetadata, и чтение/запись из буфера данных, размеромсоответствующим типу переменной или поля.

Первый способ является наиболее быстрым, второй — более гибким, так какв ряде случаев позволяет изменять типы и размеры для входных и выходныхпеременных или полей таблицы без перекомпиляции динамической библиотекисодержащей UDR.

Работа с буфером сообщения с использованием структуры

Как говорилось выше мы можем работать с буфером сообщений черезуказатель на структуру. Такая структура выглядит следующим образом:

Синтаксис
TMyStruct = record
  <var_1>: <type_1>;
  <nullIndicator_1>: WordBool;
  <var_2>: <type_1>;
  <nullIndicator_2>: WordBool;
  ...
  <var_N>: <type_1>;
  <nullIndicator_N>: WordBool;
end;
PMyStruct = ^TMyStruct;

Типы членов данных должны соответствовать типам входных/выходныхпеременных или полей (для триггеров). Null-индикатор должен быть послекаждой переменной/поля, даже если у них есть ограничение NOT NULL.Null-индикатор занимает 2 байта. Значение -1 обозначает чтопеременная/поле имеют значение NULL. Поскольку на данный момент вNULL-индикатор пишется только признак NULL, то удобно отразить его на2-х байтный логический тип. Типы данных SQL отображаются в структуреследующим образом:

Table 1. Отображение типов SQL на типы Delphi
SQL тип Delphi тип Замечание

BOOLEAN

Boolean, ByteBool

SMALLINT

Smallint

INTEGER

Integer

BIGINT

Int64

INT128

FB_I128

Доступно начиная с Firebird 4.0.

FLOAT

Single

DOUBLE PRECISION

Double

DECFLOAT(16)

FB_DEC16

Доступно начиная с Firebird 4.0.

DECFLOAT(34)

FB_DEC34

Доступно начиная с Firebird 4.0.

NUMERIC(N, M)

Тип данных зависит от точности и диалекта:

  • 1-4 — Smallint;

  • 5-9 — Integer;

  • 10-18 (3 диалект) — Int64;

  • 10-15 (1 диалект) — Double;

  • 19-38 - FB_I128 (начиная с Firebird 4.0).

В качестве значения в сообщение будет передано число умноженное на10M.

DECIMAL(N, M)

Тип данных зависит от точности и диалекта:

  • 1-4 — Integer;

  • 5-9 — Integer;

  • 10-18 (3 диалект) — Int64;

  • 10-15 (1 диалект) — Double;

  • 19-38 - FB_I128 (начиная с Firebird 4.0).

В качестве значения в сообщение будет передано число умноженное на10M.

CHAR(N)

array[0 .. M] of AnsiChar

M вычисляется по формуле M = N * BytesPerChar - 1, гдеBytesPerChar - количество байт на символ, зависит от кодировкипеременной/поля. Например, для UTF-8 - это 4 байт/символ, для WIN1251 - 1байт/символ.

VARCHAR(N)

record
  Length: Smallint;
  Data: array[0 .. M] of AnsiChar;
end

M вычисляется по формуле M = N * BytesPerChar - 1, гдеBytesPerChar - количество байт на символ, зависит от кодировкипеременной/поля. Например, для UTF-8 - это 4 байт/символ, для WIN1251 - 1байт/символ. В Length передаётся реальная длина строки в символах.

DATE

ISC_DATE

TIME

ISC_TIME

TIME WITH TIME ZONE

ISC_TIME_TZ

Доступно начиная с Firebird 4.0.

TIMESTAMP

ISC_TIMESTAMP

TIMESTAMP WITH TIME ZONE

ISC_TIMESTAMP_TZ

Доступно начиная с Firebird 4.0.

BLOB

ISC_QUAD

Содержимое BLOB никогда не передаётсянепосредственно, вместо него передаётся BlobId. Как работать с типомBLOB будет рассказано в главе Работа с типом BLOB.

Теперь рассмотрим несколько примеров того как составлять структурысообщений по декларациям процедур, функций или триггеров.

Предположим у нас есть внешняя функция объявленная следующим образом:

function SUM_ARGS(A SMALLINT, B INTEGER) RETURNS BIGINT
....

В этом случае структуры для входных и выходных сообщений будут выглядетьтак:

TInput = record
  A: Smallint;
  ANull: WordBool;
  B: Integer;
  BNull: WordBool;
end;
PInput = ^TInput;

TOutput = record
  Value: Int64;
  Null: WordBool;
end;
POutput = ^TOutput;

Если та же самая функция определена с другими типами (в 3 диалекте):

function SUM_ARGS(A NUMERIC(4, 2), B NUMERIC(9, 3)) RETURNS NUMERIC(18, 6)
....

В этом случае структуры для входных и выходных сообщений будут выглядетьтак:

TInput = record
  A: Smallint;
  ANull: WordBool;
  B: Integer;
  BNull: WordBool;
end;
PInput = ^TInput;

TOutput = record
  Value: Int64;
  Null: WordBool;
end;
POutput = ^TOutput;

Предположим у нас есть внешняя процедура объявленная следующим образом:

procedure SOME_PROC(A CHAR(3) CHARACTER SET WIN1251, B VARCHAR(10) CHARACTER SET UTF8)
....

В этом случае структура для входного сообщения будет выглядеть так:

TInput = record
  A: array[0..2] of AnsiChar;
  ANull: WordBool;
  B: record
    Length: Smallint;
    Value: array[0..39] of AnsiChar;
  end;
  BNull: WordBool;
end;
PInput = ^TInput;

Работа с буфером сообщений с помощью IMessageMetadata

Как было описано выше с буфером сообщений можно работать сиспользованием экземпляра объекта реализующего интерфейсIMessageMetadata. Этот интерфейс позволяет узнать о переменной/полеследующие сведения:

  • имя переменной/поля;

  • тип данных;

  • набор символов для строковых данных;

  • подтип для типа данных BLOB;

  • размер буфера в байтах под переменную/поле;

  • может ли переменная/поле принимать значение NULL;

  • смещение в буфере сообщений для данных;

  • смещение в буфере сообщений для NULL-индикатора.