Question

Our legacy web application heavily uses stored procedures. We have a central interface through which all database calls (i.e. queries and procedures) are made. However, the current implementation uses the OracleCommandBuilder.DeriveParameters method under the hood to bind to the appropriate stored procedure signature. From the documentation:

DeriveParameters incurs a database round-trip and should only be used during design time. To avoid unnecessary database round-trips in a production environment, the DeriveParameters method itself should be replaced with the explicit parameter settings that were returned by the DeriveParameters method at design time.

We could use the OracleCommand class to explicitly bind to the correct stored procedure signature. However, littering our code (even if only the Data Access Layer) with OracleCommand objects is not database agnostic. We already support database agnostic dynamic queries in our database interface (hereafter referred to as IDatabaseService), which looks like this:

int ExecuteNonQuery(string query, object[] parameterValues);
IDataReader ExecuteReader(string query, object[] parameterValues);
// etc.

We want to also support database agnostic stored procedure calls. What is a good pattern for this?

More information:

To bind to a specific subroutine, OracleCommands allow BindByName. We prefer to not use that approach, as a string is more error-prone than a type. The other approach for binding a subroutine call is to provide the parameter types. We could rely on the parameter values and reflect on the runtime types, but we want stronger safety than that. We want to require that the types are explicitly provided to the database interface so that we can check that the provided parameter values match the provided subroutine parameter types before we communicate to the database.

Was it helpful?

Solution

After prototyping various approaches, we settled on the following.

To IDatabaseService we added new ExecuteYYY methods that take an object implementing IDatabaseSubroutineSignature and (optionally, via an overload) an IEnumerable that are the parameter values.

The ExecuteYYY methods on IDatabaseService look like this:

DataSet ExecuteDataSet(IDatabaseSubroutineSignature signature);
DataSet ExecuteDataSet(IDatabaseSubroutineSignature signature, IEnumerable<object> parameterValues);
void ExecuteNonQuery(IDatabaseSubroutineSignature signature);
void ExecuteNonQuery(IDatabaseSubroutineSignature signature, IEnumerable<object> parameterValues);
IDataReader ExecuteReader(IDatabaseSubroutineSignature signature);
IDataReader ExecuteReader(IDatabaseSubroutineSignature signature, IEnumerable<object> parameterValues);
object ExecuteScalar(IDatabaseSubroutineSignature signature);
object ExecuteScalar(IDatabaseSubroutineSignature signature, IEnumerable<object> parameterValues);
ReadOnlyCollection<object> ExecuteScalarMultiple(IDatabaseSubroutineSignature signature);
ReadOnlyCollection<object> ExecuteScalarMultiple(IDatabaseSubroutineSignature signature, IEnumerable<object> parameterValues);

There are some differences between the standard .NET BCL ExecuteYYY methods and the above:

  • Our ExecuteNonQuery methods return void. This is because ExecuteNonQuery (on the command object) always returns -1 when a stored procedure is executed.
  • We have introduced a new ExecuteScalarMultiple method. This accounts for multiple output parameters.

IDatabaseSubroutineSignature looks like this:

public interface IDatabaseSubroutineSignature
{
    string Name { get; }
    IEnumerable<IDatabaseSubroutineParameter> Parameters { get; }
}

public interface IDatabaseSubroutineParameter
{
    ParameterType Type { get; }
    ParameterDirection Direction { get; }
}

// Using custom DbType attribute.
public enum ParameterType
{
    [DbType(DbType.Decimal)]
    Decimal,
    [DbType(DbType.String)]
    String,
    [DbType(DbType.StringFixedLength)]
    Character,
    RefCursor,
    [DbType(DbType.Double)]
    Double,
    [DbType(DbType.Int32)]
    Int32,
    [DbType(DbType.Int64)]
    Int64,
    [DbType(DbType.DateTime)]
    DateTime
}

The final problem we had is with a convenient way to create (and represent) the signatures in code. We settled on a monadesque approach by creating a subinterface of IDatabaseSubroutineSignature that exposes methods for creating parameters:

public interface IDatabaseSubroutineSignatureCreator : IDatabaseSubroutineSignature
{
    IDatabaseSubroutineSignatureCreator Input(ParameterType dbType);
    IDatabaseSubroutineSignatureCreator Output(ParameterType dbType);
    IDatabaseSubroutineSignatureCreator InputOutput(ParameterType dbType);
    IDatabaseSubroutineSignatureCreator ReturnValue(ParameterType dbType);
}

Finally, here is a usage example:

private static readonly IDatabaseSubroutineSignature MyProcedureSignature =
    DatabaseSubroutineSignatureFactory.Create("pkg.myprocedure")
        .Input(ParameterType.Decimal)
        .Input(ParameterType.String)
        .Output(ParameterType.RefCursor);

public IEnumerable<DataObject> CallMyProcedure(decimal userId, string searchQuery)
{
    using (IDatabaseService dbService = ...)
    using (IDataReader dataReader = dbService.ExecuteReader(MyProcedureSignature,
        new object[] { userId, searchQuery }))
    {
        while (dataReader.Read())
        {
            yield return new DataObject(
                dataReader.GetDecimal(0),
                dataReader.GetString(1));
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top