ORM引入的循环大大降低了性能

时间:2012-07-03 07:29:11

标签: c# linq-to-sql

我有一张桌子如下图所示。它有固定和储蓄类型的帐户。我需要更新用户1的所有帐户的状态。该用户有10000个帐户。本质上,逻辑将如以下SQL存储过程脚本中所示。该脚本执行时间不到1秒(83毫秒)。

但是当我使用 LINQ to SQL 将其转换为ORM时,它需要超过3分钟(204814毫秒)。它至少 240,000%更慢。

LINQ to SQL(或其他ORM)中是否存在有助于克服性能损失的模式?

什么可以强制它在一次数据库中进行更新?

注意:我知道从LINQ调用存储过程。我不认为这是ORM而不是我的选择。

enter image description here

手动存储过程脚本

DECLARE @UserID INT 
DECLARE @StatusForFixed VARCHAR(50)
DECLARE @StatusForSavings VARCHAR(50)

SET @UserID = 1
SET @StatusForFixed = 'FrozenFA11'
SET @StatusForSavings = 'FrozenSB22'

UPDATE BankAccount 
SET Status = 
        CASE 
            WHEN BankAccount.AccountType='Fixed' THEN @StatusForFixed
            WHEN BankAccount.AccountType='Savings' THEN @StatusForSavings
        END
 WHERE  AccountOwnerID=@UserID

LINQ生成的代码示例

Note: This type of statements happen 10000 times

UPDATE [dbo].[BankAccount]
SET [Status] = @p3
WHERE [BankAccountID] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [3585]
-- @p3: Input NChar (Size = 10; Prec = 0; Scale = 0) [FrozenSB]

应用ORM后的代码

public class BankAccountAppService
{
    public RepositoryLayer.ILijosBankRepository AccountRepository { get; set; }

    public void FreezeAllAccountsForUser(int userId)
    {
        IEnumerable<DBML_Project.BankAccount> accounts = AccountRepository.GetAllAccountsForUser(userId);
        foreach (DBML_Project.BankAccount acc in accounts)
        {

            acc.Freeze();

        }
        AccountRepository.UpdateAccount();

    }

}

public class LijosSimpleBankRepository : ILijosBankRepository
{
    public System.Data.Linq.DataContext Context
    {
        get;
        set;
    }


    public List<DBML_Project.BankAccount> GetAllAccountsForUser(int userID)
    {
        IQueryable<DBML_Project.BankAccount> queryResultEntities = Context.GetTable<DBML_Project.BankAccount>().Where(p => p.AccountOwnerID == userID);
        return queryResultEntities.ToList();
    }

    public List<T> GetAllAccountsofType<T>() where T : DBML_Project.BankAccount
    {
        var query = from p in Context.GetTable<DBML_Project.BankAccount>().OfType<T>()
                    select p;

        List<T> typeList = query.ToList();
        return typeList;

    }

    public virtual void UpdateAccount()
    {
        Context.SubmitChanges();
    }

}

namespace DBML_Project
{

public  partial class BankAccount
{
    //Define the domain behaviors
    public virtual void Freeze()
    {
        //Do nothing
    }
}

public class FixedBankAccount : BankAccount
{

    public override void Freeze()
    {
        this.Status = "FrozenFA";
    }
}

public class SavingsBankAccount : BankAccount
{

    public override void Freeze()
    {
        this.Status = "FrozenSB";
    }
}  
}

参考

  1. Pass List as XElement to be used as XML Datatype parameter

4 个答案:

答案 0 :(得分:6)

您正在比较两种截然不同的场景:

1:在SQL服务器上本地运行脚本,一个基于集合的UPDATE

2:通过网络获取10,000条记录,更新每条记录,单独提交

