Issues arising with Business Logic ADCs

The example ADCs supplied with DP4 are generic ADCs - they do not contain any code specific to a particular database, or require any knowledge of the structure of tables on a database. The same is unlikely to be true for an ADC which implements business logic. Typical tasks for such an ADC might be to maintain the total of a particular field across a group of records (or even a whole table) in a record in another table, or to create journal records containing information about fields updated in records in another table. When writing such an ADC you need to consider the problems that can arise through the use of data independence.

For example consider the following scenario:

  1. An order entry program is written to record sales orders. An ADC is written which intercepts updates to the order-line table and updates a quantity_in_stock field in the material records corresponding to order lines. Both the ADC and the order entry program use a header file that uses the database structures from generation 1.

  2. Later on it becomes necessary to add a new "status" field to the order line table. The "status" field is not required by the order-entry program itself, but by some batch program that is run later on in the life cycle of the order. Adding the "status" field causes data independence to be required for the order line table, and, as it happens, changes the position of the quantity field within the order line. (In fact adding a new field may well not change the position of the existing fields in a record, but it is likely to do so if the field was already used elsewhere on the database, or the record contains fields with roles and the new field does not have a role). The database generation is now 2

At this point the ADC and the order-entry program still both perceive the order-line table with the structure as it was at generation 1. Therefore the ADC will continue to update the stock table correctly when it intercepts calls from the order entry program. But what happens when the batch program is run? It will have been developed for generation 2, and will have a different view of the structure of the order line table. Perhaps we will be lucky and the ADC will somehow know that it does not have to update the stock table when intercepting calls from this program, but quite possibly we won't be lucky. It is very likely that the ADC will misbehave quite badly with this program.

The obvious solution is to run LIBMAKE to recreate the header files for both the order entry program and the ADC, and to rebuild both the program and the ADC. Then all the components in the system will use the most up to date structure for the table. In general you should always rebuild your programs so that the latest structure of the database is used for all tables, because relying on data independence to keep your program working will cause some performance degradation. Also updating a record from an old program will cause data to be lost from any fields added since that program was built.Prior to release 4.523 there are no other options: When you have a business logic ADC you must ensure that all programs, and the ADC itself, are compiled using header files appropriate to the latest structure of the database.

There is one exception to this - if the tables that have changed are accessed only by the ADC, then you do not need to recompile the applications, provided the ADC is careful to set and reset the database generation number when accessing the changed tables. For example, suppose the ADC writes data to a journal table which has had new fields added to it since the applications were written. You could update the journal tables inside the ADC with code something like this:

int u_post( struct DATAREC * datarec,int flags)
{
  if (db_nr == my_db_nr && datarec->typenr == _ORDER_LINE)
  {
     int save_generation = db_generation;
     struct ORDER_LINE * order_line = (struct ORDER_LINE *) datarec;
     db_generation = SALESORD_GENERATION;
     sf_get_date_time(&journal.update_date,&journal.update_time);
     journal.order_nr = order_line->order_nr;
     journal.linenr = order_line->linenr;
     MOVE(journal.username,username);
     rec_post(&journal.l);   
     db_generation = save_generation;
  }
  return 1;
}

The expression in the if statement is typical of a business logic ADC. The variable my_db_nr would be set in the code for u_dbopen(), when the database for which journalling is required is opened. You can then test for tabels of interest by examining the typenr field of records.

The constant SALESORD_GENERATION represents the generation number for the database (SALESORD) at the time the ADC is compiled. Prior to release 4.523 you have to define this constant for yourself, but from this release LIBMAKE will define a constant DBNAME_GENERATION when generating C header files. It is much safer to rely on a constant like this than querying the database generation, (for example by calling db_get_generation()), because a program can only safely use the current generation number if it dynamically queries the data structure, which requires a lot of extra knowledge of how DP4 works.

It is very important only to use a modified generation number when accessing data private to the ADC. For example suppose the code above is slightly modified:

int u_post( struct DATAREC * datarec,int flags)
{
  if (db_nr == my_db_nr && datarec->typenr == _ORDER_LINE)
  {
     int save_generation = db_generation;
     struct ORDER_LINE * order_line = (struct ORDER_LINE *) datarec;
     db_generation = SALESORD_GENERATION;
     sf_get_date_time(&journal.update_date,&journal.update_time);
     journal.order_nr = order_line->order_nr;
     journal.linenr = order_line->linenr;
     MOVE(journal.username,username);
     rec_post(&journal.l);   
     rec_post(&order_line->l);
     db_generation = save_generation;
     return 0;
  }
  return 1;
}

This code is potentially unsafe if the order_line table has been modified since the calling program was written. The order_line structure is in the application's view of the structure, possibly missing fields that have been added since (although this is contrary to our assumption that only the journal table has been modified, it is worth bearing this possibility in mind). Now however the database manager will believe the order_line structure is in the structure for the generation used by the ADC. This could lead to problems with data independence.

Use of Data Independence in an ADC

From version 4.523 it is possible, but by no means recommended, to use data independence in an ADC. In order to use this facility you have to link in the new ADC module AUXDI. This implements a function that can convert a DP4 record between any two generation numbers. The actual implementation of the function is inside the regular database manager. Using this function will certainly impact performance to some extent, so the function should be used sparingly. The following example shows how the code above could be adapted to make use of this facility:

int u_post( struct DATAREC * datarec,int flags)
{
  if (db_nr == my_db_nr && datarec->typenr == _ORDER_LINE)
  {
     int save_generation = db_generation;
     struct ORDER_LINE * order_line = (struct ORDER_LINE *) datarec;
     struct ORDER_LINE buffer;

     db_generation = SALESORD_GENERATION;
     sf_get_date_time(&journal.update_date,&journal.update_time);
     if (save_generation != SALESORD_GENERATION)
     {
       swap_data(&buffer.l,SALESORD_GENERATION,&order_line->l,save_generation);
       order_line = &buffer;
     }
     journal.order_nr = order_line->order_nr;
     journal.linenr = order_line->linenr;
     MOVE(journal.username,username);
     rec_post(&journal.l);   
     db_generation = save_generation;
  }
  return 1;
}

swap_data() is declared like this:

struct DATAREC * void swap_data(short * to,int to_generation,short * from, int from_generation)

The function converts the from record, based on the structure at database generation from_generation, to the to record, based on the structure at database generation to_generation. You should always use a different record area for the from and to records. The return value is to. to_generation and from_generation can be any valid generations for the database. You should note that conversion always takes place by converting the structure from the "from" generation up to the current generation, and then converting back to the "to" generation, so the values in any fields common to the two generations but subsequently deleted will be lost. Internally the database manager only supports conversion between generations where one generation is the current generation.

Using this function does not free you from having to manage the generation number carefully. You must still ensure that the generation number has the appropriate value when calling the database manager, and that data is returned to the application in the correct form. There are two possible ways you could do this, illustrated for calls to u_ftchprm():

Of the two methods, the first is probably to be preferred for the following reasons: