Skip to content

ORM Dao

samatstarion edited this page Aug 28, 2024 · 5 revisions

BaseDao

The BaseDao class is the abstract super class from which all Dao classes inherit. This class exposes the following methods that can be overriden in case htis is necessary to add extra business logic that needs to be executed in this layer. The following methods are available to be overriden by derived partial classes:

  • BeforeUpdate: perform any operations required to implement business logic in the ORM layer before the Update operation is executed.
  • AfterUpdate: perform any operations required to implement business logic in the ORM layer after the Update operation is executed.
  • BeforeDelete: perform any operations required to implement business logic in the ORM layer before the Delete operation is executed.
  • AfterDelete: perform any operations required to implement business logic in the ORM layer after the Delete operation is executed.
  • BeforeWrite: perform any operations required to implement business logic in the ORM layer before the Write operation is executed.
  • AfterWrite: perform any operations required to implement business logic in the ORM layer after the Write operation is executed.

Generated Dao Classes

For each class in the CDP4-COMET Data Model a corresponding Dao class is generated. For each concrete class an IDao interface is generated as well that is then also implemented by the correspding Dao class.

The IDao interfaces are injected into the Service classes of the CDP4WebServices.API project using Autofac property injection.

The code snippets below show the generated ElementDefinitionDao class with an explanation of the structure of the class

class definition

Each generated Dao inherits from its super class according to the CDP4-COMET Data Model. Concrete CDP4-COMET Data Model classess also implement the corresponding IDao interface.

The example below shows that the ElementDefinitionDao derives from ElementBaseDao and implements the IElementDefinitionDao interface.

public partial class ElementDefinitionDao : ElementBaseDao, IElementDefinitionDao

Read

The read method is used to read the data from the database, in this case the availalbe ElementDefinition instances. The following parameters need to be provided:

  • NpgsqlTransaction transaction: the Npgsql transaction that is used to read from the database.
  • string partition: the database partition the data is read from, this is the name of the PostgreSQL Schema that contains the data
  • IEnumerable ids: a list of unique identifiers that can be used to filter the requested data. In case this list is null or empty the data will not be filtered and all instances will be returned
  • bool isCachedDtoReadEnabledAndInstant: a value that indicates whether the data is to be read from the the cache, or from the relational tables. When reading from the relational tables (captured by predefined views in the database) it is possible to get data from the past. The cache tables contain the most recent data.
public virtual IEnumerable<CDP4Common.DTO.ElementDefinition> Read(NpgsqlTransaction transaction, string partition, IEnumerable<Guid> ids = null, bool isCachedDtoReadEnabledAndInstant = false)
{
    using (var command = new NpgsqlCommand())
    {
        var sqlBuilder = new System.Text.StringBuilder();

        if (isCachedDtoReadEnabledAndInstant)
        {
            sqlBuilder.AppendFormat("SELECT \"Jsonb\" FROM \"{0}\".\"ElementDefinition_Cache\"", partition);

            if (ids != null && ids.Any())
            {
                sqlBuilder.Append(" WHERE \"Iid\" = ANY(:ids)");
                command.Parameters.Add("ids", NpgsqlDbType.Array | NpgsqlDbType.Uuid).Value = ids;
            }

            sqlBuilder.Append(";");

            command.Connection = transaction.Connection;
            command.Transaction = transaction;
            command.CommandText = sqlBuilder.ToString();

            // log the sql command 
            this.LogCommand(command);

            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    var thing = this.MapJsonbToDto(reader);
                    if (thing != null)
                    {
                        yield return thing as ElementDefinition;
                    }
                }
            }
        }
        else
        {
            sqlBuilder.AppendFormat("SELECT * FROM \"{0}\".\"ElementDefinition_View\"", partition);

            if (ids != null && ids.Any()) 
            {
                sqlBuilder.Append(" WHERE \"Iid\" = ANY(:ids)");
                command.Parameters.Add("ids", NpgsqlDbType.Array | NpgsqlDbType.Uuid).Value = ids;
            }

            sqlBuilder.Append(";");

            command.Connection = transaction.Connection;
            command.Transaction = transaction;
            command.CommandText = sqlBuilder.ToString();

            // log the sql command 
            this.LogCommand(command);

            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return this.MapToDto(reader);
                }
            }
        }
    }
}

