与数据库无关的存储过程 API

发布于 2024-12-07 23:58:58 字数 1205 浏览 2 评论 0原文

我们的遗留 Web 应用程序大量使用存储过程。我们有一个中央接口,通过它进行所有数据库调用(即查询和过程)。但是,当前的实现使用OracleCommandBuilder.DeriveParameters 方法 在底层绑定到适当的存储过程签名。从文档中:

DeriveParameters 会导致数据库往返,因此只能在设计时使用。 为了避免生产环境中不必要的数据库往返, DeriveParameters 方法本身应替换为显式参数设置 是在设计时由 DeriveParameters 方法返回的。

我们可以使用OracleCommand类来显式绑定到正确的存储过程签名。然而,用 OracleCommand 对象乱扔我们的代码(即使只是数据访问层)并不是与数据库无关的。我们已经在数据库接口(以下称为IDatabaseService)中支持与数据库无关的动态查询,如下所示:

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

我们还希望支持与数据库无关的存储过程调用。什么是好的模式呢?

更多信息:

要绑定到特定子例程,OracleCommands 允许按名称绑定。我们宁愿不使用这种方法,因为字符串比类型更容易出错。绑定子例程调用的另一种方法是提供参数类型。我们可以依赖参数值并反映运行时类型,但我们想要比这更强的安全性。我们希望要求将类型显式提供给数据库接口,以便我们可以在与数据库通信之前检查提供的参数值是否与提供的子例程参数类型匹配。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

零崎曲识 2024-12-14 23:58:58

在对各种方法进行原型设计之后,我们确定了以下方案。

对于 IDatabaseService,我们添加了新的 ExecuteYYY 方法,该方法采用实现 IDatabaseSubroutineSignature 的对象,并且(可选地,通过重载)一个作为参数值的 IEnumerable

IDatabaseService 上的 ExecuteYYY 方法如下所示:

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

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));
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文