ice.NET Queries - Architecture

There are architecural commonalities between all kinds of queries supported by the ice.NET platform. Actually the core platform implements a framework for query processing and the ice.NET SDK provides several useful query templates (Find, Expand, Join) that are built on top of the framework specification and can be directly used by the application developer.

Query Processing

The following diagram illustrates how queries are processed in principle:

Query processing starts with a query specification. This specification defines the type and the static structure of the query. It contains all information that does not change between multiple executions of the query.

By running the query specification through the query compilation an intermediate data structure is created: the compiled query. The compiled query contains all information of the query specification in a preprocessed form that is suitable for query execution. All calculations that can be done independently from specific executions of the query are performed during the compilation process. The compiled query can be used for multiple executions. During the compilation process information about the data model must be available to resolve type information used in the query specification.

The actual execution of the query is performed by the query processor. It consumes additional information that can vary between different executions of the same compiled query: the query parameters. The query parameters typically contain specific values for constraints defined in the query specification. Successful execution of the query produces a query result, typically a table of data that conforms to the fields defined in the query specification.

The Query Framework

The Query API is quite simple and is directly defined in the IDatabasRepository interface. It consists of two core methods that implement the query compiler and the query processor illustrated in the diagram above:

ICompiledQuery CompileQuery(IQuerySpecification pQuerySpec);

IQueryResult ExecuteQuery(ICompiledQuery pQuery, IQueryParameters pParameters);

An additional method provides the possibility to execute a query specification directly, without explicitly compiling it. This method processes the compilation step internally:

IQueryResult ExecuteQuery(IQuerySpecification pQuerySpec, 
                          IQueryParameters pParameters);

The concepts mentioned above (query specification, compiled query, query parameters, query result) are represented by general interfaces in the API. These interfaces have been implemented for specific types of queries (Find, Expand, etc.).

Using the Query API

Queries can be defined and processed by using the Query API directly. To do this, a query specification must be instantiated. For the built-in query types the IceNetSdk class provides factory methods. The specification data can be set by proprties:

ExpandQuerySpecification pSpec = IceNetSdk.CreateExpandQuerySpecification();

pSpec.Name                          = "CustomerBookingsNewerThan";
pSpec.AnchorType                    = ExpandAnchorType.Object;
pSpec.CheckAuthorization            = false;

ExpandStep pStep = new ExpandStep();
pSpec.Steps.Add(pStep);
pStep.RelTypeName                   = "PDTec.ICR.CustomerBookings";
pStep.RelDirection                  = RelDirection.Forward;

ExpandConstraint pConstraint = new ExpandConstraint();
pStep.Constraints.Add(pConstraint);
pConstraint.Name                    = "NewerThan";
pConstraint.ItemType                = ExpandItemType.Object;
pConstraint.FieldType               = ExpandFieldType.Attribute;
pConstraint.AttrDefDeclTypeName     = "PDTec.ICR.Booking";
pConstraint.AttrDefName             = "FromDate";
pConstraint.ConstraintType          = ExpandConstraintType.Greater;
pConstraint.Parameter0Name          = "NewerThan";

ExpandField pField1 = new ExpandField();
pSpec.Fields.Add(pField1);
pField1.Name                        = "FromDate";
pField1.StepRefIndex                = 0;
pField1.ItemType                    = ExpandItemType.Object;
pField1.FieldType                   = ExpandFieldType.Attribute;
pField1.AttrDefDeclTypeName         = "PDTec.ICR.Booking";
pField1.AttrDefName                 = "FromDate";

ExpandField pField2 = new ExpandField();
pSpec.Fields.Add(pField2);
pField2.Name                        = "ToDate";
pField2.StepRefIndex                = 0;
pField2.ItemType                    = ExpandItemType.Object;
pField2.FieldType                   = ExpandFieldType.Attribute;
pField2.AttrDefDeclTypeName         = "PDTec.ICR.Booking";
pField2.AttrDefName                 = "ToDate";

ExpandField pField3 = new ExpandField();
pSpec.Fields.Add(pField3);
pSpec.Fields.Add(new ExpandField());
pField3.Name                        = "VehicleRegisterNumber";
pField3.AddSteps                    = new List<ExpandAddStep>();
pField3.AddSteps.Add(new ExpandAddStep());
pField3.AddSteps[0].RelTypeName     = "PDTec.ICR.BookedVehicle";
pField3.AddSteps[0].RelDirection    = RelDirection.Forward;
pField3.StepRefIndex                = 0;
pField3.ItemType                    = ExpandItemType.Object;
pField3.FieldType                   = ExpandFieldType.Attribute;
pField3.AttrDefDeclTypeName         = "PDTec.ICR.Vehicle";
pField3.AttrDefName                 = "RegisterNumber";

