افسانه کارایی بهتر AddRange نسبت به Add در EF-Core
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۴/۱۳ ۰۹:۵۵
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
Add را درون یک حلقه (Loop) صدا نزنید. شاید این توصیه را یک توسعهدهنده ارشد در طول بازبینی کد (Code Review) به شما کرده باشد، یا آن را در یک پاسخ با صدها امتیاز مثبت در Stack Overflow دیده باشید. استدلال پشت این توصیه در ظاهر کاملاً منطقی و معتبر به نظر میرسد: متد Add با هر بار فراخوانی، مکانیزم DetectChanges را مجدداً اجرا میکند و این فرآیند اسکن با افزایش تعداد نهادهای تحت نظر (Tracked Entities)، کندتر و کندتر میشود. طبق این افسانه، اگر از متد AddRange استفاده کنید، هزینه این پردازش سنگین از هزاران اسکن به تنها یک اسکن کاهش مییابد.Add و AddRange به صورت خودکار فرآیند DetectChanges را تحریک میکردند. فراخوانی Add به تعداد هزار بار در یک حلقه، به معنای هزار بار اسکن کامل تمام موجودیتهای تحت نظر بود که با هر تکرار حلقه بزرگتر میشدند. این رفتار یک هزینه درجه دوم ($O(N^2)$) ایجاد میکرد که به شدت کارایی را کاهش میداد. متد AddRange در EF6 این مشکل را با افزودن دستهای موجودیتها و اجرای تنها یک بار DetectChanges در پایان فرآیند حل میکرد.Add یا AddRange مکانیزم DetectChanges را به صورت خودکار یا درونحلقهای صدا نمیزنند. طبق مستندات مایکروسافت، متدهای مجموعهای (Range Methods) صرفاً یک ابزار راحتی (Convenience) هستند و عملکردی کاملاً مشابه با فراخوانیهای متوالی متدهای غیرمجموعهای دارند.Add به تعداد هزار بار در یک حلقه، صرفاً هزار موجودیت را در وضعیت Added در حافظه صفبندی میکند و تا زمانی که محرکهای اصلی اجرا نشوند، هیچ اسکنی روی آنها صورت نمیگیرد.DetectChanges در EF Core تنها توسط ۵ محرک مشخص اجرا میشود که شناخت آنها برای مدیریت کارایی سیستم الزامی است:SaveChanges یا SaveChangesAsyncChangeTracker.Entries() و اورلودهای ژنریک آنChangeTracker.HasChanges()ChangeTracker.CascadeChanges()Local در طول چرخه حیات یک Context.Add یا AddRange را صدا میزند و هیچ تعاملی با Change Tracker ندارد، هیچ هزینهای بابت DetectChanges در طول تکرارها پرداخت نمیکند. اسکن واقعی تنها یکبار و آن هم زمان فراخوانی SaveChanges رخ میدهد.foreach (var candidate in incomingCustomers)
{
// تله اصلی: دسترسی به ویژگی Local با هر بار تکرار حلقه
var exists = context.Customers.Local.Any(c => c.Sku == candidate.Sku);
if (!exists)
{
context.Customers.Add(candidate);
}
}Customers.Local برای بررسی وجود یک شیء در حافظه، در هر بار تکرار حلقه، EF Core را مجبور به اجرای DetectChanges میکند. اگر این کار را برای ۱۰,۰۰۰ رکورد انجام دهید، دقیقاً همان هزینه سنگین $O(N^2)$ قدیمی را فعال کردهاید. این مشکل به خاطر متد Add نیست، بلکه به دلیل فعالسازی ناخواسته محرکهای Change Tracker در داخل حلقه است.Add یا AddRange نیست؛ بلکه فراخوانی متد SaveChanges درون حلقه است:// آنتیپترن شدید: ایجاد درخواستهای مکرر به دیتابیس
foreach (var product in incomingProducts)
{
context.Products.Add(product);
await context.SaveChangesAsync(); // خطای اصلی اینجاست!
}Add به AddRange در این ساختار هیچ تغییری ایجاد نخواهد کرد، زیرا مشکل اصلی تعداد دفعات ارتباط با دیتابیس است.Add یا AddRange همراه با یک SaveChanges واحد در پایان حلقه: تفاوت عملکرد این دو در حد نویز سیستم (Measurement Noise) است و کاملاً همتراز هستند.SaveChanges به ازای هر نهاد (Per-entity): بین ۶۰ تا ۱۰۰ برابر کندتر از حالت اول عمل میکند.Add و AddRange به یک میزان حافظه جهت نگهداری موجودیتهای تحت ردیابی در فرآیند SaveChanges نیاز دارند.AddRange مزیت عملکردی مستقیمی نسبت به Add در حلقهها ندارد، اما همچنان استفاده از آن به دلایل معماری و خوانایی کد توصیه میشود:context.Products.AddRange(products) در یک نگاه به بررسیکننده کد میگوید که با یک عملیات دستهای سروکار دارد، در حالی که یک حلقه foreach نیاز به خواندن کل بلوک کد برای درک این موضوع دارد.var validEntities = incomingData
.Where(d => d.IsValid)
.Select(d => MapToEntity(d));
context.Products.AddRange(validEntities);List) فیلتر و یکتا سازی میکنید و سپس لیست نهایی را یکجا به AddRange میسپارید، دیگر ریسکِ برخورد با متدهایی نظیر Local یا Entries در یک حلقه متصل به دیتابیس وجود نخواهد داشت.AddRange نسبت به Add در زمان افزودن دادهها، یک مفهوم منسوخشده متعلق به دوران EF6 است. در معماری EF Core، متد AddRange صرفاً یک متد کمکی برای افزایش خوانایی و زیبایی کد است و هیچ بهبودی در سرعت پردازش داخلی ثبت دادهها نسبت به متد Add (زمانی که درست استفاده شود) ایجاد نمیکند.SaveChanges: همیشه مطمئن شوید که فراخوانی دستور ذخیرهسازی خارج از حلقههای تکرار قرار دارد.Local یا متد HasChanges() در داخل حلقههای پردازش داده خودداری کنید.AddRange باعث دستهای شدن (Batching) کوئریها میشود و Add تکی این کار را نمیکند، یک خطای تشخیصی رایج است. بیایید بررسی کنیم که در پشت صحنه EF Core واقعاً چه اتفاقی میافتد:INSERT INTO ... VALUES (...), (...), ... هیچ ارتباطی به این ندارد که شما موجودیتها را با Add درون حلقه stage کردهاید یا با AddRange. تولید کوئری دستهای، وظیفهی مکانیزم درایور دیتابیس (Provider) در زمان فراخوانی SaveChanges است، نه متدهای آمادهسازی داده.SaveChanges() را صدا میزنید، فرآیند زیر طی میشود:Added هستند را جمعآوری میکند (فرقی ندارد تکتک با Add آمده باشند یا یکجا با AddRange).MaxBatchSize)، آنها را به کوئریهای دستهای (Batched INSERTS) تبدیل میکند.SaveChanges خارج از حلقه باشد، هر دو الگوی زیر دقیقاً یک خروجی SQL یکسان و دستهای تولید میکنند:foreach(var item in items)
{
context.Products.Add(item);
}
context.SaveChanges();
// خروجی: INSERT INTO Products (...) VALUES (...), (...), (...)context.Products.AddRange(items); context.SaveChanges(); // خروجی: INSERT INTO Products (...) VALUES (...), (...), (...)
Add کوئری تکی میزند و AddRange دستهای»، معمولاً از دو سناریو آب میخورد:Add استفاده میکند، ناخودآگاه SaveChanges را هم درون همان حلقه قرار میدهد:// تولید هزاران کوئری تکی و رفتوبرگشت به دیتابیس
foreach(var item in items) {
context.Products.Add(item);
context.SaveChanges(); // علت اصلی تکی شدن کوئریها
}AddRange استفاده کند، ساختار کد او را مجبور میکند که ابتدا کل لیست را جمع کند و سپس یکبار SaveChanges را صدا بزند. در نتیجه، مقایسه بین این دو روش در اصل مقایسه Add و AddRange نیست؛ بلکه مقایسه فراخوانی مکرر SaveChanges در برابر یکبار فراخوانی SaveChanges است.Add یا حتی AddRange اضافه میکردید، EF6 به صورت پیشفرض ۱۰۰ دستور INSERT مجزا و تفکیکشده با دستور go به دیتابیس میفرستاد. در آن دوران برای دستهای کردن واقعی کوئریها، توسعهدهندگان ناچار بودند به سراغ کتابخانههای جانبی بروند. از نسخه EF Core 1.0 به بعد، مایکروسافت قابلیت Auto-Batching را به عنوان یک ویژگی کلیدی و پیشفرض معرفی کرد تا این ضعف تاریخی را پوشش دهد.Add از AddRange. تا زمانی که SaveChanges خارج از حلقه باشد، موتور تولید کوئری EF Core به متد ورود دادهها اهمیتی نمیدهد و با هوشمندی، رکوردها را در بستههای بهینهشده (معمولاً تا ۱۰۰۰ رکورد در هر بسته برای SQL Server) دستهبندی کرده و ارسال میکند.