3 年前、私は「NHibernate multi query / futures with Oracle」という質問への回答を投稿し、将来のクエリを Oracle で動作させる方法を説明しました。2 つの派生クラス EnhancedOracleDataClientDriver と EnhancedOracleResultSetsCommand をプロジェクトに追加し、NHibernate を構成して EnhancedOracleDataClientDriver クラスをデータベース ドライバーとして使用するだけでした。
最近、この問題https://nhibernate.jira.com/browse/NH-2170を確認したところ、すぐに使用できる NHibernate が Oracle との先物をまだサポートしていないことがわかりました。さらに、この「強化された」実装アプローチの導出に関する情報源や方法論を共有できるかどうか、StackOverflow で Ruben から質問を受けました。さらに、一部の人々はこの「強化された」アプローチをテストしましたが、SQL Server の将来のようにパフォーマンスの向上が見られなかったという事実に失望しました。
そこで、時間をかけてこの問題を再検討し、「拡張」アプローチのプロファイリングと最適化を試みることにしました。
プロファイラーでの私の調査結果は次のとおりです。
- Oracle.ManagedDataAccess プロバイダでは、名前によるパラメータ バインディングの実装は、位置パラメータ バインディングよりも遅くなります。全体で 500 個の名前付きパラメーターを使用して複数の基準をテストしたところ、プロファイラーは、コマンドを実行する前に、Oracle プロバイダーが名前付きパラメーターを位置パラメーターに変換するためだけに 1 秒近く費やしたことを示しました。500 個の名前付きパラメーターを使用する通常の (将来ではない) クエリでさえ、同様のパフォーマンス ペナルティを経験すると思います。したがって、1 つのボトルネックは「command.BindByName = true;」です。
- 複数のクエリを 1 つのバッチに結合する場合は、SqlStringBuilder.Add(...) を使用する必要があります (SqlString.Append(...) ではありません)。これは、文字列を結合する場合と同じです。StringBuilder は、String よりもはるかに優れたパフォーマンスを発揮します。
そのため、SQL コマンドが構築されてバッチに結合される NHibernate ソース コードを注意深く分析した後、「強化された」アプローチのバージョン #2 にたどり着きました。NHibernate のコア チームがこれに気付き、私のお気に入りの ORM に Oracle の先物を追加することを検討してくれることを願っています。
ちなみに、「拡張」アプローチは Oracle refcursors (バッチ内のクエリごとに 1 つの出力 refcursor) に依存し、注意しなければならないセッションごとの最大カーソル数の Oracle 制限があります (Oracle XE のデフォルトは最大 300 カーソルです)。 )。
使用法。以下の 2 つのクラス EnhancedOracleManagedDataClientDriver および EnhancedOracleManagedResultSetsCommand をプロジェクトに追加し、NHibernate を構成して、EnhancedOracleManagedDataClientDriver クラスをデータベース ドライバーとして使用します。
EnhancedOracleManagedDataClientDriver.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", ":");
}
}
}
}
EnhancedOracleManagedResultSetsCommand.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);
}
}
}