#oracle #stored-procedures #database-agnostic
#Oracle #хранимые процедуры #не зависящий от базы данных
Вопрос:
Наше устаревшее веб-приложение в значительной степени использует хранимые процедуры. У нас есть центральный интерфейс, через который выполняются все вызовы базы данных (т. Е. Запросы и процедуры). Однако в текущей реализации используется OracleCommandBuilder .Метод DeriveParameters под капотом для привязки к соответствующей сигнатуре хранимой процедуры. Из документации:
DeriveParameters требует обхода базы данных и должен использоваться только во время разработки. Чтобы избежать ненужных обходов базы данных в рабочей среде, сам метод DeriveParameters следует заменить явными настройками параметров, которые были возвращены методом DeriveParameters во время разработки.
Мы могли бы использовать класс OracleCommand для явной привязки к правильной сигнатуре хранимой процедуры. Однако засорение нашего кода (даже если только уровня доступа к данным) объектами OracleCommand не зависит от базы данных. Мы уже поддерживаем динамические запросы, зависящие от базы данных, в нашем интерфейсе базы данных (далее именуемом IDatabaseService), который выглядит следующим образом:
int ExecuteNonQuery(string query, object[] parameterValues);
IDataReader ExecuteReader(string query, object[] parameterValues);
// etc.
Мы хотим также поддерживать независимые от базы данных вызовы хранимых процедур. Какой хороший шаблон для этого?
Дополнительная информация:
Для привязки к определенной подпрограмме команды OracleCommands разрешают BindByName . Мы предпочитаем не использовать этот подход, поскольку строка более подвержена ошибкам, чем тип. Другой подход к привязке вызова подпрограммы заключается в предоставлении типов параметров. Мы могли бы полагаться на значения параметров и учитывать типы среды выполнения, но нам нужна более надежная защита. Мы хотим потребовать, чтобы типы были явно предоставлены интерфейсу базы данных, чтобы мы могли проверить, что предоставленные значения параметров соответствуют предоставленным типам параметров подпрограммы, прежде чем мы свяжемся с базой данных.
Комментарии:
1. Итак, сколько вы платите за Oracle, но хотите полностью универсальный / независимый подход? Если вы заплатили за Oracle, используйте его! Если нет, просто используйте Postres или mysql или аналогичный. Всего лишь мои 2 цента.
2. @tbone Мы не контролируем нашу среду развертывания, но она может быть изменена. Мы готовимся к сценарию, в котором наш клиент хочет переключиться на другую платформу БД.
3. по моему опыту, более вероятно, что промежуточное программное обеспечение и / или интерфейс изменятся раньше, чем серверная часть (особенно, если они некоторое время использовали Oracle и разработали слой пакетов / процедур / функций).
4. @tbone Здесь дело не в этом.
Ответ №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 executeyyyy и вышеупомянутыми:
- Наши методы ExecuteNonQuery возвращают void. Это связано с тем, что ExecuteNonQuery (для объекта command) всегда возвращает -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));
}
}
}