使用LINQ添加大量记录

时间:2016-10-23 11:15:41

标签: c# sql-server linq

我必须从Excel导入数百条记录到数据库。

每条记录都必须经过验证:

  1. 反对重复
  2. 必须在另一张表中有外键
  3. 我想知道如何以最高的性能做到这一点。我知道我不应该在每条记录之后使用db.SaveChanges();所以在验证之后 - 我将每条记录添加到临时列表(var recordsToAdd),然后我将保存该列表。 请检查下面的代码,这是一个很好的方法吗?

    using (var db = new DbEntities())
    {
        var recordsToAdd = new List<User>();
    
        for (var row = 2; row <= lastRow; row++)
        {
            var newRecord = new User
            {
                Id = Int32.Parse(worksheet.Cells[idColumn + row].Value.ToNullSafeString()),
                FirstName = worksheet.Cells[firstNameColumn + row].Value.ToNullSafeString(),
                LastName = worksheet.Cells[lastNameColumn + row].Value.ToNullSafeString(),
                SerialNumber = worksheet.Cells[serialNumber + row].Value.ToNullSafeString()
            };
    
            bool exists = db.User.Any(u => u.Id == newRecord.Id) || recordsToAdd.Any(u => u.Id == newRecord.Id);
            if (!exists)
            {
                bool isSerialNumberExist = db.SerialNumbers.Any(u => u.SerialNumber == newRecord.SerialNumber);
                if (isSerialNumberExist)
                {
                    recordsToAdd.Add(newRecord);
                }
                else
                {
                    resultMessages.Add(string.Format("SerialNumber doesn't exist"));
                }
            }
            else
            {
                resultMessages.Add(string.Format("Record already exist"));
            }
        }
    
        db.User.AddRange(recordsToAdd);
        db.SaveChanges();
    }
    

3 个答案:

答案 0 :(得分:1)

提高性能的一种方法是通过使用快速查找数据结构来执行验证来最小化数据库调用和线性搜索 - HashSet<string> IdDictionary<string, bool> SerialNumber 1}}:

using (var db = new DbEntities())
{
    var recordsToAdd = new List<User>();

    var userIdSet = new HashSet<string>();
    var serialNumberExistsInfo = new Dictionary<string, bool>();

    for (var row = 2; row <= lastRow; row++)
    {
        var newRecord = new User
        {
            Id = Int32.Parse(worksheet.Cells[idColumn + row].Value.ToNullSafeString()),
            FirstName = worksheet.Cells[firstNameColumn + row].Value.ToNullSafeString(),
            LastName = worksheet.Cells[lastNameColumn + row].Value.ToNullSafeString(),
            SerialNumber = worksheet.Cells[serialNumber + row].Value.ToNullSafeString()
        };

        bool exists = !userIdSet.Add(newRecord.Id) || db.User.Any(u => u.Id == newRecord.Id);
        if (!exists)
        {
            bool isSerialNumberExist;
            if (!serialNumberExistsInfo.TryGetValue(newRecord.SerialNumber, out isSerialNumberExist))
                serialNumberExistsInfo.Add(newRecord.SerialNumber, isSerialNumberExist = 
                    db.SerialNumbers.Any(u => u.SerialNumber == newRecord.SerialNumber));
            if (isSerialNumberExist)
            {
                recordsToAdd.Add(newRecord);              
            }
            else
            {
                resultMessages.Add(string.Format("SerialNumber doesn't exist"));
            }
        }
        else
        {
            resultMessages.Add(string.Format("Record already exist"));
        }
    }

    db.User.AddRange(recordsToAdd);
    db.SaveChanges();
} 

答案 1 :(得分:1)

首先让我们将代码分成两部分。第一部分是创建要插入的有效User记录的列表。第二部分是将这些记录插入数据库(代码的最后两行)。