ExpandField pField4 = new ExpandField();
pSpec.Fields.Add(pField4);
pField4.Name                        = "VehicleModelName";
pField4.AddSteps                    = new List<ExpandAddStep>();
pField4.AddSteps.Add(new ExpandAddStep());
pField4.AddSteps[0].RelTypeName     = "PDTec.ICR.BookedVehicle";
pField4.AddSteps[0].RelDirection    = RelDirection.Forward;
pField4.AddSteps.Add(new ExpandAddStep());
pField4.AddSteps[1].RelTypeName     = "PDTec.ICR.VehicleModel";
pField4.AddSteps[1].RelDirection    = RelDirection.Forward;
pField4.StepRefIndex                = 0;
pField4.ItemType                    = ExpandItemType.Object;
pField4.FieldType                   = ExpandFieldType.Name;       

Notice: If you are of the opinion that this is an inconvenient way to specify a query, then you are correct. However, to explain the query architecture fundamentally, this step is necessary. Later on, alternative approaches will be presented.

Once the query specification has been created, it can be compiled by the CompileQuery method:

ICompiledQuery pCompiledQuery = Repository.CompileQuery(pSpec);

The compiled query can be the executed. Therefore we must fill a parameter structure with values that correspond to the specific type of the query and the defined constraints:

ExpandQueryParameters pParams = (ExpandQueryParameters)pCompiledQuery.GetParameters();

pParams.AnchorObjectId = pCustomer.Id;

ExpandConstraintValue pConstraintValue = new ExpandConstraintValue();
pConstraintValue.Name               = "NewerThan";
pConstraintValue.Value              = DateTime.UtcNow;;
pParams.ConstraintValues.Add(pConstraintValue);

pParams.MaxResults = 0; // no restriction

DbTable pResultTable = (DbTable)Repository.ExecuteQuery(pCompiledQuery, pParams);

By executing the query the query processor produces a query result, normally an instance of the DbTable class. A DbTable contains a sequence of rows (the results) that have a column structure according to the field definitions in the query specification.

Using the Business Objects Builder

The approach of directly using the Query API has two disadvantages:

  • The construction of the query specification is relatively inconvenient.
  • The query result consists of a untyped table (row sequence) structure.

To overcome these disadvantages, the Business Objects Builder provides a way to define the query specifications in an elegant, XML-based syntax and to execute the query by a typesafe interface. The query specification defined above can be represented in XML (stored in the <Queries> section of an .icebob file):

<ExpandQuery Name="CustomerBookingsNewerThan">
    <Step RelTypeName="PDTec.ICR.CustomerBookings" RelDirection="Forward">
        <Field Name="FromDate" ItemType="Object" FieldType="Attribute" AttrDefName="FromDate" AttrDefDeclTypeName="PDTec.ICR.Booking" />
        <Field Name="ToDate" ItemType="Object" FieldType="Attribute" AttrDefName="ToDate" AttrDefDeclTypeName="PDTec.ICR.Booking" />
        <Field Name="VehicleRegisterNumber" ItemType="Object" FieldType="Attribute" AttrDefName="RegisterNumber" AttrDefDeclTypeName="PDTec.ICR.Vehicle"
               AddStepRelTypeName="PDTec.ICR.BookedVehicle" AddStepRelDirection="Forward" />
        <Field Name="VehicleModelName" ItemType="Object" FieldType="Name">
            <AddSteps>
                <AddStep RelTypeName="PDTec.ICR.BookedVehicle" RelDirection="Forward" />
                <AddStep RelTypeName="PDTec.ICR.VehicleModel" RelDirection="Forward" />
            </AddSteps>
        </Field>
        <Constraint Name="NewerThan" ItemType="Object" FieldType="Attribute" AttrDefDeclTypeName="PDTec.ICR.Booking" AttrDefName="FromDate" ConstraintType="Greater" Parameter0Name="NewerThan" />
    </Step>
</ExpandQuery>      

From this definition the Business Objects Builder generates a pair of classes that encapsulates the API handling behind a typesafe execution and result structure:

public class CustomerBookingsNewerThanQueryResult
{
    public DateTime FromDate;
    public DateTime ToDate;
    public string VehicleRegisterNumber;
    public string VehicleModelName;
}

public class CustomerBookingsNewerThanQuery
{
    public CustomerBookingsNewerThanQuery(
        IDatabaseRepository pRepository);

    public CustomerBookingsNewerThanQueryResult[] Execute(
        string anchorObjectId,
        DateTime NewerThan,
        int maxResults);
}

Processing the query then requires only a call of the Execute method and providing the appropriate constraint parameters:

CustomerBookingsNewerThanQueryResult[] aResults = 
    new CustomerBookingsNewerThanQuery(Repository).Execute(pCustomer.Id, DateTime.UtcNow, 0);