From 5ce84de75cc565006b6c5727aa3d321d9be0a99a Mon Sep 17 00:00:00 2001 From: Ricardo Peres Date: Wed, 29 Oct 2014 00:42:10 +0000 Subject: [PATCH 1/3] Initial version --- .../DriverTest/BulkInsertTests.cs | 56 ++++++++ src/NHibernate/Cfg/Environment.cs | 4 + src/NHibernate/Driver/BulkProvider.cs | 60 ++++++++ src/NHibernate/Driver/DriverBase.cs | 6 + src/NHibernate/Driver/IDriver.cs | 5 + .../Driver/OracleDataClientBulkProvider.cs | 55 ++++++++ .../Driver/OracleDataClientDriver.cs | 5 + src/NHibernate/Driver/SqlBulkProvider.cs | 46 ++++++ src/NHibernate/Driver/SqlClientDriver.cs | 5 + .../Driver/TableBasedBulkProvider.cs | 133 ++++++++++++++++++ src/NHibernate/SessionExtensions.cs | 34 +++++ src/NHibernate/Transaction/AdoTransaction.cs | 5 + 12 files changed, 414 insertions(+) create mode 100644 src/NHibernate.Test/DriverTest/BulkInsertTests.cs create mode 100644 src/NHibernate/Driver/BulkProvider.cs create mode 100644 src/NHibernate/Driver/OracleDataClientBulkProvider.cs create mode 100644 src/NHibernate/Driver/SqlBulkProvider.cs create mode 100644 src/NHibernate/Driver/TableBasedBulkProvider.cs create mode 100644 src/NHibernate/SessionExtensions.cs diff --git a/src/NHibernate.Test/DriverTest/BulkInsertTests.cs b/src/NHibernate.Test/DriverTest/BulkInsertTests.cs new file mode 100644 index 00000000000..c254c5f9af6 --- /dev/null +++ b/src/NHibernate.Test/DriverTest/BulkInsertTests.cs @@ -0,0 +1,56 @@ +using System.Linq; +using NHibernate.Cfg; +using NHibernate.DomainModel.Northwind.Entities; +using NHibernate.Linq; +using NHibernate.Test.Linq; +using NUnit.Framework; + +namespace NHibernate.Test.DriverTest +{ + [TestFixture] + public class BulkInsertTests : LinqTestCase + { + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.Hbm2ddlAuto, SchemaAutoAction.Create.ToString()); + + base.Configure(configuration); + } + + [Test] + public void CanBulkInsertEntitiesWithComponents() + { + using (session.BeginTransaction()) + { + var customers = new Customer[] { new Customer { Address = new Address("street", "city", "region", "postalCode", "country", "phoneNumber", "fax"), CompanyName = "Company", ContactName = "Contact", ContactTitle = "Title", CustomerId = "12345" } }; + + this.session.CreateQuery("delete from Customer").ExecuteUpdate(); + + this.session.BulkInsert(customers); + + var count = this.session.Query().Count(); + + Assert.AreEqual(customers.Count(), count); + } + } + + [Test] + public void CanBulkInsertEntitiesWithComponentsAndAssociations() + { + using (session.BeginTransaction()) + { + var superior = new Employee { Address = new Address("street", "city", "region", "zip", "country", "phone", "fax"), BirthDate = System.DateTime.Now, EmployeeId = 1, Extension = "1", FirstName = "Superior", LastName = "Last" }; + var employee = new Employee { Address = new Address("street", "city", "region", "zip", "country", "phone", "fax"), BirthDate = System.DateTime.Now, EmployeeId = 2, Extension = "2", FirstName = "Employee", LastName = "Last", Superior = superior }; + var employees = new Employee[] { superior, employee }; + + this.session.CreateQuery("delete from Employee").ExecuteUpdate(); + + this.session.BulkInsert(employees); + + var count = this.session.Query().Count(); + + Assert.AreEqual(employees.Count(), count); + } + } + } +} diff --git a/src/NHibernate/Cfg/Environment.cs b/src/NHibernate/Cfg/Environment.cs index 1368e26d3bb..7546c27069d 100644 --- a/src/NHibernate/Cfg/Environment.cs +++ b/src/NHibernate/Cfg/Environment.cs @@ -65,6 +65,10 @@ public static string Version } } + public const String BulkProviderClass = "adonet.bulk_provider_class"; + public const String BulkProviderTimeout = "adonet.bulk_provider_timeout"; + public const String BulkProviderBatchSize = "adonet.bulk_provider_batch_size"; + public const string ConnectionProvider = "connection.provider"; public const string ConnectionDriver = "connection.driver_class"; public const string ConnectionString = "connection.connection_string"; diff --git a/src/NHibernate/Driver/BulkProvider.cs b/src/NHibernate/Driver/BulkProvider.cs new file mode 100644 index 00000000000..5562a5b2dce --- /dev/null +++ b/src/NHibernate/Driver/BulkProvider.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.Id; +using NHibernate.Persister.Entity; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Driver +{ + public abstract class BulkProvider : IDisposable + { + protected BulkProvider() + { + } + + ~BulkProvider() + { + this.Dispose(false); + } + + public Int32 BatchSize { get; set; } + + public Int32 Timeout { get; set; } + + public abstract void Insert(ISessionImplementor session, IEnumerable entities) where T : class; + + public virtual void Initialize(IDictionary properties) + { + if (properties.ContainsKey(Environment.BulkProviderTimeout) == true) + { + this.Timeout = Convert.ToInt32(properties[Environment.BulkProviderTimeout]); + } + + if (properties.ContainsKey(Environment.BulkProviderBatchSize) == true) + { + this.BatchSize = Convert.ToInt32(properties[Environment.BulkProviderBatchSize]); + } + } + + protected virtual void FillIdentifier(ISessionImplementor session, IEntityPersister persister, Object entity) + { + if (!(persister.IdentifierGenerator is Assigned) && !(persister.IdentifierGenerator is ForeignGenerator)) + { + var id = persister.IdentifierGenerator.Generate(session, entity); + + persister.SetIdentifier(entity, id, session.EntityMode); + } + } + + protected virtual void Dispose(Boolean disposing) + { + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/NHibernate/Driver/DriverBase.cs b/src/NHibernate/Driver/DriverBase.cs index 938a83e3dff..8a1230e1af5 100644 --- a/src/NHibernate/Driver/DriverBase.cs +++ b/src/NHibernate/Driver/DriverBase.cs @@ -21,6 +21,12 @@ public abstract class DriverBase : IDriver, ISqlParameterFormatter private int commandTimeout; private bool prepareSql; + public virtual BulkProvider GetBulkProvider() + { + //NH-3675 + return null; + } + public virtual void Configure(IDictionary settings) { // Command timeout diff --git a/src/NHibernate/Driver/IDriver.cs b/src/NHibernate/Driver/IDriver.cs index 2916037d1f5..76b9548e8d0 100644 --- a/src/NHibernate/Driver/IDriver.cs +++ b/src/NHibernate/Driver/IDriver.cs @@ -32,6 +32,11 @@ namespace NHibernate.Driver /// public interface IDriver { + /// + /// Returns a bulk provider for the current driver. + /// + BulkProvider GetBulkProvider(); + /// /// Configure the driver using . /// diff --git a/src/NHibernate/Driver/OracleDataClientBulkProvider.cs b/src/NHibernate/Driver/OracleDataClientBulkProvider.cs new file mode 100644 index 00000000000..a787397c902 --- /dev/null +++ b/src/NHibernate/Driver/OracleDataClientBulkProvider.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using NHibernate.Engine; + +namespace NHibernate.Driver +{ + public class OracleDataClientBulkProvider : TableBasedBulkProvider + { + public const String BulkProviderOptions = "adonet.bulk_provider_options"; + + private static readonly System.Type bulkCopyOptionsType = System.Type.GetType("Oracle.DataAccess.Client.OracleBulkCopyOptions, Oracle.DataAccess"); + private static readonly System.Type bulkCopyType = System.Type.GetType("Oracle.DataAccess.Client.OracleBulkCopy, Oracle.DataAccess"); + private static readonly PropertyInfo batchSizeProperty = bulkCopyType.GetProperty("BatchSize"); + private static readonly PropertyInfo bulkCopyTimeoutProperty = bulkCopyType.GetProperty("BulkCopyTimeout"); + private static readonly PropertyInfo destinationTableNameProperty = bulkCopyType.GetProperty("DestinationTableName"); + private static readonly MethodInfo writeToServerMethod = bulkCopyType.GetMethod("WriteToServer", new System.Type[] { typeof(DataTable) }); + + public Int32 Options { get; set; } + + public Int32 NotifyAfter { get; set; } + + public override void Initialize(IDictionary properties) + { + base.Initialize(properties); + + var bulkProviderOptions = String.Empty; + + if (properties.TryGetValue(BulkProviderOptions, out bulkProviderOptions) == true) + { + this.Options = Convert.ToInt32(bulkProviderOptions); + } + } + + public override void Insert(ISessionImplementor session, IEnumerable entities) + { + if (entities.Any() == true) + { + foreach (var table in this.GetTables(session, entities)) + { + using (var copy = Activator.CreateInstance(bulkCopyType, session.Connection, Enum.ToObject(bulkCopyOptionsType, this.Options)) as IDisposable) + { + batchSizeProperty.SetValue(copy, this.BatchSize, null); + bulkCopyTimeoutProperty.SetValue(copy, this.Timeout, null); + destinationTableNameProperty.SetValue(copy, table.TableName, null); + + writeToServerMethod.Invoke(copy, new Object[] { table }); + } + } + } + } + } +} diff --git a/src/NHibernate/Driver/OracleDataClientDriver.cs b/src/NHibernate/Driver/OracleDataClientDriver.cs index 465f9da81a5..e1a2f53ca02 100644 --- a/src/NHibernate/Driver/OracleDataClientDriver.cs +++ b/src/NHibernate/Driver/OracleDataClientDriver.cs @@ -52,6 +52,11 @@ public OracleDataClientDriver() oracleDbTypeXmlType = Enum.Parse(oracleDbTypeEnum, "XmlType"); } + public override BulkProvider GetBulkProvider() + { + return new OracleDataClientBulkProvider(); + } + /// public override bool UseNamedPrefixInSql { diff --git a/src/NHibernate/Driver/SqlBulkProvider.cs b/src/NHibernate/Driver/SqlBulkProvider.cs new file mode 100644 index 00000000000..57c153fdbb4 --- /dev/null +++ b/src/NHibernate/Driver/SqlBulkProvider.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq; +using NHibernate.Engine; +using NHibernate.Transaction; + +namespace NHibernate.Driver +{ + public class SqlBulkProvider : TableBasedBulkProvider + { + public const String BulkProviderOptions = "adonet.bulk_provider_options"; + + public SqlBulkCopyOptions Options { get; set; } + + + public override void Initialize(IDictionary properties) + { + base.Initialize(properties); + + var bulkProviderOptions = String.Empty; + + if (properties.TryGetValue(BulkProviderOptions, out bulkProviderOptions) == true) + { + this.Options = (SqlBulkCopyOptions)Enum.Parse(typeof(SqlBulkCopyOptions), bulkProviderOptions, true); + } + } + + public override void Insert(ISessionImplementor session, IEnumerable entities) + { + if (entities.Any() == true) + { + var con = session.Connection as SqlConnection; + var tx = (session.ConnectionManager.Transaction as AdoTransaction).GetNativeTransaction() as SqlTransaction; + + foreach (var table in this.GetTables(session, entities)) + { + using (var copy = new SqlBulkCopy(con, this.Options, tx) { BatchSize = this.BatchSize, BulkCopyTimeout = this.Timeout, DestinationTableName = table.TableName }) + { + copy.WriteToServer(table); + } + } + } + } + } +} diff --git a/src/NHibernate/Driver/SqlClientDriver.cs b/src/NHibernate/Driver/SqlClientDriver.cs index fadec4ea5ab..c977f8bc8d8 100644 --- a/src/NHibernate/Driver/SqlClientDriver.cs +++ b/src/NHibernate/Driver/SqlClientDriver.cs @@ -26,6 +26,11 @@ public class SqlClientDriver : DriverBase, IEmbeddedBatcherFactoryProvider public const byte MaxDateTime2 = 8; public const byte MaxDateTimeOffset = 10; + public override BulkProvider GetBulkProvider() + { + return new SqlBulkProvider(); + } + /// /// Creates an uninitialized object for /// the SqlClientDriver. diff --git a/src/NHibernate/Driver/TableBasedBulkProvider.cs b/src/NHibernate/Driver/TableBasedBulkProvider.cs new file mode 100644 index 00000000000..648cd934f70 --- /dev/null +++ b/src/NHibernate/Driver/TableBasedBulkProvider.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Linq; +using System.Web.UI; +using System.Xml.Schema; +using NHibernate.Engine; +using NHibernate.Id; +using NHibernate.Impl; +using NHibernate.Mapping; +using NHibernate.Persister.Entity; +using NHibernate.Type; + +namespace NHibernate.Driver +{ + public abstract class TableBasedBulkProvider : BulkProvider + { + protected virtual IEnumerable GetTables(ISessionImplementor session, IEnumerable entities) + { + var tables = new Dictionary(); + + foreach (var entityTypes in entities.GroupBy(x => x.GetType())) + { + var entityType = entityTypes.Key; + var persister = session.GetEntityPersister(entityType.FullName, null) as AbstractEntityPersister; + var table = new DataTable(); + + if (persister is SingleTableEntityPersister) + { + table.TableName = (persister as SingleTableEntityPersister).TableName; + } + else if (persister is JoinedSubclassEntityPersister) + { + table.TableName = (persister as JoinedSubclassEntityPersister).TableName; + } + else if (persister is UnionSubclassEntityPersister) + { + table.TableName = (persister as UnionSubclassEntityPersister).TableName; + } + + tables[table.TableName] = table; + + var map = new Hashtable(); + + if (persister.IdentifierGenerator is IPostInsertIdentifierGenerator) + { + throw new ArgumentException("Post insert identifier generators cannot be used for bulk inserts."); + } + + if (String.IsNullOrWhiteSpace(persister.IdentifierPropertyName) == true) + { + throw new ArgumentException("Entities without an identity property cannot be used for bulk inserts."); + } + + for (var c = 0; c < persister.IdentifierColumnNames.Length; ++c) + { + var columnName = persister.IdentifierColumnNames[c]; + + if (persister.IdentifierType is ComponentType) + { + table.Columns.Add(columnName, (persister.IdentifierType as ComponentType).ReturnedClass.GetProperty((persister.IdentifierType as ComponentType).PropertyNames[c]).PropertyType).ExtendedProperties["PropertyName"] = String.Concat(persister.IdentifierPropertyName, ".", (persister.IdentifierType as ComponentType).PropertyNames[c]); + } + else if (persister.EntityMetamodel.Properties[c].Type is OneToOneType) + { + table.Columns.Add(columnName, (persister.EntityMetamodel.Properties[c].Type as OneToOneType).GetIdentifierOrUniqueKeyPropertyName(session.Factory).GetType()).ExtendedProperties["PropertyName"] = String.Concat(persister.EntityMetamodel.Properties[c].Name, ".", (persister.EntityMetamodel.Properties[c].Type as OneToOneType).GetIdentifierOrUniqueKeyPropertyName(session.Factory)); + } + else + { + table.Columns.Add(columnName, persister.IdentifierType.ReturnedClass).ExtendedProperties["PropertyName"] = persister.IdentifierPropertyName; + } + } + + for (var i = 0; i < persister.EntityMetamodel.Properties.Length; ++i) + { + if (persister.EntityMetamodel.PropertyInsertability[i] == false) + { + continue; + } + + if (persister.EntityMetamodel.Properties[i].Type.IsCollectionType == true) + { + continue; + } + + var columnNames = persister.GetPropertyColumnNames(persister.EntityMetamodel.Properties[i].Name); + + for (var c = 0; c < columnNames.Length; ++c) + { + var columnName = columnNames[c]; + + if (persister.EntityMetamodel.Properties[i].Type is ComponentType) + { + table.Columns.Add(columnName, (persister.EntityMetamodel.Properties[i].Type as ComponentType).ReturnedClass.GetProperty((persister.EntityMetamodel.Properties[i].Type as ComponentType).PropertyNames[c]).PropertyType).ExtendedProperties["PropertyName"] = String.Concat(persister.EntityMetamodel.Properties[i].Name, ".", (persister.EntityMetamodel.Properties[i].Type as ComponentType).PropertyNames[c]); + } + else if (persister.EntityMetamodel.Properties[i].Type is OneToOneType) + { + table.Columns.Add(columnName, (persister.EntityMetamodel.Properties[i].Type as OneToOneType).GetIdentifierOrUniqueKeyPropertyName(session.Factory).GetType()).ExtendedProperties["PropertyName"] = String.Concat(persister.EntityMetamodel.Properties[i].Name, ".", (persister.EntityMetamodel.Properties[i].Type as OneToOneType).GetIdentifierOrUniqueKeyPropertyName(session.Factory)); + } + else if (persister.EntityMetamodel.Properties[i].Type is ManyToOneType) + { + table.Columns.Add(columnName, (persister.EntityMetamodel.Properties[i].Type as ManyToOneType).GetIdentifierOrUniqueKeyPropertyName(session.Factory).GetType()).ExtendedProperties["PropertyName"] = String.Concat(persister.EntityMetamodel.Properties[i].Name, ".", (persister.EntityMetamodel.Properties[i].Type as ManyToOneType).GetIdentifierOrUniqueKeyPropertyName(session.Factory)); + } + else + { + table.Columns.Add(columnName, persister.EntityMetamodel.Properties[i].Type.ReturnedClass).ExtendedProperties["PropertyName"] = persister.EntityMetamodel.Properties[i].Name; + } + } + } + + table.BeginLoadData(); + + foreach (var entity in entityTypes) + { + var row = table.NewRow(); + + for (var c = 0; c < table.Columns.Count; ++c) + { + var value = DataBinder.Eval(entity, table.Columns[c].ExtendedProperties["PropertyName"].ToString()) ?? DBNull.Value; + row[c] = value; + } + + table.Rows.Add(row); + } + + table.EndLoadData(); + } + + return (tables.Values); + } + } +} diff --git a/src/NHibernate/SessionExtensions.cs b/src/NHibernate/SessionExtensions.cs new file mode 100644 index 00000000000..7d5c372fce1 --- /dev/null +++ b/src/NHibernate/SessionExtensions.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using NHibernate.Engine; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate +{ + public static class SessionExtensions + { + public static void BulkInsert(this ISession session, IEnumerable entities) where T : class + { + BulkInsert(session.GetSessionImplementation(), entities); + } + + public static void BulkInsert(this IStatelessSession session, IEnumerable entities) where T : class + { + BulkInsert(session.GetSessionImplementation(), entities); + } + + private static void BulkInsert(ISessionImplementor session, IEnumerable entities) where T : class + { + using (var provider = session.Factory.ConnectionProvider.Driver.GetBulkProvider()) + { + if (provider == null) + { + throw new InvalidOperationException("Current driver does not support bulk inserts."); + } + + provider.Initialize(Environment.Properties); + provider.Insert(session, entities); + } + } + } +} diff --git a/src/NHibernate/Transaction/AdoTransaction.cs b/src/NHibernate/Transaction/AdoTransaction.cs index 3b3f79ac5d6..cbdf06877e2 100644 --- a/src/NHibernate/Transaction/AdoTransaction.cs +++ b/src/NHibernate/Transaction/AdoTransaction.cs @@ -454,5 +454,10 @@ private void NotifyLocalSynchsAfterTransactionCompletion(bool success) } } } + + internal IDbTransaction GetNativeTransaction() + { + return trans; + } } } From bf83cd022354a829252c919a199029c4978257a4 Mon Sep 17 00:00:00 2001 From: Ricardo Peres Date: Wed, 29 Oct 2014 00:53:51 +0000 Subject: [PATCH 2/3] Improvements and fixes Added DefaultBulkProvider class as the default --- .../DriverTest/BulkInsertTests.cs | 2 ++ src/NHibernate/Driver/DefaultBulkProvider.cs | 23 +++++++++++++++++++ src/NHibernate/Driver/DriverBase.cs | 2 +- src/NHibernate/Driver/IDriver.cs | 1 + .../Driver/OracleDataClientBulkProvider.cs | 1 + src/NHibernate/Driver/SqlBulkProvider.cs | 1 + .../Driver/TableBasedBulkProvider.cs | 10 ++++---- 7 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 src/NHibernate/Driver/DefaultBulkProvider.cs diff --git a/src/NHibernate.Test/DriverTest/BulkInsertTests.cs b/src/NHibernate.Test/DriverTest/BulkInsertTests.cs index c254c5f9af6..1ec27b91cab 100644 --- a/src/NHibernate.Test/DriverTest/BulkInsertTests.cs +++ b/src/NHibernate.Test/DriverTest/BulkInsertTests.cs @@ -20,6 +20,7 @@ protected override void Configure(Configuration configuration) [Test] public void CanBulkInsertEntitiesWithComponents() { + //NH-3675 using (session.BeginTransaction()) { var customers = new Customer[] { new Customer { Address = new Address("street", "city", "region", "postalCode", "country", "phoneNumber", "fax"), CompanyName = "Company", ContactName = "Contact", ContactTitle = "Title", CustomerId = "12345" } }; @@ -37,6 +38,7 @@ public void CanBulkInsertEntitiesWithComponents() [Test] public void CanBulkInsertEntitiesWithComponentsAndAssociations() { + //NH-3675 using (session.BeginTransaction()) { var superior = new Employee { Address = new Address("street", "city", "region", "zip", "country", "phone", "fax"), BirthDate = System.DateTime.Now, EmployeeId = 1, Extension = "1", FirstName = "Superior", LastName = "Last" }; diff --git a/src/NHibernate/Driver/DefaultBulkProvider.cs b/src/NHibernate/Driver/DefaultBulkProvider.cs new file mode 100644 index 00000000000..0ef11d6cbf0 --- /dev/null +++ b/src/NHibernate/Driver/DefaultBulkProvider.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using NHibernate.Engine; + +namespace NHibernate.Driver +{ + sealed class DefaultBulkProvider : BulkProvider + { + public override void Insert(ISessionImplementor session, IEnumerable entities) + { + foreach (var entity in entities) + { + if (session is ISession) + { + (session as ISession).Save(entity); + } + else if (session is IStatelessSession) + { + (session as IStatelessSession).Insert(entity); + } + } + } + } +} diff --git a/src/NHibernate/Driver/DriverBase.cs b/src/NHibernate/Driver/DriverBase.cs index 8a1230e1af5..dbc98f1a521 100644 --- a/src/NHibernate/Driver/DriverBase.cs +++ b/src/NHibernate/Driver/DriverBase.cs @@ -24,7 +24,7 @@ public abstract class DriverBase : IDriver, ISqlParameterFormatter public virtual BulkProvider GetBulkProvider() { //NH-3675 - return null; + return new DefaultBulkProvider(); } public virtual void Configure(IDictionary settings) diff --git a/src/NHibernate/Driver/IDriver.cs b/src/NHibernate/Driver/IDriver.cs index 76b9548e8d0..9df6fb64bba 100644 --- a/src/NHibernate/Driver/IDriver.cs +++ b/src/NHibernate/Driver/IDriver.cs @@ -32,6 +32,7 @@ namespace NHibernate.Driver /// public interface IDriver { + //NH-3675 /// /// Returns a bulk provider for the current driver. /// diff --git a/src/NHibernate/Driver/OracleDataClientBulkProvider.cs b/src/NHibernate/Driver/OracleDataClientBulkProvider.cs index a787397c902..8afcb9f29f3 100644 --- a/src/NHibernate/Driver/OracleDataClientBulkProvider.cs +++ b/src/NHibernate/Driver/OracleDataClientBulkProvider.cs @@ -9,6 +9,7 @@ namespace NHibernate.Driver { public class OracleDataClientBulkProvider : TableBasedBulkProvider { + //NH-3675 public const String BulkProviderOptions = "adonet.bulk_provider_options"; private static readonly System.Type bulkCopyOptionsType = System.Type.GetType("Oracle.DataAccess.Client.OracleBulkCopyOptions, Oracle.DataAccess"); diff --git a/src/NHibernate/Driver/SqlBulkProvider.cs b/src/NHibernate/Driver/SqlBulkProvider.cs index 57c153fdbb4..eb4b167783e 100644 --- a/src/NHibernate/Driver/SqlBulkProvider.cs +++ b/src/NHibernate/Driver/SqlBulkProvider.cs @@ -9,6 +9,7 @@ namespace NHibernate.Driver { public class SqlBulkProvider : TableBasedBulkProvider { + //NH-3675 public const String BulkProviderOptions = "adonet.bulk_provider_options"; public SqlBulkCopyOptions Options { get; set; } diff --git a/src/NHibernate/Driver/TableBasedBulkProvider.cs b/src/NHibernate/Driver/TableBasedBulkProvider.cs index 648cd934f70..b237e777b82 100644 --- a/src/NHibernate/Driver/TableBasedBulkProvider.cs +++ b/src/NHibernate/Driver/TableBasedBulkProvider.cs @@ -1,15 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; -using System.ComponentModel; using System.Data; using System.Linq; using System.Web.UI; -using System.Xml.Schema; using NHibernate.Engine; using NHibernate.Id; -using NHibernate.Impl; -using NHibernate.Mapping; using NHibernate.Persister.Entity; using NHibernate.Type; @@ -17,8 +13,14 @@ namespace NHibernate.Driver { public abstract class TableBasedBulkProvider : BulkProvider { + //NH-3675 protected virtual IEnumerable GetTables(ISessionImplementor session, IEnumerable entities) { + if (session.EntityMode != EntityMode.Poco) + { + throw new InvalidOperationException(String.Format("Entity mode {0} is not supported for bulk inserts.", session.EntityMode)); + } + var tables = new Dictionary(); foreach (var entityTypes in entities.GroupBy(x => x.GetType())) From 3a5003d5229c805778bbe896ec741ea6f6b89f4d Mon Sep 17 00:00:00 2001 From: Ricardo Peres Date: Wed, 29 Oct 2014 10:25:06 +0000 Subject: [PATCH 3/3] Removed issue number comments Removed dependency of System.Web and DataBinder.Eval Using ADOExceptionHelper.Convert to convert DB exceptions BulkInsert only applies to IStatelessSession Small improvements --- .../DriverTest/BulkInsertTests.cs | 18 ++-- src/NHibernate/Driver/BulkProvider.cs | 11 ++- src/NHibernate/Driver/DefaultBulkProvider.cs | 19 ++-- src/NHibernate/Driver/DriverBase.cs | 1 - src/NHibernate/Driver/IDriver.cs | 1 - .../Driver/OracleDataClientBulkProvider.cs | 3 +- src/NHibernate/Driver/SqlBulkProvider.cs | 3 +- .../Driver/TableBasedBulkProvider.cs | 98 +++++++++++++++---- src/NHibernate/SessionExtensions.cs | 16 +-- 9 files changed, 119 insertions(+), 51 deletions(-) diff --git a/src/NHibernate.Test/DriverTest/BulkInsertTests.cs b/src/NHibernate.Test/DriverTest/BulkInsertTests.cs index 1ec27b91cab..5b45daa70b3 100644 --- a/src/NHibernate.Test/DriverTest/BulkInsertTests.cs +++ b/src/NHibernate.Test/DriverTest/BulkInsertTests.cs @@ -21,15 +21,16 @@ protected override void Configure(Configuration configuration) public void CanBulkInsertEntitiesWithComponents() { //NH-3675 - using (session.BeginTransaction()) + using (var statelessSession = session.SessionFactory.OpenStatelessSession()) + using (statelessSession.BeginTransaction()) { var customers = new Customer[] { new Customer { Address = new Address("street", "city", "region", "postalCode", "country", "phoneNumber", "fax"), CompanyName = "Company", ContactName = "Contact", ContactTitle = "Title", CustomerId = "12345" } }; - this.session.CreateQuery("delete from Customer").ExecuteUpdate(); + statelessSession.CreateQuery("delete from Customer").ExecuteUpdate(); - this.session.BulkInsert(customers); + statelessSession.BulkInsert(customers); - var count = this.session.Query().Count(); + var count = statelessSession.Query().Count(); Assert.AreEqual(customers.Count(), count); } @@ -39,17 +40,18 @@ public void CanBulkInsertEntitiesWithComponents() public void CanBulkInsertEntitiesWithComponentsAndAssociations() { //NH-3675 - using (session.BeginTransaction()) + using (var statelessSession = session.SessionFactory.OpenStatelessSession()) + using (statelessSession.BeginTransaction()) { var superior = new Employee { Address = new Address("street", "city", "region", "zip", "country", "phone", "fax"), BirthDate = System.DateTime.Now, EmployeeId = 1, Extension = "1", FirstName = "Superior", LastName = "Last" }; var employee = new Employee { Address = new Address("street", "city", "region", "zip", "country", "phone", "fax"), BirthDate = System.DateTime.Now, EmployeeId = 2, Extension = "2", FirstName = "Employee", LastName = "Last", Superior = superior }; var employees = new Employee[] { superior, employee }; - this.session.CreateQuery("delete from Employee").ExecuteUpdate(); + statelessSession.CreateQuery("delete from Employee").ExecuteUpdate(); - this.session.BulkInsert(employees); + statelessSession.BulkInsert(employees); - var count = this.session.Query().Count(); + var count = statelessSession.Query().Count(); Assert.AreEqual(employees.Count(), count); } diff --git a/src/NHibernate/Driver/BulkProvider.cs b/src/NHibernate/Driver/BulkProvider.cs index 5562a5b2dce..9ae5469efcd 100644 --- a/src/NHibernate/Driver/BulkProvider.cs +++ b/src/NHibernate/Driver/BulkProvider.cs @@ -26,14 +26,17 @@ protected BulkProvider() public virtual void Initialize(IDictionary properties) { - if (properties.ContainsKey(Environment.BulkProviderTimeout) == true) + var timeout = string.Empty; + var batchSize = string.Empty; + + if (properties.TryGetValue(Environment.BulkProviderTimeout, out timeout)) { - this.Timeout = Convert.ToInt32(properties[Environment.BulkProviderTimeout]); + this.Timeout = Convert.ToInt32(timeout); } - if (properties.ContainsKey(Environment.BulkProviderBatchSize) == true) + if (properties.TryGetValue(Environment.BulkProviderBatchSize, out batchSize)) { - this.BatchSize = Convert.ToInt32(properties[Environment.BulkProviderBatchSize]); + this.BatchSize = Convert.ToInt32(batchSize); } } diff --git a/src/NHibernate/Driver/DefaultBulkProvider.cs b/src/NHibernate/Driver/DefaultBulkProvider.cs index 0ef11d6cbf0..bc03324027e 100644 --- a/src/NHibernate/Driver/DefaultBulkProvider.cs +++ b/src/NHibernate/Driver/DefaultBulkProvider.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using NHibernate.Engine; namespace NHibernate.Driver @@ -7,16 +8,16 @@ sealed class DefaultBulkProvider : BulkProvider { public override void Insert(ISessionImplementor session, IEnumerable entities) { + var statelessSession = session as IStatelessSession; + + if (statelessSession == null) + { + throw new InvalidOperationException("Insert can only be called with stateless sessions."); + } + foreach (var entity in entities) { - if (session is ISession) - { - (session as ISession).Save(entity); - } - else if (session is IStatelessSession) - { - (session as IStatelessSession).Insert(entity); - } + statelessSession.Insert(entity); } } } diff --git a/src/NHibernate/Driver/DriverBase.cs b/src/NHibernate/Driver/DriverBase.cs index dbc98f1a521..deb676a2062 100644 --- a/src/NHibernate/Driver/DriverBase.cs +++ b/src/NHibernate/Driver/DriverBase.cs @@ -23,7 +23,6 @@ public abstract class DriverBase : IDriver, ISqlParameterFormatter public virtual BulkProvider GetBulkProvider() { - //NH-3675 return new DefaultBulkProvider(); } diff --git a/src/NHibernate/Driver/IDriver.cs b/src/NHibernate/Driver/IDriver.cs index 9df6fb64bba..76b9548e8d0 100644 --- a/src/NHibernate/Driver/IDriver.cs +++ b/src/NHibernate/Driver/IDriver.cs @@ -32,7 +32,6 @@ namespace NHibernate.Driver /// public interface IDriver { - //NH-3675 /// /// Returns a bulk provider for the current driver. /// diff --git a/src/NHibernate/Driver/OracleDataClientBulkProvider.cs b/src/NHibernate/Driver/OracleDataClientBulkProvider.cs index 8afcb9f29f3..d9d1bc678dd 100644 --- a/src/NHibernate/Driver/OracleDataClientBulkProvider.cs +++ b/src/NHibernate/Driver/OracleDataClientBulkProvider.cs @@ -9,7 +9,6 @@ namespace NHibernate.Driver { public class OracleDataClientBulkProvider : TableBasedBulkProvider { - //NH-3675 public const String BulkProviderOptions = "adonet.bulk_provider_options"; private static readonly System.Type bulkCopyOptionsType = System.Type.GetType("Oracle.DataAccess.Client.OracleBulkCopyOptions, Oracle.DataAccess"); @@ -29,7 +28,7 @@ public override void Initialize(IDictionary properties) var bulkProviderOptions = String.Empty; - if (properties.TryGetValue(BulkProviderOptions, out bulkProviderOptions) == true) + if (properties.TryGetValue(BulkProviderOptions, out bulkProviderOptions)) { this.Options = Convert.ToInt32(bulkProviderOptions); } diff --git a/src/NHibernate/Driver/SqlBulkProvider.cs b/src/NHibernate/Driver/SqlBulkProvider.cs index eb4b167783e..5bb7c22388f 100644 --- a/src/NHibernate/Driver/SqlBulkProvider.cs +++ b/src/NHibernate/Driver/SqlBulkProvider.cs @@ -9,7 +9,6 @@ namespace NHibernate.Driver { public class SqlBulkProvider : TableBasedBulkProvider { - //NH-3675 public const String BulkProviderOptions = "adonet.bulk_provider_options"; public SqlBulkCopyOptions Options { get; set; } @@ -21,7 +20,7 @@ public override void Initialize(IDictionary properties) var bulkProviderOptions = String.Empty; - if (properties.TryGetValue(BulkProviderOptions, out bulkProviderOptions) == true) + if (properties.TryGetValue(BulkProviderOptions, out bulkProviderOptions)) { this.Options = (SqlBulkCopyOptions)Enum.Parse(typeof(SqlBulkCopyOptions), bulkProviderOptions, true); } diff --git a/src/NHibernate/Driver/TableBasedBulkProvider.cs b/src/NHibernate/Driver/TableBasedBulkProvider.cs index b237e777b82..8e156e03879 100644 --- a/src/NHibernate/Driver/TableBasedBulkProvider.cs +++ b/src/NHibernate/Driver/TableBasedBulkProvider.cs @@ -1,9 +1,10 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Linq; -using System.Web.UI; +using System.Reflection; using NHibernate.Engine; using NHibernate.Id; using NHibernate.Persister.Entity; @@ -13,7 +14,6 @@ namespace NHibernate.Driver { public abstract class TableBasedBulkProvider : BulkProvider { - //NH-3675 protected virtual IEnumerable GetTables(ISessionImplementor session, IEnumerable entities) { if (session.EntityMode != EntityMode.Poco) @@ -27,21 +27,7 @@ protected virtual IEnumerable GetTables(ISessionImplementor sessio { var entityType = entityTypes.Key; var persister = session.GetEntityPersister(entityType.FullName, null) as AbstractEntityPersister; - var table = new DataTable(); - - if (persister is SingleTableEntityPersister) - { - table.TableName = (persister as SingleTableEntityPersister).TableName; - } - else if (persister is JoinedSubclassEntityPersister) - { - table.TableName = (persister as JoinedSubclassEntityPersister).TableName; - } - else if (persister is UnionSubclassEntityPersister) - { - table.TableName = (persister as UnionSubclassEntityPersister).TableName; - } - + var table = new DataTable(persister.TableName); tables[table.TableName] = table; var map = new Hashtable(); @@ -119,7 +105,7 @@ protected virtual IEnumerable GetTables(ISessionImplementor sessio for (var c = 0; c < table.Columns.Count; ++c) { - var value = DataBinder.Eval(entity, table.Columns[c].ExtendedProperties["PropertyName"].ToString()) ?? DBNull.Value; + var value = Eval(entity, table.Columns[c].ExtendedProperties["PropertyName"].ToString()) ?? DBNull.Value; row[c] = value; } @@ -131,5 +117,81 @@ protected virtual IEnumerable GetTables(ISessionImplementor sessio return (tables.Values); } + + private object Eval(object instance, string path) + { + if (instance == null) + { + return null; + } + + var context = instance; + object value = null; + var parts = path.Split('.'); + + for (var i = 0; i < parts.Length; ++i) + { + value = GetPropertyOrFieldValue(context, parts[i]); + context = value; + } + + return value; + } + + private MemberInfo GetPropertyOrField(object instance, string memberName) + { + if (instance == null) + { + return null; + } + + var property = instance.GetType().GetProperty(memberName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + if (property != null) + { + return property; + } + + var field = FindField(instance.GetType(), memberName); + + return field; + } + + private object GetPropertyOrFieldValue(object instance, string memberName) + { + if (instance == null) + { + return null; + } + + var member = GetPropertyOrField(instance, memberName); + + if (member != null) + { + return (member is FieldInfo) ? (member as FieldInfo).GetValue(instance) : (member as PropertyInfo).GetValue(instance, null); + } + + throw new InvalidOperationException(string.Format("Member named {0} does not exist in type {1}.", memberName, instance.GetType().FullName)); + } + + private FieldInfo FindField(System.Type type, string fieldName) + { + var currentType = type; + var field = null as FieldInfo; + + while (currentType != typeof (object)) + { + field = currentType.GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + + if (field != null) + { + return field; + } + + currentType = currentType.BaseType; + } + + return field; + } } } diff --git a/src/NHibernate/SessionExtensions.cs b/src/NHibernate/SessionExtensions.cs index 7d5c372fce1..b8708dac9c4 100644 --- a/src/NHibernate/SessionExtensions.cs +++ b/src/NHibernate/SessionExtensions.cs @@ -1,17 +1,13 @@ using System; using System.Collections.Generic; using NHibernate.Engine; +using NHibernate.Exceptions; using Environment = NHibernate.Cfg.Environment; namespace NHibernate { public static class SessionExtensions { - public static void BulkInsert(this ISession session, IEnumerable entities) where T : class - { - BulkInsert(session.GetSessionImplementation(), entities); - } - public static void BulkInsert(this IStatelessSession session, IEnumerable entities) where T : class { BulkInsert(session.GetSessionImplementation(), entities); @@ -27,7 +23,15 @@ private static void BulkInsert(ISessionImplementor session, IEnumerable en } provider.Initialize(Environment.Properties); - provider.Insert(session, entities); + + try + { + provider.Insert(session, entities); + } + catch (Exception e) + { + throw ADOExceptionHelper.Convert(session.Factory.SQLExceptionConverter, e, "could not execute bulk insert."); + } } } }