三年前,我发布了“NHibernate multi query / futures with Oracle”问题的答案,其中包含如何使未来的查询与 Oracle 一起工作的解决方案。就像在项目中添加两个派生类 EnhancedOracleDataClientDriver 和 EnhancedOracleResultSetsCommand 一样简单,并将 NHibernate 配置为使用 EnhancedOracleDataClientDriver 类作为数据库驱动程序。
我最近检查了这个问题https://nhibernate.jira.com/browse/NH-2170,发现开箱即用的 NHibernate 仍然不支持 Oracle 的期货。另外,我在 StackOverflow 上收到了 Ruben 的一个问题,我是否可以分享任何关于推导这种“增强”实现方法的资源和/或方法。此外,有些人测试了这种“增强”方法,但对 SQL Server 期货没有注意到性能提升这一事实感到失望。
所以我决定花一些时间重新审视这个问题,试图分析和优化“增强”方法。
以下是我对分析器的发现:
- 在 Oracle.ManagedDataAccess 提供程序中,按名称实现参数绑定比位置参数绑定慢。我测试了一个包含总共 500 个命名参数的多标准,分析器向我显示,在执行命令之前,Oracle 提供程序花费了将近 1 秒来将命名参数转换为位置参数。我相信即使是具有 500 个命名参数的常规(非未来)查询也会遇到类似的性能损失。因此,一个瓶颈是“command.BindByName = true;”。
- 将多个查询合并为一批时,应使用 SqlStringBuilder.Add(...)(而不是 SqlString.Append(...))。这与组合字符串相同:StringBuilder 比 String 执行得更好。
因此,在仔细分析了构建 SQL 命令并将其组合成批处理的 NHibernate 源代码之后,我提出了“增强”方法的版本 #2。我希望 NHibernate 核心团队会注意到这一点,并考虑将 Oracle 期货添加到我最喜欢的 ORM 中。
顺便说一句,“增强”方法依赖于 Oracle refcursor(一个批处理中的每个查询的一个输出 refcursor),并且我们必须注意每个会话的最大游标的 Oracle 限制(Oracle XE 上的默认设置是最大 300 个游标)。
用法。将以下两个类 EnhancedOracleManagedDataClientDriver 和 EnhancedOracleManagedResultSetsCommand 添加到您的项目中,并将 NHibernate 配置为使用 EnhancedOracleManagedDataClientDriver 类作为数据库驱动程序。
增强的 OracleManagedDataClientDriver.cs
using System;
using System.Data;
using System.Reflection;
using NHibernate.Engine;
using NHibernate.SqlTypes;
using NHibernate.Util;
namespace NHibernate.Driver
{
public class EnhancedOracleManagedDataClientDriver : OracleManagedDataClientDriver
{
private readonly PropertyInfo _oracleCommandBindByName;
private readonly PropertyInfo _oracleDbType;
private readonly object _oracleDbTypeRefCursor;
public EnhancedOracleManagedDataClientDriver()
{
_oracleCommandBindByName = ReflectHelper.TypeFromAssembly(
"Oracle.ManagedDataAccess.Client.OracleCommand", "Oracle.ManagedDataAccess", true).GetProperty("BindByName");
_oracleDbType = ReflectHelper.TypeFromAssembly(
"Oracle.ManagedDataAccess.Client.OracleParameter", "Oracle.ManagedDataAccess", true).GetProperty("OracleDbType");
var enumType = ReflectHelper.TypeFromAssembly(
"Oracle.ManagedDataAccess.Client.OracleDbType", "Oracle.ManagedDataAccess", true);
_oracleDbTypeRefCursor = Enum.Parse(enumType, "RefCursor");
}
public override bool SupportsMultipleQueries => true;
public override IResultSetsCommand GetResultSetsCommand(ISessionImplementor session)
{
return new EnhancedOracleManagedResultSetsCommand(session);
}
protected override void InitializeParameter(IDbDataParameter dbParam, string name, SqlType sqlType)
{
// this "exotic" parameter type will actually mean output refcursor
if (sqlType.DbType == DbType.VarNumeric)
{
dbParam.ParameterName = FormatNameForParameter(name);
dbParam.Direction = ParameterDirection.Output;
_oracleDbType.SetValue(dbParam, _oracleDbTypeRefCursor, null);
}
else
base.InitializeParameter(dbParam, name, sqlType);
}
protected override void OnBeforePrepare(IDbCommand command)
{
base.OnBeforePrepare(command);
if (command.CommandText.StartsWith("\nBEGIN -- multi query\n"))
{
// for better performance, in multi-queries,
// we switch to parameter binding by position (not by name)
this._oracleCommandBindByName.SetValue(command, false, null);
command.CommandText = command.CommandText.Replace(":p", ":");
}
}
}
}
增强的 OracleManagedResultSetsCommand.cs
using System.Data;
using System.Linq;
using NHibernate.Engine;
using NHibernate.Impl;
using NHibernate.Loader.Custom;
using NHibernate.Loader.Custom.Sql;
using NHibernate.SqlCommand;
using NHibernate.SqlTypes;
using NHibernate.Type;
namespace NHibernate.Driver
{
public class EnhancedOracleManagedResultSetsCommand : BasicResultSetsCommand
{
private readonly SqlStringBuilder _sqlStringBuilder = new SqlStringBuilder();
private SqlString _sqlString = new SqlString();
private QueryParameters _prefixQueryParameters;
private CustomLoader _prefixLoader;
public EnhancedOracleManagedResultSetsCommand(ISessionImplementor session)
: base(session) {}
public override SqlString Sql => _sqlString;
public override void Append(ISqlCommand command)
{
if (_prefixLoader == null)
{
var prefixQuery = (SqlQueryImpl)((ISession)Session)
// this SQL query fragment will prepend every SELECT query in multiquery/multicriteria
.CreateSQLQuery("\nOPEN :crsr \nFOR\n")
// this "exotic" parameter type will actually mean output refcursor
.SetParameter("crsr", 0, new DecimalType(new SqlType(DbType.VarNumeric)));
_prefixQueryParameters = prefixQuery.GetQueryParameters();
var querySpecification = prefixQuery.GenerateQuerySpecification(_prefixQueryParameters.NamedParameters);
_prefixLoader = new CustomLoader(new SQLCustomQuery(querySpecification.SqlQueryReturns, querySpecification.QueryString,
querySpecification.QuerySpaces, Session.Factory), Session.Factory);
}
var prefixCommand = _prefixLoader.CreateSqlCommand(_prefixQueryParameters, Session);
Commands.Add(prefixCommand);
Commands.Add(command);
_sqlStringBuilder.Add(prefixCommand.Query);
_sqlStringBuilder.Add(command.Query).Add("\n;\n\n");
}
public override IDataReader GetReader(int? commandTimeout)
{
var batcher = Session.Batcher;
var sqlTypes = Commands.SelectMany(c => c.ParameterTypes).ToArray();
ForEachSqlCommand((sqlLoaderCommand, offset) => sqlLoaderCommand.ResetParametersIndexesForTheCommand(offset));
_sqlStringBuilder.Insert(0, "\nBEGIN -- multi query\n").Add("\nEND;\n");
_sqlString = _sqlStringBuilder.ToSqlString();
var command = batcher.PrepareQueryCommand(CommandType.Text, _sqlString, sqlTypes);
if (commandTimeout.HasValue)
command.CommandTimeout = commandTimeout.Value;
BindParameters(command);
return new BatcherDataReaderWrapper(batcher, command);
}
}
}