【问题标题】:Why is NHibernate refusing to batch inserts?为什么 NHibernate 拒绝批量插入?
【发布时间】:2010-08-04 14:58:31
【问题描述】:

针对 SQL Server 2008 使用 NHibernate 2.1.2.4000。目标表没有触发器或无关索引。很简单:

create table LogEntries (
    Id INT IDENTITY NOT NULL,
   HostName NVARCHAR(32) not null,
   UserName NVARCHAR(64) not null,
   LogName NVARCHAR(512) not null,
   Timestamp DATETIME not null,
   Level INT not null,
   Thread NVARCHAR(64) not null,
   Message NVARCHAR(MAX) not null,
   primary key (Id)
)

我的实体映射是:

<class name="LogEntry" table="LogEntries">
    <id name="Id" unsaved-value="0">
        <generator class="native"/>
    </id>
    <property name="HostName" length="32" not-null="true"/>
    <property name="UserName" length="64" not-null="true"/>
    <property name="LogName" length="512" not-null="true"/>
    <property name="Timestamp" type="utcdatetime" not-null="true"/>
    <property name="Level" not-null="true"/>
    <property name="Thread" length="64" not-null="true"/>
    <property name="Message">
        <column name="Message" sql-type="NVARCHAR(MAX)" not-null="true"/>
    </property>
</class>

现在,考虑以下测试用例:

[Fact]
public void bulk_insert_test()
{
    var batchSize = 100;
    var numberItems = 10000;

    var configuration = new NHibernate.Cfg.Configuration().Configure();
    configuration.SetProperty("connection.connection_string", @"my_conn_string");
    configuration.SetProperty("adonet.batch_size", batchSize.ToString());
    var sessionFactory = configuration.BuildSessionFactory();

    var ts = this.WriteWithNH(sessionFactory, numberItems);
    ////var ts = this.WriteWithBC(sessionFactory, numberItems, batchSize);

    Console.WriteLine("Saving {0} items with batch size {1}: {2}", numberItems, batchSize, ts);
}

public TimeSpan WriteWithNH(ISessionFactory sessionFactory, int numberItems)
{
    using (var session = sessionFactory.OpenStatelessSession())
    using (var transaction = session.BeginTransaction())
    {
        session.Insert(new LogEntry()
        {
            HostName = "host",
            UserName = "user",
            LogName = "log",
            Level = 0,
            Thread = "thread",
            Timestamp = DateTime.UtcNow,
            Message = "Warm up"
        });

        transaction.Commit();
    }

    var sw = Stopwatch.StartNew();

    using (var session = sessionFactory.OpenStatelessSession())
    using (var transaction = session.BeginTransaction())
    {
        for (var i = 0; i < numberItems; ++i)
        {
            session.Insert(new LogEntry()
            {
                HostName = "host",
                UserName = "user",
                LogName = "log",
                Level = 0,
                Thread = "thread",
                Timestamp = DateTime.UtcNow,
                Message = "Message " + i
            });
        }

        transaction.Commit();
    }

    return sw.Elapsed;
}

public TimeSpan WriteWithBC(ISessionFactory sessionFactory, int numberItems, int batchSize)
{
    using (var session = sessionFactory.OpenStatelessSession())
    using (var bulkCopy = new SqlBulkCopy((SqlConnection)session.Connection))
    {
        bulkCopy.BatchSize = batchSize;
        bulkCopy.DestinationTableName = "LogEntries";
        var table = new DataTable("LogEntries");
        table.Columns.Add("Id", typeof(int));
        table.Columns.Add("HostName", typeof(string));
        table.Columns.Add("UserName", typeof(string));
        table.Columns.Add("LogName", typeof(string));
        table.Columns.Add("Timestamp", typeof(DateTime));
        table.Columns.Add("Level", typeof(int));
        table.Columns.Add("Thread", typeof(string));
        table.Columns.Add("Message", typeof(string));

        var row = table.NewRow();
        row["HostName"] = "host";
        row["UserName"] = "user";
        row["LogName"] = "log";
        row["Timestamp"] = DateTime.UtcNow;
        row["Level"] = 0L;
        row["Thread"] = "thread";
        row["Message"] = "Warm up";
        table.Rows.Add(row);

        bulkCopy.WriteToServer(table);
    }

    var sw = Stopwatch.StartNew();

    using (var session = sessionFactory.OpenStatelessSession())
    using (var bulkCopy = new SqlBulkCopy((SqlConnection)session.Connection))
    {
        bulkCopy.BatchSize = batchSize;
        bulkCopy.DestinationTableName = "LogEntries";
        var table = new DataTable("LogEntries");
        table.Columns.Add("Id", typeof(int));
        table.Columns.Add("HostName", typeof(string));
        table.Columns.Add("UserName", typeof(string));
        table.Columns.Add("LogName", typeof(string));
        table.Columns.Add("Timestamp", typeof(DateTime));
        table.Columns.Add("Level", typeof(int));
        table.Columns.Add("Thread", typeof(string));
        table.Columns.Add("Message", typeof(string));

        for (var i = 0; i < numberItems; ++i)
        {
            var row = table.NewRow();
            row["HostName"] = "host";
            row["UserName"] = "user";
            row["LogName"] = "log";
            row["Timestamp"] = DateTime.UtcNow;
            row["Level"] = 0;
            row["Thread"] = "thread";
            row["Message"] = "Message " + i;
            table.Rows.Add(row);
        }

        bulkCopy.WriteToServer(table);
    }

    return sw.Elapsed;
}

