-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #32 from DIPSAS/feature/bulksql
Added bulk sql feature
- Loading branch information
Showing
15 changed files
with
2,193 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
## Bulk Operation | ||
|
||
|
||
If you want to insert, update or delete many rows at a time from a database, the most efficient way is to minimize the number of round trips. | ||
Oracle has this built in to the client api, but using it is rather cumbersome to use. | ||
Dapper.Oracle contains some extension methods that makes this easier. | ||
|
||
Consider the following class, a simplified dataaccess: | ||
|
||
``` | ||
public class CustomerDAL | ||
{ | ||
public IDbConnection Connection { get; } | ||
public CustomerDAL(IDbConnection connection) | ||
{ | ||
Connection = connection; | ||
} | ||
public void InsertCustomers(IEnumerable<Customer> customers) | ||
{ | ||
string insertSql = "INSERT INTO CUSTOMERS(CUSTOMERID,NAME,ADDRESS,POSTALCODE,CITY) VALUES(:CUSTOMERID,:NAME,:POSTALCODE,:CITY)"; | ||
foreach (var customer in customers) | ||
{ | ||
var parameters = new OracleDynamicParameters(); | ||
parameters.Add("CUSTOMERID",customer.CustomerId); | ||
parameters.Add("NAME",customer.Name); | ||
parameters.Add("ADDRESS",customer.Address); | ||
parameters.Add("POSTALCODE",customer.Address); | ||
parameters.Add("POSTALCODE", customer.PostalCode); | ||
parameters.Add("CITY",customer.City); | ||
Connection.Execute(insertSql, parameters); | ||
} | ||
} | ||
} | ||
``` | ||
The method InsertCustomers takes in a `IEnumerable<Customer>`, and iterates over it, inserting customers into the database. | ||
This is a fairly common pattern, but it has some drawbacks, the biggest one being that it performs a full database roundtrip per row to insert into the database. | ||
A much better approach is to send over an array of parameters, and have the database execute all statements in a single roundtrip. | ||
This allows for > 1000 inserts/second. | ||
|
||
The same class, rewritten using Dapper.Oracle Bulk Sql | ||
|
||
``` | ||
public class CustomerDAL | ||
{ | ||
public IDbConnection Connection { get; } | ||
public CustomerDAL(IDbConnection connection) | ||
{ | ||
Connection = connection; | ||
} | ||
public void InsertCustomers(IEnumerable<Customer> customers) | ||
{ | ||
string insertSql = "INSERT INTO CUSTOMERS(CUSTOMERID,NAME,ADDRESS,POSTALCODE,CITY) VALUES(:CUSTOMERID,:NAME,:POSTALCODE,:CITY)"; | ||
var mapping = new BulkMapping<Customer>[] | ||
{ | ||
new BulkMapping<Customer>("CUSTOMERID",c=>c.CustomerId), | ||
new BulkMapping<Customer>("NAME",c=>c.Name), | ||
new BulkMapping<Customer>("ADDRESS",c=>c.Address), | ||
new BulkMapping<Customer>("POSTALCODE",c=>c.PostalCode), | ||
new BulkMapping<Customer>("CITY",c=>c.City), | ||
}; | ||
Connection.SqlBulk(insertSql, customers, mapping); | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Data; | ||
using System.Text; | ||
|
||
namespace Dapper.Oracle.BulkSql | ||
{ | ||
/// <summary> | ||
/// Contains mapping between a property on T and a database query parameter | ||
/// </summary> | ||
/// <typeparam name="T">Entity type for mapping</typeparam> | ||
public class BulkMapping<T> | ||
{ | ||
public string Name { get; set; } | ||
|
||
public Func<T, object> Property { get; set; } | ||
|
||
public ParameterDirection ParameterDirection { get; set; } | ||
|
||
public OracleMappingType? DbType { get; set; } | ||
|
||
public int? Size { get; set; } | ||
|
||
public bool? IsNullable { get; set; } | ||
|
||
public byte? Precision { get; set; } | ||
|
||
public byte? Scale { get; set; } | ||
|
||
public string SourceColumn { get; set; } = string.Empty; | ||
|
||
public DataRowVersion SourceVersion { get; set; } | ||
|
||
public OracleMappingCollectionType CollectionType { get; set; } = OracleMappingCollectionType.None; | ||
|
||
public int[] ArrayBindSize { get; set; } | ||
|
||
/// <summary> | ||
/// Creates an instance of parametermapping to be used in bulk operations | ||
/// </summary> | ||
/// <param name="name">Name. Must match the named parameter in the sql statement or stored procedure</param> | ||
/// <param name="property">Selectorfunc for querying an IEnumerable of T for a specific property</param> | ||
/// <param name="dbType">Oracle database type</param> | ||
/// <param name="direction">Parameter direction. Defaults to Input</param> | ||
/// <param name="size"></param> | ||
/// <param name="isNullable"></param> | ||
/// <param name="precision"></param> | ||
/// <param name="scale"></param> | ||
/// <param name="sourceColumn"></param> | ||
/// <param name="sourceVersion"></param> | ||
/// <param name="collectionType"></param> | ||
/// <param name="arrayBindSize"></param> | ||
public BulkMapping(string name, | ||
Func<T, object> property, | ||
OracleMappingType? dbType = null, | ||
ParameterDirection? direction = null, | ||
int? size = null, | ||
bool? isNullable = null, | ||
byte? precision = null, | ||
byte? scale = null, | ||
string sourceColumn = null, | ||
DataRowVersion? sourceVersion = null, | ||
OracleMappingCollectionType? collectionType = null, | ||
int[] arrayBindSize = null) | ||
{ | ||
Name = name; | ||
Property = property; | ||
ParameterDirection = direction ?? ParameterDirection.Input; | ||
DbType = dbType; | ||
Size = size; | ||
IsNullable = isNullable ?? false; | ||
Precision = precision; | ||
Scale = scale; | ||
SourceColumn = sourceColumn; | ||
SourceVersion = sourceVersion ?? DataRowVersion.Current; | ||
CollectionType = collectionType ?? OracleMappingCollectionType.None; | ||
ArrayBindSize = arrayBindSize; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Data; | ||
using System.Linq; | ||
using System.Runtime.InteropServices.ComTypes; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
|
||
namespace Dapper.Oracle.BulkSql | ||
{ | ||
public static class BulkOperation | ||
{ | ||
|
||
/// <summary> | ||
/// Executes a bulk SQL statement against database and returns the number of rows affected | ||
/// Works with UPDATE / INSERT / DELETE statements, and stored procedures. | ||
/// </summary> | ||
/// <typeparam name="T">Entity type for the bulk operation object</typeparam> | ||
/// <param name="connection">The <see cref="IDbConnection">Database connection to use</see></param> | ||
/// <param name="sql">Sql statement to execute. | ||
/// <remarks> | ||
/// Parameter names MUST MATCH property names in object entity. | ||
/// </remarks></param> | ||
/// <param name="objects">IEnumerable containing object for bulk operation</param> | ||
/// <param name="cmdType">Command type;Text or StoredProcedure</param> | ||
/// <param name="transaction">IDBtransaction to use</param> | ||
/// <returns>Number of rows affected by bulk statement</returns> | ||
public static int SqlBulk<T>(this IDbConnection connection, string sql, IEnumerable<T> objects, | ||
IEnumerable<BulkMapping<T>> mapping, IDbTransaction transaction = null, CommandType? cmdType = CommandType.Text) | ||
{ | ||
return SqlBulk<T>(connection, sql, objects, mapping, out _, cmdType, transaction); | ||
} | ||
|
||
|
||
/// <summary> | ||
/// Executes a bulk SQL statement against database and returns the number of rows affected | ||
/// Works with UPDATE / INSERT / DELETE statements, and stored procedures. | ||
/// </summary> | ||
/// <typeparam name="T">Entity type for the bulk operation object</typeparam> | ||
/// <param name="connection">The <see cref="IDbConnection">Database connection to use</see></param> | ||
/// <param name="sql">Sql statement to execute. | ||
/// <remarks> | ||
/// Parameter names MUST MATCH property names in object entity. | ||
/// </remarks></param> | ||
/// <param name="objects">IEnumerable containing object for bulk operation</param> | ||
/// <param name="parameters">Instance of <see cref="OracleDynamicParameters"/> used for executing sql statements. Can be used to retreive value from a refcursor</param> | ||
/// <param name="cmdType">Command type;Text or StoredProcedure</param> | ||
/// <param name="transaction">IDBtransaction to use</param> | ||
/// <returns>Number of rows affected by bulk statement</returns> | ||
public static int SqlBulk<T>(this IDbConnection connection, string sql, IEnumerable<T> objects, | ||
IEnumerable<BulkMapping<T>> mapping, out OracleDynamicParameters parameters, | ||
CommandType? cmdType = CommandType.Text, IDbTransaction transaction = null) | ||
{ | ||
parameters = CreateParameterFromObject(objects, mapping); | ||
return connection.Execute(sql, parameters, commandType: cmdType); | ||
} | ||
|
||
public static async Task<AsyncQueryResult> SqlBulkAsync<T>(this IDbConnection connection, string sql, IEnumerable<T> objects, IEnumerable<BulkMapping<T>> mapping, CommandType? cmdType = CommandType.Text, IDbTransaction transaction=null) | ||
{ | ||
var parameters = CreateParameterFromObject(objects, mapping); | ||
var result = await connection.ExecuteAsync(sql, parameters,transaction, commandType:cmdType); | ||
return new AsyncQueryResult | ||
{ | ||
ExecuteResult = result, | ||
Parameters = parameters | ||
}; | ||
} | ||
|
||
private static OracleDynamicParameters CreateParameterFromObject<T>(IEnumerable<T> objects, | ||
IEnumerable<BulkMapping<T>> mapping) | ||
{ | ||
var parameters = new OracleDynamicParameters(); | ||
var obj = objects.ToList(); | ||
parameters.ArrayBindCount = obj.Count; | ||
parameters.BindByName = true; | ||
foreach (var map in mapping) | ||
{ | ||
var values = map.Property != null ? obj.Select(map.Property).ToArray() : null; | ||
var dbType = map.DbType ?? OracleMapper.GuessType(obj.First().GetType()); | ||
|
||
parameters.Add( | ||
Clean(map.Name), | ||
values, | ||
dbType, | ||
map.ParameterDirection, | ||
map.Size, | ||
map.IsNullable, | ||
map.Precision, | ||
map.Scale, | ||
map.SourceColumn, | ||
map.SourceVersion, | ||
map.CollectionType, | ||
map.ArrayBindSize); | ||
} | ||
|
||
return parameters; | ||
} | ||
|
||
private static string Clean(string name) | ||
{ | ||
if (name.StartsWith("@") || name.StartsWith(":")) | ||
{ | ||
return name.Substring(1); | ||
} | ||
|
||
return name; | ||
} | ||
} | ||
|
||
public class AsyncQueryResult | ||
{ | ||
/// <summary> | ||
/// Return value from Execute statement, returns the number of rows affected. | ||
/// </summary> | ||
public int ExecuteResult { get; set; } | ||
|
||
/// <summary> | ||
/// Returns the Dynamic parameters used in query. | ||
/// </summary> | ||
public OracleDynamicParameters Parameters { get; set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
|
||
namespace Dapper.Oracle.BulkSql | ||
{ | ||
internal static class OracleMapper | ||
{ | ||
private static Dictionary<Type, OracleMappingType> OracleMappings = new Dictionary<Type, OracleMappingType> | ||
{ | ||
{typeof(Guid), OracleMappingType.Raw}, | ||
{typeof(string), OracleMappingType.Varchar2}, | ||
{typeof(long), OracleMappingType.Long}, | ||
{typeof(decimal), OracleMappingType.Decimal}, | ||
{typeof(DateTime), OracleMappingType.Date}, | ||
{typeof(int), OracleMappingType.Int32}, | ||
{typeof(bool),OracleMappingType.Int16 } | ||
}; | ||
|
||
public static OracleMappingType? GuessType(Type type) | ||
{ | ||
if (OracleMappings.ContainsKey(type)) | ||
{ | ||
return OracleMappings[type]; | ||
} | ||
|
||
return null; | ||
} | ||
|
||
public static OracleDynamicParameters Create<T>(IEnumerable<T> objects) | ||
{ | ||
var type = typeof(T); | ||
var entities = objects.ToList(); | ||
|
||
var odp = new OracleDynamicParameters(); | ||
odp.ArrayBindCount = entities.Count; | ||
odp.BindByName = true; | ||
|
||
|
||
foreach (var pi in type.GetProperties()) | ||
{ | ||
if (pi.CanRead) | ||
{ | ||
var parameterName = pi.Name; | ||
OracleMappingType? dbType = null; | ||
|
||
Func<T, object> selector; | ||
if (OracleMappings.ContainsKey(pi.PropertyType)) | ||
{ | ||
dbType = OracleMappings[pi.PropertyType]; | ||
} | ||
|
||
if (pi.PropertyType == typeof(Guid)) | ||
{ | ||
selector = p => ((Guid)pi.GetValue(p)).ToByteArray(); | ||
} | ||
else if (pi.PropertyType == typeof(bool)) | ||
{ | ||
selector = p => ((bool)pi.GetValue(p)) ? 1 : 0; | ||
} | ||
else | ||
{ | ||
selector = p => pi.GetValue(p); | ||
} | ||
|
||
var values = entities.Select(selector).ToArray(); | ||
odp.Add(parameterName, values, dbType); | ||
} | ||
} | ||
|
||
return odp; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.