Three years ago I posted an answer to the "NHibernate multi query / futures with Oracle" question with a solution how to make future queries work with Oracle.
It was as simple as adding two derived classes EnhancedOracleDataClientDriver and EnhancedOracleResultSetsCommand to a project, and configuring NHibernate to use the EnhancedOracleDataClientDriver class as the database driver.
I recently checked this issue https://nhibernate.jira.com/browse/NH-2170, and figured out that out-of-box NHibernate still does not support futures with Oracle. Plus, I got a question from Ruben on StackOverflow if I can share any sources and/or methodology about deriving this "Enhanced" implementation approach. Plus, some people tested this "Enhanced" approach and were disappointed with the fact that no performance boost noticed as with SQL Server futures.
So I decided to spend some time and revisit this issue trying to profile and optimize the "Enhanced" approach.
Here are my findings with the profiler:
- In Oracle.ManagedDataAccess provider, the implementation of
parameter binding by name is slower than positional parameter
binding. I tested a multi-criteria with overall 500 named
parameters, and the profiler showed me that before executing the
command Oracle provider spent nearly 1 second just for converting
named parameters into positional ones. I believe that even regular
(non-future) queries with like 500 named parameters would experience
similar performance penalties. So, one bottleneck is
"command.BindByName = true;".
- When combining multiple queries into one batch, the
SqlStringBuilder.Add(...) should be used (not
SqlString.Append(...)). This is the same as with combining strings:
StringBuilder performs way better than String.
So, after carefully analysing NHibernate source code where SQL commands are constructed and combined into batches, I came out with the version #2 of the "Enhanced" approach. I hope that NHibernate core team will notice this and consider adding futures with Oracle to my favourite ORM.
By the way, the "Enhanced" approach depends on Oracle refcursors (one output refcursor for each query in a batch), and there is Oracle limitation of max cursors per session that we must be aware of (default on Oracle XE is max 300 cursors).
Usage. Add the below two classes EnhancedOracleManagedDataClientDriver and EnhancedOracleManagedResultSetsCommand to your project and configure NHibernate to use the EnhancedOracleManagedDataClientDriver class as the database driver.
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);
}
}
}