Project

General

Profile

Data Model Objects

Introduction

A Data Model Object (DMO) is a Java object which represents a database record in a converted application. When records are loaded into the FWD application server from a database, each is represented by an instance of a DMO. A DMO may be created, modified, or deleted by application business logic, and those changes are propagated to the database through FWD's Object-to-Relational (ORM) framework.

The structure of the DMO is determined by the structure of its corresponding database table. During conversion, FWD creates a Java interface for each:

  • persistent table defined in a schema export file (i.e., a *.df file exported from the original, Progress database);
  • temp-table statically defined (i.e., using the DEFINE TEMP-TABLE statement) in 4GL source code.

These DMO interfaces are emitted by conversion as Java source code, which is compiled as part of the build of the converted application.

DMO interfaces also can be generated at runtime, for dynamically defined temp-tables (i.e., using the CREATE TEMP-TABLE statement). These interfaces are not emitted as source code, but rather are assembled as Java bytecode; they are loaded directly into the application server JVM at runtime.

DMO Class Hierarchy and Implementation

A hierarchy of Java interfaces and implementing classes are involved in providing database access to a converted application.

DMO Interfaces and Implementation Classes

A DMO interface is a Java interface. It defines the object-oriented API which the converted application uses to access data for a particular database table in the backing RDBMS. Using the DMO interface and the FWD runtime environment's persistence API, the application can read, insert, delete, and update database records.

DMO interfaces are generated as an output of FWD's conversion process. Static schema conversion (i.e., the "middle" part of the application conversion process) generates the Java source code of a DMO interface for each persistent table and for each statically defined temp-table used by the original application.

FWD's dynamic (runtime) conversion generates a DMO interface for each temp-table defined dynamically at runtime. No Java source code is created for dynamically defined temp-tables. Rather, Java bytecode for each dynamically defined DMO interface is assembled and loaded into the JVM at runtime.

A DMO implementation class is generated at runtime by FWD to implement each DMO interface. An implementation class is needed within the FWD runtime to manage the data of a database record. A converted application does not instantiate or manipulate instances of DMO implementation classes directly; all data access is performed via the DMO interface. No source code is generated for a DMO implementation class. Rather, Java bytecode for each DMO implementation class is assembled and loaded into the JVM at runtime.

Fields, Columns, Properties, and Methods

Progress 4GL terminology considers database tables to be relations of fields. A scalar field holds a single data value. An extent (i.e., array) field of extent N holds N values.

When discussing tables in a (non-Progress) RDBMS, we refer to fields as columns. Generally, there is a one-to-one relationship between a 4GL field and a SQL column. Extent fields may be represented in several forms in an RDBMS:

  • normalized (stored in a secondary table linked by foreign key to the primary table);
  • denormalized (exploded into N distinct columns in the primary table); or
  • in certain database implementations, as some form of array; this form differs in implementation by database dialect and may not be available in all implementations.

In a FWD DMO, each field/column is represented by a DMO property. A DMO property is represented as a Java object whose data type corresponds with the data type of the associated field/column.

Each DMO property is accessed through (at minimum) a pair of public methods: a getter method to retrieve a value of that property, and a setter method to set a value of that property.

By default, the names of properties, columns, and methods generally correspond with the original, legacy field names (with changes as needed to eliminate characters which would be illegal in the target environment). However, through certain conversion configuration options, the original field names may be mapped to new names. TODO: document name customization and extent field conversion options.

Annotations

Java annotations are used in the DMO interface to:

  • preserve legacy table information, structural and non-structural;
  • specify new information computed during conversion.

The non-structural, legacy information recorded in annotations would not strictly be considered database schema information. For example, a Progress schema might contain such information as validation expressions and messages, user interface labels, format strings, etc. This information cannot be output as part of the Data Definition Language (DDL) used to create a new RDBMS schema, because there is no analog for it in the DDL syntax. However, it is needed by the converted application, so it is preserved as annotations in the DMO interfaces.

