متى يجب علي استدعاء SaveChanges() عند إنشاء آلاف كائنات Entity Framework؟(مثل أثناء الاستيراد)

StackOverflow https://stackoverflow.com/questions/1930982

سؤال

أقوم بتشغيل عملية استيراد تحتوي على آلاف السجلات في كل عملية تشغيل.فقط أبحث عن بعض التأكيد على افتراضاتي:

أي مما يلي هو الأكثر منطقية:

  1. يجري SaveChanges() كل AddToClassName() يتصل.
  2. يجري SaveChanges() كل ن رقم ال AddToClassName() المكالمات.
  3. يجري SaveChanges() بعد الجميع التابع AddToClassName() المكالمات.

ربما يكون الخيار الأول بطيئًا، أليس كذلك؟نظرًا لأنه سيحتاج إلى تحليل كائنات EF في الذاكرة، وإنشاء SQL، وما إلى ذلك.

أفترض أن الخيار الثاني هو الأفضل في كلا الخيارين، حيث يمكننا أن نلتف حول ذلك SaveChanges() اتصل ولا تخسر إلا ن عدد السجلات في المرة الواحدة، في حالة فشل أحدها.ربما قم بتخزين كل دفعة في قائمة<>.إذا SaveChanges() نجحت المكالمة، تخلص من القائمة.إذا فشلت، قم بتسجيل العناصر.

من المحتمل أن يكون الخيار الأخير بطيئًا جدًا أيضًا، نظرًا لأن كل كائن EF يجب أن يكون في الذاكرة حتى SaveChanges() يسمى.وإذا فشل الحفظ فلن يتم الالتزام بأي شيء، أليس كذلك؟

هل كانت مفيدة؟

المحلول

سأختبره أولاً للتأكد.الأداء لا يجب أن يكون بهذا السوء.

إذا كنت بحاجة إلى إدخال جميع الصفوف في معاملة واحدة، فاتصل بها بعد كل فئة AddToClassName.إذا كان من الممكن إدخال الصفوف بشكل مستقل، فاحفظ التغييرات بعد كل صف.اتساق قاعدة البيانات مهم.

الخيار الثاني لا أحبهسيكون الأمر محيرًا بالنسبة لي (من منظور المستخدم النهائي) إذا قمت بالاستيراد إلى النظام وسيرفض 10 صفوف من أصل 1000، فقط لأن صفًا واحدًا سيئًا.يمكنك محاولة استيراد 10، وإذا فشلت، حاول واحدًا تلو الآخر ثم قم بتسجيل الدخول.

اختبار إذا كان يستغرق وقتا طويلا.لا تكتب "من المحتمل".أنت لا تعرف ذلك بعد.فقط عندما تكون هناك مشكلة بالفعل، فكر في حل آخر (marc_s).

يحرر

لقد أجريت بعض الاختبارات (الوقت بالميلي ثانية):

10000 صف:

SaveChanges() بعد صف واحد: 18510,534
SaveChanges() بعد 100 صف:4350,3075
SaveChanges() بعد 10000 صف:5233,0635

50000 صف:

SaveChanges() بعد صف واحد: 78496,929
SaveChanges() بعد 500 صف:22302,2835
SaveChanges() بعد 50000 صف:24022,8765

لذا فإن الالتزام بعد n من الصفوف يكون أسرع في الواقع من الالتزام به بعد كل شيء.

توصيتي هي:

  • SaveChanges() بعد عدد n من الصفوف.
  • إذا فشل التزام واحد، فحاول تنفيذه واحدًا تلو الآخر للعثور على الصف الخاطئ.

فئات الاختبار:

طاولة:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SomeInt] [int] NOT NULL,
    [SomeVarchar] [varchar](100) NOT NULL,
    [SomeOtherVarchar] [varchar](50) NOT NULL,
    [SomeOtherInt] [int] NULL,
 CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

فصل:

public class TestController : Controller
{
    //
    // GET: /Test/
    private readonly Random _rng = new Random();
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private string RandomString(int size)
    {
        var randomSize = _rng.Next(size);

        char[] buffer = new char[randomSize];

        for (int i = 0; i < randomSize; i++)
        {
            buffer[i] = _chars[_rng.Next(_chars.Length)];
        }
        return new string(buffer);
    }


    public ActionResult EFPerformance()
    {
        string result = "";

        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
        TruncateTable();

        return Content(result);
    }

