Write Selected Database Tables

The operation of writing to databases in AIMMS is tuned for performance.

In an application with several database table, it is not very efficient to write all database tables. Much more efficient is to skip writing to those tables where data has not changed.

To do so, you can use a combination of the function ReferencedIdentifiers, the construct DatachangeMonitor, and runtime libraries.

Example of database tables

Here is an example with two database tables. The mechanism works the same for any number of database tables.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
DeclarationSection Database_table_declarations {
   DatabaseTable db_ab {
      DataSource: "data\\abc.dsn";
      TableName: "TableAB";
      Mapping: {
            "NamesA" -->i_a,
            "NamesB" -->i_b,
            "Vals1"  -->p_AB1( i_a, i_b ),
            "vals2"  -->p_AB2( i_a, i_b )
      }
   }
   DatabaseTable db_bc {
      DataSource: "data\\abc.dsn";
      TableName: "TableBC";
      Mapping: {
            "NamesB" -->i_b,
            "NamesC" -->i_c,
            "Vals3"  -->p_BC1( i_b, i_c )
      }
   }
}
  • Writing to table db_ab saves the data of i_a, i_b, p_AB1, p_AB2. In other words, when the data of i_a, i_b, p_AB1, or p_AB2 is changed, we want to write to table db_ab.
  • Writing to table db_bc saves the data of i_b, i_c, p_BC1. Again, when the data of i_b, i_c, p_BC1 is changed, we want to write to table db_bc.

This was the original procedure to write the data:

1
2
3
4
5
6
 Procedure pr_OriginalDatabaseWriteProcedure {
   Body: {
            write to table db_ab;
            write to table db_bc;
   }
}

However, we want to change it to something like this (in pseudo code):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 Procedure pr_TargetDatabaseWriteProcedure {
     Body: {
         if a set or parameter referenced in db_ab is changed then
             write to table db_ab;
         endif ;

         if a set or parameter referenced in db_bc is changed then
             write to table db_bc;
         endif ;
     }
 }

To avoid coding errors and maintenance issues from doing this manually, AIMMS has the following facilities:

  • The predeclared function ReferencedIdentifiers (see sidebar) examines portions of AIMMS code and returns the identifiers referenced.
  • The construct DatachangeMonitor (see sidebar) checks a given set of AIMMS identifiers for changed values.
  • Runtime libraries, that is AIMMS code generated in the model that can be activated in the same session.
    Each database table is monitored separately, so you need a separate monitor for each table. We need runtime libraries because each monitor has a reference to a set, not the value of a set.

By automating the use of ReferencedIdentifiers and DatachangeMonitors we avoid maintenance problems.

Example of runtime library

Code writing runtime libraries are a bit abstract.

Before trying to understand a procedure that creates a runtime library, let’s take an example of code created by such a procedure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 LibraryModule RuntimeLibraryDatachangeMonitorsForDatabaseTablesToBeWritten {
     Prefix: rldmfdttbw;
     DeclarationSection Datachange_monitor_names_and_sets {
         Set MonitorSet_db_ab {
             SubsetOf: AllIdentifiers;
             Definition: data { s_A, s_B, p_AB1, p_AB2 };
         }
         Set MonitorSet_db_bc {
             SubsetOf: AllIdentifiers;
             Definition: data { s_B, s_C, p_BC1 };
         }
     }
     Procedure pr_InitDatachangeMonitors {
         Body: {
             DataChangeMonitorCreate("DatachangeMonitor_db_ab",MonitorSet_db_ab,1);
             DataChangeMonitorCreate("DatachangeMonitor_db_bc",MonitorSet_db_bc,1);
         }
     }
     Procedure pr_WriteTablesWhenDataChanged {
         Body: {
             if DataChangeMonitorHasChanged("DatachangeMonitor_db_ab") then
                       write to table db_ab;
                       p01_dbWritten('db_ab') := 1;
                       DataChangeMonitorReset( "DatachangeMonitor_db_ab", MonitorSet_db_ab );
             endif;

             if DataChangeMonitorHasChanged("DatachangeMonitor_db_bc") then
                       write to table db_bc;
                       p01_dbWritten('db_bc') := 1;
                       DataChangeMonitorReset( "DatachangeMonitor_db_bc", MonitorSet_db_bc );
             endif;
         }
     }
 }

An explanation of the contents for the database table db_ab follows below. In addition, the library shows how the repetition is done for subsequent tables such as db_bc.

  • line 1: The name of the runtime library. Here, unique and descriptive of the purpose in Camel Case.
  • line 2: The prefix. Here, acronym of the runtime library name in lower case.
  • line 6: The sets and parameters referenced in the first database table, constructed using the function ReferencedIdentifiers.
  • line 4-7: A set declaration and definition for the identifiers referenced in the first table.
  • line 15: Create a datachange monitor for table db_ab using the set MonitorSet_db_ab.
  • line 21: Check if data is changed for table db_ab.
  • line 22: Perform the actual write action.
  • line 23: Mark the table as written.
  • line 24: Reset the data change monitor.

