Skip to content

Commit

Permalink
Merge pull request #32 from DIPSAS/feature/bulksql
Browse files Browse the repository at this point in the history
Added bulk sql feature
  • Loading branch information
epaulsen authored May 12, 2019
2 parents 3931483 + f5851a5 commit 5451d89
Show file tree
Hide file tree
Showing 15 changed files with 2,193 additions and 14 deletions.
71 changes: 71 additions & 0 deletions doc/BulkSql.md
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);
}
}
```
5 changes: 5 additions & 0 deletions releasenotes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Releasenotes

## 1.2.1
- Fixed bug in type converter
- Added doc for Type handlers
- New feature: Bulk Sql, se doc/BulkSql.md for details.

## 1.2.0
- New buildsystem, now using Cakebuild instead of psake.
- Cleanup of file tree.
Expand Down
80 changes: 80 additions & 0 deletions src/Dapper.Oracle/BulkSql/BulkMapping.cs
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;
}
}
}
122 changes: 122 additions & 0 deletions src/Dapper.Oracle/BulkSql/BulkOperation.cs
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; }
}
}
75 changes: 75 additions & 0 deletions src/Dapper.Oracle/BulkSql/OracleMapper.cs
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;
}
}
}
14 changes: 7 additions & 7 deletions src/Dapper.Oracle/Dapper.Oracle.csproj
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.targets"/>
<Import Project="..\common.targets" />
<PropertyGroup>
<IsTestProject>false</IsTestProject>
<TargetFrameworks>netstandard2.0;net452</TargetFrameworks>
</PropertyGroup>

<ItemGroup Condition=" $(IsNetFramework) ">
<Reference Include="Microsoft.CSharp"/>
<Reference Include="System"/>
<Reference Include="System.Configuration"/>
<Reference Include="System.Core"/>
<Reference Include="System.Runtime.Serialization"/>
<Reference Include="Microsoft.CSharp" />
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Serialization" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="1.50.2"/>
<PackageReference Include="Dapper" Version="1.50.2" />
</ItemGroup>
<PropertyGroup Condition=" $(IsNetFramework) ">
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
Expand Down
Loading

0 comments on commit 5451d89

Please sign in to comment.