The MapToDto method converts the data that is returned from the database as a recordset into a DTOs specific for the Dao, in this case instances of ElementDefinition. Due to the fact that the Dao classes are generated fromt the CDP4-COMET Data Model (as are the PostgreSQL SQL schema) it makes sense to have these mappings hard-coded and not rely on reflection or existing ORM implementations such as Dapper, NHibernate or Entity Framework.

public virtual CDP4Common.DTO.ElementDefinition MapToDto(NpgsqlDataReader reader)
{
    string tempModifiedOn;
    string tempName;
    string tempShortName;

    var valueDict = (Dictionary<string, string>)reader["ValueTypeSet"];
    var iid = Guid.Parse(reader["Iid"].ToString());
    var revisionNumber = int.Parse(valueDict["RevisionNumber"]);

    var dto = new CDP4Common.DTO.ElementDefinition(iid, revisionNumber);
    dto.Alias.AddRange(Array.ConvertAll((string[])reader["Alias"], Guid.Parse));
    dto.Category.AddRange(Array.ConvertAll((string[])reader["Category"], Guid.Parse));
    dto.ContainedElement.AddRange(Array.ConvertAll((string[])reader["ContainedElement"], Guid.Parse));
    dto.Definition.AddRange(Array.ConvertAll((string[])reader["Definition"], Guid.Parse));
    dto.ExcludedDomain.AddRange(Array.ConvertAll((string[])reader["ExcludedDomain"], Guid.Parse));
    dto.ExcludedPerson.AddRange(Array.ConvertAll((string[])reader["ExcludedPerson"], Guid.Parse));
    dto.HyperLink.AddRange(Array.ConvertAll((string[])reader["HyperLink"], Guid.Parse));
    dto.Owner = Guid.Parse(reader["Owner"].ToString());
    dto.Parameter.AddRange(Array.ConvertAll((string[])reader["Parameter"], Guid.Parse));
    dto.ParameterGroup.AddRange(Array.ConvertAll((string[])reader["ParameterGroup"], Guid.Parse));
    dto.ReferencedElement.AddRange(Array.ConvertAll((string[])reader["ReferencedElement"], Guid.Parse));

    if (valueDict.TryGetValue("ModifiedOn", out tempModifiedOn))
    {
        dto.ModifiedOn = Utils.ParseUtcDate(tempModifiedOn);
    }

    if (valueDict.TryGetValue("Name", out tempName))
    {
        dto.Name = tempName.UnEscape();
    }

    if (valueDict.TryGetValue("ShortName", out tempShortName))
    {
        dto.ShortName = tempShortName.UnEscape();
    }

    return dto;
}

Write

The Write method is use to INSERT new data into the database. Property values are persisted as an Hstore, reference properties (associations, composite aggregation) are persisted as unique identifiers using columns and foreign key constraints. The following parameters need to be provided:

  • NpgsqlTransaction transaction: the Npgsql transaction that is used to write to the database.
  • string partition: the database partition the data is written to, this is the name of the PostgreSQL Schema that contains the data
  • CDP4Common.DTO.DefinedThing definedThing: the instance of DefinedThing that is to be persisted
  • Thing container: the container of the DefinedThing, which is an Iteration.
public virtual bool Write(NpgsqlTransaction transaction, string partition, CDP4Common.DTO.DefinedThing definedThing, CDP4Common.DTO.Thing container = null)
{
    bool isHandled;
    var valueTypeDictionaryAdditions = new Dictionary<string, string>();
    var beforeWrite = this.BeforeWrite(transaction, partition, definedThing, container, out isHandled, valueTypeDictionaryAdditions);
    if (!isHandled)
    {
        beforeWrite = beforeWrite && base.Write(transaction, partition, definedThing, container);

        var valueTypeDictionaryContents = new Dictionary<string, string>
        {
            { "Name", !this.IsDerived(definedThing, "Name") ? definedThing.Name.Escape() : string.Empty },
            { "ShortName", !this.IsDerived(definedThing, "ShortName") ? definedThing.ShortName.Escape() : string.Empty },
        }.Concat(valueTypeDictionaryAdditions).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

        using (var command = new NpgsqlCommand())
        {
            var sqlBuilder = new System.Text.StringBuilder();
            
            sqlBuilder.AppendFormat("INSERT INTO \"{0}\".\"DefinedThing\"", partition);
            sqlBuilder.AppendFormat(" (\"Iid\", \"ValueTypeDictionary\")");
            sqlBuilder.AppendFormat(" VALUES (:iid, :valueTypeDictionary);");

            command.Parameters.Add("iid", NpgsqlDbType.Uuid).Value = definedThing.Iid;
            command.Parameters.Add("valueTypeDictionary", NpgsqlDbType.Hstore).Value = valueTypeDictionaryContents;

            command.CommandText = sqlBuilder.ToString();
            command.Connection = transaction.Connection;
            command.Transaction = transaction;

            this.ExecuteAndLogCommand(command);
        }
    }

    return this.AfterWrite(beforeWrite, transaction, partition, definedThing, container);
}
  • In case extra business logic is required in the ORM before or after the data is written to the database, the BeforeWrite and AfterWrite can be implemented in a hand-coded partial ElementDefinitionDao class.

  • Persisting the properties of the superclass of (in this ElementBase) is delegated to the Dao super class (in this case ElementBaseDao).

  • persistence of reference properties (that are not composite aggregation) is delegated to the appropriate metod (in this case AddReferencedElement)