以下是使用 NHibernate 执行插入时的一些示例输出:

Saving 10000 items with batch size 500: 00:00:12.3064936
Saving 10000 items with batch size 100: 00:00:12.3600981
Saving 10000 items with batch size 1: 00:00:12.8102670

作为比较点,您会看到我还实现了一个基于 BCP 的解决方案。这是一些示例输出:

Saving 10000 items with batch size 500: 00:00:00.3142613
Saving 10000 items with batch size 100: 00:00:00.6757417
Saving 10000 items with batch size 1: 00:00:26.2509605

显然,BCP 解决方案比 NH 解决方案快几英里。同样明显的是,批处理会影响 BCP 解决方案的速度,但不会影响 NH 解决方案的速度。使用 NHibernate 进行插入时,NHProf 显示如下:

alt text http://img9.imageshack.us/img9/8407/screenshotac.png

只有INSERTs,没有SELECTs。有趣的是,NHProf 从来没有给我this warning

我已经尝试按照上面的测试用例在我的配置文件和代码中指定adonet.batch_size

现在,我不希望 NH 解决方案达到 BCP 解决方案的速度,但我至少想知道为什么批处理不起作用。如果启用批处理功能足够好,那么我可能会在 BCP 上使用 NH 解决方案,以保持代码库更简单。

谁能解释为什么 NH 拒绝支持 ADO.NET 批处理,以及我可以做些什么来解决它?我读过的所有分散的 NH “文档”都指出,您需要做的就是指定 adonet.batch_size 并(最好)使用无状态会话,但我正在做这两件事。

谢谢

【问题讨论】:

  • @Kent:你有没有看过NHProf的幕后情况?你确定插入操作没有被批处理吗?
  • 是的,我已经查看了 NHProf,并且在有/没有批处理的情况下运行时没有区别。
  • 所以语句没有被分组?一些想法:1)你能发布你的完整配置文件吗? 2)您是否尝试过直接在 Nhibernate 配置文件中指定 Nhibernate 批量大小,而不是在您的测试夹具中覆盖?
  • 另外,您的主键是否映射为本机?每次插入操作后,您是否看到每个新 id 的 select 语句?我想知道这是否可能是没有批处理发生的原因?
  • @DanP:我已经更新了我的帖子来回答你所有的问题。

标签: nhibernate sql-server-2008 bulkinsert batching


【解决方案1】:

使用 identity 会中断批处理。

由 Fabio Maulo here解释

最好的选择是切换到不同的生成器(我总是推荐hiloguid.comb

【讨论】:

  • 感谢 Diego,但 Fabio 的帖子只讨论了使用 ISession,而不是 IStatelessSession。根据我的问题,我使用的是后者。无需通过后续的SELECT 来获取ID,也不会出现这种情况。因此,应该可以批量处理INSERTs。
  • 此外,NH 文档 (nhforge.org/doc/nh/en/index.html#performance-batch-updates) 的第 17.6 节列出了批处理的限制,并且根本没有提到 ID 生成器。我知道与 NH 的课程标准杆,但令人难以置信的沮丧。
  • Kent,我认为IStatelessSession 根本不支持批处理。您是否尝试过使用普通的ISession?在某些情况下,它的性能更好,即使是批处理场景。
  • 确实如此,但我不怪你不确定。我认为问题的症结在于 NH 文档的糟糕状态。似乎即使使用无状态会话,NH 仍然坚持更新实体 ID,这排除了批处理。我找不到引用,尤其是在 NH 文档中,但我将您的标记为答案。
  • 顺便说一句,我的意思是说我错误地说SELECT 没有发生,以防不明显。
【解决方案2】:

如果您使用身份作为主键,ISessionIStatelessSession 都无法批量插入。

当进行插入时,Nhibernate 会将正确的值放入您的 Id 属性中。但是,当您使用身份时,唯一可以获取此 ID 的地方是数据库。使用hilo 进行批量插入。

【讨论】:

  • +1 这是普遍共识,但我希望得到引用。我对我使用无状态会话应该足以放弃更新实体 ID。如果做不到这一点,我可以用一种方法来表达我对实体 ID 的不感兴趣,因为这样就可以进行批处理。也许这是一个错误的工具,但我希望至少能试用一下。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-11
  • 1970-01-01
  • 2011-06-04
  • 1970-01-01
  • 2019-12-23
  • 1970-01-01
相关资源
最近更新 更多