Easily determine changed values in custom data-integration triggers with synchronize

Introduction

This topic provides shows how to use the custom data synchronization tool to accurately register and log the actual changed values.

Data integration tools

Invantive SQL provides out-of-the-box data synchronization methods and data synchronization tools to compare and synchronize data between systems for great data-integration and data quality. The tools synchronize data between data containers in a virtual Invantive-database and/or between tables in a single data container like a business accounting system.

In order of increasing flexibility, the most frequently used options for data synchronization are:

The synchronization process typically runs largely in-memory with or without paging, so the use of custom logic to horizontally/vertically segment, partition or distribute the data may be necessary for very large data sets to execute efficiently in terms of CPU and memory-resources and great performance.

Determining changed values with dbms_sync

The custom logic coded in Invantive PSQL allows custom actions like using a different approach than an insert or update on the tables being compared to synchronize them. Also, this logic allows extensive logging and avoiding updates for complex and known irrelevant changes, such as rounding errors due to picosecond time differences or known bugs in the linked systems.

For logging, often a list of old and new values are written to a table or output. However, maintenance is quite cumbersome: every new field needs to be handled accurately and compared. It is not uncommon for tables to have hundreds of columns, each requiring logic to determine the changes.

Starting release 22.1, a new method diff in the PSQL-package dbms_sync is available for use in data synchronization triggers of the synchronize. Calling diff within the context of a synchronization trigger will return a textual representation of the changes between the old and new data record in standard data formats.

For example, the text value printed from this statement:

dbms_output.put_line(dbms_sync());

has the following output format:

AddressLine1: [null] -> 123 Street; Blocked: [null] -> False; City: [null] -> Toronto; Code: [null] -> 2194981692; Country: [null] -> CA; Division: [null] -> 1; Email: [null] -> info@acme.com; InvoiceAttachmentType: [null] -> 3; InvoicingMethod: [null] -> 2; IsAccountant: [null] -> 0; IsPurchase: [null] -> False; IsSales: [null] -> False; IsSupplier: [null] -> False; Language: [null] -> NL; Name: [null] -> ACME; PaymentConditionSales: [null] -> 30; Phone: [null] -> +1 555 123 456; Postcode: [null] -> M5J 2M4; SearchCode: [null] -> CA-M5J2M4; Status: [null] -> C; Website: [null] -> https://acme.com

The format varies depending on the context: insert, update or delete. When values are changing, all changed values are included first in alphabetical order of the field names. After the changed values, the unchanged values are included. In the context of insert and delete solely the new, respectively, the old values are included.

Please note that also values of data fields excluded from triggering a “changed row” event using update all except ... are included in the textual output.

The simple and complete format eases debugging the harmonization process of data between systems.

Sample application

The following synchronize statement maintains logging on the details of the data synchronized between two tables on Exact Online. First, a logging table must be created on a persistent storage like SQL Server or like here in-memory:

create or replace table SyncLogs@InMemoryStorage
( occurrence_date datetime not null -- UTC
, action_type     char     not null -- 'I', 'D' or 'U' for Insert, Delete or Update.
, table_name      varchar2 not null
, nk1             varchar2 not null -- First element of business key.
, nk2             varchar2     null
, nk3             varchar2     null
, nk4             varchar2     null
, nk5             varchar2     null
, label           varchar2     null -- Textual background.
, actions         varchar2 not null -- Changed values.
--
, ip_address      varchar2 not null -- Audit fields.
, iuid            varchar2 not null
, date_created    datetime not null
, created_by      varchar2 not null
, created_at      varchar2 not null
, session_created varchar2 not null
)

The following synchronize statement updates Exact Online account information in all selected companies from an autoritative source.

By using an *Incremental table the amortized number of API calls is very limited to ultimately 2 API calls for detecting changes in the data and 1 API call for each change to be made. This enable frequent data synchronization to Exact Online, for instance every hour, even for companies with 100.000+ customer records.

synchronize ExactOnlineAccountsSoll@InMemoryStorage
to          AccountsIncremental@eol
with        update all
            --
            -- On insert these derived address fields must be set
            -- to avoid Exact Online making up address information itself.
            -- However, on update, the address fields are derived from the
            -- previously loaded addresses and maintained through Addresses.
            --
            except
            AddressLine1
            , AddressLine2
            , City
            , Country
            , Postcode
identified
by          Division
,           Code
batchsize 1
continue on first 50 errors
triggers
instead of update
begin
  dbms_output.put_line('instead of update on division ' || :new.Division || ' code ' || :new.code || ' - ' || :new.name || '. Previously updated ' || :old.Modified);
  insert into SYNCLOGS@InMemoryStorage
  ( occurrence_date
  , action_type
  , table_name
  , nk1
  , nk2
  , nk3
  , nk4
  , nk5
  , label
  , actions
  --
  , ip_address
  , iuid
  , date_created
  , created_by
  , created_at
  , session_created
  )
  values
  ( sysdateutc
  , 'U'
  , 'Accounts'
  , :new.Division
  , :new.Code
  , null
  , null
  , null
  , :new.Name
  , dbms_sync.diff()
  --
  , sys_context('USERENV', 'CLIENT_IP_ADDRESS_EXTERNAL', 'eol')
  , sys_context('USERENV', 'IUID', 'eol')
  , sysdateutc
  , sys_context('USERENV', 'CURRENT_USER', 'eol')
  , sys_context('USERENV', 'APPLICATION_FULL', 'eol')
  , sys_context('USERENV', 'SESSIONID', 'eol')
  );
  update ExactOnlineREST..Accounts@eol
  set    Name                     = :new.Name
  ,      TradeName                = :new.TradeName
  ,      Phone                    = :new.Phone
  ,      ChamberOfCommerce        = :new.ChamberOfCommerce
  ,      SearchCode               = :new.SearchCode
  ,      VATNumber                = :new.VATNumber
  ,      Website                  = :new.Website
  ,      Remarks                  = :new.Remarks
  ,      CodeAtSupplier           = :new.CodeAtSupplier
  ,      CompanySize              = :new.CompanySize
  ,      Email                    = :new.Email
  ,      EstablishedDate          = :new.EstablishedDate
  ,      InvoiceAttachmentType    = :new.InvoiceAttachmentType
  ,      InvoicingMethod          = :new.InvoicingMethod
  ,      IsAccountant             = :new.IsAccountant
  ,      IsPurchase               = :new.IsPurchase
  ,      IsSales                  = :new.IsSales
  ,      IsSupplier               = :new.IsSupplier
  ,      Blocked                  = :new.Blocked
  ,      Language                 = :new.Language
  ,      PaymentConditionSales    = :new.PaymentConditionSales
  ,      PaymentConditionPurchase = :new.PaymentConditionPurchase
  ,      SalesVATCode             = :new.SalesVATCode
  ,      Status                   = :new.Status
  ,      RSIN                     = :new.RSIN
  ,      BusinessType             = :new.BusinessType
  where  Division = :new.Division
  and    Code     = :new.Code
  ;
exception
  when others
  then
    dbms_output.put_line('Error on update division ' || :new.Division || ' code ' || :new.code || ' - ' || :new.name || ' due to ' || sqlerrm);
    raise;
end