Независимый от базы данных API хранимых процедур

Наше устаревшее веб-приложение активно использует хранимые процедуры. У нас есть центральный интерфейс, через который выполняются все обращения к базе данных (то есть запросы и процедуры). Однако в текущей реализации используется метод OracleCommandBuilder.DeriveParameters под капотом для привязки к соответствующей сигнатуре хранимой процедуры. Из документации:

DeriveParameters требует обращения к базе данных и должен использоваться только во время разработки. Чтобы избежать ненужных обращений к базе данных в производственной среде, сам метод DeriveParameters следует заменить явными настройками параметров, которые были возвращены методом DeriveParameters во время разработки.

Мы могли бы использовать класс OracleCommand для явного связывания к правильной подписи хранимой процедуры. Однако засорение нашего кода (даже если только уровня доступа к данным) объектами OracleCommand не зависит от базы данных. Мы уже поддерживаем динамические запросы, не зависящие от базы данных, в нашем интерфейсе базы данных (далее именуемом IDatabaseService), который выглядит следующим образом:

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

Мы также хотим поддерживать вызовы хранимых процедур, не зависящие от базы данных. Что является хорошим шаблоном для этого?

Дополнительная информация:

Для привязки к определенной подпрограмме OracleCommands разрешают BindByName. Мы предпочитаем не использовать этот подход, поскольку строка более подвержена ошибкам, чем тип. Другой подход к связыванию вызова подпрограммы заключается в предоставлении типов параметров. Мы могли бы полагаться на значения параметров и размышлять о типах времени выполнения, но нам нужна более сильная безопасность. Мы хотим потребовать, чтобы типы были явно предоставлены интерфейсу базы данных, чтобы мы могли проверить, соответствуют ли предоставленные значения параметров предоставленным типам параметров подпрограммы, прежде чем мы будем связываться с базой данных.


person Matthew Rodatus    schedule 03.10.2011    source источник
comment
Итак, сколько вы платите за Oracle, но хотите полностью универсальный/независимый подход? Если вы заплатили за Oracle, используйте его! Если нет, просто используйте Postres или mysql или что-то подобное. Просто мои 2 цента, хотя.   -  person tbone    schedule 03.10.2011
comment
@tbone Мы не контролируем нашу среду развертывания, но она может быть изменена. Мы готовимся к сценарию, когда наш клиент хочет перейти на другую платформу БД.   -  person Matthew Rodatus    schedule 04.10.2011
comment
по моему опыту, более вероятно, что промежуточное программное обеспечение и/или внешний интерфейс изменятся до того, как изменится серверная часть (особенно, если они какое-то время использовали Oracle и разработали слой пакетов/процедур/функций).   -  person tbone    schedule 04.10.2011
comment
@tbone Это не тот случай.   -  person Matthew Rodatus    schedule 04.10.2011


Ответы (1)


После создания прототипов различных подходов мы остановились на следующем.

В IDatabaseService мы добавили новые методы ExecuteYYY, которые принимают объект, реализующий IDatabaseSubroutineSignature, и (необязательно, через перегрузку) IEnumerable, являющиеся значениями параметров.

Методы ExecuteYYY в IDatabaseService выглядят следующим образом:

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);

Существуют некоторые различия между стандартными методами .NET BCL ExecuteYYY и приведенными выше:

  • Наши методы ExecuteNonQuery возвращают void. Это связано с тем, что ExecuteNonQuery (для объекта команды) всегда возвращает -1 при выполнении хранимой процедуры.
  • Мы ввели новый метод ExecuteScalarMultiple. Это учитывает несколько выходных параметров.

Подпись IDatabaseSubroutineSignature выглядит следующим образом:

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
}

Последняя проблема, с которой мы столкнулись, связана с удобным способом создания (и представления) подписей в коде. Мы остановились на монадском подходе, создав субинтерфейс IDatabaseSubroutineSignature, который предоставляет методы для создания параметров:

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

Наконец, вот пример использования:

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));
        }
    }
}
person Matthew Rodatus    schedule 05.10.2011