Create the runtime library

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
Procedure pr_CreateAndInitializeRuntimeLibraryForTableWriteManagement {
   Body: {
      ! Initialize writing the library.
      sp_runtimePrefix := "rldmfdttbw" ;
      if ep_runtimeLib then
            me::Delete( ep_runtimeLib );
      endif ;
         ep_runtimeLib := me::CreateLibrary( "RuntimeLibraryDatachangeMonitorsForDatabaseTablesToBeWritten", sp_runtimePrefix);
      ep_runtimeDecl := me::Create("Datachange monitor names and sets",'declaration', ep_runtimeLib);

      ! The set of database tables that are to be managed via data change monitors
      s_SelectedDatabaseTables := AllDatabaseTables * Database_table_declarations ;

      ! initialize the text for the runtime procedures
      sp_bodyInitProc := "" ;
      sp_bodyWriteProc := "" ;

      for i_db do ! for each database table to be handled

            ! Initialization per table.
            sp_bodyLineWrite := "" ;
            sp_bodyLineInit := "" ;

            ! Determine the collection of identifiers referenced in the database table.
            s_singleTon := i_db ;
            s_RefdIds := ReferencedIdentifiers(
                  searchIdentSet :  s_singleTon,
                  searchAttrSet  :  AllAttributeNames,
                  recursive      :  1)
                     * ( AllVariables + AllParameters + AllSets );
            s_ReferencedIdentifiersByDatabaseTables(i_db) := s_RefdIds ;

            ! Create the data change monitor code
            sp_set := "data " + s_RefdIds ;
             ep_datachangeMonitorSet(i_db):= me::Create(FormatString("MonitorSet_%e", i_db), 'set', ep_runtimeDecl);
             me::SetAttribute(ep_datachangeMonitorSet(i_db),'subset of', "AllIdentifiers");
             me::SetAttribute(ep_datachangeMonitorSet(i_db),'definition', sp_set);
            sp_datachangeMonitorName( i_db ) := formatString( "DatachangeMonitor_%e", i_db );

            ! To initialize a data change monitor, we'll only have to call DataChangeMonitorCreate.
            sp_bodyLineInit  := formatString("DataChangeMonitorCreate(\"DatachangeMonitor_%e\",MonitorSet_%e,1);\n",i_db,i_db);
            sp_bodyInitProc += sp_bodyLineInit ;

            ! To write to a database table, but only when data is changed, we need to
            ! 1) check DataChangeMonitorHasChanged,
            ! 2) actually write,
            ! 3) register the writing, and
            ! 4) Reset the data change monitor.
            sp_bodyLineWrite := formatString("if DataChangeMonitorHasChanged(\"DatachangeMonitor_%e\") then\n",i_db);
            sp_bodyLineWrite += formatString("          write to table %e;\n",i_db); ;
            sp_bodyLineWrite += formatString("          p01_dbWritten(\'%e\') := 1;\n",i_db); ;
            sp_bodyLineWrite += formatString("          DataChangeMonitorReset( \"DatachangeMonitor_%e\", MonitorSet_%e );\n",i_db,i_db) ;
            sp_bodyLineWrite += formatString("endif;\n\n") ;
            sp_bodyWriteProc += sp_bodyLineWrite ;

      endfor ;

         ep_InitProc := me::Create( "pr_InitDatachangeMonitors", 'procedure', ep_runtimeLib);
      me::SetAttribute( ep_InitProc, 'body', sp_bodyInitProc);

      ep_WriteProc := me::Create( "pr_WriteTablesWhenDataChanged", 'procedure', ep_runtimeLib);
      me::SetAttribute( ep_WriteProc, 'body', sp_bodyWriteProc);

      me::Compile(ep_runtimeLib);
      apply(ep_InitProc);
   }
   Set s_Singleton {
      SubsetOf: AllIdentifiers;
   }
   Set s_RefdIds {
      SubsetOf: AllIdentifiers;
   }
}

Notes:

  • sp_bodyLineInit, sp_bodyInitProc we collect the text for the datachange monitor initialization procedure.
  • sp_bodyLineWrite, sp_bodyWriteProc we collect the text for the write procedure, as illustrated in the previous section.

Call to write the database tables

1
2
3
4
5
6
7
Procedure pr_SaveModifiedDatabaseTables {
   Body: {
      p01_dbWritten(i_db) := 0 ;
      apply( ep_writeProc);
   }
   Comment: "Write the database tables by calling a runtime created procedure.""
}

Essentially just an apply statement of the procedure we created above.

Example project

Download the attached project for an example.