I´m trying to use dapper with Oracle (ODP.NET) and I would like to use the \"QueryMultiple\" functionality.
Passing this string to the QueryMultiple method:
The OP has probably long since solved the issue by now, but as of the time of writing, this question has only one answer and it doesn't really solve the problem of using Dapper's QueryMultiple()
method with Oracle. As @Kamolas81 correctly states, by using the syntax from the official examples, one will indeed get the ORA-00933: SQL command not properly ended
error message. I spent a while searching for some sort of documentation about how to do QueryMultiple()
with Oracle, but I was surprised that there wasn't really one place that had an answer. I would have thought this to be a fairly common task. I thought that I'd post an answer here to save me :) someone some time in the future just in case anybody happens to have this same problem.
Dapper seems to just pass the SQL command straight along to ADO.NET and whatever db provider is executing the command. In the syntax from the examples, where each command is separated by a line break, SQL server will interpret that as multiple queries to run against the database and it will run each of the queries and return the results into separate outputs. I'm not an ADO.NET expert, so I might be messing up the terminology, but the end effect is that Dapper gets the multiple query outputs and then works its magic.
Oracle, though, doesn't recognize the multiple queries; it thinks that the SQL command is malformed and returns the ORA-00933
message. The solution is to use cursors and return the output in a DynamicParameters collection. For example, whereas the SQL Server version would look like this:
var sql =
@"
select * from Customers where CustomerId = @id
select * from Orders where CustomerId = @id
select * from Returns where CustomerId = @id";
the Oracle version of the query would need to look like this:
var sql = "BEGIN OPEN :rslt1 FOR SELECT * FROM customers WHERE customerid = :id; " +
"OPEN :rslt2 FOR SELECT * FROM orders WHERE customerid = :id; " +
"OPEN :rslt3 FOR SELECT * FROM returns Where customerid = :id; " +
"END;";
For queries run against SQL Server, Dapper can handle it from there. However, because we're returning the result sets into cursor parameters, we'll need to use an IDynamicParameters
collection to specify parameters for the command. To add an extra wrinkle, the normal DynamicParameters.Add()
method in Dapper uses a System.Data.DbType for the optional dbType parameter, but the cursor parameters for the query need to be of type Oracle.ManagedDataAccess.Client.OracleDbType.RefCursor
. To solve this, I used the solution which @Daniel Smith proposed in this answer and created a custom implementation of the IDynamicParameters
interface:
using Dapper;
using Oracle.ManagedDataAccess.Client;
using System.Data;
public class OracleDynamicParameters : SqlMapper.IDynamicParameters
{
private readonly DynamicParameters dynamicParameters = new DynamicParameters();
private readonly List<OracleParameter> oracleParameters = new List<OracleParameter>();
public void Add(string name, OracleDbType oracleDbType, ParameterDirection direction, object value = null, int? size = null)
{
OracleParameter oracleParameter;
if (size.HasValue)
{
oracleParameter = new OracleParameter(name, oracleDbType, size.Value, value, direction);
}
else
{
oracleParameter = new OracleParameter(name, oracleDbType, value, direction);
}
oracleParameters.Add(oracleParameter);
}
public void Add(string name, OracleDbType oracleDbType, ParameterDirection direction)
{
var oracleParameter = new OracleParameter(name, oracleDbType, direction);
oracleParameters.Add(oracleParameter);
}
public void AddParameters(IDbCommand command, SqlMapper.Identity identity)
{
((SqlMapper.IDynamicParameters)dynamicParameters).AddParameters(command, identity);
var oracleCommand = command as OracleCommand;
if (oracleCommand != null)
{
oracleCommand.Parameters.AddRange(oracleParameters.ToArray());
}
}
}
So all of the code together goes something like this:
using Dapper;
using Oracle.ManagedDataAccess.Client;
using System.Data;
int selectedId = 1;
var sql = "BEGIN OPEN :rslt1 FOR SELECT * FROM customers WHERE customerid = :id; " +
"OPEN :rslt2 FOR SELECT * FROM orders WHERE customerid = :id; " +
"OPEN :rslt3 FOR SELECT * FROM returns Where customerid = :id; " +
"END;";
OracleDynamicParameters dynParams = new OracleDynamicParameters();
dynParams.Add(":rslt1", OracleDbType.RefCursor, ParameterDirection.Output);
dynParams.Add(":rslt2", OracleDbType.RefCursor, ParameterDirection.Output);
dynParams.Add(":rslt3", OracleDbType.RefCursor, ParameterDirection.Output);
dynParams.Add(":id", OracleDbType.Int32, ParameterDirection.Input, selectedId);
using (IDbConnection dbConn = new OracleConnection("<conn string here>"))
{
dbConn.Open();
var multi = dbConn.QueryMultiple(sql, param: dynParams);
var customer = multi.Read<Customer>().Single();
var orders = multi.Read<Order>().ToList();
var returns = multi.Read<Return>().ToList();
...
dbConn.Close();
}
Building on greyseal96's helpful answer, I created this implementation of IDynamicParameters
:
public class OracleDynamicParameters : SqlMapper.IDynamicParameters
{
private readonly DynamicParameters dynamicParameters;
private readonly List<OracleParameter> oracleParameters = new List<OracleParameter>();
public OracleDynamicParameters(params string[] refCursorNames) {
dynamicParameters = new DynamicParameters();
AddRefCursorParameters(refCursorNames);
}
public OracleDynamicParameters(object template, params string[] refCursorNames) {
dynamicParameters = new DynamicParameters(template);
AddRefCursorParameters(refCursorNames);
}
private void AddRefCursorParameters(params string[] refCursorNames)
{
foreach (string refCursorName in refCursorNames)
{
var oracleParameter = new OracleParameter(refCursorName, OracleDbType.RefCursor, ParameterDirection.Output);
oracleParameters.Add(oracleParameter);
}
}
public void AddParameters(IDbCommand command, SqlMapper.Identity identity)
{
((SqlMapper.IDynamicParameters)dynamicParameters).AddParameters(command, identity);
var oracleCommand = command as OracleCommand;
if (oracleCommand != null)
{
oracleCommand.Parameters.AddRange(oracleParameters.ToArray());
}
}
}
Assuming the same query, it can be used so:
var queryParams = new { id };
string[] refCursorNames = { "rslt1", "rslt2", "rslt3" };
var dynParams = new OracleDynamicParameters(queryParams, refCursorNames);
...
var multi = dbConn.QueryMultiple(sql, param: dynParams);
I suspect this is two or three separate things:
@
not :
(no idea if this depends on the RDBMS being used).If you look at the Dapper Google Code page the example given for QueryMultiple()
is:
var sql =
@"
select * from Customers where CustomerId = @id
select * from Orders where CustomerId = @id
select * from Returns where CustomerId = @id";
using (var multi = connection.QueryMultiple(sql, new {id=selectedId}))
{
var customer = multi.Read<Customer>().Single();
var orders = multi.Read<Order>().ToList();
var returns = multi.Read<Return>().ToList();
...
}
Remove the semi-colon; add a new line and if you still have issues change the bind character.