假设您使用EntityFramework作为您的ORM,可以通过批量插入来优化第二部分。它有许多现有的解决方案,很容易找到。 (example

关于第一部分,有一些建议。

  1. HashSetDictionary中加载用户ID。这些数据结构针对搜索进行了优化。 var userDbIds = new HashSet<int>(db.User.Select(x => x.Id));。您将在不向DB请求的情况下快速检查id是否存在。

  2. serialNumber执行相同的操作。 var serialNumbers = new HashSet<string>(db.SerialNumber.Select(x => x.SerialNumber));假设SerialNumber属性的类型为string

  3. 出于同样的原因,将recordToAdd变量的类型更改为Dictionary<int, User>

  4. 支票看起来像这样:

        bool exists = userDbIds.Contains(newRecord.Id) || recordsToAdd.ContainsKey(newRecord.Id);
        if (!exists)
        {
            bool isSerialNumberExist = serialNumbers.Contains(newRecord.SerialNumber);
            if (isSerialNumberExist)
            {
                recordsToAdd[newRecord.Id] = newRecord;
            }
            else
            {
                resultMessages.Add(string.Format("SerialNumber doesn't exist"));
            }
        }
        else
        {
            resultMessages.Add(string.Format("Record already exist"));
        }
    

答案 2 :(得分:1)

使用表值参数而不是LINQ是最有效的。这样,您就可以在基于集合的方法中处理此问题,该方法是单个连接,单个存储过程执行和单个事务。基本设置显示在我在以下答案中提供的示例代码中(此处在S.O.上):

How can I insert 10 million records in the shortest time possible?

存储过程可以处理两种验证:

  • 不要插入重复记录
  • 确保SerialNumber存在

用户定义的表类型(UDTT)类似于:

CREATE TYPE dbo.UserList AS TABLE
(
  Id           INT NOT NULL,
  FirstName    NVARCHAR(50) NOT NULL,
  LastName     NVARCHAR(50) NULL,
  SerialNumber VARCHAR(50) NOT NULL
);

-- Uncomment the following if you get a permissions error:
-- GRANT EXECUTE ON TYPE::[dbo].[UserList] TO [ImportUser];
GO

存储过程(通过SqlCommand.ExecuteNonQuery执行)看起来像:

CREATE PROCEDURE dbo.ImportUsers
(
  @NewUserList  dbo.UserList READONLY
)
AS
SET NOCOUNT ON;

INSERT INTO dbo.User (Id, FirstName, LastName, SerialNumber)
  SELECT  tmp.Id, tmp.FirstName, tmp.LastName, tmp.SerialNumber
  FROM    @NewUserList tmp
  WHERE   NOT EXISTS (SELECT *
                      FROM   dbo.User usr
                      WHERE  usr.Id = tmp.[Id])
  AND     EXISTS (SELECT *
                  FROM   dbo.SerialNumbers sn
                  WHERE  sn.SerialNumber = tmp.[SerialNumber]);

上面的存储过程只是忽略了无效记录。如果您需要通知“错误”,可以使用以下定义(通过SqlCommand.ExecuteReader执行):

CREATE PROCEDURE dbo.ImportUsers
(
  @NewUserList  dbo.UserList READONLY
)
AS
SET NOCOUNT ON;

CREATE TABLE #TempUsers
(
  Id           INT NOT NULL,
  FirstName    NVARCHAR(50) NOT NULL,
  LastName     NVARCHAR(50) NULL,
  SerialNumber VARCHAR(50) NOT NULL,
  UserExists          BIT NOT NULL DEFAULT (0),
  InvalidSerialNumber BIT NOT NULL DEFAULT (0)
);

INSERT INTO #TempUsers (Id, FirstName, LastName, SerialNumber)
  SELECT  tmp.Id, tmp.FirstName, tmp.LastName, tmp.SerialNumber
  FROM    @NewUserList tmp;

-- Mark existing records
UPDATE tmp
SET    tmp.UserExists = 1
FROM   #TempUsers tmp
WHERE  EXISTS (SELECT *
               FROM   dbo.User usr
               WHERE  usr.Id = tmp.[Id]);

-- Mark invalid SerialNumber records
UPDATE tmp
SET    tmp.InvalidSerialNumber = 1
FROM   #TempUsers tmp
WHERE  tmp.UserExists = 0 -- no need to check already invalid records
AND    NOT EXISTS (SELECT *
                   FROM   dbo.SerialNumbers sn
                   WHERE  sn.SerialNumber = tmp.[SerialNumber]);

-- Insert remaining valid records
INSERT INTO dbo.User (Id, FirstName, LastName, SerialNumber)
  SELECT  tmp.Id, tmp.FirstName, tmp.LastName, tmp.SerialNumber
  FROM    #TempUsers tmp
  WHERE   tmp.UserExists = 0
  AND     tmp.InvalidSerialNumber = 0;

-- return temp table to caller as it contains validation info
SELECT  tmp.Id, tmp.FirstName, tmp.LastName, tmp.SerialNumber,
        tmp.UserExists, tmp.InvalidSerialNumber
FROM    #TempUsers tmp
-- optionally only return records that had a validation error
--  WHERE   tmp.UserExists = 1
--  OR     tmp.InvalidSerialNumber = 1;

当此版本的存储过程完成时,循环浏览SqlDataReader.Read()以获取验证信息。