There are several class-level annotations:

  • The @Table annotation specifies information about the DMO interface's backing table.
    • There is exactly one @Table annotation per DMO interface.
    • @Table annotation attributes:
      • name: the SQL name of the backing table.
      • legacy: the name of the legacy table.
  • The @Index annotation defines an index on the DMO interface's backing table.
    • There are zero or more @Index annotations per DMO interface.
    • @Index annotation attributes:
      • name: the SQL name of the index.
      • legacy: the legacy name of the index.
      • primary: true if the index represents the legacy table's primary index; else false.
      • unique: true if the index defines a unique constraint; else false.
      • components: an ordered list of one or more index components (a field participating in the index), each defined by its own @IndexComponent child annotation.
      • @IndexComponent annotation attributes:
        • name: the SQL name of the column representing the corresponding index field.
        • legacy: the legacy name of the index field.

Annotations are used to hold information about the DMO's properties:

  • The @Property annotation specifies information about the DMO property.
    • There is exactly one @Property annotation per DMO property.
    • It is associated with the getter method for that property.
    • @Property annotation attributes:
      • id: sequential, unique (within the DMO interface) identifier for the property, ordered according to the legacy order of fields.
      • name: Java name of the property.
      • column: SQL name of the database column associated with the property.
      • legacy: the name of the legacy field.
      • format: format string, if any, defined for the legacy field.
      • initial: initial value expression for the legacy field, applied to newly created records.
      • initialNull: true if the initial value state of the legacy field is unknown value. If exists, initial does not.
      • columnLabel: value of "COLUMN LABEL", if any, for the legacy field.
      • columnLabel_sa: value of "COLUMN LABEL SA", if any, for the legacy field.
      • label: value of "LABEL", if any, for the legacy field.
      • width: value of "MAX WIDTH", if any, for the legacy field.
      • order: value of "ORDER", if any, for the legacy field.
      • position: value of "POSITION", if any, for the legacy field.
      • TODO: extent

DMO Example

Consider a hypothetical, 4GL customer table, defined as follows in a schema export file:

ADD TABLE "customer" 
  AREA "Schema Area" 
  DUMP-NAME "customer" 

ADD FIELD "idNum" OF "customer" AS integer
  FORMAT "->,>>>,>>9" 
  INITIAL "0" 
  POSITION 10
  MAX-WIDTH 4
  ORDER 1

ADD FIELD "name" OF "customer" AS character
  FORMAT "x(8)" 
  INITIAL "" 
  POSITION 20
  MAX-WIDTH 16
  ORDER 2

ADD INDEX "pk" ON "customer" 
  AREA "Schema Area" 
  UNIQUE
  PRIMARY
  INDEX-FIELD "idNum" ASCENDING

The corresponding DMO interface might be defined as follows:

@Table(name = "customer", legacy = "customer")
@Indices(
{
   @Index(name = "customer_pk", legacy = "pk", primary = true, unique = true, components = 
   {
      @IndexComponent(name = "idNum", legacy = "idNum")
   })
})
public interface Customer
extends DataModelObject
{
   @Property(id = 1, name = "idNum", column = "id_num", legacy = "idNum", format = "->,>>>,>>9", initial = "0", width = 4, order = 1, position = 10)
   public integer getIdNum();

   public void setIdNum(NumberType idNum);

   @Property(id = 2, name = "name", column = "name", legacy = "name", format = "x(8)", width = 16, order = 2, position = 20)
   public character getName();

   public void setName(Text name);

   public interface Buf
   extends Customer, Buffer
   {
   }
}

DMO Interface Hierarchy

Persistent Table

The hierarchy for a top-level, generated DMO interface for a persistent table is:

DataModelObject (com.goldencode.p2j.persist)
 |
 +-<Generated DMO Interface>

All generated DMO interfaces for persistent tables directly extend com.goldencode.p2j.persist.DataModelObject. DataModelObject is a marker interface with no declared methods; it is used within the FWD runtime for type safety.

For example:

Customer.java

public interface Customer
extends DataModelObject
{
   // declared methods ...
   // ...

   /**
    * DMO interface for use by converted business logic.
    */
   public interface Buf
   extends Customer, Buffer
   {
   }
}