    private void TruncateTable()
    {
        using (var context = new CamelTrapEntities())
        {
            var connection = ((EntityConnection)context.Connection).StoreConnection;
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = @"TRUNCATE TABLE TestTable";
            command.ExecuteNonQuery();
        }
    }

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
    {
        var startDate = DateTime.Now;

        using (var context = new CamelTrapEntities())
        {
            for (int i = 1; i <= noOfRows; ++i)
            {
                var testItem = new TestTable();
                testItem.SomeVarchar = RandomString(100);
                testItem.SomeOtherVarchar = RandomString(50);
                testItem.SomeInt = _rng.Next(10000);
                testItem.SomeOtherInt = _rng.Next(200000);
                context.AddToTestTable(testItem);

                if (i % commitAfterRows == 0) context.SaveChanges();
            }
        }

        var endDate = DateTime.Now;

        return endDate.Subtract(startDate);
    }
}

نصائح أخرى

لقد قمت للتو بتحسين مشكلة مشابهة جدًا في الكود الخاص بي وأود أن أشير إلى التحسين الذي نجح معي.

لقد وجدت أن معظم الوقت الذي تقضيه في معالجة SaveChanges، سواء معالجة 100 أو 1000 سجل في وقت واحد، يكون مرتبطًا بوحدة المعالجة المركزية (CPU).لذلك، من خلال معالجة السياقات باستخدام نمط المنتج/المستهلك (الذي تم تنفيذه باستخدام BlockingCollection)، تمكنت من الاستفادة بشكل أفضل من مراكز وحدة المعالجة المركزية وحصلت على إجمالي 4000 تغيير في الثانية (كما ورد في قيمة الإرجاع الخاصة بـ SaveChanges) إلى أكثر من 14000 تغيير/ثانية.انتقل استخدام وحدة المعالجة المركزية من حوالي 13% (لدي 8 مراكز) إلى حوالي 60%.حتى باستخدام سلاسل عمليات متعددة للمستهلكين، بالكاد قمت بفرض ضرائب على نظام الإدخال/الإخراج (السريع جدًا) للقرص ولم يكن استخدام وحدة المعالجة المركزية لـ SQL Server أعلى من 15٪.

عن طريق إلغاء تحميل الحفظ إلى سلاسل رسائل متعددة، لديك القدرة على ضبط كل من عدد السجلات قبل الالتزام وعدد سلاسل الرسائل التي تنفذ عمليات الالتزام.

لقد وجدت أن إنشاء مؤشر ترابط منتج واحد و(# من نواة وحدة المعالجة المركزية) -1 سلاسل مستهلكين سمح لي بضبط عدد السجلات الملتزم بها لكل دفعة بحيث يتقلب عدد العناصر في BlockingCollection بين 0 و1 (بعد أن يستغرق مؤشر ترابط المستهلك واحدًا غرض).بهذه الطريقة، كان هناك ما يكفي من العمل حتى تعمل الخيوط المستهلكة على النحو الأمثل.

يتطلب هذا السيناريو بالطبع إنشاء سياق جديد لكل دفعة، وهو ما أجده أسرع حتى في السيناريو المفرد لحالة الاستخدام الخاصة بي.

إذا كنت بحاجة إلى استيراد آلاف السجلات، فسأستخدم شيئًا مثل SqlBulkCopy، وليس Entity Framework لذلك.

استخدم الإجراء المخزن.

  1. إنشاء نوع بيانات محدد من قبل المستخدم في Sql Server
  2. قم بإنشاء وملء مصفوفة من هذا النوع في التعليمات البرمجية الخاصة بك (سريع جدًا).
  3. قم بتمرير المصفوفة إلى الإجراء المخزن الخاص بك بمكالمة واحدة (سريعة جدًا).

أعتقد أن هذه ستكون الطريقة الأسهل والأسرع للقيام بذلك.

آسف، أعرف أن هذا الموضوع قديم، ولكن أعتقد أن هذا يمكن أن يساعد الآخرين في حل هذه المشكلة.

لقد واجهت نفس المشكلة، ولكن هناك إمكانية للتحقق من صحة التغييرات قبل تنفيذها.يبدو الكود الخاص بي هكذا وهو يعمل بشكل جيد.مع ال chUser.LastUpdated أتحقق مما إذا كان إدخالًا جديدًا أم مجرد تغيير.لأنه ليس من الممكن إعادة تحميل إدخال غير موجود في قاعدة البيانات بعد.

// Validate Changes
var invalidChanges = _userDatabase.GetValidationErrors();
foreach (var ch in invalidChanges)
{
    // Delete invalid User or Change
    var chUser  =  (db_User) ch.Entry.Entity;
    if (chUser.LastUpdated == null)
    {
        // Invalid, new User
        _userDatabase.db_User.Remove(chUser);
        Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey);
    }
    else
    {
        // Invalid Change of an Entry
        _userDatabase.Entry(chUser).Reload();
        Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey);
    }                    
}

_userDatabase.SaveChanges();
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top