با سلام و تشکر مثل همیشه عالی
پس از برپایی تنظیمات اولیهی کار با EF Core در ASP.NET Core، اکنون نوبت به تبدیل کلاس Person، به جدول معادل آن در بانک اطلاعاتی برنامه است. در EF Core نیز همانند EF Code First 6.x، برای انجام یک چنین اعمالی از مفهومی به نام Migrations استفاده میشود که در ادامه به آن خواهیم پرداخت.
پیشنیازهای کار با EF Core Migrations
در قسمت قبل در حین بررسی «برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است.
بررسی ابزارهای خط فرمان EF Core و تشکیل ساختار بانک اطلاعاتی بر اساس کلاسهای برنامه
پس از تکمیل پیشنیازهای کار با EF Core، از طریق خط فرمان به پوشهی جاری پروژه وارد شده و دستور dotnet ef را صادر کنید.
یک نکته: در ویندوز اگر در پوشهای، کلید shift را نگه دارید و در آن پوشه کلیک راست کنید، در منوی باز شده، گزینهی جدیدی را به نام Open command window here مشاهده خواهید کرد که میتواند به سرعت خط فرمان را از پوشهی جاری شروع کند.
خروجی صدور فرمان dotnet ef را در ذیل مشاهده میکنید:
در قسمت Commands آن در انتهای لیست، از فرمان migrations آن استفاده خواهیم کرد. برای این منظور در همین پوشهی جاری، دستور ذیل را صادر کنید:
دستورات migrations با dotnet ef migrations شروع شده و سپس یک سری پارامتر را دریافت میکنند برای مثال در اینجا سوئیچ add، به همراه یک نام دلخواه ذکر شدهاست (نام این مرحله را InitialDatabase گذاشتهایم). پس از فراخوانی این دستور، اگر به Solution explorer مراجعه کنید، پوشهی جدید Migrations، قابل مشاهده است:
نام دلخواه InitialDatabase را در انتهای نام فایل 13950526050417_InitialDatabase مشاهده میکنید.
اگر قصد حذف این مرحله را داشته باشیم، میتوان دستور dotnet ef migrations remove را مجددا صادر کرد.
فایل 13950526050417_InitialDatabase به همراه کلاسی است که در آن دو متد Up و Down قابل مشاهده هستند. متد Up نحوهی ایجاد جدول جدیدی را از کلاس Person بیان میکند و متد Down نحوهی Drop این جدول را پیاده سازی کردهاست.
فایل ApplicationDbContextModelSnapshot.cs دارای کلاسی است که خلاصهای از تعاریف موجودیتهای ذکر شدهی در DB Context برنامه را به همراه دارد و تفسیر آنها را از دیدگاه EF در اینجا میتوان مشاهده کرد.
پس از مرحلهی افزودن migrations، نوبت به اعمال آن به بانک اطلاعاتی است. تا اینجا EF تنها متدهای Up و Down مربوط به ساخت و حذف ساختار جداول را ایجاد کردهاست. اما هنوز آنها را به بانک اطلاعاتی برنامه اعمال نکردهاست. برای اینکار در پوشهی جاری دستور ذیل را صادر کنید:
همانطور که ملاحظه میکنید، دستور dotnet ef database update سبب اعمال اطلاعات فایل 13950526050417_InitialDatabase به بانک اطلاعاتی شدهاست.
اکنون اگر به لیست بانکهای اطلاعاتی مراجعه کنیم، بانک اطلاعاتی جدید TestDbCore2016 را به همراه جدول متناظر کلاس Person میتوان مشاهده کرد:
در اینجا جدول دیگری به نام __EFMigrationsHistory نیز قابل مشاهدهاست که کار آن ذخیره سازی وضعیت فعلی Migrations در بانک اطلاعاتی، جهت مقایسههای آتی است. این جدول صرفا توسط ابزارهای EF استفاده میشود و نباید به صورت مستقیم تغییری در آن ایجاد کنید.
مقدار دهی اولیهی جداول بانکهای اطلاعاتی در EF Core
در همین حالت اگر کنترلر TestDB مطرح شدهی در انتهای بحث قسمت قبل را اجرا کنیم، به این استثناء خواهیم رسید:
این تصویر بدین معنا است که کار Migrations موفقیت آمیز بودهاست و اینبار امکان اتصال و کار با بانک اطلاعاتی وجود دارد، اما این جدول حاوی اطلاعات اولیهای برای نمایش نیست.
در نگارش قبلی EF Code First، امکانات Migrations به همراه یک متد Seed نیز بود که توسط آن کار مقدار دهی اولیهی جداول را میتوان انجام داد (زمانیکه جدولی ایجاد میشود، در همان هنگام، چند رکورد خاص نیز به آن اضافه شوند. برای مثال به جدول کاربران، رکورد اولین کاربر یا همان Admin اضافه شود). این متد در EF Core 1.0 وجود ندارد.
برای این منظور کلاس جدیدی را به نام ApplicationDbContextSeedData به همان پوشهی جدید Migrations اضافه کنید؛ با این محتوا:
و سپس نحوهی فراخوانی آن در متد Configure کلاس آغازین برنامه به صورت زیر است:
به همراه این تغییر در نحوهی معرفی Db Context برنامه:
توضیحات:
- برای پیاده سازی الگوی واحد کار، اولین قدم، مشخص سازی طول عمر Db Context برنامه است. برای اینکه تنها یک Context در طول یک درخواست وهله سازی شود، نیاز است به نحو صریحی طول عمر آنرا به حالت Scoped تنظیم کرد. متد AddDbContext دارای پارامتری است که این طول عمر را دریافت میکند. بنابراین در اینجا ServiceLifetime.Scoped ذکر شدهاست. همچنین در این مثال از نمونهای که IConfigurationRoot به سازندهی کلاس ApplicationDbContext تزریق شده (نکتهی انتهای بحث قسمت قبل)، استفاده شدهاست. به همین جهت تنظیمات options آنرا ملاحظه نمیکنید.
- مرحلهی بعد نحوهی دسترسی به این سرویس ثبت شده در یک کلاس static دارای متدی الحاقی است. در اینجا دیگر دسترسی مستقیمی به تزریق وابستگیها نداریم و باید کار را با IServiceScopeFactory شروع کنیم. در اینجا میتوانیم به صورت دستی یک Scope را ایجاد کرده، سپس توسط ServiceProvider آن، به سرویس ApplicationDbContext دسترسی پیدا کنیم و در ادامه از آن به نحو متداولی استفاده نمائیم. IServiceScopeFactory جزو سرویسهای توکار ASP.NET Core است و در صورت ذکر آن به عنوان پارامتر جدیدی در متد Configure، به صورت خودکار وهله سازی شده و در اختیار ما قرار میگیرد.
- نکتهی مهمی که در اینجا بکار رفتهاست، ایجاد Scope و dispose خودکار آن توسط عبارت using است. باید دقت داشت که ایجاد Scope و تخریب آن به صورت خودکار در ابتدا و انتهای درخواستها توسط ASP.NET Core انجام میشود. اما چون شروع کار ما از متد Configure است، در اینجا خارج از Scope قرار داریم و باید مدیریت ایجاد و تخریب آنرا به صورت دستی انجام دهیم که نمونهای از آنرا در متد SeedData کلاس ApplicationDbContextSeedData ملاحظه میکنید. در اینجا Scope ایی ایجاد شدهاست. سپس دادههای اولیهی مدنظر به بانک اطلاعاتی اضافه گردیده و در آخر این Scope تخریب شدهاست.
- اگر کار ایجاد و تخریب scope، به نحوی که مشخص شدهاست انجام نگیرد، طول عمر درخواستی خارج از Scope، همواره Singleton خواهد بود. چون خارج از طول عمر درخواست جاری قرار داریم و هنوز کار به سرویس دهی درخواستها نرسیدهاست. بنابراین مدیریت Scopeها هنوز شروع نشدهاست و باید به صورت دستی انجام شود.
در این حالت اگر برنامه را اجرا کنیم، این خروجی قابل مشاهده است:
که به معنای کار کردن متد SeedData و ثبت اطلاعات اولیهای در بانک اطلاعاتی است.
اعمال تغییرات به مدلهای برنامه و به روز رسانی ساختار بانک اطلاعاتی
فرض کنید به کلاس Person قسمت قبل، خاصیت Age را هم اضافه کردهایم:
در این حالت اگر برنامه را اجرا کنیم، به استثنای ذیل خواهیم رسید:
برای رفع این مشکل نیاز است مجددا مراحل Migrations را اجرا کرد:
در اینجا همان دستورات قبل را مجددا اجرا میکنیم. با این تفاوت که اینبار نام دلخواه این مرحله را مثلا v2، به معنای نگارش دوم وارد کردهایم.
با اجرا این دستورات، فایل جدید 13950526073248_v2 به پوشهی Migrations اضافه میشود. این فایل حاوی نحوهی به روز رسانی بانک اطلاعاتی، بر اساس خاصیت جدید Age است. سپس با اجرای دستور dotnet ef database update، کار به روز رسانی بانک اطلاعاتی بر اساس مرحلهی v2 انجام میشود.
بنابراین هر بار که تغییری را در مدلهای خود ایجاد میکنید، یکبار باید کلاس مهاجرت آنرا ایجاد کنید و سپس آنرا به بانک اطلاعاتی اعمال نمائید.
تهیه اسکریپت تغییرات بجای اعمال تغییرات توسط ابزارهای EF
شاید علاقمند باشید که پیش از اعمال تغییرات به بانک اطلاعاتی، یک اسکریپت SQL از آن تهیه کنید (جهت مطالعه و یا اعمال دستی آن توسط خودتان). برای اینکار میتوانید دستور ذیل را در پوشهی جاری پروژه اجرا کنید:
در این حالت اسکریپت SQL تغییرات، در فایلی به نام v2.sql، در ریشهی جاری پروژه تولید میشود.
تغییرات ساختار جدول __EFMigrationsHistory در EF Core 1.0
در EF 6.x، ساختار اطلاعات جدول نگهداری تاریخچهی تغییرات، بسیار پیچیده بود و شامل رشتهای gzip شدهی حاوی یک snapshot از کل ساختار دیتابیس در هر مرحلهی migration بود. در این نگارش، این snapshot حذف شدهاست و بجای آن فایل ApplicationDbContextModelSnapshot.cs را مشاهده میکنید (تنها یک snapshot به ازای کل context برنامه). همچنین در اینجا کاملا مشخص است که چه مراحلی به بانک اطلاعاتی اعمال شدهاند و دیگر خبری از رشتهی gzip شدهی قبلی نیست (تصویر فوق).
در شکل زیر ساختار قبلی این جدول را در EF 6.x مشاهده میکنید. در EF 6.x حتی فضای نام کلاسهای موجودیتهای برنامه هم مهم هستند و در صورت تغییر، مشکل ایجاد میشود:
مهاجرت خودکار از EF Core حذف شدهاست
در EF 6.x در کنار کلاس Db Context یک کلاس Configuration هم وجود داشت که برای مثال امکان چنین تعریفی در آن میسر هست:
کار آن مهاجرت خودکار اطلاعات context به بانک اطلاعاتی بود؛ بدون نیازی به استفاده از دستورات خط فرمان مرتبط. تمام این موارد از EF Core حذف شدهاند و علت آنرا میتوانید در توضیحات یکی از اعضای تیم EF Core در اینجا مطالعه کنید و خلاصهی آن به این شرح است:
با حذف مهاجرت خودکار:
- دیگر نیازی نیست تا model snapshots در بانک اطلاعاتی ذخیره شوند (همان ساده شدن ساختار جدول ذخیره سازی تاریخچهی مهاجرتهای فوق).
- در حالت افزودن یک مرحلهی مهاجرت، دیگر نیازی به کوئری گرفتن از بانک اطلاعاتی نخواهد بود (سرعت بیشتر).
- میتوان چندین مرحلهی مهاجرت را افزود بدون اینکه الزاما مجبور به اعمال آنها به بانک اطلاعاتی باشیم.
- کاهش کدهای مدیریت ساختار بانک اطلاعاتی.
- تیمها برای یکی کردن تغییرات خود مشکلی نخواهند داشت چون دیگر snapshot مدلها در جدول __EFMigrationsHistory ذخیره نمیشود.
بنابراین در EF Core میتوان مهاجرت v1 را اضافه کرد. سپس تغییراتی را در کدها اعمال کرد. در ادامه مهاجرت v2 را تولید کرد و در آخر کار اعمال یکجای اینها را به بانک اطلاعاتی انجام داد.
هرچند در اینجا اگر میخواهید مرحلهی اجرای دستور dotnet ef database update را حذف کنید، میتوانید از کدهای ذیل نیز استفاده نمائید:
روش فراخوانی آن نیز همانند روش فراخوانی متد SeedData است که پیشتر بحث شد.
کار متد Migrate، ایجاد بانک اطلاعاتی در صورت عدم وجود و سپس اعمال تمام مراحل migration ایی است که در جدول __EFMigrationsHistory ثبت نشدهاند (دقیقا همان کار دستور dotnet ef database update را انجام میدهد).
تفاوت متد Database.EnsureCreated با متد Database.Migrate
اگر به متدهای context.Database دقت کنید، یکی از آنها EnsureCreated نام دارد. این متد نیز سبب تولید بانک اطلاعاتی بر اساس ساختار Context برنامه میشود. اما هدف آن صرفا استفادهی از آن در آزمونهای واحد سریع است. از این جهت که جدول __EFMigrationsHistory را تولید نمیکند (برخلاف متد Migrate). بنابراین بجز آزمونهای واحد، در جای دیگری از آن استفاده نکنید چون به دلیل عدم تولید جدول __EFMigrationsHistory توسط آن، قابلیت استفادهی از بانک اطلاعاتی تولید شدهی توسط آن با امکانات migrations وجود ندارد. در پایان آزمون واحد نیز میتوان از متد EnsureDeleted برای حذف این بانک اطلاعاتی موقتی استفاده کرد.
در قسمت بعد، مطالب تکمیلی مهاجرتها را بررسی خواهیم کرد. برای مثال چگونه میتوان کلاسهای موجودیتها را به اسمبلیهای دیگری منتقل کرد.
پیشنیازهای کار با EF Core Migrations
در قسمت قبل در حین بررسی «برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است.
بررسی ابزارهای خط فرمان EF Core و تشکیل ساختار بانک اطلاعاتی بر اساس کلاسهای برنامه
پس از تکمیل پیشنیازهای کار با EF Core، از طریق خط فرمان به پوشهی جاری پروژه وارد شده و دستور dotnet ef را صادر کنید.
یک نکته: در ویندوز اگر در پوشهای، کلید shift را نگه دارید و در آن پوشه کلیک راست کنید، در منوی باز شده، گزینهی جدیدی را به نام Open command window here مشاهده خواهید کرد که میتواند به سرعت خط فرمان را از پوشهی جاری شروع کند.
خروجی صدور فرمان dotnet ef را در ذیل مشاهده میکنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef _/\__ ---==/ \\ ___ ___ |. \|\ | __|| __| | ) \\\ | _| | _| \_/ | //|\\ |___||_| / \\\/\\ Entity Framework .NET Core CLI Commands 1.0.0-preview2-21431 Usage: dotnet ef [options] [command] Options: -h|--help Show help information -v|--verbose Enable verbose output --version Show version information --assembly <ASSEMBLY> The assembly file to load. --startup-assembly <ASSEMBLY> The assembly file containing the startup class. --data-dir <DIR> The folder used as the data directory (defaults to current working directory). --project-dir <DIR> The folder used as the project directory (defaults to current working directory). --content-root-path <DIR> The folder used as the content root path for the application (defaults to application base directory). --root-namespace <NAMESPACE> The root namespace of the target project (defaults to the project assembly name). Commands: database Commands to manage your database dbcontext Commands to manage your DbContext types migrations Commands to manage your migrations Use "dotnet ef [command] --help" for more information about a command.
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase
نام دلخواه InitialDatabase را در انتهای نام فایل 13950526050417_InitialDatabase مشاهده میکنید.
اگر قصد حذف این مرحله را داشته باشیم، میتوان دستور dotnet ef migrations remove را مجددا صادر کرد.
فایل 13950526050417_InitialDatabase به همراه کلاسی است که در آن دو متد Up و Down قابل مشاهده هستند. متد Up نحوهی ایجاد جدول جدیدی را از کلاس Person بیان میکند و متد Down نحوهی Drop این جدول را پیاده سازی کردهاست.
فایل ApplicationDbContextModelSnapshot.cs دارای کلاسی است که خلاصهای از تعاریف موجودیتهای ذکر شدهی در DB Context برنامه را به همراه دارد و تفسیر آنها را از دیدگاه EF در اینجا میتوان مشاهده کرد.
پس از مرحلهی افزودن migrations، نوبت به اعمال آن به بانک اطلاعاتی است. تا اینجا EF تنها متدهای Up و Down مربوط به ساخت و حذف ساختار جداول را ایجاد کردهاست. اما هنوز آنها را به بانک اطلاعاتی برنامه اعمال نکردهاست. برای اینکار در پوشهی جاری دستور ذیل را صادر کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update Applying migration '13950526050417_InitialDatabase'. Done.
اکنون اگر به لیست بانکهای اطلاعاتی مراجعه کنیم، بانک اطلاعاتی جدید TestDbCore2016 را به همراه جدول متناظر کلاس Person میتوان مشاهده کرد:
در اینجا جدول دیگری به نام __EFMigrationsHistory نیز قابل مشاهدهاست که کار آن ذخیره سازی وضعیت فعلی Migrations در بانک اطلاعاتی، جهت مقایسههای آتی است. این جدول صرفا توسط ابزارهای EF استفاده میشود و نباید به صورت مستقیم تغییری در آن ایجاد کنید.
مقدار دهی اولیهی جداول بانکهای اطلاعاتی در EF Core
در همین حالت اگر کنترلر TestDB مطرح شدهی در انتهای بحث قسمت قبل را اجرا کنیم، به این استثناء خواهیم رسید:
این تصویر بدین معنا است که کار Migrations موفقیت آمیز بودهاست و اینبار امکان اتصال و کار با بانک اطلاعاتی وجود دارد، اما این جدول حاوی اطلاعات اولیهای برای نمایش نیست.
در نگارش قبلی EF Code First، امکانات Migrations به همراه یک متد Seed نیز بود که توسط آن کار مقدار دهی اولیهی جداول را میتوان انجام داد (زمانیکه جدولی ایجاد میشود، در همان هنگام، چند رکورد خاص نیز به آن اضافه شوند. برای مثال به جدول کاربران، رکورد اولین کاربر یا همان Admin اضافه شود). این متد در EF Core 1.0 وجود ندارد.
برای این منظور کلاس جدیدی را به نام ApplicationDbContextSeedData به همان پوشهی جدید Migrations اضافه کنید؛ با این محتوا:
using System.Collections.Generic; using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.Extensions.DependencyInjection; namespace Core1RtmEmptyTest.Migrations { public static class ApplicationDbContextSeedData { public static void SeedData(this IServiceScopeFactory scopeFactory) { using (var serviceScope = scopeFactory.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>(); if (!context.Persons.Any()) { var persons = new List<Person> { new Person { FirstName = "Admin", LastName = "User" } }; context.AddRange(persons); context.SaveChanges(); } } } } }
public void Configure(IServiceScopeFactory scopeFactory) { scopeFactory.SeedData();
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
- برای پیاده سازی الگوی واحد کار، اولین قدم، مشخص سازی طول عمر Db Context برنامه است. برای اینکه تنها یک Context در طول یک درخواست وهله سازی شود، نیاز است به نحو صریحی طول عمر آنرا به حالت Scoped تنظیم کرد. متد AddDbContext دارای پارامتری است که این طول عمر را دریافت میکند. بنابراین در اینجا ServiceLifetime.Scoped ذکر شدهاست. همچنین در این مثال از نمونهای که IConfigurationRoot به سازندهی کلاس ApplicationDbContext تزریق شده (نکتهی انتهای بحث قسمت قبل)، استفاده شدهاست. به همین جهت تنظیمات options آنرا ملاحظه نمیکنید.
- مرحلهی بعد نحوهی دسترسی به این سرویس ثبت شده در یک کلاس static دارای متدی الحاقی است. در اینجا دیگر دسترسی مستقیمی به تزریق وابستگیها نداریم و باید کار را با IServiceScopeFactory شروع کنیم. در اینجا میتوانیم به صورت دستی یک Scope را ایجاد کرده، سپس توسط ServiceProvider آن، به سرویس ApplicationDbContext دسترسی پیدا کنیم و در ادامه از آن به نحو متداولی استفاده نمائیم. IServiceScopeFactory جزو سرویسهای توکار ASP.NET Core است و در صورت ذکر آن به عنوان پارامتر جدیدی در متد Configure، به صورت خودکار وهله سازی شده و در اختیار ما قرار میگیرد.
- نکتهی مهمی که در اینجا بکار رفتهاست، ایجاد Scope و dispose خودکار آن توسط عبارت using است. باید دقت داشت که ایجاد Scope و تخریب آن به صورت خودکار در ابتدا و انتهای درخواستها توسط ASP.NET Core انجام میشود. اما چون شروع کار ما از متد Configure است، در اینجا خارج از Scope قرار داریم و باید مدیریت ایجاد و تخریب آنرا به صورت دستی انجام دهیم که نمونهای از آنرا در متد SeedData کلاس ApplicationDbContextSeedData ملاحظه میکنید. در اینجا Scope ایی ایجاد شدهاست. سپس دادههای اولیهی مدنظر به بانک اطلاعاتی اضافه گردیده و در آخر این Scope تخریب شدهاست.
- اگر کار ایجاد و تخریب scope، به نحوی که مشخص شدهاست انجام نگیرد، طول عمر درخواستی خارج از Scope، همواره Singleton خواهد بود. چون خارج از طول عمر درخواست جاری قرار داریم و هنوز کار به سرویس دهی درخواستها نرسیدهاست. بنابراین مدیریت Scopeها هنوز شروع نشدهاست و باید به صورت دستی انجام شود.
در این حالت اگر برنامه را اجرا کنیم، این خروجی قابل مشاهده است:
که به معنای کار کردن متد SeedData و ثبت اطلاعات اولیهای در بانک اطلاعاتی است.
اعمال تغییرات به مدلهای برنامه و به روز رسانی ساختار بانک اطلاعاتی
فرض کنید به کلاس Person قسمت قبل، خاصیت Age را هم اضافه کردهایم:
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } } }
An unhandled exception occurred while processing the request. SqlException: Invalid column name 'Age'.
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add v2 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
با اجرا این دستورات، فایل جدید 13950526073248_v2 به پوشهی Migrations اضافه میشود. این فایل حاوی نحوهی به روز رسانی بانک اطلاعاتی، بر اساس خاصیت جدید Age است. سپس با اجرای دستور dotnet ef database update، کار به روز رسانی بانک اطلاعاتی بر اساس مرحلهی v2 انجام میشود.
بنابراین هر بار که تغییری را در مدلهای خود ایجاد میکنید، یکبار باید کلاس مهاجرت آنرا ایجاد کنید و سپس آنرا به بانک اطلاعاتی اعمال نمائید.
تهیه اسکریپت تغییرات بجای اعمال تغییرات توسط ابزارهای EF
شاید علاقمند باشید که پیش از اعمال تغییرات به بانک اطلاعاتی، یک اسکریپت SQL از آن تهیه کنید (جهت مطالعه و یا اعمال دستی آن توسط خودتان). برای اینکار میتوانید دستور ذیل را در پوشهی جاری پروژه اجرا کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations script -o v2.sql
تغییرات ساختار جدول __EFMigrationsHistory در EF Core 1.0
در EF 6.x، ساختار اطلاعات جدول نگهداری تاریخچهی تغییرات، بسیار پیچیده بود و شامل رشتهای gzip شدهی حاوی یک snapshot از کل ساختار دیتابیس در هر مرحلهی migration بود. در این نگارش، این snapshot حذف شدهاست و بجای آن فایل ApplicationDbContextModelSnapshot.cs را مشاهده میکنید (تنها یک snapshot به ازای کل context برنامه). همچنین در اینجا کاملا مشخص است که چه مراحلی به بانک اطلاعاتی اعمال شدهاند و دیگر خبری از رشتهی gzip شدهی قبلی نیست (تصویر فوق).
در شکل زیر ساختار قبلی این جدول را در EF 6.x مشاهده میکنید. در EF 6.x حتی فضای نام کلاسهای موجودیتهای برنامه هم مهم هستند و در صورت تغییر، مشکل ایجاد میشود:
مهاجرت خودکار از EF Core حذف شدهاست
در EF 6.x در کنار کلاس Db Context یک کلاس Configuration هم وجود داشت که برای مثال امکان چنین تعریفی در آن میسر هست:
public Configuration() { AutomaticMigrationsEnabled = true; }
با حذف مهاجرت خودکار:
- دیگر نیازی نیست تا model snapshots در بانک اطلاعاتی ذخیره شوند (همان ساده شدن ساختار جدول ذخیره سازی تاریخچهی مهاجرتهای فوق).
- در حالت افزودن یک مرحلهی مهاجرت، دیگر نیازی به کوئری گرفتن از بانک اطلاعاتی نخواهد بود (سرعت بیشتر).
- میتوان چندین مرحلهی مهاجرت را افزود بدون اینکه الزاما مجبور به اعمال آنها به بانک اطلاعاتی باشیم.
- کاهش کدهای مدیریت ساختار بانک اطلاعاتی.
- تیمها برای یکی کردن تغییرات خود مشکلی نخواهند داشت چون دیگر snapshot مدلها در جدول __EFMigrationsHistory ذخیره نمیشود.
بنابراین در EF Core میتوان مهاجرت v1 را اضافه کرد. سپس تغییراتی را در کدها اعمال کرد. در ادامه مهاجرت v2 را تولید کرد و در آخر کار اعمال یکجای اینها را به بانک اطلاعاتی انجام داد.
هرچند در اینجا اگر میخواهید مرحلهی اجرای دستور dotnet ef database update را حذف کنید، میتوانید از کدهای ذیل نیز استفاده نمائید:
using Core1RtmEmptyTest.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Core1RtmEmptyTest.Migrations { public static class DbInitialization { public static void Initialize(this IServiceScopeFactory scopeFactory) { using (var serviceScope = scopeFactory.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>(); // Applies any pending migrations for the context to the database. // Will create the database if it does not already exist. context.Database.Migrate(); } } } }
کار متد Migrate، ایجاد بانک اطلاعاتی در صورت عدم وجود و سپس اعمال تمام مراحل migration ایی است که در جدول __EFMigrationsHistory ثبت نشدهاند (دقیقا همان کار دستور dotnet ef database update را انجام میدهد).
تفاوت متد Database.EnsureCreated با متد Database.Migrate
اگر به متدهای context.Database دقت کنید، یکی از آنها EnsureCreated نام دارد. این متد نیز سبب تولید بانک اطلاعاتی بر اساس ساختار Context برنامه میشود. اما هدف آن صرفا استفادهی از آن در آزمونهای واحد سریع است. از این جهت که جدول __EFMigrationsHistory را تولید نمیکند (برخلاف متد Migrate). بنابراین بجز آزمونهای واحد، در جای دیگری از آن استفاده نکنید چون به دلیل عدم تولید جدول __EFMigrationsHistory توسط آن، قابلیت استفادهی از بانک اطلاعاتی تولید شدهی توسط آن با امکانات migrations وجود ندارد. در پایان آزمون واحد نیز میتوان از متد EnsureDeleted برای حذف این بانک اطلاعاتی موقتی استفاده کرد.
در قسمت بعد، مطالب تکمیلی مهاجرتها را بررسی خواهیم کرد. برای مثال چگونه میتوان کلاسهای موجودیتها را به اسمبلیهای دیگری منتقل کرد.
در این قسمت قصد داریم اطلاعات یادداشتهای کاربران را توسط کامپوننت mat-table نمایش دهیم که به همراه قابلیتهایی مانند صفحه بندی، مرتب سازی و فیلتر کردن دادهها است.
کامپوننت mat-table
کار کامپوننت mat-table نمایش اطلاعات در ردیفها و ستونها است. به همراه آن mat-paginator برای نمایش UI صفحه بندی اطلاعات، دایرکتیو matSort و mat-sort-header برای افزودن رابط کاربری مرتب سازی اطلاعات و امکان تغییر منبع داده آن برای فیلتر کردن دادهها، نیز وجود دارند.
افزودن کامپوننت جدید notes برای نمایش یادداشتهای کاربران
برای نمایش لیست یادداشتهای هر شخص، کامپوننت جدید Notes را به صورت زیر در پوشهی components ایجاد میکنیم:
علت اینجا است که نمیخواهیم کامپوننت نمایش جزئیات شخص را بیش از اندازه شلوغ کنیم. بنابراین به قالب کامپوننت main-content (فایل main-content.component.html) مراجعه کرده و selector این کامپوننت را در آنجا درج میکنیم:
همانطور که ملاحظه میکنید app-notes در برگهی دوم کامپوننت mat-tab-group درج شدهاست. همچنین قصد داریم لیست userNotes جاری را به خاصیت notes آن نیز ارسال کنیم. به همین جهت به کامپوننت notes مراجعه کرده و این ورودی را ایجاد میکنیم:
فعلا جهت بررسی صحت عملکرد آن به قالب این کامپوننت (فایل notes.component.html) مراجعه کرده و آنرا به صورت json نمایش میدهیم:
تکمیل کامپوننت Notes توسط یک data table
در ادامه قصد داریم این اطلاعات خام را توسط یک data table نمایش دهیم. به همین جهت ابتدا به مستندات mat-table مراجعه کرده و همانند قبل، مثالی را پیدا میکنیم که به منظور ما نزدیکتر باشد. سپس کدهای آنرا به برنامه اضافه کرده و سفارشی سازی میکنیم. در ابتدا مثال basic آنرا دقیقا به همان نحوی که هست کپی کرده و سپس آنرا تغییر میدهیم:
محتوای فایل notes.component.ts
در اینجا برای نمایش یک mat-table، نیاز به یک منبع داده وجود دارد که روش تعریف آنرا توسط MatTableDataSource از نوع UserNote مشاهده میکنید.
سپس این منبع داده در قسمت ngOnInit بر اساس ورودی آرایهی notes که از کامپوننت main-content مقدار دهی میشود، تامین خواهد شد.
displayedColumns نیز لیست ستونها را مشخص میکند.
محتوای فایل notes.component.html
در اینجا ترتیب ردیفها بر اساس mat-row انتهای جدول مشخص میشود. بنابراین مهم نیست که ng-container matColumnDefها چه ترتیبی دارند.
سپس به ازای هر ستون، یک ng-container اضافه شدهاست. matColumnDef معادل نامهای displayedColumns خواهد بود. matCellDef نیز بر اساس متغیر حلقهای که بر روی منبع داده تشکیل میشود، تعریف خواهد شد. این تعریف امکان دسترسی به مقدار آنرا در ادامه میسر میکند.
در این حالت اگر برنامه را اجرا کنیم، خروجی زیر قابل مشاهده خواهد بود:
افزودن صفحه بندی به mat-table یادداشتهای یک کاربر
اگر مجددا به مستندات mat-table مراجعه کنیم، مثالی در مورد mat-paginator نیز دارد که جهت نمایش رابط کاربری صفحه بندی مورد استفاده قرار میگیرد. بنابراین از مثال آن جهت تکمیل این قسمت ایده میگیریم:
پس از بسته شدن تگ mat-table، کامپوننت mat-paginator به صفحه اضافه میشود که pageSize آن تعداد ردیفهای در هر صفحه را مشخص میکند و pageSizeOptions سبب نمایش یک دراپ داون برای انتخاب تعداد ردیفهای هر صفحه توسط کاربر خواهد شد.
در ادامه به کدهای کامپوننت مراجعه کرده و توسط ViewChild به template reference variable ایی به نام paginator دسترسی پیدا میکنیم:
سپس مطابق مستندات آن، این کامپوننت باید به خاصیت paginator منبع دادهی data table در رخداد ngAfterViewInit، متصل شود.
اکنون اگر برنامه را اجرا کنیم، صفحه بندی فعال شدهاست:
افزودن جستجو و فیلتر کردن اطلاعات به mat-table یادداشتهای یک کاربر
مستندات mat-table به همراه مثال filtering نیز هست که از آن جهت تکمیل این قسمت به نحو ذیل ایده خواهیم گرفت:
ابتدا فیلد ورود اطلاعات جستجو، پیش از Mat-table به قالب کامپوننت اضافه میشود:
سپس متد applyFilter که به ازای هر keyup فعال میشود، در کدهای کامپوننت به نحو زیر تکمیل خواهد شد:
همین اندازه تنظیم سبب فعالسازی جستجو بر روی جدول میشود:
افزودن مرتب سازی اطلاعات به mat-table یادداشتهای یک کاربر
مستندات mat-table به همراه مثال sorting نیز هست که از آن جهت تکمیل این قسمت به نحو ذیل ایده خواهیم گرفت:
برای فعالسازی مرتب سازی اطلاعات، در قالب کامپوننت، به mat-table، دایرکتیو matSort و به هر ستونی که نیاز است مرتب سازی شود، دایرکتیو mat-sort-header را به mat-headerها اضافه میکنیم:
در کدهای کامپوننت نیز ابتدا توسط ViewChild به matSort دسترسی پیدا میکنیم و سپس آنرا به خاصیت sort منبع داده در رخداد ngAfterViewInit، متصل خواهیم کرد:
نتیجهی این تغییرات را در تصویر زیر با فعالسازی مرتب سازی بر روی ستون Title مشاهده میکنید:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-04.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
کامپوننت mat-table
کار کامپوننت mat-table نمایش اطلاعات در ردیفها و ستونها است. به همراه آن mat-paginator برای نمایش UI صفحه بندی اطلاعات، دایرکتیو matSort و mat-sort-header برای افزودن رابط کاربری مرتب سازی اطلاعات و امکان تغییر منبع داده آن برای فیلتر کردن دادهها، نیز وجود دارند.
افزودن کامپوننت جدید notes برای نمایش یادداشتهای کاربران
برای نمایش لیست یادداشتهای هر شخص، کامپوننت جدید Notes را به صورت زیر در پوشهی components ایجاد میکنیم:
ng g c contact-manager/components/notes --no-spec
<mat-tab-group> <mat-tab label="Bio"> <p> {{user.bio}} </p> </mat-tab> <mat-tab label="Notes"> <app-notes [notes]="user.userNotes"></app-notes> </mat-tab> </mat-tab-group>
import { Component, Input, OnInit } from "@angular/core"; import { UserNote } from "../../models/user-note"; @Component({ selector: "app-notes", templateUrl: "./notes.component.html", styleUrls: ["./notes.component.css"] }) export class NotesComponent implements OnInit { @Input() notes: UserNote[];
<p> {{notes | json}} </p>
تکمیل کامپوننت Notes توسط یک data table
در ادامه قصد داریم این اطلاعات خام را توسط یک data table نمایش دهیم. به همین جهت ابتدا به مستندات mat-table مراجعه کرده و همانند قبل، مثالی را پیدا میکنیم که به منظور ما نزدیکتر باشد. سپس کدهای آنرا به برنامه اضافه کرده و سفارشی سازی میکنیم. در ابتدا مثال basic آنرا دقیقا به همان نحوی که هست کپی کرده و سپس آنرا تغییر میدهیم:
محتوای فایل notes.component.ts
import { Component, Input, OnInit } from "@angular/core"; import { MatTableDataSource } from "@angular/material"; import { UserNote } from "../../models/user-note"; @Component({ selector: "app-notes", templateUrl: "./notes.component.html", styleUrls: ["./notes.component.css"] }) export class NotesComponent implements OnInit { @Input() notes: UserNote[]; displayedColumns = ["position", "title", "date"]; dataSource: MatTableDataSource<UserNote>; constructor() { } ngOnInit() { this.dataSource = new MatTableDataSource<UserNote>(this.notes); } }
سپس این منبع داده در قسمت ngOnInit بر اساس ورودی آرایهی notes که از کامپوننت main-content مقدار دهی میشود، تامین خواهد شد.
displayedColumns نیز لیست ستونها را مشخص میکند.
محتوای فایل notes.component.html
<div class="example-container mat-elevation-z8" fxLayout="column"> <mat-table #table [dataSource]="dataSource"> <ng-container matColumnDef="position"> <mat-header-cell *matHeaderCellDef> No. </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.id}} </mat-cell> </ng-container> <ng-container matColumnDef="title"> <mat-header-cell *matHeaderCellDef> Title </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.title}} </mat-cell> </ng-container> <ng-container matColumnDef="date"> <mat-header-cell *matHeaderCellDef> Date </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.date | date:'yyyy-MM-dd'}} </mat-cell> </ng-container> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row> </mat-table> </div>
سپس به ازای هر ستون، یک ng-container اضافه شدهاست. matColumnDef معادل نامهای displayedColumns خواهد بود. matCellDef نیز بر اساس متغیر حلقهای که بر روی منبع داده تشکیل میشود، تعریف خواهد شد. این تعریف امکان دسترسی به مقدار آنرا در ادامه میسر میکند.
در این حالت اگر برنامه را اجرا کنیم، خروجی زیر قابل مشاهده خواهد بود:
افزودن صفحه بندی به mat-table یادداشتهای یک کاربر
اگر مجددا به مستندات mat-table مراجعه کنیم، مثالی در مورد mat-paginator نیز دارد که جهت نمایش رابط کاربری صفحه بندی مورد استفاده قرار میگیرد. بنابراین از مثال آن جهت تکمیل این قسمت ایده میگیریم:
</mat-table> <mat-paginator #paginator [pageSize]="2" [pageSizeOptions]="[2, 4, 6]"> </mat-paginator> </div>
در ادامه به کدهای کامپوننت مراجعه کرده و توسط ViewChild به template reference variable ایی به نام paginator دسترسی پیدا میکنیم:
export class NotesComponent implements OnInit, AfterViewInit { dataSource: MatTableDataSource<UserNote>; @ViewChild(MatPaginator) paginator: MatPaginator; ngAfterViewInit() { this.dataSource.paginator = this.paginator; } }
اکنون اگر برنامه را اجرا کنیم، صفحه بندی فعال شدهاست:
افزودن جستجو و فیلتر کردن اطلاعات به mat-table یادداشتهای یک کاربر
مستندات mat-table به همراه مثال filtering نیز هست که از آن جهت تکمیل این قسمت به نحو ذیل ایده خواهیم گرفت:
ابتدا فیلد ورود اطلاعات جستجو، پیش از Mat-table به قالب کامپوننت اضافه میشود:
<div class="example-container mat-elevation-z8" fxLayout="column"> <div class="example-header"> <mat-form-field> <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter"> </mat-form-field> </div>
applyFilter(filterValue: string) { this.dataSource.filter = filterValue.trim().toLowerCase(); // MatTableDataSource defaults to lowercase matches }
افزودن مرتب سازی اطلاعات به mat-table یادداشتهای یک کاربر
مستندات mat-table به همراه مثال sorting نیز هست که از آن جهت تکمیل این قسمت به نحو ذیل ایده خواهیم گرفت:
برای فعالسازی مرتب سازی اطلاعات، در قالب کامپوننت، به mat-table، دایرکتیو matSort و به هر ستونی که نیاز است مرتب سازی شود، دایرکتیو mat-sort-header را به mat-headerها اضافه میکنیم:
<mat-table #table [dataSource]="dataSource" matSort> <ng-container matColumnDef="position"> <mat-header-cell *matHeaderCellDef mat-sort-header> No. </mat-header-cell>
export class NotesComponent implements OnInit, AfterViewInit { dataSource: MatTableDataSource<UserNote>; @ViewChild(MatSort) sort: MatSort; ngAfterViewInit() { this.dataSource.sort = this.sort; } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-04.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
میشود متد SaveChanges را بر اساس مفاهیم Tracking سفارشی سازی کرد (مثال «ب» آن). در اینجا مثلا یک متد SaveChangesWithApplyYeKe جدید را درست کنید که این یکسان سازی را اعمال کند (مطابق مثال ب) و برای متد اصلی SaveChanges خیر. به این صورت میتوانید تصمیم گیری کنید که کجا این موارد اعمال شوند (با فراخوانی SaveChangesWithApplyYeKe سفارشی) یا خیر (با فراخوانی SaveChanges معمولی).
ضمن تشکر از شما. یک اصلاح کوچک: جدول موقتی ایجاد شده در پایان کار رویه ذخیره شده باید drop بشه.
نظرات مطالب
شبیه سازی outer Join در entity framework
با تشکر اما جناب نصیری برای این مشکل من ، که هر 2 جدول اختیاری است و نال پذیر است فکر نکنم بشه از این روش استفاده کرد
نظرات مطالب
نمایش تعداد کل صفحات در iTextSharp
با تشکر مطلب جالبی اما یک سوال داشتم و ان اینکه چطور میتوانم در سرصفحه یا پاصفحه به صورت
صفحه 1 از 20 بنویسیم من سعی کردم از جدول استفاده کنم اما جواب نداد میشود راهنمایی کنید.
صفحه 1 از 20 بنویسیم من سعی کردم از جدول استفاده کنم اما جواب نداد میشود راهنمایی کنید.
در قسمت قبل راجع به مدل پیشفرض پرووایدر منابع در ASP.NET بحث نسبتا مفصلی شد. در این قسمت تولید یک پرووایدر سفارشی برای استفاده از دیتابیس به جای فایلهای resx. به عنوان منبع نگهداری دادهها بحث میشود.
قبلا هم اشاره شده بود که در پروژههای بزرگ ذخیره تمام ورودیهای منابع درون فایلهای resx. بازدهی مناسبی نخواهد داشت. همچنین به مرور زمان و با افزایش تعداد این فایلها، کار مدیریت آنها بسیار دشوار و طاقتفرسا خواهد شد. درضمن بهدلیل رفتار سیستم کشینگ این منابع در ASP.NET، که محتویات کل یک فایل را بلافاصله پس از اولین درخواست یکی از ورودیهای آن در حافظه سرور کش میکند، در صورت وجود تعداد زیادی فایل منبع و با ورودیهای بسیار، با گذشت زمان بازدهی کلی سایت به شدت تحت تاثیر قرار خواهد گرفت.
بنابراین استفاده از یک منبع مثل دیتابیس برای چنین شرایطی و نیز کنترل مدیریت دسترسی به ورودیهای آن به صورت سفارشی، میتواند به بازدهی بهتر برنامه کمک زیادی کند. درضمن فرایند بهروزرسانی مقادیر این ورودیها در صورت استفاده از یک دیتابیس میتواند سادهتر از حالت استفاده از فایلهای resx. انجام شود.
تولید یک پرووایدر منابع دیتابیسی - بخش اول
در بخش اول این مطلب با نحوه پیادهسازی کلاسهای اصلی و اولیه موردنیاز آشنا خواهیم شد. مفاهیم پیشرفتهتر (مثل کشکردن ورودیها و عملیات fallback) و نیز ساختار مناسب جدول یا جداول موردنیاز در دیتابیس و نحوه ذخیره ورودیها برای انواع منابع در دیتابیس در مطلب بعدی آورده میشود.
با توجه به توضیحاتی که در قسمت قبل داده شد، میتوان از طرح اولیهای به صورت زیر برای سفارشیسازی یک پرووایدر منابع دیتابیسی استفاده کرد:
اگر مطالب قسمت قبل را خوب مطالعه کرده باشید، پیاده سازی اولیه طرح بالا نباید کار سختی باشد. در ادامه یک نمونه از پیادهسازیهای ممکن نشان داده شده است.
برای آغاز کار ابتدا یک پروژه ClassLibrary جدید مثلا با نام DbResourceProvider ایجاد کنید و ریفرنسی از اسمبلی System.Web به این پروژه اضافه کنید. سپس کلاسهایی که در ادامه شرح داده شدهاند را به آن اضافه کنید.
کلاس DbResourceProviderFactory
همه چیز از یک ResourceProviderFactory شروع میشود. نسخه سفارشی نشان داده شده در زیر برای منابع محلی و کلی از کلاسهای پرووایدر سفارشی استفاده میکند که در ادامه آورده شدهاند.
using System.Web.Compilation; namespace DbResourceProvider { public class DbResourceProviderFactory : ResourceProviderFactory { #region Overrides of ResourceProviderFactory public override IResourceProvider CreateGlobalResourceProvider(string classKey) { return new GlobalDbResourceProvider(classKey); } public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { return new LocalDbResourceProvider(virtualPath); } #endregion } }
درباره اعضای کلاس ResourceProviderFactory در قسمت قبل توضیحاتی داده شد. در نمونه سفارشی بالا دو متد این کلاس برای برگرداندن پرووایدرهای سفارشی منابع محلی و کلی بازنویسی شدهاند. سعی شده است تا نمونههای سفارشی در اینجا رفتاری همانند نمونههای پیشفرض در ASP.NET داشته باشند، بنابراین برای پرووایدر منابع کلی (GlobalDbResourceProvider) نام منبع درخواستی (className) و برای پرووایدر منابع محلی (LocalDbResourceProvider) مسیر مجازی درخواستی (virtualPath) به عنوان پارامتر کانستراکتور ارسال میشود.
نکته: برای استفاده از این کلاس به جای کلاس پیشفرض ASP.NET باید یکسری تنظیمات در فایل کانفیگ برنامه مقصد اعمال کرد که در ادامه آورده شده است.
کلاس BaseDbResourceProvider
برای پیادهسازی راحتتر کلاسهای موردنظر، بخشهای مشترک بین دو پرووایدر محلی و کلی در یک کلاس پایه به صورت زیر قرار داده شده است. این طرح دقیقا مشابه نمونه پیشفرض ASP.NET است.
using System.Globalization; using System.Resources; using System.Web.Compilation; namespace DbResourceProvider { public abstract class BaseDbResourceProvider : IResourceProvider { private DbResourceManager _resourceManager; protected abstract DbResourceManager CreateResourceManager(); private void EnsureResourceManager() { if (_resourceManager != null) return; _resourceManager = CreateResourceManager(); } #region Implementation of IResourceProvider public object GetObject(string resourceKey, CultureInfo culture) { EnsureResourceManager(); if (_resourceManager == null) return null; if (culture == null) culture = CultureInfo.CurrentUICulture; return _resourceManager.GetObject(resourceKey, culture); } public virtual IResourceReader ResourceReader { get { return null; } } #endregion } }
کلاس بالا چون یک کلاس صرفا پایه است بنابراین به صورت abstract تعریف شده است. در این کلاس، از نمونه سفارشی DbResourceManager برای بازیابی دادهها از دیتابیس استفاده شده است که در ادامه شرح داده شده است.
در اینجا، از متد CreateResourceManager برای تولید نمونه مناسب از کلاس DbResourceManager استفاده میشود. این متد به صورت abstract و protected تعریف شده است بنابراین پیادهسازی آن باید در کلاسهای مشتق شده که در ادامه آورده شدهاند انجام شود.
در متد EnsureResourceManager کار بررسی نال نبودن resouceManager_ انجام میشود تا درصورت نال بودن آن، بلافاصله نمونهای تولید شود.
نکته: ازآنجاکه نقطه آغازین فرایند یعنی تولید نمونهای از کلاس DbResourceProviderFactory توسط خود ASP.NET انجام خواهد شد، بنابراین مدیریت تمام نمونههای ساخته شده از کلاسهایی که در این مطلب شرح داده میشوند درنهایت عملا برعهده ASP.NET است. در ASP.NET درطول عمر یک برنامه تنها یک نمونه از کلاس Factory تولید خواهد شد، و متدهای موجود در آن در حالت عادی تنها یکبار به ازای هر منبع درخواستی (کلی یا محلی) فراخوانی میشوند. درنتیجه به ازای هر منبع درخواستی (کلی یا محلی) هر یک از کلاسهای پرووایدر منابع تنها یکبار نمونهسازی خواهد شد. بنابراین بررسی نال نبودن این متغیر و تولید نمونهای جدید تنها در صورت نال بودن آن، کاری منطقی است. این نمونه بعدا توسط ASP.NET به ازای هر منبع یا صفحه درخواستی کش میشود تا در درخواستهای بعدی تنها از این نسخه کششده استفاده شود.
در متد GetObject نیز کار استخراج ورودی منابع انجام میشود. ابتدا با استفاده از متد EnsureResourceManager از وجود نمونهای از کلاس DbResourceManager اطمینان حاصل میشود. سپس درصورتیکه مقدار این کلاس همچنان نال باشد مقدار نال برگشت داده میشود. این حالت وقتی پیش میآید که نتوان با استفاده از دادههای موجود نمونهای مناسب از کلاس DbResourceManager تولید کرد.
سپس مقدار کالچر ورودی بررسی میشود و درصورتیکه نال باشد مقدار کالچر UI ثرد جاری که در CultureInfo.CurrentUICulture قرار دارد برای آن درنظر گرفته میشود. درنهایت با فراخوانی متد GetObject از DbResourceManager تولیدی برای کلید و کالچر مربوطه کار استخراج ورودی درخواستی پایان میپذیرد.
پراپرتی ResourceReader در این کلاس به صورت virtual تعریف شده است تا بتوان پیادهسازی مناسب آن را در هر یک از کلاسهای مشتقشده اعمال کرد. فعلا برای این کلاس پایه مقدار نال برگشت داده میشود.
کلاس GlobalDbResourceProvider
برای پرووایدر منابع کلی از این کلاس استفاده میشود. نحوه پیادهسازی آن نیز دقیقا همانند طرح نمونه پیشفرض ASP.NET است.
using System; using System.Resources; namespace DbResourceProvider { public class GlobalDbResourceProvider : BaseDbResourceProvider { private readonly string _classKey; public GlobalDbResourceProvider(string classKey) { _classKey = classKey; } #region Implementation of BaseDbResourceProvider protected override DbResourceManager CreateResourceManager() { return new DbResourceManager(_classKey); } public override IResourceReader ResourceReader { get { throw new NotSupportedException(); } } #endregion } }
GlobalDbResourceProvider از کلاس پایهای که در بالا شرح داده شد مشتق شده است. بنابراین تنها بخشهای موردنیاز یعنی متد CreateResourceManager و پراپرتی ResourceReader در این کلاس پیادهسازی شده است.
در اینجا نمونه مخصوص کلاس ResourceManager (همان DbResourceManager) با توجه به نام فایل مربوط به منبع کلی تولید میشود. نام فایل در اینجا همان چیزی است که در دیتابیس برای نام منبع مربوطه ذخیره میشود. ساختار آن بعدا بحث میشود.
همانطور که میبینید برای پراپرتی ResourceReader خطای عدم پشتیبانی صادر میشود. دلیل آن در قسمت قبل و نیز بهصورت کمی دقیقتر در ادامه آورده شده است.
کلاس LocalDbResourceProvider
برای منابع محلی نیز از طرحی مشابه نمونه پیشفرض ASP.NET که در قسمت قبل نشان داده شد، استفاده شده است.
using System.Resources; namespace DbResourceProvider { public class LocalDbResourceProvider : BaseDbResourceProvider { private readonly string _virtualPath; public LocalDbResourceProvider(string virtualPath) { _virtualPath = virtualPath; } #region Implementation of BaseDbResourceProvider protected override DbResourceManager CreateResourceManager() { return new DbResourceManager(_virtualPath); } public override IResourceReader ResourceReader { get { return new DbResourceReader(_virtualPath); } } #endregion } }
این کلاس نیز از کلاس پایهای BaseDbResourceProvider مشتق شده و پیادهسازیهای مخصوص منابع محلی برای متد CreateResourceManager و پراپرتی ResourceReader در آن انجام شده است.
در متد CreateResourceManager کار تولید نمونهای از DbResourceManager با استفاده از مسیر مجازی صفحه درخواستی انجام میشود. این فرایند شبیه به پیادهسازی پیشفرض ASP.NET است. در واقع در پیادهسازی جاری، نام منابع محلی همنام با مسیر مجازی متناظر آنها در دیتابیس ذخیره میشود. درباره ساختار جدول دیتابیس بعدا بحث میشود.
در این کلاس کار بازخوانی کلیدهای موجود برای پراپرتیهای موجود در یک صفحه از طریق نمونهای از کلاس DbResourceReader انجام شده است. شرح این کلاس در ادامه آمده است.
نکته: همانطور که در قسمت قبل هم اشاره کوتاهی شده بود، از خاصیت ResourceReader در پرووایدر منابع برای تعیین تمام پراپرتیهای موجود در منبع استفاده میشود تا کار جستجوی کلیدهای موردنیاز در عبارات بومیسازی ضمنی برای رندر صفحه وب راحتتر انجام شود. بنابراین از این پراپرتی تنها در پرووایدر منابع محلی استفاده میشود. ازآنجاکه در عبارات بومیسازی ضمنی تنها قسمت اول نام کلید ورودی منبع آورده میشود، بنابراین قسمت دوم (و یا قسمتهای بعدی) کلید موردنظر که همان نام پراپرتی کنترل متناظر است از جستجو میان ورودیهای یافته شده توسط این پراپرتی بدست میآید تا ASP.NET بداند که برای رندر صفحه چه پراپرتیهایی نیاز به رجوع به پرووایدر منبع محلی مربوطه دارد (برای آشنایی بیشتر با عبارت بومیسازی ضمنی رجوع شود به قسمت قبل).
نکته: دقت کنید که پس از اولین درخواست، خروجی حاصل از enumerator این ResourceReader کش میشود تا در درخواستهای بعدی از آن استفاده شود. بنابراین در حالت عادی، به ازای هر صفحه تنها یکبار این پراپرتی فراخوانده میشود. درباره این enumerator در ادامه بحث شده است.
کلاس DbResourceManager
کار اصلی مدیریت و بازیابی ورودیهای منابع از دیتابیس از طریق کلاس DbResourceManager انجام میشود. نمونهای بسیار ساده و اولیه از این کلاس را در زیر مشاهده میکنید:
using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { private readonly string _resourceName; public DbResourceManager(string resourceName) { _resourceName = resourceName; } public object GetObject(string resourceKey, CultureInfo culture) { var data = new ResourceData(); return data.GetResource(_resourceName, resourceKey, culture.Name).Value; } } }
کار استخراج ورودیهای منابع با استفاده از نام منبع درخواستی در این کلاس مدیریت خواهد شد. این کلاس با استفاده نام منیع درخواستی به عنوان پارامتر کانستراکتور ساخته میشود. با استفاده از متد GetObject که نام کلید ورودی موردنظر و کالچر مربوطه را به عنوان پارامتر ورودی دریافت میکند فرایند استخراج انجام میشود.
برای کپسولهسازی عملیات از کلاس جداگانهای (ResourceData) برای تبادل با دیتابیس استفاده شده است. شرح بیشتر درباره این کلاس و نیز پیاده سازی کاملتر کلاس DbResourceManager به همراه مدیریت کش ورودیهای منابع و نیز عملیات fallback در مطلب بعدی آورده میشود.کلاس DbResourceReader
این کلاس که درواقع پیادهسازی اینترفیس IResourceReader است برای یافتن تمام کلیدهای تعریف شده برای یک منبع بهکار میرود، پیادهسازی آن نیز به صورت زیر است:
using System.Collections; using System.Resources; using System.Security; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceReader : IResourceReader { private readonly string _resourceName; private readonly string _culture; public DbResourceReader(string resourceName, string culture = "") { _resourceName = resourceName; _culture = culture; } #region Implementation of IResourceReader public void Close() { } public IDictionaryEnumerator GetEnumerator() { return new DbResourceEnumerator(new ResourceData().GetResources(_resourceName, _culture)); } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion #region Implementation of IDisposable public void Dispose() { Close(); } #endregion } }
این کلاس تنها با استفاده از نام منبع و عنوان کالچر موردنظر کار بازخوانی ورودیهای موجود را انجام میدهد.
تنها نکته مهم در کد بالا متد GetEnumerator است که نمونهای از اینترفیس IDictionaryEnumerator را برمیگرداند. در اینجا از کلاس DbResourceEnumerator که برای کار با دیتابیس طراحی شده، استفاده شده است. همانطور که قبلا هم اشاره شده بود، هر یک از اعضای این enumerator از نوع DictionaryEntry هستند که یک struct است. این کلاس در ادامه شرح داده شده است.
متد Close برای بستن و از بین بردن منابعی است که در تهیه enumerator موردبحث نقش داشتهاند. مثل منایع شبکهای یا فایلی که باید قبل از اتمام کار با این کلاس به صورت کامل بسته شوند. هرچند در نمونه جاری چنین موردی وجود ندارد و بنابراین این متد بلااستفاده است.
در کلاس فوق نیز برای دریافت اطلاعات از ResourceData استفاده شده است که بعدا به همراه ساختار مناسب جدول دیتابیس شرح داده میشود.نکته: دقت کنید که در پیادهسازی نشان داده شده برای کلاس LocalDbResourceProvider برای یافتن ورودیهای موجود از مقدار پیشفرض (یعنی رشته خالی) برای کالچر استفاده شده است تا از ورودیهای پیشفرض که در حالت عادی باید شامل تمام موارد تعریف شده موجود هستند استفاده شود (قبلا هم شرح داده شد که منبع اصلی و پیشفرض یعنی همانی که برای زبان پیشفرض برنامه درنظر گرفته میشود و بدون نام کالچر مربوطه است، باید شامل حداکثر ورودیهای تعریف شده باشد. منابع مربوطه به سایر کالچرها میتوانند همه این ورودیهای تعریفشده در منبع اصلی و یا قسمتی از آن را شامل شوند. عملیات fallback تضمین میدهد که درنهایت نزدیکترین گزینه متناظر با درخواست جاری را برگشت دهد).
کلاس DbResourceEnumerator
کلاس دیگری که در اینجا استفاده شده است، DbResourceEnumerator است. این کلاس در واقع پیاده سازی اینترفیس IDictionaryEnumerator است. محتوای این کلاس در زیر آورده شده است:
using System.Collections; using System.Collections.Generic; using DbResourceProvider.Models; namespace DbResourceProvider { public sealed class DbResourceEnumerator : IDictionaryEnumerator { private readonly List<Resource> _resources; private int _dataPosition; public DbResourceEnumerator(List<Resource> resources) { _resources = resources; Reset(); } public DictionaryEntry Entry { get { var resource = _resources[_dataPosition]; return new DictionaryEntry(resource.Key, resource.Value); } } public object Key { get { return Entry.Key; } } public object Value { get { return Entry.Value; } } public object Current { get { return Entry; } } public bool MoveNext() { if (_dataPosition >= _resources.Count - 1) return false; ++_dataPosition; return true; } public void Reset() { _dataPosition = -1; } } }
تفاوت این اینترفیس با اینترفیس IEnumerable در سه عضو اضافی است که برای استفاده در سیستم مدیریت منابع ASP.NET نیاز است. همانطور که در کد بالا مشاهده میکنید این سه عضو عبارتند از پراپرتیهای Entry و Key و Value. پراپرتی Entry که ورودی جاری در enumerator را مشخص میکند از نوع DictionaryEntry است. پراپرتیهای Key و Value هم که از نوع object تعریف شدهاند برای کلید و مقدار ورودی جاری استفاده میشوند.
این کلاس لیستی از Resource به عنوان پارامتر کانستراکتور برای تولید enumerator دریافت میکند. کلاس Resource مدل تولیدی از ساختار جدول دیتابیس برای ذخیره ورودیهای منابع است که در مطلب بعدی شرح داده میشود. بقیه قسمتهای کد فوق هم پیادهسازی معمولی یک enumerator است.
تنظیمات فایل کانفیگ
برای اجبار کردن ASP.NET به استفاده از Factory موردنظر باید تنظیمات زیر را در فایل web.config اعمال کرد:
<system.web> ... <globalization resourceProviderFactoryType=" نام کامل اسمبلی مربوطه ,نام پرووایدر فکتوری به همراه فضای نام آن " /> ... </system.web>
روش نشان داده شده در بالا حالت کلی تعریف و تنظیم یک نوع داده در فایل کانفیگ را نشان میدهد. درباره نام کامل اسمبلی در اینجا شرح داده شده است.
مثلا برای پیادهسازی نشان داده شده در این مطلب خواهیم داشت:<globalization resourceProviderFactoryType="DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
در مطلب بعدی درباره ساختار مناسب جدول یا جداول دیتابیس برای ذخیره ورودهای منابع و نیز پیادهسازی کاملتر کلاسهای مورداستفاده بحث خواهد شد.
منابع:
مطالب
EF Code First #4
آشنایی با Code first migrations
ویژگی Code first migrations برای اولین بار در EF 4.3 ارائه شد و هدف آن سهولت هماهنگ سازی کلاسهای مدل برنامه با بانک اطلاعاتی است؛ به صورت خودکار یا با تنظیمات دقیق دستی.
همانطور که در قسمتهای قبل نیز به آن اشاره شد، تا پیش از EF 4.3، پنج روال جهت آغاز به کار با بانک اطلاعاتی در EF code first وجود داشت و دارد:
1) در اولین بار اجرای برنامه، در صورتیکه بانک اطلاعاتی اشاره شده در رشته اتصالی وجود خارجی نداشته باشد، نسبت به ایجاد خودکار آن اقدام میگردد. اینکار پس از وهله سازی اولین DbContext و همچنین صدور یک کوئری به بانک اطلاعاتی انجام خواهد شد.
2) DropCreateDatabaseAlways : همواره پس از شروع برنامه، ابتدا بانک اطلاعاتی را drop کرده و سپس نمونه جدیدی را ایجاد میکند.
3) DropCreateDatabaseIfModelChanges : اگر EF Code first تشخیص دهد که تعاریف مدلهای شما با بانک اطلاعاتی مشخص شده توسط رشته اتصالی، هماهنگ نیست، آنرا drop کرده و نمونه جدیدی را تولید میکند.
4) با مقدار دهی پارامتر متد System.Data.Entity.Database.SetInitializer به نال، میتوان فرآیند آغاز خودکار بانک اطلاعاتی را غیرفعال کرد. در این حالت شخص میتواند تغییرات انجام شده در کلاسهای مدل برنامه را به صورت دستی به بانک اطلاعاتی اعمال کند.
5) میتوان با پیاده سازی اینترفیس IDatabaseInitializer، یک آغاز کننده بانک اطلاعاتی سفارشی را نیز تولید کرد.
اکثر این روشها در حین توسعه یک برنامه یا خصوصا جهت سهولت انجام آزمونهای خودکار بسیار مناسب هستند، اما به درد محیط کاری نمیخورند؛ زیرا drop یک بانک اطلاعاتی به معنای از دست دادن تمام اطلاعات ثبت شده در آن است. برای رفع این مشکل مهم، مفهومی به نام «Migrations» در EF 4.3 ارائه شده است تا بتوان بانک اطلاعاتی را بدون تخریب آن، بر اساس اطلاعات تغییر کردهی کلاسهای مدل برنامه، تغییر داد. البته بدیهی است زمانیکه توسط NuGet نسبت به دریافت و نصب EF اقدام میشود، همواره آخرین نگارش پایدار که حاوی اطلاعات و فایلهای مورد نیاز جهت کار با «Migrations» است را نیز دریافت خواهیم کرد.
تنظیمات ابتدایی Code first migrations
در اینجا قصد داریم همان مثال قسمت قبل را ادامه دهیم. در آن مثال از یک نمونه سفارشی سازی شده DropCreateDatabaseAlways استفاده شد.
نیاز است از منوی Tools در ویژوال استودیو، گزینه Library package manager آن، گزینه package manager console را انتخاب کرد تا کنسول پاورشل NuGet ظاهر شود.
اطلاعات مرتبط با پاورشل EF، به صورت خودکار توسط NuGet نصب میشود. برای مثال جهت مشاهده آنها به مسیر packages\EntityFramework.4.3.1\tools در کنار پوشه پروژه خود مراجعه نمائید.
در ادامه در پایین صفحه، زمانیکه کنسول پاورشل NuGet ظاهر میشود، ابتدا باید دقت داشت که قرار است فرامین را بر روی چه پروژهای اجرا کنیم. برای مثال اگر تعاریف DbContext را به یک اسمبلی و پروژه class library مجزا انتقال دادهاید، گزینه Default project را در این قسمت باید به این پروژه مجزا، تغییر دهید.
سپس در خط فرمان پاور شل، دستور enable-migrations را وارد کرده و دکمه enter را فشار دهید.
پس از اجرای این دستور، یک سری اتفاقات رخ خواهد داد:
الف) پوشهای به نام Migrations به پروژه پیش فرض مشخص شده در کنسول پاورشل، اضافه میشود.
ب) دو کلاس جدید نیز در آن پوشه تعریف خواهند شد به نامهای Configuration.cs و یک نام خودکار مانند number_InitialCreate.cs
ج) در کنسول پاور شل، پیغام زیر ظاهر میگردد:
Detected database created with a database initializer. Scaffolded migration '201205050805256_InitialCreate'
corresponding to current database schema. To use an automatic migration instead, delete the Migrations
folder and re-run Enable-Migrations specifying the -EnableAutomaticMigrations parameter.
با توجه به اینکه در مثال قسمت سوم، از آغاز کننده سفارشی سازی شده DropCreateDatabaseAlways استفاده شده بود، اطلاعات آن در جدول سیستمی dbo.__MigrationHistory در بانک اطلاعاتی برنامه موجود است (تصویری از آنرا در قسمت اول این سری مشاهده کردید). سپس با توجه به ساختار بانک اطلاعاتی جاری، دو کلاس خودکار زیر را ایجاد کرده است:
namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
}
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class InitialCreate : DbMigration
{
public override void Up()
{
CreateTable(
"Users",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(),
LastName = c.String(),
Email = c.String(),
Description = c.String(),
Photo = c.Binary(),
RowVersion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
Interests_Interest1 = c.String(maxLength: 450),
Interests_Interest2 = c.String(maxLength: 450),
AddDate = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.Id);
CreateTable(
"Projects",
c => new
{
Id = c.Int(nullable: false, identity: true),
Title = c.String(maxLength: 50),
Description = c.String(),
RowVesrion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
AddDate = c.DateTime(nullable: false),
AdminUser_Id = c.Int(),
})
.PrimaryKey(t => t.Id)
.ForeignKey("Users", t => t.AdminUser_Id)
.Index(t => t.AdminUser_Id);
}
public override void Down()
{
DropIndex("Projects", new[] { "AdminUser_Id" });
DropForeignKey("Projects", "AdminUser_Id", "Users");
DropTable("Projects");
DropTable("Users");
}
}
}
در این کلاس خودکار، نحوه ایجاد جداول بانک اطلاعاتی تعریف شدهاند. در متد تحریف شده Up، کار ایجاد بانک اطلاعاتی و در متد تحریف شده Down، دستورات حذف جداول و قیود ذکر شدهاند.
به علاوه اینبار متد Seed را در کلاس مشتق شده از DbMigrationsConfiguration، میتوان تحریف و مقدار دهی کرد.
علاوه بر اینها جدول سیستمی dbo.__MigrationHistory نیز با اطلاعات جاری مقدار دهی میگردد.
فعال سازی گزینههای مهاجرت خودکار
برای استفاده از این کلاسها، ابتدا به فایل Configuration.cs مراجعه کرده و خاصیت AutomaticMigrationsEnabled را true کنید:
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
پس از آن EF به صورت خودکار کار استفاده و مدیریت «Migrations» را عهدهدار خواهد شد. البته برای این منظور باید نوع آغاز کننده بانک اطلاعاتی را از DropCreateDatabaseAlways قبلی به نمونه جدید MigrateDatabaseToLatestVersion نیز تغییر دهیم:
//Database.SetInitializer(new Sample2DbInitializer());
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample2Context, Migrations.Configuration>());
یک نکته:
کلاس Migrations.Configuration که باید در حین وهله سازی از MigrateDatabaseToLatestVersion قید شود (همانند کدهای فوق)، از نوع internal sealed معرفی شده است. بنابراین اگر این کلاس را در یک اسمبلی جداگانه قرار دادهاید، نیاز است فایل را ویرایش کرده و internal sealed آنرا به public تغییر دهید.
روش دیگر معرفی کلاسهای Context و Migrations.Configuration، حذف متد Database.SetInitializer و استفاده از فایل app.config یا web.config است به نحو زیر ( در اینجا حرف ` اصطلاحا back tick نام دارد. فشردن دکمه ~ در حین تایپ انگلیسی):
<entityFramework>
<contexts>
<context type="EF_Sample02.Sample2Context, EF_Sample02">
<databaseInitializer
type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[EF_Sample02.Sample2Context, EF_Sample02],
[EF_Sample02.Migrations.Configuration, EF_Sample02]], EntityFramework"
/>
</context>
</contexts>
</entityFramework>
آزمودن ویژگی مهاجرت خودکار
اکنون برای آزمایش این موارد، یک خاصیت دلخواه را به کلاس Project به نام public string SomeProp اضافه کنید. سپس برنامه را اجرا نمائید.
در ادامه به بانک اطلاعاتی مراجعه کرده و فیلدهای جدول Projects را بررسی کنید:
CREATE TABLE [dbo].[Projects](
---...
[SomeProp] [nvarchar](max) NULL,
---...
بله. اینبار فیلد SomeProp بدون از دست رفتن اطلاعات و drop بانک اطلاعاتی، به جدول پروژهها اضافه شده است.
عکس العمل ویژگی مهاجرت خودکار در مقابل از دست رفتن اطلاعات
در ادامه، خاصیت public string SomeProp را که در قسمت قبل به کلاس پروژه اضافه کردیم، حذف کنید. اکنون مجددا برنامه را اجرا نمائید. برنامه بلافاصله با استثنای زیر متوقف خواهد شد:
Automatic migration was not applied because it would result in data loss.
از آنجائیکه حذف یک خاصیت مساوی است با حذف یک ستون در جدول بانک اطلاعاتی، امکان از دست رفتن اطلاعات در این بین بسیار زیاد است. بنابراین ویژگی مهاجرت خودکار دیگر اعمال نخواهد شد و این مورد به نوعی یک محافظت خودکار است که درنظر گرفته شده است.
البته در EF Code first این مساله را نیز میتوان کنترل نمود. به کلاس Configuration اضافه شده توسط پاورشل مراجعه کرده و خاصیت AutomaticMigrationDataLossAllowed را به true تنظیم کنید:
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = true;
this.AutomaticMigrationDataLossAllowed = true;
}
این تغییر به این معنا است که خودمان صریحا مجوز حذف یک ستون و اطلاعات مرتبط به آنرا صادر کردهایم.
پس از این تغییر، مجددا برنامه را اجرا کنید. ستون SomeProp به صورت خودکار حذف خواهد شد، اما اطلاعات رکوردهای موجود تغییری نخواهند کرد.
استفاده از Code first migrations بر روی یک بانک اطلاعاتی موجود
تفاوت یک دیتابیس موجود با بانک اطلاعاتی تولید شده توسط EF Code first در نبود جدول سیستمی dbo.__MigrationHistory است.
به این ترتیب زمانیکه فرمان enable-migrations را در یک پروژه EF code first متصل به بانک اطلاعاتی قدیمی موجود اجرا میکنیم، پوشه Migration در آن ایجاد خواهد شد اما تنها حاوی فایل Configuration.cs است و نه فایلی شبیه به number_InitialCreate.cs .
بنابراین نیاز است به صورت صریح به EF اعلام کنیم که نیاز است تا جدول سیستمی dbo.__MigrationHistory و فایل number_InitialCreate.cs را نیز تولید کند. برای این منظور کافی است دستور زیر را در خط فرمان پاورشل NuGet پس از فراخوانی enable-migrations اولیه، اجرا کنیم:
add-migration Initial -IgnoreChanges
با بکارگیری پارامتر IgnoreChanges، متد Up در فایل number_InitialCreate.cs تولید نخواهد شد. به این ترتیب نگران نخواهیم بود که در اولین بار اجرای برنامه، تعاریف دیتابیس موجود ممکن است اندکی تغییر کند.
سپس دستور زیر را جهت به روز رسانی جدول سیستمی dbo.__MigrationHistory اجرا کنید:
update-database
پس از آن جهت سوئیچ به مهاجرت خودکار، خاصیت AutomaticMigrationsEnabled = true را در فایل Configuration.cs همانند قبل مقدار دهی کنید.
مشاهده دستوارت SQL به روز رسانی بانک اطلاعاتی
اگر علاقمند هستید که دستورات T-SQL به روز رسانی بانک اطلاعاتی را نیز مشاهده کنید، دستور Update-Database را با پارامتر Verbose آغاز نمائید:
Update-Database -Verbose
و اگر تنها نیاز به مشاهده اسکریپت تولیدی بدون اجرای آنها بر روی بانک اطلاعاتی مدنظر است، از پارامتر Script باید استفاده کرد:
update-database -Script
نکتهای در مورد جدول سیستمی dbo.__MigrationHistory
تنها دلیلی که این جدول در SQL Server البته (ونه برای مثال در SQL Server CE) به صورت سیستمی معرفی میشود این است که «جلوی چشم نباشد»! به این ترتیب در SQL Server management studio در بین سایر جداول معمولی بانک اطلاعاتی قرار نمیگیرد. اما برای EF تفاوتی نمیکند که این جدول سیستمی است یا خیر.
همین سیستمی بودن آن ممکن است بر اساس سطح دسترسی کاربر اتصالی به بانک اطلاعاتی مساله ساز شود. برای نمونه ممکن است schema کاربر متصل dbo نباشد. همینجا است که کار به روز رسانی این جدول متوقف خواهد شد.
بنابراین اگر قصد داشتید خواص سیستمی آنرا لغو کنید، تنها کافی است دستورات T-SQL زیر را در SQL Server اجرا نمائید:
SELECT * INTO [TempMigrationHistory]
FROM [__MigrationHistory]
DROP TABLE [__MigrationHistory]
EXEC sp_rename [TempMigrationHistory], [__MigrationHistory]
ساده سازی پروسه مهاجرت خودکار
کل پروسهای را که در این قسمت مشاهده کردید، به صورت ذیل نیز میتوان خلاصه کرد:
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.IO;
namespace EF_Sample02
{
public class Configuration<T> : DbMigrationsConfiguration<T> where T : DbContext
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
}
public class SimpleDbMigrations
{
public static void UpdateDatabaseSchema<T>(string SQLScriptPath = "script.sql") where T : DbContext
{
var configuration = new Configuration<T>();
var dbMigrator = new DbMigrator(configuration);
saveToFile(SQLScriptPath, dbMigrator);
dbMigrator.Update();
}
private static void saveToFile(string SQLScriptPath, DbMigrator dbMigrator)
{
if (string.IsNullOrWhiteSpace(SQLScriptPath)) return;
var scriptor = new MigratorScriptingDecorator(dbMigrator);
var script = scriptor.ScriptUpdate(sourceMigration: null, targetMigration: null);
File.WriteAllText(SQLScriptPath, script);
Console.WriteLine(script);
}
}
}
سپس برای استفاده از آن خواهیم داشت:
SimpleDbMigrations.UpdateDatabaseSchema<Sample2Context>();
در این کلاس ذخیره سازی اسکریپت تولیدی جهت به روز رسانی بانک اطلاعاتی جاری در یک فایل نیز درنظر گرفته شده است.
تا اینجا مهاجرت خودکار را بررسی کردیم. در قسمت بعدی Code-Based Migrations را ادامه خواهیم داد.
سلام. تشکر بابت این مطلب مفید .
- بنده در حالت آنلاین بعد از افزودن پست جدید و یا بر روز رسانی پست هایی که قبلا وجود داشته اند ولی ایندکس نشده اند ، میبایست برنامه را ری استارت کنم تا موارد جدید در نتایج جستجو نمایش داده شوند . البته اگر پارامتر create را مقدار دهی نکنم یعنی به صورت زیر مشکلی وجود ندارد :
در غیر اینصورت در صورت عدم فراخوانی متد CreateIndex ، خطای زیر نمایش داده میشود :
ممنون
- بنده در حالت آنلاین بعد از افزودن پست جدید و یا بر روز رسانی پست هایی که قبلا وجود داشته اند ولی ایندکس نشده اند ، میبایست برنامه را ری استارت کنم تا موارد جدید در نتایج جستجو نمایش داده شوند . البته اگر پارامتر create را مقدار دهی نکنم یعنی به صورت زیر مشکلی وجود ندارد :
using (var indexWriter = new IndexWriter(_directory, analyzer, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
در غیر اینصورت در صورت عدم فراخوانی متد CreateIndex ، خطای زیر نمایش داده میشود :
no segments* file found in Lucene.Net.Store.SimpleFSDirectory@E:\\Lucene_Index lockFactory=Lucene.Net.Store.NativeFSLockFactory: files: write.lock