Update

The Update method is use to UPDATE data in the database. The following parameters need to be provided:

  • NpgsqlTransaction transaction: the Npgsql transaction that is used to write to the database.
  • string partition: the database partition the data is written to, this is the name of the PostgreSQL Schema that contains the data
  • CDP4Common.DTO.DefinedThing definedThing: the instance of DefinedThing that is to be updated (persisted)
  • Thing container: the container of the DefinedThing, which is an Iteration.
public virtual bool Update(NpgsqlTransaction transaction, string partition, CDP4Common.DTO.ElementDefinition elementDefinition, CDP4Common.DTO.Thing container = null)
{
    bool isHandled;
    var valueTypeDictionaryAdditions = new Dictionary<string, string>();
    var beforeUpdate = this.BeforeUpdate(transaction, partition, elementDefinition, container, out isHandled, valueTypeDictionaryAdditions);
    if (!isHandled)
    {
        beforeUpdate = beforeUpdate && base.Update(transaction, partition, elementDefinition, container);

        using (var command = new NpgsqlCommand())
        {
            var sqlBuilder = new System.Text.StringBuilder();
            sqlBuilder.AppendFormat("UPDATE \"{0}\".\"ElementDefinition\"", partition);
            sqlBuilder.AppendFormat(" SET \"Container\"");
            sqlBuilder.AppendFormat(" = :container");
            sqlBuilder.AppendFormat(" WHERE \"Iid\" = :iid;");

            command.Parameters.Add("iid", NpgsqlDbType.Uuid).Value = elementDefinition.Iid;
            command.Parameters.Add("container", NpgsqlDbType.Uuid).Value = container.Iid;

            command.CommandText = sqlBuilder.ToString();
            command.Connection = transaction.Connection;
            command.Transaction = transaction;

            this.ExecuteAndLogCommand(command);
        }
    }

    return this.AfterUpdate(beforeUpdate, transaction, partition, elementDefinition, container);
}

  • In case extra business logic is required in the ORM before or after the data is written to the database, the BeforeUpdate and AfterUpdate can be implemented in a hand-coded partial ElementDefinitionDao class.

  • Persisting the properties of the superclass of (in this case ElementBase) is delegated to the Dao super class (in this case ElementBaseDao).

Delete

The Delete method is use to DELETE data from the database. The following parameters need to be provided:

  • NpgsqlTransaction transaction: the Npgsql transaction that is used to write to the database.
  • string partition: the database partition the data is deleted from, this is the name of the PostgreSQL Schema that contains the data
  • Guid iid: the unique identifier of the ElementDefinition that is to be deleted.
public override bool Delete(NpgsqlTransaction transaction, string partition, Guid iid)
{
    bool isHandled;
    var beforeDelete = this.BeforeDelete(transaction, partition, iid, out isHandled);
    if (!isHandled)
    {
        beforeDelete = beforeDelete && base.Delete(transaction, partition, iid);
    }

    return this.AfterDelete(beforeDelete, transaction, partition, iid);
}
  • In case extra business logic is required in the ORM before or after the data is deleted from the database, the BeforeDelete and AfterDelete can be implemented in a hand-coded partial ElementDefinitionDao class.

  • Deleting the instance of the superclass of (in this case ElementBase) is delegated to the Dao super class (in this case ElementBaseDao).