Link Search Menu Expand Document

Database Helper


A database helper is an object that is used by the library as a helper class for a specific RDBMS data provider. It is a part of the extensibility feature in order for you (as a developer) to override the default implementation of the library, specially when it comes to optimizing the database fields extraction.

You have use the IDbHelper interface as the contract for the implementation and DbHelperMapper class for the mapping.

Pre-requisites

  • The naming must be Desired Name + DbHelper (i.e.: SqlServerDbHelper).
  • The implementation must be internal sealed.
  • The namespace must be RepoDb.DbHelpers.
  • The class must be residing inside the DbHelpers folder.

Please see the folder structuring page.

Get Started

First, you have to manually create a class that implements the IDbHelper interface.

Let us say, you are targeting to create a database helper for SQL Server.

internal sealed class OptimizedSqlServerDbHelper : IDbHelper
{
    public IEnumerable<DbField> GetFields(IDbConnection connection,
        string tableName,
        IDbTransaction transaction = null)
    {
        // Implementations for the GetFields() here
    }

    public async Task<IEnumerable<DbField>> GetFieldsAsync(IDbConnection connection,
        string tableName,
        IDbTransaction transaction = null)
    {
        // Implementations for the GetFieldsAsync() here
    }

    public object GetScopeIdentity(IDbConnection connection,
        IDbTransaction transaction = null)
    {
        // Implementations for the GetScopeIdentity() here
    }

    public async Task<object> GetScopeIdentityAsync(IDbConnection connection,
        IDbTransaction transaction = null)
    {
        // Implementations for the GetScopeIdentityAsync() here
    }
}

You have to implement all the methods needed by this interface. With this, you have the full control of you helper object.

Method GetFields

This method is returning the list of DbField objects. Below are the sample implementations for SQL Server.

First, create the helper methods to extract the schema.

private string GetSchema(string tableName,
    IDbSetting dbSetting)
{
    if (tableName.IndexOf(dbSetting.SchemaSeparator) > 0)
    {
        var splitted = tableName.Split(dbSetting.SchemaSeparator.ToCharArray());
        return splitted[0].AsUnquoted(true, dbSetting);
    }
    return dbSetting.DefaultSchema;
}

Then, a method to extract the tablename.

private string GetTableName(string tableName,
    IDbSetting dbSetting)
{
    if (tableName.IndexOf(dbSetting.SchemaSeparator) > 0)
    {
        var splitted = tableName.Split(dbSetting.SchemaSeparator.ToCharArray());
        return splitted[1].AsUnquoted(true, dbSetting);
    }
    return tableName.AsUnquoted(true, dbSetting);
}

Then, a method that returns a SQL text.

private string GetCommandText()
{
    return @"
        SELECT C.COLUMN_NAME AS ColumnName
            , CONVERT(BIT, COALESCE(TC.is_primary, 0)) AS IsPrimary
            , CONVERT(BIT, COALESCE(TMP.is_identity, 1)) AS IsIdentity
            , CONVERT(BIT, COALESCE(TMP.is_nullable, 1)) AS IsNullable
            , C.DATA_TYPE AS DataType
            , CONVERT(INT, COALESCE(TMP.max_length, 1)) AS Size
            , CONVERT(TINYINT, COALESCE(TMP.precision, 1)) AS Precision
            , CONVERT(TINYINT, COALESCE(TMP.scale, 1)) AS Scale
        FROM INFORMATION_SCHEMA.COLUMNS C
        OUTER APPLY
        (
            SELECT 1 AS is_primary
            FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU
            LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC
                ON TC.TABLE_SCHEMA = C.TABLE_SCHEMA
                AND TC.TABLE_NAME = C.TABLE_NAME
                AND TC.CONSTRAINT_NAME = KCU.CONSTRAINT_NAME
            WHERE KCU.TABLE_SCHEMA = C.TABLE_SCHEMA
                AND KCU.TABLE_NAME = C.TABLE_NAME
                AND KCU.COLUMN_NAME = C.COLUMN_NAME
                AND TC.CONSTRAINT_TYPE = 'PRIMARY KEY'
        ) TC 
        OUTER APPLY
        (
            SELECT SC.name
                , SC.is_identity
                , SC.is_nullable
                , SC.max_length
                , SC.scale
                , SC.precision
            FROM [sys].[columns] SC
            INNER JOIN [sys].[tables] ST ON ST.object_id = SC.object_id
            WHERE SC.name = C.COLUMN_NAME
                AND ST.name = C.TABLE_NAME
        ) TMP
        WHERE
            C.TABLE_SCHEMA = @Schema
            AND C.TABLE_NAME = @TableName;";
}

Then, a method that converts the instance of the DbDataReader into a DbField object.

private DbField ReaderToDbField(IDataReader reader)
{
    return new DbField(reader.GetString(0),
        reader.IsDBNull(1) ? false : reader.GetBoolean(1),
        reader.IsDBNull(2) ? false : reader.GetBoolean(2),
        reader.IsDBNull(3) ? false : reader.GetBoolean(3),
        reader.IsDBNull(4) ? DbTypeResolver.Resolve("text") : DbTypeResolver.Resolve(reader.GetString(4)),
        reader.IsDBNull(5) ? 0 : GetConvertedSize(reader.GetString(4), reader.GetInt32(5)),
        reader.IsDBNull(6) ? (byte?)0 : reader.GetByte(6),
        reader.IsDBNull(7) ? (byte?)0 : reader.GetByte(7),
        reader.IsDBNull(7) ? "text" : reader.GetString(4),
        "SQLSVR");
}

private int? GetConvertedSize(string type, int size)
{
    return (size == -1) ? 8000 : size;
}

Lastly, call everything on within the GetFields() method.

public IEnumerable<DbField> GetFields(IDbConnection connection,
    string tableName,
    IDbTransaction transaction = null)
{
    var commandText = GetCommandText();
    var setting = connection.GetDbSetting();
    var param = new
    {
        Schema = GetSchema(tableName, setting),
        TableName = GetTableName(tableName, setting)
    };

    using (var reader = connection.ExecuteReader(commandText, param, transaction: transaction))
    {
        var dbFields = new List<DbField>();
        while (reader.Read())
        {
            dbFields.Add(ReaderToDbField(reader));
        }
        return dbFields;
    }
}

Method GetScopedIdentity

This method is returning the latest generated identity from the database (scoped level). If not present at the scoped level, then it will return the global level. Below is a sample implementation.

Simply call the SCOPE_IDENTITY and @@IDENTITY function of SQL Server.

public object GetScopeIdentity(IDbConnection connection,
    IDbTransaction transaction = null)
{
    return connection.ExecuteScalar("SELECT COALESCE(SCOPE_IDENTITY(), @@IDENTITY);");
}

Usability

Once you have the database helper implemented in a customized class, you can use the DbHelperMapper class to map it in your target RDDMS data provider.

DbHelperMapper.Add(typeof(SqlConnection), new OptimizedSqlServerDbHelper(), true);

The library will then use your customized database helper when you are calling the extended methods of the SqlConnection object.

How to Retrieve?

You can use the DbHelperMapper to get the database helper by connection type.

var helper = DbHelperMapper.Get<SqlConnection>();
// Use the 'helper' here

Or, you can use the GetDbHelper() extended method.

using (var connection = new SqlConnection(connectionString))
{
    var helper = connection.GetDbHelper();
    // Use the 'helper' here
}