یک نکتهی تکمیلی: پشتیبانی توکار ASP.NET Core 2.0 از Range headers
نظرات مطالب
آشنایی با NHibernate - قسمت هفتم
بحث entity framework با NHibernate متفاوت است.
در NHibernate این متد BuildSessionFactory فوق کار بارگذاری متادیتا و نگاشتها و غیره رو انجام میده؛ یعنی خودکار نیست و اگر قرار باشه به ازای هر کوئری یکبار فراخوانی شود اصلا نمیشود با برنامه کار کرد چون به شدت کند خواهد بود. به همین جهت کش کردن آنرا با استفاده از الگوی singleton به صورت فوق تنها یکبار باید انجام داد. یکبار در طول عمر برنامه باید نگاشتها صورت گیرد و پس از آن بارها و بارها از آن استفاده شود چون قرار نیست در طول عمر یک برنامه در حال اجرا تغییری کند.
در حالیکه در Entity framework اینکار (بارگذاری متادیتا و نگاشتهای تعریف شده) به صورت خودکار زمانیکه برنامه برای بار اول اجرا میشود رخ داده و به صورت خودکار هم کش میشود. ماخذ: (+) ؛ بنابراین برای مدیریت آن اصلا لازم نیست شما کاری انجام دهید.
فقط در Entity framework یک لایه بسیار کم هزینه به نام ObjectContext وجود دارد که توصیه شده در برنامههای ASP.NET به ازای هر درخواست ایجاد و تخریب شود که میتوانید از مقاله فوق ایده بگیرید و اصلا نباید کش شود یا هر بحث دیگری. ماخذ: (+) ؛ حتی اگر اینکار را هم انجام ندادید مهم نیست چون سربار بسیار کمی دارد.
در NHibernate این متد BuildSessionFactory فوق کار بارگذاری متادیتا و نگاشتها و غیره رو انجام میده؛ یعنی خودکار نیست و اگر قرار باشه به ازای هر کوئری یکبار فراخوانی شود اصلا نمیشود با برنامه کار کرد چون به شدت کند خواهد بود. به همین جهت کش کردن آنرا با استفاده از الگوی singleton به صورت فوق تنها یکبار باید انجام داد. یکبار در طول عمر برنامه باید نگاشتها صورت گیرد و پس از آن بارها و بارها از آن استفاده شود چون قرار نیست در طول عمر یک برنامه در حال اجرا تغییری کند.
در حالیکه در Entity framework اینکار (بارگذاری متادیتا و نگاشتهای تعریف شده) به صورت خودکار زمانیکه برنامه برای بار اول اجرا میشود رخ داده و به صورت خودکار هم کش میشود. ماخذ: (+) ؛ بنابراین برای مدیریت آن اصلا لازم نیست شما کاری انجام دهید.
فقط در Entity framework یک لایه بسیار کم هزینه به نام ObjectContext وجود دارد که توصیه شده در برنامههای ASP.NET به ازای هر درخواست ایجاد و تخریب شود که میتوانید از مقاله فوق ایده بگیرید و اصلا نباید کش شود یا هر بحث دیگری. ماخذ: (+) ؛ حتی اگر اینکار را هم انجام ندادید مهم نیست چون سربار بسیار کمی دارد.
در حین وبگردیهای روزانه به مرجع آموزشی رایگان 500 صفحهای زیر برخوردم، گفتم شاید برای شما هم جالب توجه باشد:
Entity Framework learning guide
1. Introduction to Entity Framework
2. Modeling Entities
3. Eager and Lazy Loading entities and Navigation properties
4. Views
5. Inheritance
6. Working with Objects
7. Improving Entity framework performance
8. Inserting, Updating and Deleting entities and associations
9. Querying with Linq to entities
10. Concurrency and Transactions
11. Consuming Stored Procedures
12. Mapping Crud Operations to Stored Procedure
Download
در نگارش قبلی EF Code first به ازای یک پروژه تنها یک سیستم Migration قابل تعریف بود و این سیستم مهاجرت، تنها با یک DbContext کار میکرد. در نگارش ششم این کتابخانه، سیستم مهاجرت Code first آن از چندین DbContext، به ازای یک پروژه که به یک بانک اطلاعاتی اشاره میکنند، پشتیبانی میکند. مزیت اینکار اندکی بهبود در نگهداری تنها کلاس DbContext تعریف شده است. برای مثال میتوان یک کلاس DbContext مخصوص قسمت ثبت نام را ایجاد کرد. یک کلاس DbContext مخصوص کلیه جداول مرتبط با مقالات را و همینطور الی آخر. نهایتا تمام این Contextها سبب ایجاد یک بانک اطلاعاتی واحد خواهند شد.
اگر در یک پروژه EF Code first چندین Context وجود داشته باشد و دستور enable-migrations را بدون پارامتری فراخوانی کنیم، پیغام خطای More than one context type was found in the assmbly xyz را دریافت خواهیم کرد.
الف) اما در EF 6 میتوان با بکار بردن سوئیچ جدید ContextTypeName، به ازای هر Context، مهاجرت مرتبط با آنرا تنظیم نمود:
همچنین در اینجا نیز میتوان با استفاده از سوئیچ MigrationDirectory، فایلهای تولید شده را در پوشههای مجزایی ذخیره کرد.
ب) در مرحله بعد، نیاز به فراخوانی دستور add-migration است:
با اجرای دستور enable-migrations یک کلاس Configuration جهت DbContext مشخص شده، ایجاد میشود. سپس آدرس کامل این کلاس را به همراه ذکر دقیق فضای نام آن در اختیار دستور add-migration قرار میدهیم.
ذکر کامل فضای نام، از این جهت مهم است که کلاس Configuration به ازای Contextهای مختلف ایجاد شده، یک نام را خواهد داشت؛ اما در فضاهای نام متفاوتی قرار میگیرد.
با اجرای دستور add-migration، کدهای سی شارپ مورد نیاز جهت اعمال تغییرات بر روی ساختار بانک اطلاعاتی تولید میشوند. در مرحله بعد، این کدها تبدیل به دستورات SQL متناظری شده و بر روی بانک اطلاعاتی اجرا خواهند شد.
بدیهی است اگر دو Context در برنامه تعریف کرده باشید، دوبار باید دستور enable-migrations و دوبار دستور add-migration را با پارامترهای اشاره کننده به Conetxtهای مدنظر اجرا کرد.
ج) سپس برای اعمال این تغییرات، باید دستور update-database را اجرا کرد.
اینبار دستور update-database نیز بر اساس نام کامل کلاس Configuration مدنظر باید اجرا گردد و به ازای هر Context موجود، یکبار نیاز است اجرا گردد.
نهایتا اگر به بانک اطلاعاتی مراجعه کنید، تمام جداول و تعاریف را یکجا در همان بانک اطلاعاتی میتوانید مشاهده نمائید.
داشتن چندین Context در برنامه و مدیریت تراکنشها
در EF، هر DbContext معرف یک واحد کار است. یعنی تراکنشها و چندین عمل متوالی مرتبط انجام شده، درون یک DbContext معنا پیدا میکنند. متد SaveChanges نیز بر همین اساس است که کلیه اعمال ردیابی شده در طی یک واحد کار را در طی یک تراکنش به بانک اطلاعاتی اعمال میکند. همچنین مباحثی مانند lazy loading نیز در طی یک Context مفهوم دارند. به علاوه دیگر امکان join نویسی بین دو Context وجود نخواهد داشت. باید اطلاعات را از یکی واکشی و سپس این اطلاعات درون حافظهای را به دیگری ارسال کنید.
یک نکته
میتوان یک DbSet را در چندین Context تعریف کرد. یعنی اگر بحث join نویسی مطرح است، با تکرار تعریف DbSetها اینکار قابل انجام است اما این مساله اساس جداسازی Contextها را نیز زیر سؤال میبرد.
داشتن چندین Context در برنامه و مدیریت رشتههای اتصالی
در EF Code first روشهای مختلفی برای تعریف رشته اتصالی به بانک اطلاعاتی وجود دارند. اگر تغییر خاصی در کلاس مشتق شده از DbContext ایجاد نکنیم، نام کلید رشته اتصالی تعریف شده در فایل کانفیگ باید به نام کامل کلاس Context برنامه اشاره کند. اما با داشتن چندین Context به ازای یک دیتابیس میتوان از روش ذیل استفاده کرد:
در اینجا در سازنده کلاسهای Context تعریف شده، نام کلید رشته اتصالی موجود در فایل کانفیگ برنامه ذکر شده است. به این ترتیب این دو Context به یک بانک اطلاعاتی اشاره خواهند کرد.
چه زمانی بهتر است از چندین Context در برنامه استفاده کرد؟
عموما در طراحیهای سازمانی SQL Server، تمام جداول از schema مدیریتی به نام dbo استفاده نمیکنند. جداول فروش از schema خاص خود و جداول کاربران از schema دیگری استفاده خواهند کرد. با استفاده از چندین Context میتوان به ازای هر کدام از schemaهای متفاوت موجود، «یک ناحیه ایزوله» را ایجاد و مدیریت کرد.
در این حالت در EF 6 میتوان DefaultSchema کلی یک Context را در متد بازنویسی شده OnModelCreating به نحو فوق تعریف و مدیریت کرد. در این مثال به صورت خودکار کلیه DbSetهای Ctx2 از schema ایی به نام sales استفاده میکنند.
اگر در یک پروژه EF Code first چندین Context وجود داشته باشد و دستور enable-migrations را بدون پارامتری فراخوانی کنیم، پیغام خطای More than one context type was found in the assmbly xyz را دریافت خواهیم کرد.
الف) اما در EF 6 میتوان با بکار بردن سوئیچ جدید ContextTypeName، به ازای هر Context، مهاجرت مرتبط با آنرا تنظیم نمود:
enable-migrations -ContextTypeName dbContextName1 -MigrationDirectory DataContexts\Name1Migrations
ب) در مرحله بعد، نیاز به فراخوانی دستور add-migration است:
add-migration -ConfigurationTypeName FullNameSpaceCtx1.Configuration "InitialCreate"
ذکر کامل فضای نام، از این جهت مهم است که کلاس Configuration به ازای Contextهای مختلف ایجاد شده، یک نام را خواهد داشت؛ اما در فضاهای نام متفاوتی قرار میگیرد.
با اجرای دستور add-migration، کدهای سی شارپ مورد نیاز جهت اعمال تغییرات بر روی ساختار بانک اطلاعاتی تولید میشوند. در مرحله بعد، این کدها تبدیل به دستورات SQL متناظری شده و بر روی بانک اطلاعاتی اجرا خواهند شد.
بدیهی است اگر دو Context در برنامه تعریف کرده باشید، دوبار باید دستور enable-migrations و دوبار دستور add-migration را با پارامترهای اشاره کننده به Conetxtهای مدنظر اجرا کرد.
ج) سپس برای اعمال این تغییرات، باید دستور update-database را اجرا کرد.
update-database -ConfigurationTypeName FullNameSpaceCtx1.Configuration
نهایتا اگر به بانک اطلاعاتی مراجعه کنید، تمام جداول و تعاریف را یکجا در همان بانک اطلاعاتی میتوانید مشاهده نمائید.
داشتن چندین Context در برنامه و مدیریت تراکنشها
در EF، هر DbContext معرف یک واحد کار است. یعنی تراکنشها و چندین عمل متوالی مرتبط انجام شده، درون یک DbContext معنا پیدا میکنند. متد SaveChanges نیز بر همین اساس است که کلیه اعمال ردیابی شده در طی یک واحد کار را در طی یک تراکنش به بانک اطلاعاتی اعمال میکند. همچنین مباحثی مانند lazy loading نیز در طی یک Context مفهوم دارند. به علاوه دیگر امکان join نویسی بین دو Context وجود نخواهد داشت. باید اطلاعات را از یکی واکشی و سپس این اطلاعات درون حافظهای را به دیگری ارسال کنید.
یک نکته
میتوان یک DbSet را در چندین Context تعریف کرد. یعنی اگر بحث join نویسی مطرح است، با تکرار تعریف DbSetها اینکار قابل انجام است اما این مساله اساس جداسازی Contextها را نیز زیر سؤال میبرد.
داشتن چندین Context در برنامه و مدیریت رشتههای اتصالی
در EF Code first روشهای مختلفی برای تعریف رشته اتصالی به بانک اطلاعاتی وجود دارند. اگر تغییر خاصی در کلاس مشتق شده از DbContext ایجاد نکنیم، نام کلید رشته اتصالی تعریف شده در فایل کانفیگ باید به نام کامل کلاس Context برنامه اشاره کند. اما با داشتن چندین Context به ازای یک دیتابیس میتوان از روش ذیل استفاده کرد:
public class Ctx1 : DbContext { public Ctx1() : base("DefaultConnection") { //Database.Log = sql => Debug.Write(sql); } } public class Ctx2 : DbContext { public Ctx2() : base("DefaultConnection") { //Database.Log = sql => Debug.Write(sql); } }
<connectionStrings> <add name="DefaultConnection" connectionString="…." providerName="System.Data.SqlClient" /> </connectionStrings>
چه زمانی بهتر است از چندین Context در برنامه استفاده کرد؟
عموما در طراحیهای سازمانی SQL Server، تمام جداول از schema مدیریتی به نام dbo استفاده نمیکنند. جداول فروش از schema خاص خود و جداول کاربران از schema دیگری استفاده خواهند کرد. با استفاده از چندین Context میتوان به ازای هر کدام از schemaهای متفاوت موجود، «یک ناحیه ایزوله» را ایجاد و مدیریت کرد.
public class Ctx2 : DbContext { public Ctx2() : base("DefaultConnection") { //Database.Log = sql => Debug.Write(sql); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("sales"); base.OnModelCreating(modelBuilder); } }
در ادامهی مباحث پشتیبانی از XML در SQL Server، به کارآیی فیلدهای XML ایی و نحوهی ایندکس گذاری بر روی آنها خواهیم پرداخت. این مساله در تولید برنامههایی سریع و مقیاس پذیر، بسیار حائز اهمیت است.
در SQL Server، کوئریهای انجام شده بر روی فیلدهای XML، توسط همان پردازشگر کوئریهای رابطهای متداول آن، خوانده و اجرا خواهند شد و امکان تعریف یک XQuery خارج از یک عبارت SQL و یا T-SQL وجود ندارد. متدهای XQuery بسیار شبیه به system defined functions بوده و Query Plan یکپارچهای را با سایر قسمتهای رابطهای یک عبارت SQL دارند.
مفهوم Node table
دادههای XML ایی برای اینکه توسط SQL Server قابل استفاده باشند، به صورت درونی تبدیل به یک node table میشوند. به این معنا که نودهای یک سند XML، به یک جدول رابطهای به صورت خودکار تجزیه میشوند. این جدول درونی در صورت بکارگیری XML Indexes در جدول سیستمی sys.internal_tables قابل مشاهده خواهد بود. SQL Server برای انجام اینکار از یک XmlReader خاص خودش استفاده میکند. در مورد XMLهای ایندکس نشده، این تجزیه در زمان اجرا صورت میگیرد؛ پس از اینکه Query Plan آن تشکیل شد.
بررسی Query Plan فیلدهای XML ایی
جهت فراهم کردن مقدمات آزمایش، ابتدا جدول xmlInvoice را با یک فیلد XML ایی untyped درنظر بگیرید:
سپس 6 ردیف را به آن اضافه میکنیم:
همچنین برای مقایسه، دقیقا جدول مشابهی را اینبار با یک XML Schema مشخص ایجاد میکنیم.
سپس مجددا همان 6 رکورد قبلی را در این جدول جدید نیز insert خواهیم کرد.
در این جدول دوم، حالت پیش فرض content قبلی، به document تغییر کردهاست. با توجه به اینکه میدانیم اسناد ما چه فرمتی دارند و بیش از یک root element نخواهیم داشت، انتخاب document سبب خواهد شد تا Query Plan بهتری حاصل شود.
در ادامه برای مشاهدهی بهتر نتایج، کش Query Plan و اطلاعات آماری جدول xmlInvoice را حذف و به روز میکنیم:
به علاوه در management studio بهتر است از منوی Query، گزینهی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمههای Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده نمود. برای خواندن یک Query Plan عموما از بالا به پایین و از راست به چپ باید عمل کرد. در آن نهایتا باید به عدد estimated subtree cost کوئری، دقت داشت.
کوئریهایی را که در این قسمت بررسی خواهیم کرد، در ادامه ملاحظه میکنید. بار اول این کوئریها را بر روی xmlInvoice و بار دوم، بر روی نگارش دوم دارای اسکیمای آن اجرا خواهیم کرد:
کوئری 1
همانطور که عنوان شد، از منوی Query گزینهی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمههای Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده کرد.
در کوئری 1، با استفاده از متد exist به دنبال رکوردهایی هستیم که دارای ویژگی InvoiceId مساوی 1003 هستند. پس از اجرای کوئری، تصویر Query Plan آن به شکل زیر خواهد بود:
برای خواندن این تصویر، از بالا به پایین و چپ به راست باید عمل شود. هزینهی انجام کوئری را نیز با نگه داشتن کرسر ماوس بر روی select نهایی سمت چپ تصویر میتوان مشاهده کرد. البته باید درنظر داشت که این اعداد از دیدگاه Query Processor مفهوم پیدا میکنند. پردازشگر کوئری، بر اساس اطلاعاتی که در اختیار دارد، سعی میکند بهترین روش پردازش کوئری دریافتی را پیدا کند. برای اندازه گیری کارآیی، باید اندازه گیری زمان اجرای کوئری، مستقلا انجام شود.
در این کوئری، مطابق تصویر اول، ابتدا قسمت SQL آن (چپ بالای تصویر) پردازش میشود و سپس قسمت XML آن. قسمت XQuery این عبارت در دو قسمت سمت چپ، پایین تصویر مشخص شدهاند. Table valued functionها جاهایی هستند که node table ابتدای بحث جاری در آنها ساخته میشوند. در اینجا دو مرحلهی تولید Table valued functionها مشاهده میشود. اگر به جمع درصدهای آنها دقت کنید، هزینهی این دو قسمت، 98 درصد کل Query plan است.
سؤال: چرا دو مرحلهی تولید Table valued functionها در اینجا قابل مشاهده است؟ یک مرحلهی آن مربوط است به انتخاب نود Invoice و مرحلهی دوم مربوط است به فیلتر داخل [] ذکر شد برای یافتن ویژگیهای مساوی 1003.
در اینجا و در کوئریهای بعدی، هر Query Plan ایی که تعداد مراحل تولید Table valued function کمتری داشته باشد، بهینهتر است.
کوئری 5
اگر کوئری پلن شماره 5 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید. یک XML Reader برای خارج از [] (اصطلاحا به آن predicate گفته میشود) و دو مورد برای داخل [] تشکیل شدهاست؛ یکی برای انتخاب نود متنی و دیگری برای تساوی.
کوئری 7
اگر کوئری پلن شماره 7 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید که بسیار شبیه است به مورد 5. بنابراین در اینجا عمق بررسی و سلسله مراتب اهمیتی ندارد.
کوئری 9
کوئری 9 دقیقا معادل است با کوئری 1 نوشته شده؛ با این تفاوت که از روش FLOWR استفاده کردهاست. نکتهی جالب آن، وجود تنها یک XML reader در Query plan آن است که باید آنرا بخاطر داشت.
کوئری 2
کوئری 3
کوئری 4
کوئری 6
کوئری 8
اگر به این 5 کوئری یاد شده دقت کنید، از یک دات به معنای self استفاده کردهاند (یعنی پردازش بیشتری را انجام نده و از همین نود جاری برای پردازش نهایی استفاده کن). با توجه به بکارگیری متد exist، معنای کوئریهای یک و دو، یکیاست. اما در کوئری شماره 2، تنها یک XML Reader در Query plan نهایی وجود دارد (همانند عبارت FLOWR کوئری شماره 9).
یک نکته: اگر میخواهید بدانید بین کوئریهای 1 و 2 کدامیک بهتر عمل میکنند، از بین تمام کوئریهای موجود، دو کوئری یاد شده را انتخاب کرده و سپس با فرض روش بودن نمایش Query plan، هر دو کوئری را با هم اجرا کنید.
در این حالت، کوئری پلنهای هر دو کوئری را با هم یکجا میتوان مشاهده کرد؛ به علاوهی هزینهی نسبی آنها را در کل عملیات صورت گرفته. در حالت استفاده از دات و وجود تنها یک XML Reader، این هزینه تنها 6 درصد است، در مقابل هزینهی 94 درصدی کوئری شماره یک.
بنابراین از دیدگاه پردازشگر کوئریهای SQL Server، کوئری شماره 2، بسیار بهتر است از کوئری شماره 1.
در کوئریهای 3 و 4، شماره نود مدنظر را دقیقا مشخص کردهایم. این مورد در حالت سوم تفاوت محسوسی را از لحاظ کارآیی ایجاد نمیکند و حتی کارآیی را به علت اضافه کردن یک XML Reader دیگر برای پردازش عدد نود وارد شده، کاهش میدهد. اما کوئری 4 که عدد اولین نود را خارج از پرانتز قرار دادهاست، تنها در کل یک XML Reader را به همراه خواهد داشت.
سؤال: بین کوئریهای 2، 3 و 4 کدامیک بهینهتر است؟
بله. اگر هر سه کوئری را با هم انتخاب کرده و اجرا کنیم، میتوان در قسمت کوئری پلنها، هزینهی هر کدام را نسبت به کل مشاهده کرد. در این حالت کوئری 4 بهتر است از کوئری 2 و تنها یک درصد هزینهی کل را تشکیل میدهد.
کوئری 10
کوئری 10 اندکی متفاوت است نسبت به کوئریهای دیگر. در اینجا بجای متد exist از متد value استفاده شدهاست. یعنی ابتدا صریحا مقدار ویژگی InvoiceId استخراج شده و با 1003 مقایسه میشود.
اگر کوئری پلن آنرا با کوئری 4 که بهترین کوئری سری exist است مقایسه کنیم، کوئری 10، هزینهی 70 درصدی کل عملیات را به خود اختصاص خواهد داد، در مقابل 30 درصد هزینهی کوئری 4. بنابراین در این موارد، استفاده از متد exist بسیار بهینهتر است از متد value.
استفاده از Schema collection و تاثیر آن بر کارآیی
تمام مراحلی را که در اینجا ملاحظه کردید، صرفا با تغییر نام xmlInvoice به xmlInvoice2، تکرار کنید. xmlInvoice2 دارای ساختاری مشخص است، به همراه ذکر صریح document حین تعریف ستون XML ایی آن.
تمام پاسخهایی را که دریافت خواهید کرد با حالت بدون Schema collection یکی است.
برای مقایسه بهتر، یکبار نیز سعی کنید کوئری 1 جدول xmlInvoice را با کوئری 1 جدول xmlInvoice2 با هم در طی یک اجرا مقایسه کنید، تا بهتر بتوان Query plan نسبی آنها را بررسی کرد.
پس از این بررسی و مقایسه، به این نتیجه خواهید رسید که تفاوت محسوسی در اینجا و بین این دو حالت، قابل ملاحظه نیست. در SQL Server از Schema collection بیشتر برای اعتبارسنجی ورودیها استفاده میشود تا بهبود کارآیی کوئریها.
بنابراین به صورت خلاصه
- متد exist را به value ترجیح دهید.
- اصطلاحا ordinal (همان مشخص کردن نود 1 در اینجا) را در آخر قرار دهید (نه در بین نودها).
- مراحل اجرایی را با معرفی دات (استفاده از نود جاری) تا حد ممکن کاهش دهید.
و ... کوئری 4 در این سری، بهترین کارآیی را ارائه میدهد.
در SQL Server، کوئریهای انجام شده بر روی فیلدهای XML، توسط همان پردازشگر کوئریهای رابطهای متداول آن، خوانده و اجرا خواهند شد و امکان تعریف یک XQuery خارج از یک عبارت SQL و یا T-SQL وجود ندارد. متدهای XQuery بسیار شبیه به system defined functions بوده و Query Plan یکپارچهای را با سایر قسمتهای رابطهای یک عبارت SQL دارند.
مفهوم Node table
دادههای XML ایی برای اینکه توسط SQL Server قابل استفاده باشند، به صورت درونی تبدیل به یک node table میشوند. به این معنا که نودهای یک سند XML، به یک جدول رابطهای به صورت خودکار تجزیه میشوند. این جدول درونی در صورت بکارگیری XML Indexes در جدول سیستمی sys.internal_tables قابل مشاهده خواهد بود. SQL Server برای انجام اینکار از یک XmlReader خاص خودش استفاده میکند. در مورد XMLهای ایندکس نشده، این تجزیه در زمان اجرا صورت میگیرد؛ پس از اینکه Query Plan آن تشکیل شد.
بررسی Query Plan فیلدهای XML ایی
جهت فراهم کردن مقدمات آزمایش، ابتدا جدول xmlInvoice را با یک فیلد XML ایی untyped درنظر بگیرید:
CREATE TABLE xmlInvoice ( invoiceId INT IDENTITY PRIMARY KEY, invoice XML )
INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1000" dept="hardware"> <CustomerName>Vahid</CustomerName> <LineItems> <LineItem><Description>Gear</Description><Price>9.5</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1002" dept="garden"> <CustomerName>Mehdi</CustomerName> <LineItems> <LineItem><Description>Shovel</Description><Price>19.2</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1003" dept="garden"> <CustomerName>Mohsen</CustomerName> <LineItems> <LineItem><Description>Trellis</Description><Price>8.5</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1004" dept="hardware"> <CustomerName>Hamid</CustomerName> <LineItems> <LineItem><Description>Pen</Description><Price>1.5</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1005" dept="IT"> <CustomerName>Ali</CustomerName> <LineItems> <LineItem><Description>Book</Description><Price>3.2</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1006" dept="hardware"> <CustomerName>Reza</CustomerName> <LineItems> <LineItem><Description>M.Board</Description><Price>19.5</Price></LineItem> </LineItems> </Invoice> ')
CREATE XML SCHEMA COLLECTION invoice_xsd AS ' <xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Invoice"> <xs:complexType> <xs:sequence> <xs:element name="CustomerName" type="xs:string" /> <xs:element name="LineItems"> <xs:complexType> <xs:sequence> <xs:element name="LineItem"> <xs:complexType> <xs:sequence> <xs:element name="Description" type="xs:string" /> <xs:element name="Price" type="xs:decimal" /> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="InvoiceId" type="xs:unsignedShort" use="required" /> <xs:attribute name="dept" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:schema>' Go CREATE TABLE xmlInvoice2 ( invoiceId INT IDENTITY PRIMARY KEY, invoice XML(document invoice_xsd) ) Go
در این جدول دوم، حالت پیش فرض content قبلی، به document تغییر کردهاست. با توجه به اینکه میدانیم اسناد ما چه فرمتی دارند و بیش از یک root element نخواهیم داشت، انتخاب document سبب خواهد شد تا Query Plan بهتری حاصل شود.
در ادامه برای مشاهدهی بهتر نتایج، کش Query Plan و اطلاعات آماری جدول xmlInvoice را حذف و به روز میکنیم:
UPDATE STATISTICS xmlInvoice DBCC FREEPROCCACHE
کوئریهایی را که در این قسمت بررسی خواهیم کرد، در ادامه ملاحظه میکنید. بار اول این کوئریها را بر روی xmlInvoice و بار دوم، بر روی نگارش دوم دارای اسکیمای آن اجرا خواهیم کرد:
-- query 1 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice[@InvoiceId = "1003"]') = 1 -- query 2 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice/@InvoiceId[. = "1003"]') = 1 -- query 3 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice[1]/@InvoiceId[. = "1003"]') = 1 -- query 4 SELECT * FROM xmlInvoice WHERE invoice.exist('(/Invoice/@InvoiceId)[1][. = "1003"]') = 1 -- query 5 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice[CustomerName = "Vahid"]') = 1 -- query 6 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice/CustomerName [.= "Vahid"]') = 1 -- query 7 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice/LineItems/LineItem[Description = "Trellis"]') = 1 -- query 8 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice/LineItems/LineItem/Description [.= "Trellis"]') = 1 -- query 9 SELECT * FROM xmlInvoice WHERE invoice.exist(' for $x in /Invoice/@InvoiceId where $x = 1003 return $x ') = 1 -- query 10 SELECT * FROM xmlInvoice WHERE invoice.value('(/Invoice/@InvoiceId)[1]', 'VARCHAR(10)') = '1003' -- یکبار هم با جدول شماره 2 که اسکیما دارد تمام این موارد تکرار شود UPDATE STATISTICS xmlInvoice DBCC FREEPROCCACHE GO
کوئری 1
همانطور که عنوان شد، از منوی Query گزینهی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمههای Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده کرد.
در کوئری 1، با استفاده از متد exist به دنبال رکوردهایی هستیم که دارای ویژگی InvoiceId مساوی 1003 هستند. پس از اجرای کوئری، تصویر Query Plan آن به شکل زیر خواهد بود:
برای خواندن این تصویر، از بالا به پایین و چپ به راست باید عمل شود. هزینهی انجام کوئری را نیز با نگه داشتن کرسر ماوس بر روی select نهایی سمت چپ تصویر میتوان مشاهده کرد. البته باید درنظر داشت که این اعداد از دیدگاه Query Processor مفهوم پیدا میکنند. پردازشگر کوئری، بر اساس اطلاعاتی که در اختیار دارد، سعی میکند بهترین روش پردازش کوئری دریافتی را پیدا کند. برای اندازه گیری کارآیی، باید اندازه گیری زمان اجرای کوئری، مستقلا انجام شود.
در این کوئری، مطابق تصویر اول، ابتدا قسمت SQL آن (چپ بالای تصویر) پردازش میشود و سپس قسمت XML آن. قسمت XQuery این عبارت در دو قسمت سمت چپ، پایین تصویر مشخص شدهاند. Table valued functionها جاهایی هستند که node table ابتدای بحث جاری در آنها ساخته میشوند. در اینجا دو مرحلهی تولید Table valued functionها مشاهده میشود. اگر به جمع درصدهای آنها دقت کنید، هزینهی این دو قسمت، 98 درصد کل Query plan است.
سؤال: چرا دو مرحلهی تولید Table valued functionها در اینجا قابل مشاهده است؟ یک مرحلهی آن مربوط است به انتخاب نود Invoice و مرحلهی دوم مربوط است به فیلتر داخل [] ذکر شد برای یافتن ویژگیهای مساوی 1003.
در اینجا و در کوئریهای بعدی، هر Query Plan ایی که تعداد مراحل تولید Table valued function کمتری داشته باشد، بهینهتر است.
کوئری 5
اگر کوئری پلن شماره 5 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید. یک XML Reader برای خارج از [] (اصطلاحا به آن predicate گفته میشود) و دو مورد برای داخل [] تشکیل شدهاست؛ یکی برای انتخاب نود متنی و دیگری برای تساوی.
کوئری 7
اگر کوئری پلن شماره 7 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید که بسیار شبیه است به مورد 5. بنابراین در اینجا عمق بررسی و سلسله مراتب اهمیتی ندارد.
کوئری 9
کوئری 9 دقیقا معادل است با کوئری 1 نوشته شده؛ با این تفاوت که از روش FLOWR استفاده کردهاست. نکتهی جالب آن، وجود تنها یک XML reader در Query plan آن است که باید آنرا بخاطر داشت.
کوئری 2
کوئری 3
کوئری 4
کوئری 6
کوئری 8
اگر به این 5 کوئری یاد شده دقت کنید، از یک دات به معنای self استفاده کردهاند (یعنی پردازش بیشتری را انجام نده و از همین نود جاری برای پردازش نهایی استفاده کن). با توجه به بکارگیری متد exist، معنای کوئریهای یک و دو، یکیاست. اما در کوئری شماره 2، تنها یک XML Reader در Query plan نهایی وجود دارد (همانند عبارت FLOWR کوئری شماره 9).
یک نکته: اگر میخواهید بدانید بین کوئریهای 1 و 2 کدامیک بهتر عمل میکنند، از بین تمام کوئریهای موجود، دو کوئری یاد شده را انتخاب کرده و سپس با فرض روش بودن نمایش Query plan، هر دو کوئری را با هم اجرا کنید.
در این حالت، کوئری پلنهای هر دو کوئری را با هم یکجا میتوان مشاهده کرد؛ به علاوهی هزینهی نسبی آنها را در کل عملیات صورت گرفته. در حالت استفاده از دات و وجود تنها یک XML Reader، این هزینه تنها 6 درصد است، در مقابل هزینهی 94 درصدی کوئری شماره یک.
بنابراین از دیدگاه پردازشگر کوئریهای SQL Server، کوئری شماره 2، بسیار بهتر است از کوئری شماره 1.
در کوئریهای 3 و 4، شماره نود مدنظر را دقیقا مشخص کردهایم. این مورد در حالت سوم تفاوت محسوسی را از لحاظ کارآیی ایجاد نمیکند و حتی کارآیی را به علت اضافه کردن یک XML Reader دیگر برای پردازش عدد نود وارد شده، کاهش میدهد. اما کوئری 4 که عدد اولین نود را خارج از پرانتز قرار دادهاست، تنها در کل یک XML Reader را به همراه خواهد داشت.
سؤال: بین کوئریهای 2، 3 و 4 کدامیک بهینهتر است؟
بله. اگر هر سه کوئری را با هم انتخاب کرده و اجرا کنیم، میتوان در قسمت کوئری پلنها، هزینهی هر کدام را نسبت به کل مشاهده کرد. در این حالت کوئری 4 بهتر است از کوئری 2 و تنها یک درصد هزینهی کل را تشکیل میدهد.
کوئری 10
کوئری 10 اندکی متفاوت است نسبت به کوئریهای دیگر. در اینجا بجای متد exist از متد value استفاده شدهاست. یعنی ابتدا صریحا مقدار ویژگی InvoiceId استخراج شده و با 1003 مقایسه میشود.
اگر کوئری پلن آنرا با کوئری 4 که بهترین کوئری سری exist است مقایسه کنیم، کوئری 10، هزینهی 70 درصدی کل عملیات را به خود اختصاص خواهد داد، در مقابل 30 درصد هزینهی کوئری 4. بنابراین در این موارد، استفاده از متد exist بسیار بهینهتر است از متد value.
استفاده از Schema collection و تاثیر آن بر کارآیی
تمام مراحلی را که در اینجا ملاحظه کردید، صرفا با تغییر نام xmlInvoice به xmlInvoice2، تکرار کنید. xmlInvoice2 دارای ساختاری مشخص است، به همراه ذکر صریح document حین تعریف ستون XML ایی آن.
تمام پاسخهایی را که دریافت خواهید کرد با حالت بدون Schema collection یکی است.
برای مقایسه بهتر، یکبار نیز سعی کنید کوئری 1 جدول xmlInvoice را با کوئری 1 جدول xmlInvoice2 با هم در طی یک اجرا مقایسه کنید، تا بهتر بتوان Query plan نسبی آنها را بررسی کرد.
پس از این بررسی و مقایسه، به این نتیجه خواهید رسید که تفاوت محسوسی در اینجا و بین این دو حالت، قابل ملاحظه نیست. در SQL Server از Schema collection بیشتر برای اعتبارسنجی ورودیها استفاده میشود تا بهبود کارآیی کوئریها.
بنابراین به صورت خلاصه
- متد exist را به value ترجیح دهید.
- اصطلاحا ordinal (همان مشخص کردن نود 1 در اینجا) را در آخر قرار دهید (نه در بین نودها).
- مراحل اجرایی را با معرفی دات (استفاده از نود جاری) تا حد ممکن کاهش دهید.
و ... کوئری 4 در این سری، بهترین کارآیی را ارائه میدهد.
روش بهینه سازی شده تنظیم پایگاه داده در نسخه ۲.۱.۰
Database Initializer
Database
Initializer همانطور که از نامش پیداست، به شما کمک میکند پایگاه داده
پَرباد را به هر صورتیکه در نظر دارید راه اندازی کنید.
هدف از افزودن Database Initializer ها
از آنجایی که Entity Framework، پایگاههای داده زیادی را پشتیبانی میکند و
هر کدام از آنها دارای شرایط، ویژگیها و محدودیت های خاص خود
هستند، شما
میتوانید پایگاه داده مورد نظر خود را آنطور که در نظر دارید ایجاد کنید.
برای مثال پایگاه داده Sqlite طبق اعلام خود تیم Entity Framework دارای
محدودیتهایی برای Migration است. به همین دلیل این پایگاه داده
را نمیتوان مانند SQL Server به راحتی Migrate کرد.
مواردی که توسط Database Initializerها قابل استفاده هستند شامل:
- Create Database
- Delete Database
- Migrate Database
- Seed Database
نمونه مثال جهت تنظیم پایگاه داده به روش جدید:
services.AddParbad() .ConfigureDatabase(builder => { // Choose your database provider (SQL Server, MySql, Sqlite, etc.) builder.Use.... }) .ConfigureDatabaseInitializer(builder => { builder.UseInitializer(async context => { await context.Database.EnsureDeletedAsync(); // OR await context.Database.EnsureCreatedAsync(); // OR await context.Database.MigrateAsync(); }); });
در مثال بالا ابتدا در متد ConfigureDatabase نوع پایگاه داده مورد نظر خود را مشخص میکنیم.
سپس
در متد ConfigureDatabaseInitializer تعیین میکنیم که پایگاه داده به چه
صورتی باید ایجاد شود. همانطور که مشاهده میکنید، شما میتوانید پایگاه
داده را حذف، ایجاد و یا Migrate کنید.
همچنین جهت نوشتن راحتتر تنظیمات، متدهایی برای ایجاد، حذف و Migrate محیا شده که میتوانید مانند زیر از آنها استفاده کنید:
services.AddParbad() .ConfigureDatabase(builder => { // Choose your Entity Framework provider (SQL Server, MySql, Sqlite, etc.) builder.Use.... }) .ConfigureDatabaseInitializer(builder => { builder.CreateDatabase(); // OR builder.DeleteAndCreateDatabase(); // OR builder.CreateAndMigrateDatabase(); });
نکته ۱: اگر هیچکدام از متدهای کمکی بالا برای شما مناسب نیستند، از همان روش اول یعنی UseInitializer استفاده کنید.
نکته ۲: Database Initializer ها، به همان ترتیبی که اضافه شدهاند، اجرا خواهند شد.
نکته ۳:
جهت سادگی در Migrate کردن پایگاه داده، در هنگام تنظیم پایگاه داده مورد
نظر خود میتوانید از متد UseParbadMigrations استفاده کنید.
مثال:
services.AddParbad() .ConfigureDatabase(builder => { // SQL Server builder.UseSqlServer("Connection String", options => options.UseParbadMigrations()); }) .ConfigureDatabaseInitializer(builder => { builder.CreateAndMigrateDatabase(); });
متد UseParbadMigrations معادل متد زیر است:
builder.UseSqlServer("Connection String", options => options.MigrationsAssembly("Parbad"));
نمونه مثالها را همچنین میتوانید در صفحه GitHub پروژه مشاهده کنید.
نظرات اشتراکها
Bulk delete و Bulk update در Entity framework
ارجاع به ستونهای دیگر را دو سالی هست که اضافه کردهاند. سایر درخواستها را هم در issue tracker آنها ارسال کنید.
اشتراکها