از چند مانیتور برای برنامه نویسی استفاده میکنید؟
تزئین مسیر انتخاب شده در منوی سایت
برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کردهاست. این مورد شامل سلسه مراتب مسیریابیها نیز میشود؛ برای مثال فعالسازی حالت انتخاب شدهی منوی سایت، به همراه برگهی انتخاب شده در یکی از Child Routes.
برای پیاده سازی این قابلیت، دایرکتیو ویژهای به نام routerLinkActive تدارک دیده شدهاست. این دایرکتیو را میتوان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آنرا نیز میتوان به یکی از کلاسهای CSS برنامه مانند کلاس active تعریف شدهی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه میکند و برعکس.
برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
<div class="wizard"> <a [routerLink]="['info']" routerLinkActive="active"> Basic Information </a> <a [routerLink]="['tags']" routerLinkActive="active"> Search Tags </a> </div>
یک نکته: از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.
همانطور که مشاهده میکنید، همین دو تنظیم ساده سبب مشخص شدن برگهی انتخابی شدهاند.
منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرندهی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال میکنیم:
<ul class="nav navbar-nav"> <li routerLinkActive="active"> <a [routerLink]="['/home']">Home</a> </li> <li routerLinkActive="active"> <a [routerLink]="['/products']">Product List</a> </li> <li routerLinkActive="active"> <a [routerLink]="['/products', 0, 'edit']">Add Product</a> </li> </ul>
همانطور که مشاهده میکنید، در این حالت انتخاب منوی نمایش لیست محصولات، سبب تزئین آن به حالت انتخاب شده نیز گردیدهاست.
مشکل! در همین حالت که مسیر نمایش لیست محصولات انتخاب شدهاست، لینک افزودن یک محصول جدید را نیز انتخاب کنید:
اینبار هر دو گزینه با هم انتخاب شدهاند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کردهاست. برای مدیریت میدان دید آن میتوان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
<li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a [routerLink]="['/products']">Product List</a> </li>
اکنون کاربران بهتر میتوانند درک کنند در کجای برنامه قرار دارند.
افزودن آیکن خطا به برگهای که دارای مشکل اعتبارسنجی است
در ادامه میخواهیم اگر برگهای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران سادهتر خواهد شد و به سادگی میتوانند برگههای مشکل دار را پیدا کنند.
در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر میگرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگهها استفاده خواهیم کرد.
<div class="wizard"> <a [routerLink]="['info']" routerLinkActive="active"> Basic Information <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('info')}"></span> </a> <a [routerLink]="['tags']" routerLinkActive="active"> Search Tags <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('tags')}"></span> </a> </div>
رخدادهای مسیریابی
هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش میکند، مسیریاب تعدادی رخداد را نیز تولید خواهد کرد. از این رخدادها جهت تحت نظر قرار دادن، عیبیابی و یا اجرای منطقی میتوان استفاده کرد. این رخدادها شامل موارد ذیل هستند:
- NavigationStart، با آغاز پیمایش یک مسیر رخ میدهد.
- RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المانهای تعریف شدهی در تنظیمات مسیریابی رخ میدهد.
- NavigationEnd، با پایان پیمایش یک مسیر رخ میدهد.
- NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظهای مسیرها و یا هدایت به یک جهت دیگر رخ میدهد.
- NavigationError، با شکست پیمایش یک مسیر رخ میدهد.
این رخدادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال میشوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
@NgModule({ imports: [RouterModule.forRoot(routes/*, { useHash: true }*/, { enableTracing: true })],
در اینجا ترتیب اجرای رخدادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده میکنید.
- Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت میدهد. برای نمونه، این مسیر خاص، id:2 را یافتهاست. از این id میتوان برای دسترسی به مجموعهای از رخدادها استفاده کرد.
- در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شدهاست.
- یکی از روشهایی که برای دیباگ مسیریابیها میتوان استفاده کرد، همین فعالسازی enableTracing است.
کار با رخدادهای مسیریابی با کدنویسی
به رخدادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز میتوان دسترسی یافت. برای مثال میتوان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آنرا مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آنها باید مشترکشان شد:
this.router.events.subscribe((routerEvent: Event) => { if (routerEvent instanceof NavigationStart) { //... } });
در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربهی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازهی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
در اینجا میخواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخدادها را در نقطهی آغازین برنامه و یا همان src\app\app.component.ts انجام میدهیم:
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router'; import { AuthService } from './user/auth.service'; import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { pageTitle: string = 'Routing Lab'; loading: boolean = true; constructor(private authService: AuthService, private router: Router) { router.events.subscribe((routerEvent: Event) => { this.checkRouterEvent(routerEvent); }); } checkRouterEvent(routerEvent: Event): void { if (routerEvent instanceof NavigationStart) { this.loading = true; } if (routerEvent instanceof NavigationEnd || routerEvent instanceof NavigationCancel || routerEvent instanceof NavigationError) { this.loading = false; } } logOut(): void { this.authService.logout(); this.router.navigateByUrl('/welcome'); } }
- ابتدا وابستگیهای لازم آن import شدهاند.
- سپس میخواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آنرا false خواهیم کرد.
اکنون برای استفادهی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
/* Spinner */ .spinner { font-size:300%; position:absolute; top: 50%; left: 50%; z-index:10 } .glyphicon-spin { -webkit-animation: spin 1000ms infinite linear; animation: spin 1000ms infinite linear; } @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } } @keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(359deg); transform: rotate(359deg); } }
اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آنها از سرور دریافت میشود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
البته میتوان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
ساختار موجودیت تنظیمات برنامه
تنظیمات برنامه با هر قالبی که تهیه شوند، دست آخر به صورت یک <Dictionary<string,string در برنامه پردازش شده و قابل دسترسی میشوند. بنابراین موجودیت معادل این Dictionary را به صورت زیر تعریف میکنیم:
namespace DbConfig.Web.DomainClasses { public class ConfigurationValue { public int Id { get; set; } public string Key { get; set; } public string Value { get; set; } } }
ساختار Context برنامه و مقدار دهی اولیهی آن
پس از تعریف موجودیت تنظیمات برنامه، آنرا به صورت زیر به Context برنامه معرفی میکنیم:
public class MyAppContext : DbContext, IUnitOfWork { public MyAppContext(DbContextOptions options) : base(options) { } public virtual DbSet<ConfigurationValue> Configurations { set; get; }
protected override void OnModelCreating(ModelBuilder builder) { // it should be placed here, otherwise it will rewrite the following settings! base.OnModelCreating(builder); // Custom application mappings builder.Entity<ConfigurationValue>(entity => { entity.Property(e => e.Key).HasMaxLength(450).IsRequired(); entity.HasIndex(e => e.Key).IsUnique(); entity.Property(e => e.Value).IsRequired(); entity.HasData(new ConfigurationValue { Id = 1, Key = "key-1", Value = "value_from_ef_1" }); entity.HasData(new ConfigurationValue { Id = 2, Key = "key-2", Value = "value_from_ef_2" }); }); }
انواع و اقسام تامین کنندههای تنظیمات برنامه در پروژههای ASP.NET Core، در حقیقت یک پیاده سازی سفارشی از اینترفیس IConfigurationSource هستند. به همین جهت در ادامه یک نمونهی مبتنی بر EF Core آن را تهیه میکنیم:
public class EFConfigurationSource : IConfigurationSource { private readonly IServiceProvider _serviceProvider; public EFConfigurationSource(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new EFConfigurationProvider(_serviceProvider); } }
public class EFConfigurationProvider : ConfigurationProvider { private readonly IServiceProvider _serviceProvider; public EFConfigurationProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; ensureDatabaseIsCreated(); } public override void Load() { using (var scope = _serviceProvider.CreateScope()) { var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>(); this.Data?.Clear(); this.Data = uow.Set<ConfigurationValue>() .AsNoTracking() .ToList() .ToDictionary(c => c.Key, c => c.Value); } } private void ensureDatabaseIsCreated() { using (var scope = _serviceProvider.CreateScope()) { var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>(); uow.Migrate(); } } }
در اینجا فراخوانی متد ensureDatabaseIsCreated را نیز مشاهده میکنید. کلاس EFConfigurationProvider در آغاز برنامه و پیش از هر عمل دیگری وهله سازی شده و سپس متد Load آن فراخوانی میشود. به همین جهت نیاز است یا پیشتر، بانک اطلاعاتی را توسط دستورات Migration ایجاد کرده باشید و یا متد ensureDatabaseIsCreated، اطلاعات Migration موجود را به بانک اطلاعاتی برنامه اعمال میکند.
معرفی EFConfigurationSource به برنامه
جهت معرفی سادهتر EFConfigurationSource تهیه شده، ابتدا یک متد الحاقی را بر اساس آن تهیه میکنیم:
public static class EFExtensions { public static IConfigurationBuilder AddEFConfig(this IConfigurationBuilder builder, IServiceProvider serviceProvider) { return builder.Add(new EFConfigurationSource(serviceProvider)); } }
namespace DbConfig.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IUnitOfWork, MyAppContext>(); services.AddScoped<IConfigurationValuesService, ConfigurationValuesService>(); var connectionString = Configuration.GetConnectionString("SqlServerConnection") .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data")); services.AddDbContext<MyAppContext>(options => { options.UseSqlServer( connectionString, dbOptions => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; dbOptions.CommandTimeout(minutes); dbOptions.EnableRetryOnFailure(); }); }); var serviceProvider = services.BuildServiceProvider(); var configuration = new ConfigurationBuilder() .AddConfiguration(Configuration) // Adds all of the existing configurations .AddEFConfig(serviceProvider) .Build(); services.AddSingleton<IConfigurationRoot>(sp => configuration); // Replace services.AddSingleton<IConfiguration>(sp => configuration); // Replace
همچنین روش دسترسی به serviceProvider مورد نیاز AddEFConfig، توسط متد services.BuildServiceProvider نیز در کدهای فوق مشخص است. به همین جهت مجبور شدیم این تعریف را در اینجا قرار دهیم و گرنه میشد از کلاس Program و یا حتی سازندهی کلاس Startup نیز استفاده کرد. مشکل این دو مکان عدم دسترسی به سرویس IUnitOfWork و سایر تنظیمات برنامه است.
آزمایش برنامه
اگر به قسمت «ساختار Context برنامه و مقدار دهی اولیهی آن» مطلب جاری دقت کرده باشید، دو کلید پیشفرض در اینجا ثبت شدهاند. به همین جهت در ادامه با تزریق سرویس IConfiguration به سازندهی یک کنترلر، سعی در خواندن مقادیر آنها خواهیم کرد:
namespace DbConfig.Web.Controllers { public class HomeController : Controller { private readonly IConfiguration _configuration; public HomeController(IConfiguration configuration) { _configuration = configuration; } public IActionResult Index() { return Json( new { key1 = _configuration["key-1"], key2 = _configuration["key-2"] }); }
به روز رسانی بانک اطلاعاتی برنامه و بارگذاری مجدد اطلاعات IConfiguration
فرض کنید توسط سرویسی، اطلاعات جدول ConfigurationValue را تغییر دادهاید. نکتهی مهم اینجا است که اینکار سبب فراخوانی مجدد متد Load کلاس EFConfigurationProvider نخواهد شد و عملا این تغییرات در سراسر برنامه توسط تزریق اینترفیس IConfiguration قابل دسترسی نخواهند بود (مگر اینکه برنامه مجددا ریاستارت شود). نکتهی به روز رسانی این اطلاعات به صورت زیر است:
public class ConfigurationValuesService : IConfigurationValuesService { private readonly IConfiguration _configuration; public ConfigurationValuesService(IConfiguration configuration) { _configuration = configuration; } private void reloadEFConfigurationProvider() { ((IConfigurationRoot)_configuration).Reload(); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: EFCoreDbConfig.zip
1- مقدمه
پارتیشن بندی در بانک اطلاعاتی SQL Server، از ویژگیهایی است که از نسخه 2005، به این محصول اضافه شده است. بکارگیری این قابلیت که با Split کردن، محتوای یک جدول و قرار دادن آنها در چندین فایل، برای جداول حجیم، به ویژه جداولی که دادههای آن حاوی مقادیر تاریخچهای است، بسیار سودمند است.سادگی در مدیریت دادهها و شاخصهای موجود یک جدول (از قبیل اندازه فضای ذخیره سازی و استراتژی جدید Back up گیری)، اجرای سریعتر کوئری هایی که روی یک محدوده از دادهها کار میکنند و سهولت در آرشیو دادههای قدیمی یک جدول، از قابلیتهایی است که استفاده از این ویژگی بوجود میآورد.
محدوده استفاده از این ویژگی روی یک بانک اطلاعاتی و در یک Instance است. بنابراین مباحث مرتبط با معماری Scalability را پوشش نمیدهد و صرفاً Solution ایی است که در یک Instance بانک اطلاعاتی استفاده میشود.
2- Data File و Filegroup
هر بانک اطلاعاتی در حالت پیش فرض، شامل یک فایل دادهای (MDF.) و یک فایل ثبت تراکنشی (LDF.) میباشد. میتوان جهت ذخیره سطرهای دادهای از فایلهای بیشتری تحت نام فایلهای ثانویه (NDF.) استفاده نمود. به همان طریق که در فایل سیستم، فایلها به پوشهها تخصیص داده میشوند، میتوان Data File را به Filegroup تخصیص داد. چنانچه چندین Data File به یک Filegroup تخصیص داده شوند، دادهها در تمامی Data Fileها به طریق Round-Robin توزیع میشوند.3- Partition Function
مطابق با مقادیر تعریف شده در بدنه دستور، محدوده دادهای (پارتیشنها) با استفاده از Partition Function ایجاد میشود. با در نظر گرفتن ستونی که به عنوان Partition Key انتخاب شده، این تابع یک Data Type را به عنوان ورودی دریافت میکند. در هنگام تعریف محدوده برای پارتیشنها، به منظور مشخص کردن محدوده هر پارتیشن از Right و Left استفاده میشود.Left نمایش دهندهی حد بالای هر محدوده است و به طور مشابه، Right برای مشخص کردن حد پائین آن محدوده استفاده میشود. به منظور درک بهتر، به شکل زیر توجه نمائید:
همانطور که مشاهده میشود، همواره نیاز به یک Filegroup اضافهتری از آنچه مورد نظرتان در تعریف تابع است، میباشد. بنابراین اگر Function دارای n مقدار باشد، به n+1 مقدار برای Filegroup نیاز است.
همچنین هیچ محدودیتی برای اولین و آخرین بازه در نظر گرفته نمیشود. بنابراین جهت محدود کردن مقادیری که در این بازهها قرار میگیرند، میتوان از Check Constraint استفاده نمود.
3-1- Right or Left
یک سوال متداول اینکه از کدام مورد استفاده شود؟ در پاسخ باید گفت، به چگونگی تعریف پارتیشن هایتان وابسته است. مطابق شکل، تنها تفاوت این دو، در نقاط مرزی هر یک از پارتیشنها میباشد. در بیشتر اوقات هنگام کار با دادههای عددی میتوان از Left استفاده نمود و بطور مشابه هنگامیکه نوع دادهها از جنس زمان است، میتوان از Right استفاده کرد.
4- Partition Schema
گام بعدی پس از ایجاد Partition Function، تعریف Partition Schema است، که به منظور قرار گرفتن هر یک از پارتیشنهای تعریف شده توسط Function در Filegroupهای مناسب آن استفاده میشود.
5- Partition Table
گام پایانی ایجاد یک جدول، استفاده از Partition Schema است، که دادهها را با توجه به رویه درون Partition Function مورد استفاده، ذخیره میکند. همانطور که میدانید هنگام ایجاد یک جدول، میتوان مکان ذخیره شدن آنرا مشخص نمود.
Create Table <name> (…) ON …
در هنگام ایجاد یک جدول، معمولاً جدول در Filegroup پیش فرض که PRIMARY است، قرار میگیرد. میتوان با نوشتن نام Partition Schema و همچنین Partition Key که پیشتر ذکر آن رفت، بعد از بخش ON، برای جدول مشخص نمائیم که دادههای آن به چه ترتیبی ذخیره شوند. ارتباط این سه به شرح زیر است:
توجه شود زمانیکه یک Primary Key Constraint به یک جدول اضافه میشود، یک Unique Clustered Index نیز همراه با آن ساخته میشود. چنانچه Primary Key شامل یک Clustered Index باشد، جدول با استفاده از این ستون (ستونهای) شاخص ذخیره خواهد شد، در حالیکه اگر Primary Key شامل یک Non Clustered Index باشد، یک ساختار ذخیره-سازی اضافی ایجاد خواهد شد که دادههای جدول در آن قرار خواهند گرفت.
6- Index & Data Alignment
به عنوان یک Best Practice هنگام ایجاد یک Partition Table به منظور پارتیشن بندی، از ساختار Aligned Index استفاده شود. بدین ترتیب که تعریف Index، شامل Partition Key (ستونی که معیاری برای پارتیشن بندی است) باشد. چنانچه این عمل انجام شود، دادههای ذخیره شده مرتبط با هر پارتیشن متناظر با همان شاخص، در فایل دادهای (NDF.) ذخیره خواهند شد. از این رو چنانچه کوئری درخواست شده از جدول روی یک محدوده باشد
Where [OrderDate] Between …
بدین ترتیب برای بهرمندی از این مزایا، استفاده از Aligned Index توصیه شده است.
7- Operations
از نیازمندیهای متداول در پارتیشنینگ میتوان به افزودن، حذف پارتیشنها و جابجایی محتوای یک پارتیشن که برای عملیات آرشیو استفاده میشود، اشاره کرد.
7-1- Split Partition
به منظور ایجاد یک محدوده جدید به پارتیشنها استفاده میشود. یک نکته مهم مادامی که عملیات انتقال دادهها به پارتیشن جدید انجام میگیرد، روی جدول یک قفل انحصاری قرار میگیرد و بدین ترتیب عملیات ممکن است زمانبر باشد.
به عنوان یک Best Practice همواره یک Partition خالی را Split نمائید و پس از آن اقدام به بارگذاری داده در آن نمائید.
به یاد داشته باشید پیش از انجام عملیات splitting روی Partition Function با تغییر در Partition Schema (و بکارگیری Next Used) مشخص نمائید چه محدودهای در این Filegroup جدید قرار خواهد گرفت.
7-2- Merge Partition
به منظور ادغام پارتیشنها استفاده میشود، چنانچه پارتیشن خالی نیست، برای عملیات ادغام مسائل Performance به علت اینکه در طول عملیات از Lock (قفل انحصاری) استفاده میشود، در نظر گرفته شود.
7-3- Switch Partition
چنانچه جدول و شاخصهای آن به صورت Aligned هستند، میتوانید از Switch in و Switch out استفاده نمائید. عملیات بدین ترتیب انجام میشود که بلافاصله محتوای یک پارتیشن یا جدول (Source) در یک پارتیشن خالی جدولی دیگر و یا یک جدول خالی (Target) قرار میگیرد. عملیات تنها روی Meta Data انجام میگیرد و هیچ داده ای منتقل نمیشود.
محدودیتهای بکارگیری به شرح زیر است:
- جدول یا پارتیشن Target باید حتماً خالی باشد.
- جداول Source و Target حتماً باید در یک Filegroup یکسان قرار داشته باشند.
- جدول Source باید حاوی Aligned Indexهای مورد نیاز Target و همچنین مطابقت در Filegroup را دارا باشد.
- چنانچه Target به عنوان یک پارتیشن است، اگر Source جدول است بایست دارای یک Check Constraint باشد در غیر این صورت چنانچه یک پارتیشن است باید محدوده آن در محدوده Target قرار گیرد.
8- بررسی یک سناریوی نمونه
در ابتدا یک بانک اطلاعاتی را به طریق زیر ایجاد میکنیم:
این بانک مطابق تصویر، شامل 3 عدد فایل گروپ (FG1، FG2 و FG3) و 3 عدد دیتا فایل (P1، P2 و P3) میباشد. Filegroup پیش فرض Primary است، که چنانچه در تعریف جداول به نام Partition Schema و Partition Key مرتبط اشاره نشود، به طور پیش فرض در Filegroup موسوم به Primary قرار میگیرد. چنانچه چک باکس Default انتخاب شود، همانطور که قابل حدس زدن است، آن Filegroup در صورت مشخص نکردن نام Filegroup در تعریف جدول، به عنوان مکان ذخیره سازی انتخاب میشود. چک باکس Read Only نیز همانطور که از نامش پیداست، چنانچه روی یک Filegroup تنظیم گردد، عملیات مربوط به Write روی دادههای آن قابل انجام نیست و برای Filegroup هایی که جنبه نگهداری آرشیو را دارند، قابل استفاده است.
چنانچه Filegroup ای را از حالت Read Only دوباره خارج کنیم، میتوان عملیات Write را دوباره برای آن انجام داد.
پس از ایجاد بانک اطلاعاتی، گام بعدی ایجاد یک Partition Function و پس از آن یک Partition Schema است. همانطور که مشاهده میکنید در Partition Function از سه مقدار استفاده شده، بنابراین در Partition Schema باید از چهار Filegroup استفاده شود، که در مثال ما از Filegroup پیش فرض که Primary است، استفاده شده است.
USE [PartitionDB] GO CREATE PARTITION FUNCTION pfOrderDateRange(DATETIME) AS RANGE LEFT FOR VALUES ('2010/12/31','2011/12/31','2012/12/31') GO CREATE PARTITION SCHEME psOrderDateRange AS PARTITION pfOrderDateRange TO (FG1,FG2,FG3,[PRIMARY]) GO
پس از طی گامهای قبل، به ایجاد یک جدول به صورت Aligned Index مبادرت ورزیده میشود.
CREATE TABLE Orders ( OrderID INT IDENTITY(1,1) NOT NULL, OrderDate DATETIME NOT NULL, OrderFreight MONEY NULL, ProductID INT NULL, CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED (OrderID ASC, OrderDate ASC) ON psOrderDateRange (OrderDate) ) ON psOrderDateRange (OrderDate) GO
در ادامه برای بررسی درج اطلاعات در پارتیشن با توجه به محدوده آنها اقدام به افزودن رکوردهایی در جدول ساخته شده مینمائیم.
SET NOCOUNT ON DECLARE @OrderDate DATETIME DECLARE @X INT SET @OrderDate = '2010/01/01' SET @X = 0 WHILE @X < 300 BEGIN INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID) VALUES( @OrderDate + @X, @X + 10, @X) SET @X = @X + 1 END GO SET NOCOUNT ON DECLARE @OrderDate DATETIME DECLARE @X INT SET @OrderDate = '2011/01/01' SET @X = 0 WHILE @X < 300 BEGIN INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID) VALUES( @OrderDate + @X, @X + 10, @X) SET @X = @X + 1 END GO SET NOCOUNT ON DECLARE @OrderDate DATETIME DECLARE @X INT SET @OrderDate = '2012/01/01' SET @X = 0 WHILE @X < 300 BEGIN INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID) VALUES( @OrderDate + @X, @X + 10, @X) SET @X = @X + 1 END GO
از طریق دستور Select زیر میتوان نحوه توزیع دادهها را در جدول مشاهده کرد.
USE [PartitionDB] GO SELECT OBJECT_NAME(i.object_id) AS OBJECT_NAME, p.partition_number, fg.NAME AS FILEGROUP_NAME, ROWS, au.total_pages, CASE boundary_value_on_right WHEN 1 THEN 'Less than' ELSE 'Less or equal than' END AS 'Comparition',VALUE FROM sys.partitions p JOIN sys.indexes i ON p.object_id = i.object_id AND p.index_id = i.index_id JOIN sys.partition_schemes ps ON ps.data_space_id = i.data_space_id JOIN sys.partition_functions f ON f.function_id = ps.function_id LEFT JOIN sys.partition_range_values rv ON f.function_id = rv.function_id AND p.partition_number = rv.boundary_id JOIN sys.destination_data_spaces dds ON dds.partition_scheme_id = ps.data_space_id AND dds.destination_id = p.partition_number JOIN sys.filegroups fg ON dds.data_space_id = fg.data_space_id JOIN (SELECT container_id, SUM(total_pages) AS total_pages FROM sys.allocation_units GROUP BY container_id) AS au ON au.container_id = p.partition_id WHERE i.index_id < 2
خروجی دستور فوق به شرح زیر است:
در ادامه به ایجاد یک Filegroup جدید میپردازیم.
/* Query 2-3- Split a partition*/ -- Add FG4: ALTER DATABASE PartitionDB ADD FILEGROUP FG4 Go ALTER PARTITION SCHEME [psOrderDateRange] NEXT USED FG4 GO ALTER PARTITION FUNCTION [pfOrderDateRange]() SPLIT RANGE('2013/12/31') GO -- Add Partition 4 (P4) to FG4: GO ALTER DATABASE PartitionDB ADD FILE ( NAME = P4, FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL10_50.SQLEXPRESS\MSSQL\DATA\P4.NDF' , SIZE = 1024KB , MAXSIZE = UNLIMITED, FILEGROWTH = 10%) TO FILEGROUP [FG4] -- GO
و در ادامه به درج اطلاعاتی برای بررسی نحوه توزیع دادهها در Filegroup هایمان میپردازیم.
SET NOCOUNT ON DECLARE @OrderDate DATETIME DECLARE @X INT SET @OrderDate = '2013/01/01' SET @X = 0 WHILE @X < 300 BEGIN INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID) VALUES( @OrderDate + @X, @X + 10, @X) SET @X = @X + 1 END GO SET NOCOUNT ON DECLARE @OrderDate DATETIME DECLARE @X INT SET @OrderDate = '2012/01/01' SET @X = 0 WHILE @X < 300 BEGIN INSERT dbo.Orders ( OrderDate, OrderFreight, ProductID) VALUES( @OrderDate + @X, @X + 10, @X) SET @X = @X + 1 END GO
جهت ادغام پارتیشنها به طریق زیر عمل میشود:
/* Query 2-4- Merge Partitions */ ALTER PARTITION FUNCTION [pfOrderDateRange]() MERGE RANGE('2010/12/31') Go
به منظور آرشیو نمودن اطلاعات به طریق زیر از Switch استفاده میکنیم. ابتدا یک جدول موقتی برای ذخیره رکوردهایی که قصد آرشیو آنها را داریم، ایجاد میکنیم. همانگونه که در تعریف جدول مشاهده میکنید، نام Filegroup ای که برای ساخت این جدول استفاده میشود، با Filegroup ای که قصد آرشیو اطلاعات آنرا داریم، یکسان است.
در ادامه میتوان مثلاً با ایجاد یک Temporary Table به انتقال این اطلاعات بدون توجه به Filegroup آنها پرداخت.
/* Query 2-5- Switch Partitions */ USE [PartitionDB] GO CREATE TABLE [dbo].[Orders_Temp]( [OrderID] [int] IDENTITY(1,1) NOT NULL, [OrderDate] [datetime] NOT NULL, [OrderFreight] [money] NULL, [ProductID] [int] NULL, CONSTRAINT [PK_OrdersTemp] PRIMARY KEY CLUSTERED ([OrderID] ASC,[OrderDate] ASC)ON FG2 ) ON FG2 GO USE [tempdb] GO CREATE TABLE [dbo].[Orders_Hist]( [OrderID] [int] NOT NULL, [OrderDate] [datetime] NOT NULL, [OrderFreight] [money] NULL, [ProductID] [int] NULL, CONSTRAINT [PK_OrdersTemp] PRIMARY KEY CLUSTERED ([OrderID] ASC,[OrderDate] ASC) ) GO USE [PartitionDB] GO ALTER TABLE [dbo].[Orders] SWITCH PARTITION 1 TO [dbo].[Orders_Temp] GO INSERT INTO [tempdb].[dbo].[Orders_Hist] SELECT * FROM [dbo].[Orders_Temp] GO DROP TABLE [dbo].[Orders_Temp] GO SELECT * FROM [tempdb].[dbo].[Orders_Hist]
public class OwnerInputType : InputObjectGraphType { public OwnerInputType() { Name = "ownerInput"; Field<NonNullGraphType<StringGraphType>>("name"); Field<NonNullGraphType<StringGraphType>>("address"); } }
public class AppMutation : ObjectGraphType { public AppMutation() { } }
public class AppSchema : Schema { public AppSchema(IDependencyResolver resolver) :base(resolver) { Query = resolver.Resolve<AppQuery>(); Mutation = resolver.Resolve<AppMutation>(); } }
public interface IOwnerRepository { ... Owner CreateOwner(Owner owner); } public class OwnerRepository : IOwnerRepository { ... public Owner CreateOwner(Owner owner) { owner.Id = Guid.NewGuid(); _context.Add(owner); _context.SaveChanges(); return owner; } }
public class AppMutation : ObjectGraphType { // Add public AppMutation(IOwnerRepository repository) { Field<OwnerType>( "createOwner", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<OwnerInputType>> { Name = "owner" }), resolve: context => { var owner = context.GetArgument<Owner>("owner"); return repository.CreateOwner(owner); } ); } }
mutation($owner:ownerInput!){ createOwner(owner:$owner){ id, name, address } }
{ "owner":{ "name":"Abolfazl-Roshanzamir", "address":"Address - User 4" } }
public interface IOwnerRepository { ... Owner UpdateOwner(Owner dbOwner, Owner owner); } public class OwnerRepository : IOwnerRepository { ... public Owner UpdateOwner(Owner dbOwner, Owner owner) { dbOwner.Name = owner.Name; dbOwner.Address = owner.Address; _context.SaveChanges(); return dbOwner; } }
public class AppMutation : ObjectGraphType { public AppMutation(IOwnerRepository repository) { ... // Update Field<OwnerType>( "updateOwner", arguments: new QueryArguments( new QueryArgument<NonNullGraphType<OwnerInputType>> { Name = "owner" }, new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }), resolve: context => { var owner = context.GetArgument<Owner>("owner"); var ownerId = context.GetArgument<Guid>("ownerId"); var dbOwner = repository.GetById(ownerId); if (dbOwner == null) { context.Errors.Add(new ExecutionError("Couldn't find owner in db.")); return null; } return repository.UpdateOwner(dbOwner, owner); } ); } }
mutation($owner:ownerInput!,$ownerId:ID!){ updateOwner(owner:$owner,ownerId:$ownerId){ id, name, address } }
{ "owner":{ "name":"Andy Madaidan", "address":"Address - User 1" }, "ownerId": "53270061-3ba1-4aa6-b937-1f6bc57d04d2" }
public interface IOwnerRepository { ... void DeleteOwner(Owner owner); } public class OwnerRepository : IOwnerRepository { ... public void DeleteOwner(Owner owner) { _context.Remove(owner); _context.SaveChanges(); } }
public class AppMutation : ObjectGraphType { public AppMutation(IOwnerRepository repository) { ... //Delete Field<StringGraphType>( "deleteOwner", arguments: new QueryArguments(new QueryArgument<NonNullGraphType<IdGraphType>> { Name = "ownerId" }), resolve: context => { var ownerId = context.GetArgument<Guid>("ownerId"); var owner = repository.GetById(ownerId); if (owner == null) { context.Errors.Add(new ExecutionError("Couldn't find owner in db.")); return null; } repository.DeleteOwner(owner); return $"The owner with the id: {ownerId} has been successfully deleted from db."; } ); } }
mutation($ownerId:ID!){ deleteOwner(ownerId:$ownerId) }
{ "ownerId": "6f513773-be46-4001-8adc-2e7f17d52d83" }
کلاسهای موجودیتهای مثال جاری
برای توضیح قابلیت جدید مقدار دهی اولیهی بانک اطلاعاتی در +EF Core 2.1، از کلاسهای موجودیتهای ذیل استفاده خواهیم کرد:
public class Magazine { public int MagazineId { get; set; } public string Name { get; set; } public string Publisher { get; set; } public List<Article> Articles { get; set; } } public class Article { public int ArticleId { get; set; } public string Title { get; set; } public DateTime PublishDate { get; set; } public int MagazineId { get; set; } public Author Author { get; set; } public int? AuthorId { get; set; } } public class Author { public int AuthorId { get; set; } public string Name { get; set; } public List<Article> Articles { get; set; } }
روش مقدار دهی اولیهی تک موجودیتها
اکنون فرض کنید قصد داریم جدول مجلات را مقدار دهی اولیه کنیم. برای اینکار خواهیم داشت:
protected override void OnModelCreating (ModelBuilder modelBuilder) { modelBuilder.Entity<Magazine>().HasData(new Magazine { MagazineId = 1, Name = "DNT Magazine" }); }
- ذکر صریح مقدار Id یک رکورد (هرچند نوع Id آن auto-increment است).
- عدم ذکر مقدار Publisher.
اکنون اگر توسط دستورات Migrations مانند dotnet ef migrations add init، کار تولید کدهای متناظر به روز رسانی بانک اطلاعاتی را بر اساس این کدها تولید کنیم، در قسمتی از آن، یک چنین خروجی را دریافت خواهیم کرد:
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher" }, values: new object[] { 1, "DNT Magazine", null });
set IDENTITY_INSERT ON INSERT INTO "Magazines" ("MagazineId", "Name", "Publisher") VALUES (1, 'DNT Magazine', NULL);
توسط متد HasData امکان درج چندین رکورد با هم نیز وجود دارد:
modelBuilder.Entity<Magazine>() .HasData(new Magazine{ MagazineId=2, Name="This Mag" }, new Magazine{ MagazineId=3, Name="That Mag" } );
البته باید دقت داشت که متد HasData، برای کار با یک تک موجودیت، طراحی شدهاست و توسط آن نمیتوان در چندین جدول بانک اطلاعاتی، مقادیری را درج کرد.
در مورد دادههای نالنپذیر چطور؟
در مثال فوق اگر تنظیمات خاصیت Publisherای را که نال وارد کردیم، نالنپذیر تعریف کنیم:
modelBuilder.Entity<Magazine>().Property(m=>m.Publisher).IsRequired();
"The seed entity for entity type 'Magazine' cannot be added because there was no value provided for the required property 'Publisher'."
امکان استفادهی از Anonymous Types در متد HasData
فرض کنید برای کلاس موجودیت خود یک سازنده را نیز تعریف کردهاید:
public Magazine(string name, string publisher) { Name=name; Publisher=publisher; }
modelBuilder.Entity<Magazine>().HasData(new Magazine("DNT Magazine", "1105 Media"));
modelBuilder.Entity<Magazine>().HasData(new {MagazineId=1, Name="DNT Mag", Publisher="1105 Media"});
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher" }, values: new object[] { 1, "DNT Mag", "1105 Media" });
حالت دیگر استفادهی از این قابلیت، کار با خواصی هستند که private set میباشند. فرض کنید کلاس موجودیت Magazine را به صورت زیر تغییر دادهاید:
public class Magazine { public Magazine(string name, string publisher) { Name=name; Publisher=publisher; MagazineId=Guid.NewGuid(); } public Guid MagazineId { get; private set; } public string Name { get; private set; } public string Publisher { get; private set; } public List<Article> Articles { get; set; } }
modelBuilder.Entity<Magazine>().HasData(new Magazine("DNT Mag", "1105 Media");
var mag1=new {MagazineId= new Guid("0483b59c-f7f8-4b21-b1df-5149fb57984e"), Name="DNT Mag", Publisher="1105 Media"}; modelBuilder.Entity<Magazine>().HasData(mag1);
مقدار دهی اولیهی اطلاعات به هم مرتبط
همانطور که پیشتر نیز ذکر شد، متد HasData تنها با یک تک موجودیت کار میکند و روش کار آن همانند کار با DbSetها نیست. به همین جهت نمیتوان اشیاء به هم مرتبط را توسط آن در بانک اطلاعاتی درج کرد. بنابراین برای درج اطلاعات یک مجله و مقالات مرتبط با آن، ابتدا باید مجله را ثبت کرد و سپس بر اساس Id آن مجله، کلید خارجی مقالات را به صورت جداگانهای مقدار دهی نمود:
modelBuilder.Entity<Article>().HasData(new Article { ArticleId = 1, MagazineId = 1, Title = "EF Core 2.1 Query Types"});
var mag1=new {MagazineId= new Guid("0483b59c-f7f8-4b21-b1df-5149fb57984e"), Name="DNT Mag", Publisher="1105 Media"}; modelBuilder.Entity<Magazine>().HasData(mag1);
مقدار دهی اولیهی Owned Entities
complex types در EF 6x با مفهوم دیگری به نام owned types در EF Core جایگزین شدهاند:
public class Publisher { public string Name { get; set; } public int YearFounded { get; set; } } public class Magazine { public int MagazineId { get; set; } public string Name { get; set; } public Publisher Publisher { get; set; } public List<Article> Articles { get; set; } }
modelBuilder.Entity<Magazine>().HasData (new Magazine { MagazineId = 1, Name = "DNT Magazine" }); modelBuilder.Entity<Magazine>().OwnsOne (m => m.Publisher) .HasData (new { Name = "1105 Media", YearFounded = 2006, MagazineId=1 });
این دو دستور، خروجی Migrations زیر را تولید میکنند:
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher_Name", "Publisher_YearFounded" }, values: new object[] { 1, "DNT Magazine", "1105 Media", 2006 });
محل صحیح اجرای Migrations در برنامههای ASP.NET Core 2x
زمانیکه متد ()context.Database.Migrate را اجرا میکنید، تمام مهاجرتهای اعمال نشده را به بانک اطلاعاتی اعمال میکند که این مورد شامل اجرای دستورات HasData نیز هست. روش فراخوانی این متد در ASP.NET Core 1x به صورت زیر در متد Configure کلاس Startup بود (و البته هنوز هم کار میکند):
namespace EFCoreMultipleDb.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { applyPendingMigrations(app); // ... } private static void applyPendingMigrations(IApplicationBuilder app) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var uow = scope.ServiceProvider.GetService<IUnitOfWork>(); uow.Migrate(); } } } }
namespace EFCoreMultipleDb.DataLayer.SQLite.Context { public class SQLiteDbContext : DbContext, IUnitOfWork { // ... public void Migrate() { this.Database.Migrate(); } } }
public static void Main(string[] args) { var host = BuildWebHost(args); using (var scope = host.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService<yourDBContext>(); context.Database.Migrate(); } host.Run(); }
بررسی ساختار pre-generated views
برای کامپایل نگاشتهای EF در خود برنامه (بجای تولید پویای هربار آنها)، ابتدا باید فایل edmx متناظر با مدلها و روابط بین آنها تشکیل شود:
var ms = new MemoryStream(); using (var writer = XmlWriter.Create(ms)) { EdmxWriter.WriteEdmx(new Context(), writer); }
الف) ssdl : storageModels
ب) csdl : conceptualModels
ج) msl : mappings
اینکار را به صورت زیر میتوان انجام داد:
var xDoc = XDocument.Load(ms); var ssdl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2009/02/edm/ssdl}Schema").Single(); var csdl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2008/09/edm}Schema").Single(); var msl = xDoc.Descendants("{http://schemas.microsoft.com/ado/2008/09/mapping/cs}Mapping").Single();
EdmGen.exe /mode:ViewGeneration /incsdl:Context.csdl /inmsl:Context.msl /inssdl:Context.ssdl /outviews:Context.Views.cs
علاوه بر اینها اگر علاقمند باشید که کار فایل EdmGen را شبیه سازی کنید، کلاس زیر اینکار را انجام داده و قادر است خروجی vb یا cs متناظری را نیز تولید کند:
using System; using System.Data.Entity; using System.Data.Entity.Design; using System.Data.Entity.Infrastructure; using System.Data.Mapping; using System.Data.Metadata.Edm; using System.IO; using System.Linq; using System.Xml; using System.Xml.Linq; namespace EfUtils { public static class PreGeneratedViewsWriter { public static void CreatePreGeneratedViewsFile( this DbContext contextInstance, LanguageOption language = LanguageOption.GenerateCSharpCode, string viewsFile = "Context.Views.cs", string edmxFile = "context.edmx", string ssdlFile = "context.ssdl.xml", string csdlFile = "context.csdl.xml", string mslFile = "context.msl.xml") { using (var contextViewsMemoryStream = new MemoryStream()) { using (var edmxMemoryStream = new MemoryStream()) { var edmx = createEdmx(contextInstance, edmxFile, edmxMemoryStream); var mappingItemCollection = createMappingItemCollection(ssdlFile, csdlFile, mslFile, edmx); generateViews(language, viewsFile, contextViewsMemoryStream, mappingItemCollection); } } } private static void generateViews(LanguageOption language, string viewsFile, MemoryStream contextViewsMemoryStream, StorageMappingItemCollection mappingItemCollection) { var viewGenerator = new EntityViewGenerator // It's defined in System.Data.Entity.Design.dll { LanguageOption = language }; using (var streamWriter = new StreamWriter(contextViewsMemoryStream)) { var errors = viewGenerator.GenerateViews(mappingItemCollection, streamWriter).ToList(); if (errors.Any()) throw new InvalidOperationException(errors.First().Message); contextViewsMemoryStream.Position = 0; using (var reader = new StreamReader(contextViewsMemoryStream)) { var codeData = reader.ReadToEnd(); File.WriteAllText(viewsFile, codeData); } } } private static StorageMappingItemCollection createMappingItemCollection(string ssdlFile, string csdlFile, string mslFile, XDocument edmx) { var ssdl = edmx.Descendants("{http://schemas.microsoft.com/ado/2009/02/edm/ssdl}Schema").Single(); ssdl.Save(ssdlFile); var storeItemCollection = new StoreItemCollection(new[] { ssdl.CreateReader() }); var csdl = edmx.Descendants("{http://schemas.microsoft.com/ado/2008/09/edm}Schema").Single(); csdl.Save(csdlFile); var edmItemCollection = new EdmItemCollection(new[] { csdl.CreateReader() }); var msl = edmx.Descendants("{http://schemas.microsoft.com/ado/2008/09/mapping/cs}Mapping").Single(); msl.Save(mslFile); var mappingItemCollection = new StorageMappingItemCollection(edmItemCollection, storeItemCollection, new[] { msl.CreateReader() }); return mappingItemCollection; } private static XDocument createEdmx(DbContext contextInstance, string edmxFile, MemoryStream edmxMemoryStream) { var settings = new XmlWriterSettings { Indent = true }; using (var writer = XmlWriter.Create(edmxMemoryStream, settings)) { EdmxWriter.WriteEdmx(contextInstance, writer); } File.WriteAllBytes(edmxFile, edmxMemoryStream.ToArray()); edmxMemoryStream.Position = 0; var edmx = XDocument.Load(edmxMemoryStream); return edmx; } } }
پس از تولید فایل Context.Views.cs یا Context.Views.vb، آنرا به پروژه اضافه کنید.
اینبار نحوه استفاده از آن باید به صورت زیر باشد:
Database.SetInitializer<MyContext>(null);
مرجع:
Entity Framework Code First View Generation Templates On Visual Studio Code Gallery
var store = GetStore(); string postCode = null; if (store != null && store.Address != null && store.Address.PostCode != null) postCode = store.Address.PostCode.ToString();
public static TResult IfNotNull<TResult, TSource>( this TSource source, Func<TSource, TResult> onNotDefault) where TSource : class { if (onNotDefault == null) throw new ArgumentNullException("onNotDefault"); return source == null ? default(TResult) : onNotDefault(source); }
var postCode = GetStore() .IfNotNull(x => x.Address) .IfNotNull(x => x.PostCode) .IfNotNull(x => x.ToString());
- این متد فقط با انواع ارجاعی (reference types) کار میکند و میبایست برای کار با انواع مقداری (value types) اصلاح شود.
- با انواع داده ای مثل string چه باید کرد؟ در مورد این نوع دادهها تنها مطمئن شدن از null نبودن کافی نیست. برای مثال در مورد string ، گاهی اوقات ما میخواهیم از خالی نبودن آن نیز مطمئن شویم. و یا در مورد collectionها تنها null نبودن کافی نیست بلکه زمانی که نیاز به محاسبه مجموع و یا یافتن بزرگترین عضو است، باید از خالی نبودن مجموعه و وجود حداقل یک عضو در آن مطمئن باشیم.
public static TResult IfNotDefault<TResult, TSource>( this TSource source, Func<TSource, TResult> onNotDefault, Predicate<TSource> isNotDefault = null) { if (onNotDefault == null) throw new ArgumentNullException("onNotDefault"); var isDefault = isNotDefault == null ? EqualityComparer<TSource>.Default.Equals(source, default(TSource)) : !isNotDefault(source); return isDefault ? default(TResult) : onNotDefault(source); }
return person . IfNotDefault(x => x.Name) . IfNotDefault(SomeOperation, x => !string.IsNullOrEmpty(x));
var avg = students .Where(IsNotAGraduate) .FirstOrDefault() .IfNotDefault(s => s.Grades) .IfNotDefault(g => g.Average(), g => g != null && g.Length > 0);
برای مطالعه بیشتر
Get rid of deep null checks
Chained null checks and the Maybe monad
Maybe or IfNotNull using lambdas for deep expressions
Dynamically Check Nested Values for IsNull Values
تصمیم گرفتم در طی چندین پست در حد توانم به آموزش jQuery بپردازم. (مطالب نوشته شده برداشت ازادی از کتاب jQuery in action است)
جی کوئری (jQuery) چیست؟
نکته: برای استفاده از جی کوئری باید HTML و CSS و جاوا اسکریپت آشنایی داشته باشید.
چگونه از جی کوئری استفاده کنیم؟
برای استفاده از جی کوئری باید ابتدا فایل آن را از سایت آن دانلود کرده و در پروژه خود استفاده نمایید. البته روشهای دیگری برای استفاده از این فایل وجود دارد که در آینده بیشتر با آن آشنا خواهیم شد. برای استفاده از این فایل در پروژه باید به شکل زیر آن را به صفحه HTML خود معرفی کنیم.
<html> <head> <script type="text/javascript" src="jquery-1.9.1.min.js"></script> </head> <body> </body> </html>
کوتاه کردن کد: هر زمان شما خواسته باشید کارکرد یک صفحه وب را پویاتر کنید، در اکثر مواقع به ناچار این کار از طریق عناصری بروی صفحه انجام داده اید که با توجه به انتخاب شدن آنها، صفحه کارکردی خاص خواهد داشت. مثلا در جاوا اسکریپت اگر بخواهیم عنصری را که در یک radioGroup انتخاب شده است را برگردانیم باید کدهای زیر را بنویسیم:
var checkedValue; var elements = document.getElementByTagName ('input'); for (var n = 0; n < elements.length; n++) { if (elements[n].type == 'radio' && elements[n].name == 'myRadioGroup' && elements[n].checked) { checkedValue = elements[n].value; } }
var checkedValue = $ ('[name="myRadioGroup"]:checked').val();
قدرت اصلی جی کوئری برگفته از انتخابکنندهها (Selector) هاست، انتخابکننده ، یک عبارت است که دسترسی به عنصری خاص بر روی صفحه را موجب میشود؛ انتخابکننده این امکان را فراهم میسازد تا به سادگی عنصر مورد نظر را مشخص و به آن دسترسی پیدا کنیم که در مثال فوق، عنصر مورد نظر ما گزینه انتخاب شده از myRadioGroup بود.
Unobtrusive JavaScript: اگر پیش از پیدایش CSS در کار ایجاد صفحههای اینترنتی بودهاید حتما مشکلات و مشقات آن دوران را به خاطر میآورید. در آن زمان برای فرمتدهی به اجزای مختلف صفحه ، به ناچار علائم فرمتدهی را به همراه دستورات خود اجزا، در صفحههای HTML استفاده میکردیم. اکنون بسیار بعید به نظر میرسد کسی ترجیح دهد فرمتدهی اجزا را به همراه دستورهای HTML آن انجام دهد. اگر چه هنوز دستوری مانند زیر بسیار عادی به نظر میآید:
<button type="button" onclick="document.getElementById('xyz').style.color='red';"> Click Me </button>
مجموعه عناصر در جی کوئری:
زمانی که CSS به عنوان یک تکنولوژی به منظور جداسازی طراحی از ساختار به دنیای صفحههای اینترنتی معرفی شد، میبایست راهی برای اشاره به اجزای صفحات از طرف فایل CSS نیز معرفی میشد. این امر از طریق انتخابکنندهها (Selector) صورت پذیرفت.
برای مثال انتخابکننده زیر، به تمام عناصر <a> اشاره دارد که در یک عنصر <p> قرار گرفتهاند:
p a
برای انتخاب مجموعهای از عناصر از یکی از دو Syntax زیر استفاده میکنیم.
$(Selector) یا jQuery(Selector)
مثال زیر نمونهای دیگر است که در آن مجموعهای از تمام لینکهایی که درون تگ <p> قرار دارند را انتخاب میکند:
$("p a")
در اصطلاح برنامه نویسی به چنین توابعی که گروهی از عناصر را جمع میکنند، Wrapper میگویند زیرا تمام عناصر مطلوب را تحت یک شی بستهبندی میکند. در جیکوئری به آنها Wrapped Set یا jQuery Wrapper میگویند و به متدهایی که قابل اعمال بروی اینها به نام jQuery Wrapper Methodes شناخته میشوند.
در مثال زیر میخواهیم تمام عناصر <div> در صورتی که دارای کلاس notLongForThisWorldباشند را مخفی (با فید شدن) کنیم.
$("div.notLongForThisWorld").fadeOut();
فرض کنید در مثال بالا بخواهیم پس از مخفی کردن هر <div> بخواهیم یک کلاس به نام removedبه آن بیافزاییم. به این منظور میتوان کدی مانند زیر نوشت:
$("div.notLongForThisWorld").fadeOut().addClass("removed");
چند نمونه انتخاب کننده:
نتیجه | انتخاب کننده | |
تمام <p>های زوج را انتخاب میکند | $('p:even') | |
سطر اول هر جدول را انتخاب میکند | $("tr:nth-child(1)"); | |
<div>هایی که مستقیما در <body> تعریف شده باشند را انتخاب میکند. | $("body > div"); | |
لینک هایی که به یک فایل pdf اشاره دارند را انتخاب میکند. | $("a[href$=pdf]"); | |
تمام <div> هایی که مستقیما در <body> معرفی شده اند و دارای لینک میباشند را انتخاب میکند. | $("body > div:has(a)") | |
ادامه مطالب در پستهای بعدی تشریح خواهد شد.
جهت مطالعه بیشتر میتوانید از این منابع ^ و ^ و ^ و ^ و ^ استفاده کنید.
موفق و موید باشید