您可以通过将SubmitChanges()推迟到10,000个而不是10,000个批次的1个批次中来提高2个(只是:不要调用SubmitChanges()直到但是仍然涉及向两个方向发送10,000条记录的详细信息,加上所有开销(例如,SubmitChanges()可能选择这样做通过10,000个人电话)。

基本上,基于对象的工具不适用于对记录进行批量更新。如果SP有效,请使用SP。也许通过数据上下文调用 SP,只是为了方便添加方法/参数等。

答案 1 :(得分:1)

您仍然可以从应用程序执行存储过程/自定义SQL脚本。您甚至可以在Linq-to-sql模型中映射该过程,这样您就不需要手动打开连接和创建命令。

我不确定Linq-to-sql是否总是在单独的往返数据库中执行每个修改命令,但我想它确实如此(至少在大多数情况下)。 EF总能做到。 NHibernate对这些操作有更好的支持,因为它有命令批处理。

您在此处展示的不是批量更新(单个命令更新大量记录) - 大多数ORM将始终单独更新每个记录 - 这就是这些工具的工作方式。如果加载记录并在循环中修改每个记录,则与用于加载记录的原始查询的关系将丢失。您现在在应用程序中有10.000个已加载的记录,必须更新。无法进行批量更新,因为您必须将10.000更改从应用程序移动到数据库。

如果要进行批量更新,您应该使用直接SQL或实现一些逻辑,这些逻辑将从Linq-to-sql进行更新,而不是在应用程序中加载记录和更新它们。检查this article或只是在Linq-to-sql中搜索批量/批量更新。

答案 2 :(得分:0)

这是因为Linq to SQL First从服务器加载数据,然后单独更新每条记录,包括数据查询/传输到客户端,更新每条记录的请求。而在SP的情况下,只有一个SP的调用,它直接在服务器上执行Update查询,它不包括每个记录的数据获取和更新。它批量更新记录

答案 3 :(得分:0)

我做的另一种方法是将对象值作为XML数据类型传递给存储过程。但是当记录数超过1000时会出现超时异常(大约25秒后)。这是由于巨大的xml文件造成的吗?

注意:1000条记录大约需要5秒

   public virtual void UpdateBankAccountUsingParseXML_SP(System.Xml.Linq.XElement inputXML)
    {
        string connectionstring = "Data Source=.;Initial Catalog=LibraryReservationSystem;Integrated Security=True;Connect Timeout=600";
        var myDataContext = new DBML_Project.MyDataClassesDataContext(connectionstring);
        myDataContext.ParseXML(inputXML);

    }

    public void FreezeAllAccountsForUser(int userId)
    {
        List<DTOLayer.BankAccountDTOForStatus> bankAccountDTOList = new List<DTOLayer.BankAccountDTOForStatus>(); 

        IEnumerable<DBML_Project.BankAccount> accounts = AccountRepository.GetAllAccountsForUser(userId);
        foreach (DBML_Project.BankAccount acc in accounts)
        {
            string typeResult = Convert.ToString(acc.GetType());
            string baseValue = Convert.ToString(typeof(DBML_Project.BankAccount));

            if (String.Equals(typeResult, baseValue))
            {
                throw new Exception("Not correct derived type");
            }

            acc.Freeze();

            DTOLayer.BankAccountDTOForStatus presentAccount = new DTOLayer.BankAccountDTOForStatus();
            presentAccount.BankAccountID = acc.BankAccountID;
            presentAccount.Status = acc.Status;
            bankAccountDTOList.Add(presentAccount);

        }



        IEnumerable<System.Xml.Linq.XElement> el = bankAccountDTOList.Select(x =>
                        new System.Xml.Linq.XElement("BankAccountDTOForStatus",
                          new System.Xml.Linq.XElement("BankAccountID", x.BankAccountID),
                          new System.Xml.Linq.XElement("Status", x.Status)
                        ));

        System.Xml.Linq.XElement root = new System.Xml.Linq.XElement("root", el);


        AccountRepository.UpdateBankAccountUsingParseXML_SP(root);
        //AccountRepository.Update();

    }

存储过程

ALTER PROCEDURE [dbo].[ParseXML] (@InputXML xml)
AS
BEGIN

DECLARE @MyTable TABLE (RowNumber int, BankAccountID int, StatusVal varchar(max))

INSERT INTO @MyTable(RowNumber, BankAccountID,StatusVal)

SELECT ROW_NUMBER() OVER(ORDER BY c.value('BankAccountID[1]','int') ASC) AS Row,
    c.value('BankAccountID[1]','int'),
    c.value('Status[1]','varchar(32)')
FROM
    @inputXML.nodes('//BankAccountDTOForStatus') T(c);


DECLARE @Count INT
SET @Count = 0

DECLARE @NumberOfRows INT
SELECT @NumberOfRows = COUNT(*) FROM @MyTable


WHILE @Count < @NumberOfRows
BEGIN



      SET @Count = @Count + 1

     DECLARE @BankAccID INT
     DECLARE @Status VARCHAR(MAX)

     SELECT @BankAccID = BankAccountID 
     FROM @MyTable
     WHERE RowNumber = @Count

     SELECT @Status = StatusVal 
     FROM @MyTable
     WHERE RowNumber = @Count


     UPDATE BankAccount 
     SET Status= @Status
     WHERE BankAccountID = @BankAccID

END


END


GO