Model Mapping
A big benefit when using ADO is the model mapping with the Ada and SQL code generator.
The model describes the database tables, their columns and relations with each others. It is then used to generate the Ada implementation which provides operations to create, update and delete records from the database and map them in Ada transparently.
The model can be defined in:
- UML with a modeling tool that exports the model in XMI,
- XML files following the Hibernate description,
- YAML files according to the Doctrine mapping.
This chapter focuses on the YAML description.
Table definition
In YAML, the type definition follows the pattern below:
<table-type-name>:
type: entity
table: <table-name>
description: <description>
hasList: true|false
indexes:
id:
fields:
oneToOne:
oneToMany:
The table-type-name
represents the Ada type name with the full package specification.
The code generator will add the _Ref
prefix to the Ada type name to define the
final type with reference counting. A private type is also generated with the
_Impl
prefix.
The YAML fields have the following meanings:
Field | Description |
---|---|
type | Must be 'entity' to describe a database table |
table | The name of the database table. This must be a valid SQL name |
description | A comment description for the table and type definition |
hasList | When true , a List operation is also generated for the type |
indexes | Defines the indexes for the table |
id | Defines the primary keys for the table |
fields | Defines the simple columns for the table |
oneToOne | Defines the one to one table relations |
oneToMany | Defines the one to many table relations |
Column mapping
Simple columns are represented within the fields
section.
<table-type-name>:
fields:
<member-name>:
type: <type>
length: <length>
description: <description>
column: <column-name>
not-null: true|false
unique: true|false
readonly: true|false
version: false
The YAML fields have the following meanings:
Field | Description |
---|---|
type | The column type. This type maps to an Ada type and an SQL type |
length | For variable length columns, this is the maximum length of the column |
description | A comment description for the table and type definition |
column | The database table column name |
not-null | When true, indicates that the column cannot be null |
unique | When true, indicates that the column must be unique in the table rows |
readonly | When true, the column cannot be updated. The Save operation will ignore updated. |
version | Must be 'false' for simple columns |
The type
column describes the type of the column using a string that is agnostic of the
Ada and SQL languages. The mapping of the type to SQL depends on the database.
The not-null
definition has an impact on the Ada type since when the column can be null,
a special Ada type is required to represent that null value.
The ADO.Nullable_X
types are all represented by the following record:
type Nullable_X is record
Value : X := <default-value>;
Is_Null : Boolean := True;
end record;
The Is_Null
boolean member must be checked to see if the value is null or not.
The comparison operation (=
) ignores the Value
comparison when one of the record
to compare has Is_Null
set.
Type | not-null | SQL | Ada |
---|---|---|---|
boolean | true | TINYINT | Boolean |
false | TINYINT | ADO.Nullable_Boolean | |
byte | true | TINYINT | - |
false | TINYINT | - | |
integer | true | INTEGER | Integer |
false | INTEGER | ADO.Nullable_Integer | |
long | true | BIGINT | Long_Long_Integer |
false | BIGINT | ADO.Nullable_Long_Integer | |
identifier | BIGINT | ADO.Identifier | |
entity_type | true | INTEGER | ADO.Entity_Type |
false | INTEGER | ADO.Nullable_Entity_Type | |
string | true | VARCHAR(N) | Unbounded_String |
false | VARCHAR(N) | ADO.Nullable_String | |
date | true | DATE | Ada.Calendar.Time |
false | DATE | ADO.Nullable_Time | |
time | true | DATETIME | Ada.Calendar.Time |
false | DATETIME | ADO.Nullable_Time | |
blob | BLOB | ADO.Blob_Ref |
The identifier
type is used to represent a foreign key mapped to a BIGINT
in the database.
It is always represented by the Ada type ADO.Identifier
and the null value is represented by
the special value ADO.NO_IDENTIFIER
.
The blob
type is represented by an Ada stream array held by a reference counted object.
The reference can be null.
The entity_type
type allows to uniquely identify the type of a database entity.
Each database table is associated with an entity_type
unique value. Such value is
created statically when the database schema is created and populated in the database.
The entity_type
values are maintained in the entity_type
ADO database table.
Primary keys
Primary keys are used to uniquely identify a row within a table. For the ADO framework, only the identifier and string primary types are supported.
<table-type-name>:
id:
<member-name>:
type: {identifier|string}
length: <length>
description: <description>
column: <column-name>
not-null: true
unique: true
version: false
generator:
strategy: {none|auto|sequence}
The generator
section describes how the primary key is generated.
Strategy | description |
---|---|
none | the primary key is managed by the application |
auto | use the database auto increment support |
sequence | use the ADO sequence generator |
Relations
A one to many relation is described by the following YAML description:
<table-type-name>:
oneToMany:
<member-name>:
type: <model-type>
description: <description>
column: <column-name>
not-null: true|false
readonly| true|false
This represents the foreign key and this YAML description is to be put in the table that holds it.
The type
definition describes the type of object at the end of the relation.
This can be the identifier
type which means the relation will not be strongly typed
and mapped to the ADO.Identifier
type. But it can be the table type name used for
another table definition. In that case, the code generator will generate a getter
and setter that will use the object reference instance.
Circular dependencies are allowed within the same Ada package. That is, two tables
can reference each other as long as they are defined in the same Ada package.
A relation can use a reference of a type declared in another YAML description from
another Ada package. In that case, with
clauses are generated to import them.
Versions
Optimistic locking is a mechanism that allows updating the same database record
from several transactions without having to take a strong row lock that would block
transactions. By having a version column that is incremented after each change,
it is possible to detect that the database row was modified when we want to update it.
When this happens, the optimistic lock exception ADO.Objects.LAZY_LOCK
is raised
and it is the responsibility of the application to handle the failure by retrying
the update.
For the optimistic locking to work, a special integer based column must be declared.
<table-type-name>:
fields:
<member-name>:
type: <type>
description: <description>
column: <column-name>
not-null: true
unique: false
version: true
The generated Ada code gives access to the version value but it does not allow its modification.
The version column is incremented only by the Save
procedure and only if at least one
field of the record was modified (otherwise the Save
has no effect).
The version number starts with the value 1
.
Objects
When a database table is mapped into an Ada object, the application holds a reference
to that object through the Object_Ref
type.
The Object_Ref
tagged type is the root type of any database record reference.
Reference counting is used so that the object can be stored, shared and the memory
management is handled automatically. It defines generic operations to be able to:
- load the database record and map it to the Ada object,
- save the Ada object into the database either by inserting or updating it,
- delete the database record.
The Dynamo code generator will generate a specific tagged type for each database table
that is mapped. These tagged type will inherit from the Object_Ref
and will implement
the required abstract operations. For each of them, the code generator will generate
the Get_X
and Set_X
operation for each column mapped in Ada.
Before the Object_Ref
is a reference, it does not hold the database record itself.
The ADO.Objects.Object_Record
tagged record is used for that and it defines the
root type for the model representation. The type provides operations to modify a
data field of the record while tracking its changes so that when the Save
operation
is called, only the data fields that have been modified are updated in the database.
An application will not use nor access the Object_Record
. The Dynamo code generator
generates a private type to make sure it is only accessed through the reference.
Several predicate operations are available to help applications check the validity of an object reference:
Function | Description |
---|---|
Is_Null | When returning True, it indicates the reference is NULL. |
Is_Loaded | When returning True, it indicates the object was loaded from the database. |
Is_Inserted | When returning True, it indicates the object was inserted in the database. |
Is_Modified | When returning True, it indicates the object was modified and must be saved. |
Let's assume we have a User_Ref
mapped record, an instance of the reference would
be declared as follows:
with Samples.User.Model;
...
User : Samples.User.Model.User_Ref;
After this declaration, the reference is null and the following assumption is true:
User.Is_Null and not User.Is_Loaded and not User.Is_Inserted
If we set a data field such as the name, an object is allocated and the reference is no longer null.
User.Set_Name ("Ada Lovelace");
After this statement, the following assumption is true:
not User.Is_Null and not User.Is_Loaded and not User.Is_Inserted
With this, it is therefore possible to identify that this object is not yet
saved in the database. After calling the Save
procedure, a primary key is
allocated and the following assumption becomes true:
not User.Is_Null and not User.Is_Loaded and User.Is_Inserted
Loading Objects
Three operations are generated by the Dynamo code generator to help in loading
a object from the database: two Load
procedures and a Find
procedure.
The Load
procedures are able to load an object by using its primary key.
Two forms of Load
are provided: one that raises the ADO.Objects.NOT_FOUND
exception and another that returns an additional Found
boolean parameter.
Within the application, if the database row is expected to exist, the first
form should be used. In other cases, when the application expects that the
database record may not exist, the second form is easier and avoids raising
and handling an exception for a common case.
User.Load (Session, 1234);
The Find
procedure allows to retrieve a database record by specifying a
filter. The filter object is represented by the ADO.SQL.Query
tagged record.
A simple query filter is declared as follows:
Filter : ADO.SQL.Query;
The filter is an SQL fragment that is inserted within the WHERE
clause to
find the object record. The filter can use parameters that are configured
by using the Bind_Param
or Add_Param
operations. For example, to find
a user from its name, the following filter could be set:
Filter.Set_Filter ("name = :name");
Filter.Bind_Param ("name", "Ada Lovelace");
Once the query filter is initialized and configured with its parameters,
the Find
procedure can be called:
Found : Boolean;
...
User.Find (Session, Filter, Found);
The Find
procedure does not raise an exception if the database record is not found.
Instead, it returns a boolean status in the Found
output parameter. The Find
procedure will execute an SQL SELECT
statement with a WHERE
clause to retrieve
the database record. The Found
output parameter is set when the query returns
exactly one row.
Modifying Objects
To modify an object, applications will use one of the Set_X
operation generated
for each mapped column. The ADO runtime will keep track of which data fields are
modified. The Save
procedure must be called to update the database record.
When calling it, an SQL UPDATE
statement is generated to update the modified
data fields.
User.Set_Status (1);
User.Save (Session);
Deleting Objects
Deleting objects is made by using the Delete
operation.
User.Delete (Session);
Sometimes you may want to delete an object without having to load it first.
This is possible by delete an object without loading it. For this, set the
primary key on the object and call the Delete
operation:
User.Set_Id (42);
User.Delete (Session);
Sequence Generators
The sequence generator is responsible for creating unique ID's across all database objects.
Each table can be associated with a sequence generator. The sequence factory is shared by several sessions and the implementation is thread-safe.
The HiLoGenerator
implements a simple High Low sequence generator
by using sequences that avoid to access the database.
Example:
F : Factory;
Id : Identifier;
...
Allocate (Manager => F, Name => "user", Id => Id);
HiLo Sequence Generator
The HiLo sequence generator. This sequence generator uses a database table
ado_sequence
to allocate blocks of identifiers for a given sequence name.
The sequence table contains one row for each sequence. It keeps track of
the next available sequence identifier (in the value
column).
To allocate a sequence block, the HiLo generator obtains the next available sequence identified and updates it by adding the sequence block size. The HiLo sequence generator will allocate the identifiers until the block is full after which a new block will be allocated.