Temp-Table

The hierarchy for a top-level, generated DMO interface for a temp-table is:

DataModelObject (com.goldencode.p2j.persist)
 |
 +-Temporary (com.goldencode.p2j.persist)
    |
    +-<Generated DMO Interface> (e.g., TtCust_1)
       |
       +-<Generated DMO Interface> (e.g., TtCust_1_1)

All generated DMO interfaces for temp-tables directly extend com.goldencode.p2j.persist.Temporary. Tempory is a marker interface with no declared methods which itself extends DataModelObject; it is used within the FWD runtime for type safety in temp-table related APIs.

For example:

TtCust_1.java

public interface TtCust_1
extends Temporary
{
   // declared methods ...
   // ...

   /**
    * DMO interface for use by converted business logic.
    */
   public interface Buf
   extends TtCust_1, TempTableBuffer
   {
   }
}

TtCust_1_1.java

public interface TtCust_1_1
extends TtCust_1
{
   /**
    * DMO interface for use by converted business logic.
    */
   public  interface Buf
   extends TtCust_1_1, TempTableBuffer, TtCust_1.Buf
   {
   }
}
FAQ:
  • What are the possible differences between the super-interface and the sub-interfaces? Are the differences all non-structural? Please make a specific list.
    We realized a flat DMO hierarchy is not enough in a project (#2595) where the shared tables were used extensively. Some of the definitions were slightly different. The field definitions must match in name and base type, but they can be different with regard to their extent or initial value. The super-interfaces were added to allow implement DMO implementation of compatible tables to expose these differences while still being shared between the procedures using their super-interface. Each procedure sees its own/local values for this DMO.
  • What are the structural matching rules? Is this the same as documented in the Exact Matches rules for TABLE in #3751-492?
    No, in this case the rules are described in #2595-6.
  • Does FWD implement the super-interface with the widest possible types? Doing so would allow the same interface to be used in many places.
    No, there is no co-/contra- variance here. The types of the fields must fully match as their name and primarily the name of the table.
  • How does FWD decide on a single super-interface for all of the same structural matches, since they can be defined in so many places?
    There is a 2-pass process. The first one (p2o_pre.xml) splits all temp-tables in partitions based on their table name using an AstKey constructed in create_temp_table_key_criteria function. These have the super-interface as their prototype. In case there are two partitions (that is, with incompatible structures) with prototypes with same name, the first counter is added/incremented.
    The p2o.xml will refine the processing by grouping together only DMOs with same attributes, not only basic types. If needed a new DMO sub-interface is created and the second counter is added/incremented.
  • Are the parameter types for the call targets always written as the super-interface type?
    Access to the table is done through SharedVariableManager API, not table parameters. The model (when the table is declared with new shared) created the shared instance using the specific interface (TtShared_1_1.Buf.class, see below) while the 'view' (the procedure which grabs the already instantiated temp-table) will use the super-interface. This will allow that two 'models' with compatible interfaces to call the same 'view'. At the same time, a 'model' can call different 'view' procedures for the same temp-table.

The hierarchy is imposed by the way the SHARED TEMP-TABLES are handled by P4GL and because P4GL allows a bit of flexibility when defining and using the shared temporary tables.

Suppose we have two procedures which define the prototypes for a NEW SHARED TEMP-TABLE ttShared. Because of programmer error, or maybe intentionally, these definitions are slightly different in our case. Let's call these m1 and m2. The FWD conversion will analyse the table structure and, if in spite of the small differences, finds that the two tables named identically are compatible, will create a common DMO super-interface named TtShared_1 with all properties and, for each particular procedure, a custom DMO interface. Let's assume these are called TtShared_1_1 for procedure m1 and TtShared_1_2 for procedure m2. In these conditions the declarations of the buffers will be:

  TtShared_1_1.Buf ttShared = SharedVariableManager.addTempTable("ttShared", TtShared_1_1.Buf.class, "ttShared", "ttShared");
and
  TtShared_1_2.Buf ttShared = SharedVariableManager.addTempTable("ttShared", TtShared_1_2.Buf.class, "ttShared", "ttShared");

At the same time, each of these 'model' procedures invokes a third 'view' procedure (let's call it v1) which declares the our table as SHARED TEMP-TABLE ttShared. It will also have a possibly altered version of the same table, but if the structure is compatible, the FWD conversion will detect this and associate the local table with the prototypes defined by the 'model' procedures. In fact, at execution time, the local customisation of the temp-table, are not taken into consideration, and the result of the execution depends on the table definition from the procedure which invoked v1. In this conditions, the actual shared temp-table used is obtained using the following code generated by FWD conversion:

  TtShared_1.Buf ttShared = (TtShared_1.Buf) TemporaryBuffer.useShared("ttShared", TtShared_1.Buf.class);

Since TtShared_1 is the super interface of both TtShared_1_1 and TtShared_1_2, the cast to TtShared_1.Buf will work correctly. And, depending on the actual table defined the last in SharedVariableManager before invocation of v1, the outcome will reflect the respective customization (due to polymorphism).

On the other hand, if a third 'model' procedure defines a temp-table named the same and with a rather similar structure but not fully compatible, the FWD conversion will detect the difference and will create another 'partition' with the super-interface named TtShared_2. At the attempt to call a 'view' method like our v1 whose definition of ttShared was detected incompatible at conversion time will be immediately detected at runtime and error 2075 will be raised, as P4GL does.

DMO Implementation Class Hierarchy

The hierarchy for a top-level, generated DMO implementation class is:

BaseRecord (com.goldencode.p2j.persist.orm)
 |
 +-Record (com.goldencode.p2j.persist)
    |
    +-<Generated DMO Implementation Class>

The generated DMO implementation class implements the DMO interface and is assembled and loaded at runtime, so there is no source code example.

DMO Proxies

Converted business logic does not use DMO interfaces or DMO implementation classes directly. Rather, it uses DMO proxy objects. Within each generated DMO interface, there is another, public, inner interface, named Buf. For instance, if the top-level DMO interface is named Customer, this inner interface is referred to by external classes as Customer.Buf:

public interface Customer
extends DataModelObject
{
   public integer getIdNum();

   public void setIdNum(NumberType idNum);

   public character getName();

   public void setName(Text name);

   public interface Buf
   extends Customer, Buffer
   {
   }
}

The inner Buf interface declares no new methods. Its purpose is to extend both the Customer DMO interface and the com.goldencode.p2j.persist.Buffer interface. The Customer DMO interface provides access to a database record's data via getter and setter methods. The Buffer interface provides access to various methods which in the 4GL environment would be invoked on a handle of type BUFFER or would be invoked as a built-in function operating on a record buffer.

When business logic defines a record buffer, a DMO proxy is provided, which implements the inner Buf interface of a DMO interface. This approach allows both data access methods and standard BUFFER methods to be invoked on the same object (i.e., the DMO proxy), using object-oriented syntax.

For example, consider the following 4GL code:

DEFINE VARIABLE cust-num as INTEGER NO-UNDO.
FIND FIRST customer NO-LOCK NO-ERROR.
IF AVAILABLE customer THEN
   cust-num = customer.idNum.

The DEFINE BUFFER statement converts to:

Customer.Buf customer = RecordBuffer.define(Customer.Buf.class, "p2j_test", "customer", "customer");

The customer variable holds a DMO proxy which implements the Customer.Buf interface. This is then used as follows:

RecordBuffer.openScope(customer);
silent(() -> new FindQuery(customer, (String) null, null, "customer.recid asc", LockType.NONE).first());

if (customer._available())
{
   custNum.assign(customer.getIdNum());
}

Note how the methods _available() and getIdNum() both are invoked on the same object. The DMO proxy implements Buffer._available(), which provides the functionality of the 4GL built-in function AVAILABLE. It also implements Customer.getIdNum(), which provides the functionality of retrieving the value of the DMO's idNum property.