شما میتوانید یک container مخصوص SQL Server داشته باشید و یکی هم مخصوص برنامهی ASP.NET خودتان که IIS را اجرا میکند. بعد این وسط باید بین اینها سیم کشی کرد. این سیم کشی در قسمت بعدی تحت عنوان docker-compose بحث شده. همچنین چند مثال هم مانند «اجرای پروژهی ASP.NET Core Music Store توسط docker-compose» در آن بحث شدند که به همراه این سیمکشیها هستند.
- Collation بانک اطلاعاتی ایجاد شده، بر اساس collation «کل سرور» تنظیم میشود. برای مثال بر روی سیستم من Server collation به Persian_100_CI_AS تنظیم شدهاست. بنابراین بانک اطلاعاتی ایجاد شدهی در اینجا هم دقیقا همین Collation را دارد (امتحان کردم).
- Collation، ارتباطی به نحوهی ذخیره شدن اطلاعات یونیکد ندارد. بیشتر هدف آن sort صحیح اطلاعات است و همچنین مشخص سازی نحوهی مقایسهی عبارات و حساس بودن به بزرگی و کوچکی حروف. شما در Collation فارسی، هم ی فارسی و هم ی عربی را میتوانید بدون مشکل ثبت کنید. اگر این Collation وجود نداشت، شبیه به SQLite، در مرتب سازی حروف فارسی، عملیات نهایی بر اساس کد اسکی آنها انجام میشد و در این حالت مثلا «پ» را در انتهای لیست مشاهده میکردید.
- اگر مشکل ثبت اطلاعات یونیکد را داشت، شما در بانک اطلاعاتی فقط ????? را مشاهده میکردید و نه هیچ چیز دیگری را و نه اینکه قسمتی درست ثبت شود و قسمتی نادرست.
- ویژوال استودیو عموما بر مبنای «تنظیمات محلی سیستم عامل شما» فایلها را ذخیره میکند. اگر میخواهید این مورد را همواره به UTF8 تغییر دهید، از افزونهی ForceUTF8 (with BOM) استفاده کنید.
/* Identify SQL Server Collation Settings*/ USE Master GO SELECT SERVERPROPERTY('collation') AS SQLServerCollation GO
- اگر مشکل ثبت اطلاعات یونیکد را داشت، شما در بانک اطلاعاتی فقط ????? را مشاهده میکردید و نه هیچ چیز دیگری را و نه اینکه قسمتی درست ثبت شود و قسمتی نادرست.
- ویژوال استودیو عموما بر مبنای «تنظیمات محلی سیستم عامل شما» فایلها را ذخیره میکند. اگر میخواهید این مورد را همواره به UTF8 تغییر دهید، از افزونهی ForceUTF8 (with BOM) استفاده کنید.
با به پایان رسیدن مرحلهی توسعهی ASP.NET Identity 2.x مخصوص نگارشهای ASP.NETایی که از Full .NET Framework استفاده میکنند، نگارش جدید آن صرفا بر پایهی ASP.NET Core تهیه شدهاست و در طی یک سری، نحوهی سفارشی سازی تقریبا تمام اجزای آنرا بررسی خواهیم کرد. جهت سهولت پیگیری این سری، پروژهی کامل سفارشی سازی شدهی ASP.NET Core Identity را از مخزن کد DNT Identity میتوانید دریافت کنید.
پیشنیازهای اجرای پروژهی DNT Identity
- ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
- همچنین بانک اطلاعاتی پایهی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل میشود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشتهی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شدهاست و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
- کاربر پیش فرض Admin سیستم و کلمهی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده میشوند.
- تنظیمات ایمیل پیش فرض برنامه به استفادهی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شدهاست. بنابراین تمام ایمیلهای برنامه را جهت آزمایش محلی میتوانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیلها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامهی Outlook میتوان مشاهده کرد.
سفارشی سازی کلید اصلی موجودیتهای ASP.NET Core Identity
ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری سادهی آن که از یک string به عنوان نوع کلید اصلی استفاده میکنند و سری دوم، حالت جنریک که در آن میتوان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیتهای تغییر یافته را در پوشهی src\ASPNETCoreIdentitySample.Entities\Identity نیز میتوانید مشاهده کنید و شامل موارد ذیل هستند:
جدول نقشهای سیستم
کار با ارث بری از نگارش جنریک کلاس IdentityRole شروع میشود. این کلاس پایه، حاوی تعاریف اصلی فیلدهای جدول نقشهای سیستم است که اولین آرگومان جنریک آن، نوع کلید اصلی جدول مرتبط را نیز مشخص میکند و در اینجا به int تنظیم شدهاست. همچنین یک اینترفیس جدید IAuditableEntity را نیز در انتهای این تعریفها مشاهده میکنید. در مورد این اینترفیس و Shadow properties متناظر با آن، در ادامهی بحث با سفارشی سازی DbContext برنامه بیشتر توضیح داده خواهد شد.
در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.
جدول کاربران منتسب به نقشها
کلاس پایهی جدول کاربران منتسب به نقشها، کلاس جنریک IdentityUserRole است که در اینجا با تغییر آرگومان جنریک آن به int، نوع فیلدهای UserId و RoleId آن به int تنظیم میشوند. در کلاس سفارشی سازی شدهی فوق، دو خاصیت اضافهتر User و Role نیز را مشاهده میکنید. مزیت تعریف آنها، دسترسی سادهتر به اطلاعات کاربران و نقشها توسط EF Core است.
در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده میشود.
جدول جدید IdentityRoleClaim سیستم
در ASP.NET Core Identity، جدول جدیدی به نام RoleClaim نیز اضافه شدهاست. در این سری از آن برای پیاده سازی سطوح دسترسی پویای به صفحات استفاده خواهیم کرد. ابتدا یک سری نقش ثابت در جدول Roles ثبت خواهند شد. سپس تعدادی کاربر به هر نقش نسبت داده میشوند. اکنون میتوان به هر نقش نیز تعدادی Claim را انتساب داد. برای مثال یک Claim سفارشی که شامل ID سفارشی area:controller:action باشد. به این ترتیب و با بررسی سفارشی آن میتوان سطوح دسترسی پویا را نیز پیاده سازی کرد و مزیت آن این است که تمام این Claims به صورت خودکار به کوکی شخص نیز اضافه شده و دسترسی به اطلاعات آن بسیار سریع است و نیازی به مراجعهی به بانک اطلاعاتی را ندارد.
جدول UserClaim سیستم
میتوان به هر کاربر یک سری Claim مخصوص را نیز انتساب داد. برای مثال مسیر عکس ذخیره شدهی او در سرور، چه موردی است و این اطلاعات به صورت خودکار به کوکی او نیز توسط ASP.NET Core Identity اضافه میشوند. البته ما در این سری روش دیگری را برای سفارشی سازی Claims عمومی کاربران بکار خواهیم گرفت (با سفارشی سازی کلاس ApplicationClaimsPrincipalFactory آن).
جداول توکن و لاگینهای کاربران
دراینجا نیز نحوهی سفارشی سازی و تغییر جداول لاگینهای کاربران و توکنهای مرتبط با آنها را مشاهده میکنید. این جداول بیشتر جهت دسترسی به حالتهایی مانند لاگین با حساب کاربری جیمیل مورد استفاده قرار میگیرند و کاربرد پیش فرضی ندارند (اگر از تامین کنندههای لاگین خارجی نمیخواهید استفاده کنید).
جدول کاربران سیستم
در اینجا علاوه بر نحوهی تغییر نوع کلید اصلی جدول کاربران سیستم، نحوهی افزودن خواص اضافهتری مانند نام، تاریخ تولد، مکان، تصویر و غیره را نیز مشاهده میکنید. به علاوه جدولی نیز جهت ثبت سابقهی کلمات عبور هش شدهی کاربران نیز تدارک دیده شدهاست تا کاربران نتوانند از 5 کلمهی عبور اخیر خود (تنظیم NotAllowedPreviouslyUsedPasswords در فایل appsettings.json) استفاده کنند.
فیلد IsActive نیز از این جهت اضافه شدهاست تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.
تعریف Shadow properties ثبت تغییرات رکوردها
در #C ارثبری چندگانهی کلاسها ممنوع است؛ مگر اینکه از اینترفیسها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمیتوان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافهتری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده میکنید. این اینترفیس کار علامتگذاری کلاسهایی را انجام میدهد که قصد داریم به آنها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاسهایی از نوع IAuditableEntity اعمال میشود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیتهای علامتگذاری شده اضافه میکند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.
تاثیر افزودن Shadow properties را بر روی کلاس نقشهای سیستم، در تصویر فوق ملاحظه میکنید. خواصی که به صورت معمول در کلاسهای برنامه ظاهر نمیشوند و صرفا هدف بازبینی سیستم را برآورده میکنند و مدیریت آنها نیز در اینجا کاملا خودکار است.
سفارشی سازی DbContext برنامه
نحوهی سفارشی سازی DbContext برنامه را در پوشهی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه میکنید. پوشهی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازیهای لازم بر روی آن انجام شدهاست؛ شامل:
- تغییر نوع کلید اصلی موجودیتها به همراه معرفی موجودیتهای تغییر یافته:
ما در ابتدای بحث، برای مثال کلاس Role را سفارشی سازی کردیم. اما برنامه از وجود آن بیاطلاع است. با ارث بری از IdentityDbContext و ذکر این کلاسهای سفارشی به همراه نوع int کلید اصلی مورد استفاده، کار معرفی موجودیتهای سفارشی سازی شده انجام میشود.
- اعمال متد BeforeSaveTriggers به تمام نگارشهای مختلف SaveChanges
در اینجا پیش از ذخیرهی اطلاعات، ابتدا موجودیتها اعتبارسنجی میشوند. سپس مقادیر Shadow properties تنظیم شده و دست آخر، ی و ک فارسی نیز به اطلاعات ثبت شده، جهت یک دست سازی اطلاعات سیستم، اعمال میشوند.
- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شدهی در فایل appsettings.json خوانده شده و اعمال میشوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده میکنید. کار آن استفادهی از بانک اطلاعاتی درون حافظهای، جهت انجام آزمونهای واحد و یا استفادهی از LocalDb و یا نگارش کامل SQL Server میباشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.
پس از تعریف این DbContext پایهی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده میکنید. این کلاس Context ایی است که در برنامه از آن استفاده میشود و از کلاس پایه ApplicationDbContextBase مشتق شدهاست:
تعاریف موجودیتهای جدید خود را به این کلاس اضافه کنید.
تنظیمات mapping آنها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوهی استفادهی از آن را بهخاطر داشته باشید:
ابتدا باید base.OnModelCreating را ذکر کنید. در غیراینصورت تمام سفارشی سازیهای شما بازنویسی میشوند.
سپس متد AddCustomIdentityMappings ذکر شدهاست. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه میکند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شدهاند.
در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایهای که پیشتر در مورد آنها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشتهای سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.
در قسمت بعدی، نحوهی سفارشی سازی سرویسهای پایهی Identity را بررسی خواهیم کرد. بدون این سفارشی سازی و اطلاعات رسانی به سرویسهای پایه که از چه موجودیتهای جدید سفارشی سازی شدهایی در حال استفاده هستیم، کار Migrations انجام نخواهد شد.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
پیشنیازهای اجرای پروژهی DNT Identity
- ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
- همچنین بانک اطلاعاتی پایهی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل میشود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشتهی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شدهاست و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
- کاربر پیش فرض Admin سیستم و کلمهی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده میشوند.
- تنظیمات ایمیل پیش فرض برنامه به استفادهی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شدهاست. بنابراین تمام ایمیلهای برنامه را جهت آزمایش محلی میتوانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیلها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامهی Outlook میتوان مشاهده کرد.
سفارشی سازی کلید اصلی موجودیتهای ASP.NET Core Identity
ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری سادهی آن که از یک string به عنوان نوع کلید اصلی استفاده میکنند و سری دوم، حالت جنریک که در آن میتوان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیتهای تغییر یافته را در پوشهی src\ASPNETCoreIdentitySample.Entities\Identity نیز میتوانید مشاهده کنید و شامل موارد ذیل هستند:
جدول نقشهای سیستم
public class Role : IdentityRole<int, UserRole, RoleClaim>, IAuditableEntity { public Role() { } public Role(string name) : this() { Name = name; } public Role(string name, string description) : this(name) { Description = description; } public string Description { get; set; } }
در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.
جدول کاربران منتسب به نقشها
public class UserRole : IdentityUserRole<int>, IAuditableEntity { public virtual User User { get; set; } public virtual Role Role { get; set; } }
در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده میشود.
جدول جدید IdentityRoleClaim سیستم
public class RoleClaim : IdentityRoleClaim<int>, IAuditableEntity { public virtual Role Role { get; set; } }
جدول UserClaim سیستم
public class UserClaim : IdentityUserClaim<int>, IAuditableEntity { public virtual User User { get; set; } }
جداول توکن و لاگینهای کاربران
public class UserToken : IdentityUserToken<int>, IAuditableEntity { public virtual User User { get; set; } } public class UserLogin : IdentityUserLogin<int>, IAuditableEntity { public virtual User User { get; set; } }
جدول کاربران سیستم
public class User : IdentityUser<int, UserClaim, UserRole, UserLogin>, IAuditableEntity { public User() { UserUsedPasswords = new HashSet<UserUsedPassword>(); UserTokens = new HashSet<UserToken>(); } [StringLength(450)] public string FirstName { get; set; } [StringLength(450)] public string LastName { get; set; } [NotMapped] public string DisplayName { get { var displayName = $"{FirstName} {LastName}"; return string.IsNullOrWhiteSpace(displayName) ? UserName : displayName; } } [StringLength(450)] public string PhotoFileName { get; set; } public DateTimeOffset? BirthDate { get; set; } public DateTimeOffset? CreatedDateTime { get; set; } public DateTimeOffset? LastVisitDateTime { get; set; } public bool IsEmailPublic { get; set; } public string Location { set; get; } public bool IsActive { get; set; } = true; public virtual ICollection<UserUsedPassword> UserUsedPasswords { get; set; } public virtual ICollection<UserToken> UserTokens { get; set; } } public class UserUsedPassword : IAuditableEntity { public int Id { get; set; } public string HashedPassword { get; set; } public virtual User User { get; set; } public int UserId { get; set; } }
فیلد IsActive نیز از این جهت اضافه شدهاست تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.
تعریف Shadow properties ثبت تغییرات رکوردها
در #C ارثبری چندگانهی کلاسها ممنوع است؛ مگر اینکه از اینترفیسها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمیتوان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافهتری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده میکنید. این اینترفیس کار علامتگذاری کلاسهایی را انجام میدهد که قصد داریم به آنها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاسهایی از نوع IAuditableEntity اعمال میشود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیتهای علامتگذاری شده اضافه میکند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.
تاثیر افزودن Shadow properties را بر روی کلاس نقشهای سیستم، در تصویر فوق ملاحظه میکنید. خواصی که به صورت معمول در کلاسهای برنامه ظاهر نمیشوند و صرفا هدف بازبینی سیستم را برآورده میکنند و مدیریت آنها نیز در اینجا کاملا خودکار است.
سفارشی سازی DbContext برنامه
نحوهی سفارشی سازی DbContext برنامه را در پوشهی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه میکنید. پوشهی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازیهای لازم بر روی آن انجام شدهاست؛ شامل:
- تغییر نوع کلید اصلی موجودیتها به همراه معرفی موجودیتهای تغییر یافته:
public abstract class ApplicationDbContextBase : IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>, IUnitOfWork
- اعمال متد BeforeSaveTriggers به تمام نگارشهای مختلف SaveChanges
protected void BeforeSaveTriggers() { ValidateEntities(); SetShadowProperties(); this.ApplyCorrectYeKe(); }
- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شدهی در فایل appsettings.json خوانده شده و اعمال میشوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده میکنید. کار آن استفادهی از بانک اطلاعاتی درون حافظهای، جهت انجام آزمونهای واحد و یا استفادهی از LocalDb و یا نگارش کامل SQL Server میباشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.
پس از تعریف این DbContext پایهی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده میکنید. این کلاس Context ایی است که در برنامه از آن استفاده میشود و از کلاس پایه ApplicationDbContextBase مشتق شدهاست:
public class ApplicationDbContext : ApplicationDbContextBase
تنظیمات mapping آنها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوهی استفادهی از آن را بهخاطر داشته باشید:
protected override void OnModelCreating(ModelBuilder builder) { // it should be placed here, otherwise it will rewrite the following settings! base.OnModelCreating(builder); // Adds all of the ASP.NET Core Identity related mappings at once. builder.AddCustomIdentityMappings(SiteSettings.Value); // Custom application mappings // This should be placed here, at the end. builder.AddAuditableShadowProperties(); }
سپس متد AddCustomIdentityMappings ذکر شدهاست. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه میکند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شدهاند.
در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایهای که پیشتر در مورد آنها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشتهای سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.
در قسمت بعدی، نحوهی سفارشی سازی سرویسهای پایهی Identity را بررسی خواهیم کرد. بدون این سفارشی سازی و اطلاعات رسانی به سرویسهای پایه که از چه موجودیتهای جدید سفارشی سازی شدهایی در حال استفاده هستیم، کار Migrations انجام نخواهد شد.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
در قسمتهای قبل ( ^ ، ^ و ^ ) GraphQL را در ASP.Net Core راه اندازی کردیم و در قسمت ( فراخوانی GraphQL API در یک کلاینت ASP.NET Core ) از GraphQL API فراهم شده در یک کلاینت ASP Net Core استفاده کردیم. اکنون میخواهیم چگونگی استفاده از GraphQL را در انگیولار، یاد بگیریم.
Apollo Angular، به شما اجازه میدهد دادهها را از یک سرور GraphQL دریافت و از آن برای ساختن UI های واکنشی و پیچیده در انگیولار استفاده کنید. وقتی که از Apollo Client استفاده میکنیم، نیازی نیست هیچ چیز خاصی را در مورد سینتکس query ها یادبگیریم؛ به دلیل اینکه همه چیز همان استاندارد GraphQL میباشد. هر چیزی را که شما در GraphQL query IDE تایپ میکنید، میتوانید آنها را در کدهای Apollo Client نیز قرار دهید.
Installation with Angular Schematics
بعد از ایجاد یک پروژه انگیولار با دستور زیر
ng new apollo-angular-project
ng add apollo-angular
const uri = 'https://localhost:5001/graphql';
اکنون همه چیز تمام شدهاست. شما میتوانید اولین query خود را اجرا کنید.
Installation without Angular Schematics
اگر میخواهید Apollo را بدون کمک گرفتن از Angular Schematics نصب کنید، در ابتدا کتابخانههای زیر را نصب نمائید:
npm install --save apollo-angular \ apollo-angular-link-http \ apollo-link \ apollo-client \ apollo-cache-inmemory \ graphql-tag \ graphql
{ "compilerOptions": { // ... "lib": [ "es2017", "dom", "esnext.asynciterable" ] } }
در ادامه، فایل app.module.ts را باز کرده و آن را مطابق زیر ویرایش کنید:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from "@angular/common/http"; import { ApolloModule, APOLLO_OPTIONS } from "apollo-angular"; import { HttpLinkModule, HttpLink } from "apollo-angular-link-http"; import { InMemoryCache } from "apollo-cache-inmemory"; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, ApolloModule, HttpLinkModule ], providers: [ { provide: APOLLO_OPTIONS, useFactory: (httpLink: HttpLink) => { return { cache: new InMemoryCache(), link: httpLink.create({ uri: "https://localhost:5001/graphql" }) } }, deps: [ HttpLink ] }], bootstrap: [ AppComponent ] }) export class AppModule { }
- با استفاده از سرویس apollo-angular-link-http و HttpLink، کلاینت را به یک سرور GraphQL متصل میکنیم.
- apollo-cache-inmemory و InMemoryCache محلی برای ذخیره سازی دادهها میباشد.
- APOLLO_OPTIONS فراهم کننده تنظیمات Apollo Client است.
- HttpLink نیاز به HttpClient دارد. به همین خاطر است که ما از HttpClientModule استفاده کردهایم.
Links and Cache
Apollo Client، یک لایه واسط شبکه قابل تعویض را دارد که به شما اجازه میدهد تا تنظیم کنید که چگونه query ها در HTTP ارسال شوند؛ یا کل بخش Network را با چیزی کاملا سفارشی سازی شده جایگزین کنید؛ مثل یک websocket transport.
apollo-angular-link-http : از Http برای ارسال query ها استفاده میکند.
apollo-cache-inmemory : پیاده سازی کش پیش فرض برای Apollo Client 2.0 میباشد.
نکته
Apollo یک سرویس export شده انگیولار از apollo-angular، برای به اشتراک گذاشتن دادههای GraphQL با UI شما میباشد.
شروع کار
به همان روش که فایلهای Model را برای کلاینت ASP.NET Core ایجاد کردیم، در اینجا هم ایجاد میکنیم. کار را با ایجاد کردن یک پوشه جدید به نام types، شروع میکنیم و چند type را در آن تعریف خواهیم کرد ( OwnerInputType ،AccountType و OwnerType ):
export type OwnerInputType = { name: string; address: string; }
export type AccountType = { 'id': string; 'description': string; 'ownerId' : string; 'type': string; }
import { AccountType } from './accountType'; export type OwnerType = { 'id': string; 'name': string; 'address': string; 'accounts': AccountType[]; }
سپس یک سرویس را به نام graphql ایجاد میکنیم:
ng g s graphql
و آن را همانند زیر ویرایش میکنیم:
import { Injectable } from '@angular/core'; import { Apollo } from 'apollo-angular'; import gql from 'graphql-tag'; @Injectable({ providedIn: 'root' }) export class GraphqlService { constructor(private apollo: Apollo) { } }
اکنون همه چیز آماده است تا تعدادی query و mutation را اجرا کنیم ( providedIn ).
بازیابی تمامی Owner ها
سرویس graphql را باز میکنیم و آن را همانند زیر ویرایش میکنیم ( اضافه کردن متد getOwners ):
public getOwners = () => { return this.apollo.query({ query: gql`query getOwners{ owners{ id, name, address, accounts{ id, description, type } } }` }); }
سپس فایل app.component.ts را باز کرده و همانند زیر ویرایش میکنیم:
export class AppComponent implements OnInit { public owners: OwnerType[]; public loading = true; constructor(private graphQLService: GraphqlService) { } ngOnInit() { this.graphQLService.getOwners().subscribe(result => { this.owners = result.data["owners"] as OwnerType[]; this.loading = result.loading; }); } }
و هم چنین app.component.html:
<div> <div *ngIf="!this.loading"> <table> <thead> <tr> <th> # </th> <th> نام و نام خانوادگی </th> <th> آدرس </th> </tr> </thead> <tbody> <ng-container *ngFor="let item of this.owners;let idx=index" [ngTemplateOutlet]="innertable" [ngTemplateOutletContext]="{item:item, index:idx}"></ng-container> </tbody> </table> </div> <div *ngIf="this.loading"> <p> در حال بارگذاری لیست ... </p> </div> </div> <ng-template #innertable let-item="item" let-idx="index"> <tr> <td>{{idx+1}}</td> <td>{{item.name}}</td> <td>{{item.address}}</td> </tr> <tr *ngIf="this.item.accounts && this.item.accounts.length > 0"> <td colspan="4"> <div> <p>Accounts</p> </div> <div> <table> <thead> <tr> <th>#</th> <th>نوع</th> <th>توضیحات</th> </tr> </thead> <tbody> <tr *ngFor="let innerItem of this.item.accounts;let innerIndex=index"> <td> {{innerIndex+1}} </td> <td> {{innerItem.type}} </td> <td> {{innerItem.description}} </td> </tr> </tbody> </table> </div> </td> </tr> </ng-template>
dotnet restore dotnet run
سپس پروژه را اجرا کنید:
ng serve
خروجی به صورت زیر میباشد (لیست تمامی Owner ها به همراه Accountهای مربوط به هر Owner):
در متد getOwner، بجای apollo.query میتوان از apollo.watchQuery استفاده کرد که در نمونه زیر، در ابتدا، GraphQL query را در تابع gql ( از graphql-tag ) برای خصوصیت query در متد apollo.watchQuery پاس میدهیم.
public getOwners = () => { return this.apollo.watchQuery<any>({ query: gql`query getOwners{ owners{ id, name, address, accounts{ id, description, type } } }` }) }
export class AppComponent implements OnInit { loading: boolean; public owners: OwnerType[]; private querySubscription: Subscription; constructor(private graphQLService: GraphqlService) { } ngOnInit() { this.querySubscription = this.graphQLService.getOwners() .valueChanges .subscribe(result => { this.loading = result.loading; this.owners = result.data["owners"] as OwnerType[]; }); } ngOnDestroy() { this.querySubscription.unsubscribe(); } }
مابقی query ها و mutation ها از سرویس graphql
بازیابی یک Owner مشخص
public getOwner = (id) => { return this.apollo.query({ query: gql`query getOwner($ownerID: ID!){ owner(ownerId: $ownerID){ id, name, address, accounts{ id, description, type } } }`, variables: { ownerID: id } }) }
ایجاد یک Owner جدید
public createOwner = (ownerToCreate: OwnerInputType) => { return this.apollo.mutate({ mutation: gql`mutation($owner: ownerInput!){ createOwner(owner: $owner){ id, name, address } }`, variables: { owner: ownerToCreate } }) }
ویرایش یک Owner
public updateOwner = (ownerToUpdate: OwnerInputType, id: string) => { return this.apollo.mutate({ mutation: gql`mutation($owner: ownerInput!, $ownerId: ID!){ updateOwner(owner: $owner, ownerId: $ownerId){ id, name, address } }`, variables: { owner: ownerToUpdate, ownerId: id } }) }
و در نهایت حذف یک Owner
public deleteOwner = (id: string) => { return this.apollo.mutate({ mutation: gql`mutation($ownerId: ID!){ deleteOwner(ownerId: $ownerId) }`, variables: { ownerId: id } }) }
کدهای کامل این قسمت را از ایجا دریافت کنید : GraphQL_Angular.zip
کدهای کامل قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را از اینجا دریافت کنید : ASPCoreGraphQL_3.rar
همانطور که مطلع هستید در تنظیمات یک دایرکتوری مجازی در IIS6 یا 5، حتی پس از نصب دات نت فریم ورک سه و نیم، گزینه انتخاب نگارش 3.5 ظاهر نمیشود و همان تنظیمات ASP.Net 2.0 کافی است (شکل زیر) (دات نت 3 و سه و نیم را میتوان بعنوان افزونههایی با مقیاس سازمانی (WF ، WCF و ...) برای دات نت 2 درنظر گرفت).
هنگام استفاده از VS.Net 2008 و تنظیم نوع پروژه به دات نت فریم ورک 3.5 ، به صورت خودکار تنظیمات لازم به وب کانفیگ برنامه جهت استفاده از کامپایلرهای مربوطه نیز اضافه میشوند که شاید از نظر دور بمانند.
برای آزمایش این مورد، فرض کنید صفحه زیر را بدون استفاده از code behind و VS.Net ایجاد کرده ایم (جهت آزمایش سریع یک قطعه کد Linq ).
<%@ Page Language="C#" %>
<%@ Import Namespace="System" %>
<%@ Import Namespace="System.Linq" %>
<form id="Form1" method="post" runat="server">
<asp:GridView ID="GridView1" runat="server" />
</form>
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
string[] cities = {
"London", "Amsterdam", "San Francisco", "Las Vegas",
"Boston", "Raleigh", "Chicago", "Charlestown",
"Helsinki", "Nice", "Dublin"
};
GridView1.DataSource = from city in cities
where city.Length > 4
orderby city
select city.ToUpper();
GridView1.DataBind();
}
</script>
این قطعه کد چون از قابلیتهای کامپایلر جدید سی شارپ استفاده میکند، با کامپایلر پیش فرض و تنظیم شده دات نت 2 کار نخواهد کرد و باید برای رفع این مشکل، فایل web.config جدیدی را نیز به پوشه برنامه اضافه کنیم:
<?xml version="1.0"?>
<configuration>
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs" warningLevel="4" type="Microsoft.CSharp.CSharpCodeProvider, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<providerOption name="CompilerVersion" value="v3.5"/>
<providerOption name="WarnAsError" value="false"/>
</compiler>
</compilers>
</system.codedom>
<system.web>
<compilation defaultLanguage="c#">
<assemblies>
<add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
</assemblies>
</compilation>
</system.web>
</configuration>
همانطور که ذکر شد اگر از VS.Net 2008 استفاده کنید، هیچ وقت درگیر این مباحث نخواهید شد و همه چیز از پیش تنظیم شده است.
در ادامهی مباحث پشتیبانی از XML در SQL Server، به کارآیی فیلدهای XML ایی و نحوهی ایندکس گذاری بر روی آنها خواهیم پرداخت. این مساله در تولید برنامههایی سریع و مقیاس پذیر، بسیار حائز اهمیت است.
در SQL Server، کوئریهای انجام شده بر روی فیلدهای XML، توسط همان پردازشگر کوئریهای رابطهای متداول آن، خوانده و اجرا خواهند شد و امکان تعریف یک XQuery خارج از یک عبارت SQL و یا T-SQL وجود ندارد. متدهای XQuery بسیار شبیه به system defined functions بوده و Query Plan یکپارچهای را با سایر قسمتهای رابطهای یک عبارت SQL دارند.
مفهوم Node table
دادههای XML ایی برای اینکه توسط SQL Server قابل استفاده باشند، به صورت درونی تبدیل به یک node table میشوند. به این معنا که نودهای یک سند XML، به یک جدول رابطهای به صورت خودکار تجزیه میشوند. این جدول درونی در صورت بکارگیری XML Indexes در جدول سیستمی sys.internal_tables قابل مشاهده خواهد بود. SQL Server برای انجام اینکار از یک XmlReader خاص خودش استفاده میکند. در مورد XMLهای ایندکس نشده، این تجزیه در زمان اجرا صورت میگیرد؛ پس از اینکه Query Plan آن تشکیل شد.
بررسی Query Plan فیلدهای XML ایی
جهت فراهم کردن مقدمات آزمایش، ابتدا جدول xmlInvoice را با یک فیلد XML ایی untyped درنظر بگیرید:
سپس 6 ردیف را به آن اضافه میکنیم:
همچنین برای مقایسه، دقیقا جدول مشابهی را اینبار با یک XML Schema مشخص ایجاد میکنیم.
سپس مجددا همان 6 رکورد قبلی را در این جدول جدید نیز insert خواهیم کرد.
در این جدول دوم، حالت پیش فرض content قبلی، به document تغییر کردهاست. با توجه به اینکه میدانیم اسناد ما چه فرمتی دارند و بیش از یک root element نخواهیم داشت، انتخاب document سبب خواهد شد تا Query Plan بهتری حاصل شود.
در ادامه برای مشاهدهی بهتر نتایج، کش Query Plan و اطلاعات آماری جدول xmlInvoice را حذف و به روز میکنیم:
به علاوه در management studio بهتر است از منوی Query، گزینهی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمههای Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده نمود. برای خواندن یک Query Plan عموما از بالا به پایین و از راست به چپ باید عمل کرد. در آن نهایتا باید به عدد estimated subtree cost کوئری، دقت داشت.
کوئریهایی را که در این قسمت بررسی خواهیم کرد، در ادامه ملاحظه میکنید. بار اول این کوئریها را بر روی xmlInvoice و بار دوم، بر روی نگارش دوم دارای اسکیمای آن اجرا خواهیم کرد:
کوئری 1
همانطور که عنوان شد، از منوی Query گزینهی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمههای Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده کرد.
در کوئری 1، با استفاده از متد exist به دنبال رکوردهایی هستیم که دارای ویژگی InvoiceId مساوی 1003 هستند. پس از اجرای کوئری، تصویر Query Plan آن به شکل زیر خواهد بود:
برای خواندن این تصویر، از بالا به پایین و چپ به راست باید عمل شود. هزینهی انجام کوئری را نیز با نگه داشتن کرسر ماوس بر روی select نهایی سمت چپ تصویر میتوان مشاهده کرد. البته باید درنظر داشت که این اعداد از دیدگاه Query Processor مفهوم پیدا میکنند. پردازشگر کوئری، بر اساس اطلاعاتی که در اختیار دارد، سعی میکند بهترین روش پردازش کوئری دریافتی را پیدا کند. برای اندازه گیری کارآیی، باید اندازه گیری زمان اجرای کوئری، مستقلا انجام شود.
در این کوئری، مطابق تصویر اول، ابتدا قسمت SQL آن (چپ بالای تصویر) پردازش میشود و سپس قسمت XML آن. قسمت XQuery این عبارت در دو قسمت سمت چپ، پایین تصویر مشخص شدهاند. Table valued functionها جاهایی هستند که node table ابتدای بحث جاری در آنها ساخته میشوند. در اینجا دو مرحلهی تولید Table valued functionها مشاهده میشود. اگر به جمع درصدهای آنها دقت کنید، هزینهی این دو قسمت، 98 درصد کل Query plan است.
سؤال: چرا دو مرحلهی تولید Table valued functionها در اینجا قابل مشاهده است؟ یک مرحلهی آن مربوط است به انتخاب نود Invoice و مرحلهی دوم مربوط است به فیلتر داخل [] ذکر شد برای یافتن ویژگیهای مساوی 1003.
در اینجا و در کوئریهای بعدی، هر Query Plan ایی که تعداد مراحل تولید Table valued function کمتری داشته باشد، بهینهتر است.
کوئری 5
اگر کوئری پلن شماره 5 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید. یک XML Reader برای خارج از [] (اصطلاحا به آن predicate گفته میشود) و دو مورد برای داخل [] تشکیل شدهاست؛ یکی برای انتخاب نود متنی و دیگری برای تساوی.
کوئری 7
اگر کوئری پلن شماره 7 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید که بسیار شبیه است به مورد 5. بنابراین در اینجا عمق بررسی و سلسله مراتب اهمیتی ندارد.
کوئری 9
کوئری 9 دقیقا معادل است با کوئری 1 نوشته شده؛ با این تفاوت که از روش FLOWR استفاده کردهاست. نکتهی جالب آن، وجود تنها یک XML reader در Query plan آن است که باید آنرا بخاطر داشت.
کوئری 2
کوئری 3
کوئری 4
کوئری 6
کوئری 8
اگر به این 5 کوئری یاد شده دقت کنید، از یک دات به معنای self استفاده کردهاند (یعنی پردازش بیشتری را انجام نده و از همین نود جاری برای پردازش نهایی استفاده کن). با توجه به بکارگیری متد exist، معنای کوئریهای یک و دو، یکیاست. اما در کوئری شماره 2، تنها یک XML Reader در Query plan نهایی وجود دارد (همانند عبارت FLOWR کوئری شماره 9).
یک نکته: اگر میخواهید بدانید بین کوئریهای 1 و 2 کدامیک بهتر عمل میکنند، از بین تمام کوئریهای موجود، دو کوئری یاد شده را انتخاب کرده و سپس با فرض روش بودن نمایش Query plan، هر دو کوئری را با هم اجرا کنید.
در این حالت، کوئری پلنهای هر دو کوئری را با هم یکجا میتوان مشاهده کرد؛ به علاوهی هزینهی نسبی آنها را در کل عملیات صورت گرفته. در حالت استفاده از دات و وجود تنها یک XML Reader، این هزینه تنها 6 درصد است، در مقابل هزینهی 94 درصدی کوئری شماره یک.
بنابراین از دیدگاه پردازشگر کوئریهای SQL Server، کوئری شماره 2، بسیار بهتر است از کوئری شماره 1.
در کوئریهای 3 و 4، شماره نود مدنظر را دقیقا مشخص کردهایم. این مورد در حالت سوم تفاوت محسوسی را از لحاظ کارآیی ایجاد نمیکند و حتی کارآیی را به علت اضافه کردن یک XML Reader دیگر برای پردازش عدد نود وارد شده، کاهش میدهد. اما کوئری 4 که عدد اولین نود را خارج از پرانتز قرار دادهاست، تنها در کل یک XML Reader را به همراه خواهد داشت.
سؤال: بین کوئریهای 2، 3 و 4 کدامیک بهینهتر است؟
بله. اگر هر سه کوئری را با هم انتخاب کرده و اجرا کنیم، میتوان در قسمت کوئری پلنها، هزینهی هر کدام را نسبت به کل مشاهده کرد. در این حالت کوئری 4 بهتر است از کوئری 2 و تنها یک درصد هزینهی کل را تشکیل میدهد.
کوئری 10
کوئری 10 اندکی متفاوت است نسبت به کوئریهای دیگر. در اینجا بجای متد exist از متد value استفاده شدهاست. یعنی ابتدا صریحا مقدار ویژگی InvoiceId استخراج شده و با 1003 مقایسه میشود.
اگر کوئری پلن آنرا با کوئری 4 که بهترین کوئری سری exist است مقایسه کنیم، کوئری 10، هزینهی 70 درصدی کل عملیات را به خود اختصاص خواهد داد، در مقابل 30 درصد هزینهی کوئری 4. بنابراین در این موارد، استفاده از متد exist بسیار بهینهتر است از متد value.
استفاده از Schema collection و تاثیر آن بر کارآیی
تمام مراحلی را که در اینجا ملاحظه کردید، صرفا با تغییر نام xmlInvoice به xmlInvoice2، تکرار کنید. xmlInvoice2 دارای ساختاری مشخص است، به همراه ذکر صریح document حین تعریف ستون XML ایی آن.
تمام پاسخهایی را که دریافت خواهید کرد با حالت بدون Schema collection یکی است.
برای مقایسه بهتر، یکبار نیز سعی کنید کوئری 1 جدول xmlInvoice را با کوئری 1 جدول xmlInvoice2 با هم در طی یک اجرا مقایسه کنید، تا بهتر بتوان Query plan نسبی آنها را بررسی کرد.
پس از این بررسی و مقایسه، به این نتیجه خواهید رسید که تفاوت محسوسی در اینجا و بین این دو حالت، قابل ملاحظه نیست. در SQL Server از Schema collection بیشتر برای اعتبارسنجی ورودیها استفاده میشود تا بهبود کارآیی کوئریها.
بنابراین به صورت خلاصه
- متد exist را به value ترجیح دهید.
- اصطلاحا ordinal (همان مشخص کردن نود 1 در اینجا) را در آخر قرار دهید (نه در بین نودها).
- مراحل اجرایی را با معرفی دات (استفاده از نود جاری) تا حد ممکن کاهش دهید.
و ... کوئری 4 در این سری، بهترین کارآیی را ارائه میدهد.
در SQL Server، کوئریهای انجام شده بر روی فیلدهای XML، توسط همان پردازشگر کوئریهای رابطهای متداول آن، خوانده و اجرا خواهند شد و امکان تعریف یک XQuery خارج از یک عبارت SQL و یا T-SQL وجود ندارد. متدهای XQuery بسیار شبیه به system defined functions بوده و Query Plan یکپارچهای را با سایر قسمتهای رابطهای یک عبارت SQL دارند.
مفهوم Node table
دادههای XML ایی برای اینکه توسط SQL Server قابل استفاده باشند، به صورت درونی تبدیل به یک node table میشوند. به این معنا که نودهای یک سند XML، به یک جدول رابطهای به صورت خودکار تجزیه میشوند. این جدول درونی در صورت بکارگیری XML Indexes در جدول سیستمی sys.internal_tables قابل مشاهده خواهد بود. SQL Server برای انجام اینکار از یک XmlReader خاص خودش استفاده میکند. در مورد XMLهای ایندکس نشده، این تجزیه در زمان اجرا صورت میگیرد؛ پس از اینکه Query Plan آن تشکیل شد.
بررسی Query Plan فیلدهای XML ایی
جهت فراهم کردن مقدمات آزمایش، ابتدا جدول xmlInvoice را با یک فیلد XML ایی untyped درنظر بگیرید:
CREATE TABLE xmlInvoice ( invoiceId INT IDENTITY PRIMARY KEY, invoice XML )
INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1000" dept="hardware"> <CustomerName>Vahid</CustomerName> <LineItems> <LineItem><Description>Gear</Description><Price>9.5</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1002" dept="garden"> <CustomerName>Mehdi</CustomerName> <LineItems> <LineItem><Description>Shovel</Description><Price>19.2</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1003" dept="garden"> <CustomerName>Mohsen</CustomerName> <LineItems> <LineItem><Description>Trellis</Description><Price>8.5</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1004" dept="hardware"> <CustomerName>Hamid</CustomerName> <LineItems> <LineItem><Description>Pen</Description><Price>1.5</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1005" dept="IT"> <CustomerName>Ali</CustomerName> <LineItems> <LineItem><Description>Book</Description><Price>3.2</Price></LineItem> </LineItems> </Invoice> ') INSERT INTO xmlInvoice VALUES(' <Invoice InvoiceId="1006" dept="hardware"> <CustomerName>Reza</CustomerName> <LineItems> <LineItem><Description>M.Board</Description><Price>19.5</Price></LineItem> </LineItems> </Invoice> ')
CREATE XML SCHEMA COLLECTION invoice_xsd AS ' <xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="Invoice"> <xs:complexType> <xs:sequence> <xs:element name="CustomerName" type="xs:string" /> <xs:element name="LineItems"> <xs:complexType> <xs:sequence> <xs:element name="LineItem"> <xs:complexType> <xs:sequence> <xs:element name="Description" type="xs:string" /> <xs:element name="Price" type="xs:decimal" /> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:sequence> <xs:attribute name="InvoiceId" type="xs:unsignedShort" use="required" /> <xs:attribute name="dept" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:schema>' Go CREATE TABLE xmlInvoice2 ( invoiceId INT IDENTITY PRIMARY KEY, invoice XML(document invoice_xsd) ) Go
در این جدول دوم، حالت پیش فرض content قبلی، به document تغییر کردهاست. با توجه به اینکه میدانیم اسناد ما چه فرمتی دارند و بیش از یک root element نخواهیم داشت، انتخاب document سبب خواهد شد تا Query Plan بهتری حاصل شود.
در ادامه برای مشاهدهی بهتر نتایج، کش Query Plan و اطلاعات آماری جدول xmlInvoice را حذف و به روز میکنیم:
UPDATE STATISTICS xmlInvoice DBCC FREEPROCCACHE
کوئریهایی را که در این قسمت بررسی خواهیم کرد، در ادامه ملاحظه میکنید. بار اول این کوئریها را بر روی xmlInvoice و بار دوم، بر روی نگارش دوم دارای اسکیمای آن اجرا خواهیم کرد:
-- query 1 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice[@InvoiceId = "1003"]') = 1 -- query 2 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice/@InvoiceId[. = "1003"]') = 1 -- query 3 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice[1]/@InvoiceId[. = "1003"]') = 1 -- query 4 SELECT * FROM xmlInvoice WHERE invoice.exist('(/Invoice/@InvoiceId)[1][. = "1003"]') = 1 -- query 5 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice[CustomerName = "Vahid"]') = 1 -- query 6 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice/CustomerName [.= "Vahid"]') = 1 -- query 7 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice/LineItems/LineItem[Description = "Trellis"]') = 1 -- query 8 SELECT * FROM xmlInvoice WHERE invoice.exist('/Invoice/LineItems/LineItem/Description [.= "Trellis"]') = 1 -- query 9 SELECT * FROM xmlInvoice WHERE invoice.exist(' for $x in /Invoice/@InvoiceId where $x = 1003 return $x ') = 1 -- query 10 SELECT * FROM xmlInvoice WHERE invoice.value('(/Invoice/@InvoiceId)[1]', 'VARCHAR(10)') = '1003' -- یکبار هم با جدول شماره 2 که اسکیما دارد تمام این موارد تکرار شود UPDATE STATISTICS xmlInvoice DBCC FREEPROCCACHE GO
کوئری 1
همانطور که عنوان شد، از منوی Query گزینهی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمههای Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده کرد.
در کوئری 1، با استفاده از متد exist به دنبال رکوردهایی هستیم که دارای ویژگی InvoiceId مساوی 1003 هستند. پس از اجرای کوئری، تصویر Query Plan آن به شکل زیر خواهد بود:
برای خواندن این تصویر، از بالا به پایین و چپ به راست باید عمل شود. هزینهی انجام کوئری را نیز با نگه داشتن کرسر ماوس بر روی select نهایی سمت چپ تصویر میتوان مشاهده کرد. البته باید درنظر داشت که این اعداد از دیدگاه Query Processor مفهوم پیدا میکنند. پردازشگر کوئری، بر اساس اطلاعاتی که در اختیار دارد، سعی میکند بهترین روش پردازش کوئری دریافتی را پیدا کند. برای اندازه گیری کارآیی، باید اندازه گیری زمان اجرای کوئری، مستقلا انجام شود.
در این کوئری، مطابق تصویر اول، ابتدا قسمت SQL آن (چپ بالای تصویر) پردازش میشود و سپس قسمت XML آن. قسمت XQuery این عبارت در دو قسمت سمت چپ، پایین تصویر مشخص شدهاند. Table valued functionها جاهایی هستند که node table ابتدای بحث جاری در آنها ساخته میشوند. در اینجا دو مرحلهی تولید Table valued functionها مشاهده میشود. اگر به جمع درصدهای آنها دقت کنید، هزینهی این دو قسمت، 98 درصد کل Query plan است.
سؤال: چرا دو مرحلهی تولید Table valued functionها در اینجا قابل مشاهده است؟ یک مرحلهی آن مربوط است به انتخاب نود Invoice و مرحلهی دوم مربوط است به فیلتر داخل [] ذکر شد برای یافتن ویژگیهای مساوی 1003.
در اینجا و در کوئریهای بعدی، هر Query Plan ایی که تعداد مراحل تولید Table valued function کمتری داشته باشد، بهینهتر است.
کوئری 5
اگر کوئری پلن شماره 5 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید. یک XML Reader برای خارج از [] (اصطلاحا به آن predicate گفته میشود) و دو مورد برای داخل [] تشکیل شدهاست؛ یکی برای انتخاب نود متنی و دیگری برای تساوی.
کوئری 7
اگر کوئری پلن شماره 7 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید که بسیار شبیه است به مورد 5. بنابراین در اینجا عمق بررسی و سلسله مراتب اهمیتی ندارد.
کوئری 9
کوئری 9 دقیقا معادل است با کوئری 1 نوشته شده؛ با این تفاوت که از روش FLOWR استفاده کردهاست. نکتهی جالب آن، وجود تنها یک XML reader در Query plan آن است که باید آنرا بخاطر داشت.
کوئری 2
کوئری 3
کوئری 4
کوئری 6
کوئری 8
اگر به این 5 کوئری یاد شده دقت کنید، از یک دات به معنای self استفاده کردهاند (یعنی پردازش بیشتری را انجام نده و از همین نود جاری برای پردازش نهایی استفاده کن). با توجه به بکارگیری متد exist، معنای کوئریهای یک و دو، یکیاست. اما در کوئری شماره 2، تنها یک XML Reader در Query plan نهایی وجود دارد (همانند عبارت FLOWR کوئری شماره 9).
یک نکته: اگر میخواهید بدانید بین کوئریهای 1 و 2 کدامیک بهتر عمل میکنند، از بین تمام کوئریهای موجود، دو کوئری یاد شده را انتخاب کرده و سپس با فرض روش بودن نمایش Query plan، هر دو کوئری را با هم اجرا کنید.
در این حالت، کوئری پلنهای هر دو کوئری را با هم یکجا میتوان مشاهده کرد؛ به علاوهی هزینهی نسبی آنها را در کل عملیات صورت گرفته. در حالت استفاده از دات و وجود تنها یک XML Reader، این هزینه تنها 6 درصد است، در مقابل هزینهی 94 درصدی کوئری شماره یک.
بنابراین از دیدگاه پردازشگر کوئریهای SQL Server، کوئری شماره 2، بسیار بهتر است از کوئری شماره 1.
در کوئریهای 3 و 4، شماره نود مدنظر را دقیقا مشخص کردهایم. این مورد در حالت سوم تفاوت محسوسی را از لحاظ کارآیی ایجاد نمیکند و حتی کارآیی را به علت اضافه کردن یک XML Reader دیگر برای پردازش عدد نود وارد شده، کاهش میدهد. اما کوئری 4 که عدد اولین نود را خارج از پرانتز قرار دادهاست، تنها در کل یک XML Reader را به همراه خواهد داشت.
سؤال: بین کوئریهای 2، 3 و 4 کدامیک بهینهتر است؟
بله. اگر هر سه کوئری را با هم انتخاب کرده و اجرا کنیم، میتوان در قسمت کوئری پلنها، هزینهی هر کدام را نسبت به کل مشاهده کرد. در این حالت کوئری 4 بهتر است از کوئری 2 و تنها یک درصد هزینهی کل را تشکیل میدهد.
کوئری 10
کوئری 10 اندکی متفاوت است نسبت به کوئریهای دیگر. در اینجا بجای متد exist از متد value استفاده شدهاست. یعنی ابتدا صریحا مقدار ویژگی InvoiceId استخراج شده و با 1003 مقایسه میشود.
اگر کوئری پلن آنرا با کوئری 4 که بهترین کوئری سری exist است مقایسه کنیم، کوئری 10، هزینهی 70 درصدی کل عملیات را به خود اختصاص خواهد داد، در مقابل 30 درصد هزینهی کوئری 4. بنابراین در این موارد، استفاده از متد exist بسیار بهینهتر است از متد value.
استفاده از Schema collection و تاثیر آن بر کارآیی
تمام مراحلی را که در اینجا ملاحظه کردید، صرفا با تغییر نام xmlInvoice به xmlInvoice2، تکرار کنید. xmlInvoice2 دارای ساختاری مشخص است، به همراه ذکر صریح document حین تعریف ستون XML ایی آن.
تمام پاسخهایی را که دریافت خواهید کرد با حالت بدون Schema collection یکی است.
برای مقایسه بهتر، یکبار نیز سعی کنید کوئری 1 جدول xmlInvoice را با کوئری 1 جدول xmlInvoice2 با هم در طی یک اجرا مقایسه کنید، تا بهتر بتوان Query plan نسبی آنها را بررسی کرد.
پس از این بررسی و مقایسه، به این نتیجه خواهید رسید که تفاوت محسوسی در اینجا و بین این دو حالت، قابل ملاحظه نیست. در SQL Server از Schema collection بیشتر برای اعتبارسنجی ورودیها استفاده میشود تا بهبود کارآیی کوئریها.
بنابراین به صورت خلاصه
- متد exist را به value ترجیح دهید.
- اصطلاحا ordinal (همان مشخص کردن نود 1 در اینجا) را در آخر قرار دهید (نه در بین نودها).
- مراحل اجرایی را با معرفی دات (استفاده از نود جاری) تا حد ممکن کاهش دهید.
و ... کوئری 4 در این سری، بهترین کارآیی را ارائه میدهد.
هدف از این مبحث، آشنایی با مفاهیم پایهای اغلب بانکهای اطلاعاتی NoSQL است که به صورت مشترکی در تمام آنها بکار رفته است. برای مثال بانکهای اطلاعاتی NoSQL چگونه مباحث یکپارچگی اطلاعات را مدیریت میکنند؟ نحوه ایندکس نمودن اطلاعات در آنها چگونه است؟ چگونه از اطلاعات کوئری میگیرند؟ الگوریتمهای محاسباتی مانند MapReduce چیستند و چگونه در اینگونه بانکهای اطلاعاتی بکار رفتهاند؟ همچنین الگوهای Sharding و Partitioning که در اغلب بانکهای اطلاعاتی NoSQL مشترکند، به چه نحوی پیاده سازی شدهاند.
لیست مشترکات بانکهای اطلاعاتی NoSQL
قبل از اینکه بخواهیم وارد ریز جزئیات بانکهای اطلاعاتی NoSQL شویم، نیاز است لیست و سرفصلی از مفاهیم اصلی و مشترک بین اینگونه بانکهای اطلاعاتی را تدارک ببینیم که شامل موارد ذیل میشود:
الف) Non-Relational یا غیر رابطهای
از کلمه NoSQL عموما اینطور برداشت میشود که در اینجا دیگر خبری از SQL نویسی نیست که در عمل برداشت نادرستی است. شاید جالب باشد که بدانید، تعدادی از بانکهای اطلاعاتی NoSQL از زبان SQL نیز به عنوان اینترفیسی برای نوشتن کوئریهای مرتبط، پشتیبانی میکنند.
کلمه NoSQL بیشتر به Non-Relational یا غیر رابطهای بودن اینگونه بانکهای اطلاعاتی بر میگردد. مباحثی مانند مدلهای دادهای نرمال شده، اتصالات و Join جداول، در دنیای NoSQL وجود خارجی ندارند.
ب) Non-schematized/schema free یا بدون اسکیما
مفهوم مهم و مشترک دیگری که در بین بانکهای اطلاعاتی NoSQL وجود دارد، بدون اسکیما بودن اطلاعات آنها است. به این معنا که با حرکت از رکورد یک به رکورد دو، ممکن است با دو ساختار دادهای متفاوت مواجه شوید.
ج) Eventual consistency یا عاقبت یک دست شدن
عاقبت یک دست شدن، به معنای دریافت دستوری از شما و نحوه پاسخ دادن به آن (یا حتی پاسخ ندادن به آن) از طرف بانک اطلاعاتی NoSQL است. برای مثال، زمانیکه یک رکورد جدید را اضافه میکنید، یا اطلاعات موجودی را به روز رسانی خواهید کرد، اغلب بانکهای اطلاعاتی NoSQL این دستور را بسیار سریع دریافت و پردازش خواهند کرد. اما تفاوت است بین دریافت پیام و پردازش واقعی آن در اینجا.
اکثر بانکهای اطلاعاتی NoSQL، پردازش و اعمال واقعی دستورات دریافتی را با یک تاخیر انجام میدهند. به این ترتیب میتوان خیلی سریع به بانک اطلاعاتی اعلام کرد که چه میخواهیم و بانک اطلاعاتی بلافاصله مجددا کنترل را به شما بازخواهد گرداند. اما اعمال و انتشار واقعی این دستور، مدتی زمان خواهد برد.
د) Open source یا منبع باز بودن
اغلب بانکهای اطلاعاتی NoSQL موجود، منبع باز هستند که علاوه بر بهره بردن از مزایای اینگونه پروژهها، استفاده کنندگان سورس باز دیگری را نیز ترغیب به استفاده از آنها کردهاند.
ه) Distributed یا توزیع شده
هرچند امکان پیاده سازی توزیع شده بانکهای اطلاعاتی رابطهای نیز وجود دارد، اما نیاز به تنظیمات قابل توجهی برای حصول این امر میباشد. در دنیای NoSQL، توزیع شده بودن جزئی از استاندارد تهیه اینگونه بانکهای اطلاعاتی است و بر اساس این مدل ذهنی شکل گرفتهاند. به این معنا که اطلاعات را میتوان بین چندین سیستم تقسیم کرد، که حتی این سیستمها ممکن است فواصل جغرافیایی قابل توجهی نیز با یکدیگر داشته باشند.
و) Web scale یا مناسب برای برنامههای تحت وب پر کاربر
امروزه بسیاری از کمپانیهای بزرگ اینترنتی، برای مدیریت تعداد بالایی از کاربران همزمان خود، مانند فیسبوک، یاهو، گوگل، Linkedin، مایکروسافت و غیره، نیاز به بانکهای اطلاعاتی پیدا کردهاند که باید در مقابل این حجم عظیم درخواستها و همچنین اطلاعاتی که دارند، بسیار بسیار سریع پاسخ دهند. به همین جهت بانکهای اطلاعاتی NoSQL ابداع شدهاند تا بتوان برای این نوع سناریوها پاسخی را ارائه داد.
و نکته مهم دیگر اینجا است که خود این کمپانیهای بزرگ اینترنتی، بزرگترین توسعه دهندههای بانکهای اطلاعاتی NoSQL نیز هستند.
نحوه مدیریت یکپارچگی اطلاعات در بانکهای اطلاعاتی NoSQL
مدیریت یکپارچگی اطلاعات بانکهای اطلاعاتی NoSQL به علت ذات و طراحی توزیع شده آنها، با نحوه مدیریت یکپارچگی اطلاعات بانکهای اطلاعاتی رابطهای متفاوت است. اینجا است که تئوری خاصی به نام CAP مطرح میشود که شامل یکپارچگی یا Consistency به همراه Availability یا دسترسی پذیری (همیشه برقرار بودن) و partition tolerance یا توزیع پذیری است. در تئوری CAP مطرح میشود که هر بانک اطلاعاتی خاص، تنها دو مورد از سه مورد مطرح شده را میتواند با هم پوشش دهد.
به این ترتیب بانکهای اطلاعاتی رابطهای عموما دو مورد C و P یا یکپارچگی (Consistency) و partition tolerance یا میزان تحمل تقسیم شدن اطلاعات را ارائه میدهند. اما بانکهای اطلاعاتی NoSQL از این تئوری، تنها دو مورد A و P را پوشش میدهند (دسترسی پذیری و توزیع پذیری مطلوب).
بنابراین مفهومی به نام ACID که در بانکهای اطلاعاتی رابطهای ضامن یکپارچگی اطلاعات آنها است، در دنیای NoSQL وجود خارجی ندارد. کلمه ACID مخفف موارد ذیل است:
Atomicity، Consistency، Isolation و Durability
ACID در بانکهای اطلاعاتی رابطهای تضمین شده است. در این نوع سیستمها، با ایجاد تراکنشها، مباحث ایزوله سازی و یکپارچگی اطلاعات به نحو مطلوبی مدیریت میگردد؛ اما دنیای NoSQL، دسترسی پذیری را به یکپارچگی ترجیح داده است و به همین جهت پیشتر مطرح شد که مفهوم «Eventual consistency یا عاقبت یک دست شدن» در این نوع بانکهای اطلاعاتی در پشت صحنه بکار گرفته میشود. یک مثال دنیای واقعی از عاقبت یک دست شدن اطلاعات را حتما در مباحث DNS مطالعه کردهاید. زمانیکه یک رکورد DNS اضافه میشود یا به روز خواهد شد، اعمال این دستورات در سراسر دنیا به یکباره و همزمان نیست. هرچند اعمال این اطلاعات جدید در یک نود شبکه ممکن است آنی باشد، اما پخش و توزیع آن در سراسر سرورهای DNS دنیا، مدتی زمان خواهد برد (گاهی تا یک روز یا بیشتر).
به همین جهت است که بانکهای اطلاعاتی رابطهای در حجمهای عظیم اطلاعات و تعداد کاربران همزمان بالا، کند عمل میکنند. حجم اطلاعات بالا است، مدتی زمان خواهد برد تا تغییرات اعمال شوند، و چون مفهوم ACID در این نوع بانکهای اطلاعاتی تضمین شده است، کاربران باید مدتی منتظر بمانند و نمونهای از آنها را با dead lockهای شایع، احتمالا پیشتر بررسی یا تجربه کردهاید. در مقابل، بانکهای اطلاعاتی NoSQL بجای یکپارچگی، دسترسی پذیری را اولویت اول خود میدانند و نه یکپارچگی اطلاعات را. در یک بانک اطلاعاتی NoSQL، دستور ثبت اطلاعات دریافت میشود (این مرحله آنی است)، اما اعمال نهایی آن آنی نیست و مدتی زمان خواهد برد تا تمام اطلاعات در کلیه سرورها یک دست شوند.
نحوه مدیریت Indexing اطلاعات در بانکهای اطلاعاتی NoSQL
اغلب بانکهای اطلاعاتی NoSQL تنها بر اساس اطلاعات کلیدهای اصلی جداول آنها index میشوند (البته نام خاصی به نام «جدول»، بسته به نوع بانک اطلاعاتی NoSQL ممکن است متفاوت باشد، اما منظور ظرف دربرگیرنده تعدادی رکورد است در اینجا). این ایندکس نیز از نوع clustered است. به این معنا که اطلاعات به صورت فیزیکی، بر همین مبنا ذخیره و مرتب خواهند شد.
یک مثال: بانک اطلاعاتی NoSQL خاصی به نام Hbase که بر فراز Hadoop distributed file system طراحی شده است، دقیقا به همین روش عمل میکند. این فایل سیستم، تنها از روش Append only برای ذخیره سازی اطلاعات استفاده میکند و در آن مفهوم دسترسی اتفاقی یا random access پیاده سازی نشده است. در این حالت، تمام نوشتنها در بافر، لاگ میشوند و در بازههای زمانی متناوب و مشخصی سبب باز تولید فایلهای موجود و مرتب سازی مجدد آنها از ابتدا خواهند شد. دسترسی به این اطلاعات پس از تکمیل نوشتن، به علت مرتب سازی فیزیکی که صورت گرفته، بسیار سریع است. همچنین مصرف کننده سیستم نیز چون بلافاصله پس از ثبت اطلاعات در بافر سیستم، کنترل را به دست میگیرد، احساس کار با سیستمی را خواهد داشت که بسیار سریع است.
به علاوه Indexهای دیگری نیز وجود دارند که بر اساس کلیدهای اصلی جداول تولید نمیشوند و به آنها ایندکسهای ثانویه یا secondary indexes نیز گفته میشود و تنها تعداد محدودی از بانکهای اطلاعاتی NoSQL از آنها پشتیبانی میکنند. این مساله هم از اینجا ناشی میشود که با توجه به بدون اسکیما بودن جداول بانکهای اطلاعاتی NoSQL، چگونه میتوان اطلاعاتی را ایندکس کرد که ممکن است در رکورد دیگری، ساختار متناظر با آن اصلا وجود خارجی نداشته باشد.
نحوه پردازش Queries در بانکهای اطلاعاتی NoSQL
بانکهای اطلاعاتی NoSQL عموما از زبان کوئری خاصی پشتیبانی نمیکنند. در اینجا باید به اطلاعات به شکل فایلهایی که حاوی رکوردها هستند نگاه کرد. به این ترتیب برای پردازش و یافتن اطلاعات درون این فایلها، نیاز به ایجاد برنامههایی است که این فایلها را گشوده و بر اساس منطق خاصی، اطلاعات مورد نظر را استخراج کنند. گاهی از اوقات زبان SQL نیز پشتیبانی میشود ولی آنچنان عمومیت ندارد. الگوریتمی که در این برنامهها بکار گرفته میشود، Map Reduce نام دارد.
Map Reduce به معنای نوشتن کدی است، با دو تابع. اولین تابع اصطلاحا Map step یا مرحله نگاشت نام دارد. در این مرحله کوئری به قسمتهای کوچکتری خرد شده و بر روی سیستمهای توزیع شده به صورت موازی اجرا میشود. مرحله بعد Reduce step نام دارد که در آن، نتیجه دریافتی حاصل از کوئریهای اجرا شده بر روی سیستمهای مختلف، با هم یکی خواهند شد.
این روش برای نمونه در سیستم Hadoop بسیار مرسوم است. Hadoop دارای یک فایل سیستم توزیع شده است (که پیشتر در مورد آن بحث شد) به همراه یک موتور Map Reduce توکار. همچنین رده دیگری از بانکهای اطلاعاتی NoSQL، اصطلاحا Wide column store نام دارند (مانند Hbase) که عموما به همراه Hadoop بکارگرفته میشوند. موتور Map Reduce متعلق به Hadoop بر روی جداول Hbase اجرا میشوند.
به علاوه Amazon web services دارای سرویسی است به نام Elastic map reduce یا EMR که در حقیقت مجموعهی پردازش ابری است که بر مبنای Hadoop کار میکند. این سرویس قادر است با بانکهای اطلاعاتی NoSQL دیگر و یا حتی بانکهای اطلاعاتی رابطهای نیز کار کند.
بنابراین MapReduce، یک بانک اطلاعاتی نیست؛ بلکه یک روش پردازش اطلاعات است که فایلها را به عنوان ورودی دریافت کرده و یک فایل را به عنوان خروجی تولید میکند. از آنجائیکه بسیاری از بانکهای اطلاعاتی NoSQL کار عمدهاشان، ایجاد و تغییر فایلها است، اغلب جداول اطلاعات آنها ورودی و خروجیهای معتبری برای یک موتور Map reduce به حساب میآیند.
در این بین، افزونهای برای Hadoop به نام Hive طراحی شده است که با ارائه HiveSQL، امکان نوشتن کوئریهایی SQL مانند را بر فراز موتورهای Map reduce ممکن میسازد. این افزونه با Hive tables خاص خودش و یا با Hbase سازگار است.
آشنایی مقدماتی با مفاهیمی مانند الگوهای Sharding و Partitioning در بانکهای اطلاعاتی NoSQL
Sharding (شاردینگ تلفظ میشود) یک الگوی تقسیم اطلاعات بر روی چندین سرور است که اساس توزیع شده بودن بانکهای اطلاعاتی NoSQL را تشکیل میدهد. این نوع تقسیم اطلاعات، از کوئریهایی به نام Fan-out پشتیبانی میکند. به این معنا که شما کوئری خود را به نود اصلی ارسال میکنید و سپس به کمک موتورهای Map reduce، این کوئری بر روی سرورهای مختلف اجرا شده و نتیجه نهایی جمع آوری خواهد شد. به این ترتیب تقسیم اطلاعات، صرفا به معنای قرار دادن یک سری فایل بر روی سرورهای مختلف نیست، بلکه هر کدام از این سرورها به صورت مستقل نیز قابلیت پردازش اطلاعات را دارند.
امکان تکثیر و همچنین replication هر کدام از سرورها نیز وجود دارد که قابلیت بازیابی سریع و مقاومت در برابر خرابیها و مشکلات را افزایش میدهند.
از آنجائیکه Shardها را میتوان در سرورهای بسیار متفاوت و گستردهای از لحاظ جغرافیایی قرار داد، هر Shard میتواند همانند مفاهیم CDN نیز عمل کند؛ به این معنا که میتوان Shard مورد نیاز سروری خاص را در محلی نزدیکتر به او قرار داد. به این ترتیب سرعت عملیات افزایش یافته و همچنین بار شبکه نیز کاهش مییابد.
لیست مشترکات بانکهای اطلاعاتی NoSQL
قبل از اینکه بخواهیم وارد ریز جزئیات بانکهای اطلاعاتی NoSQL شویم، نیاز است لیست و سرفصلی از مفاهیم اصلی و مشترک بین اینگونه بانکهای اطلاعاتی را تدارک ببینیم که شامل موارد ذیل میشود:
الف) Non-Relational یا غیر رابطهای
از کلمه NoSQL عموما اینطور برداشت میشود که در اینجا دیگر خبری از SQL نویسی نیست که در عمل برداشت نادرستی است. شاید جالب باشد که بدانید، تعدادی از بانکهای اطلاعاتی NoSQL از زبان SQL نیز به عنوان اینترفیسی برای نوشتن کوئریهای مرتبط، پشتیبانی میکنند.
کلمه NoSQL بیشتر به Non-Relational یا غیر رابطهای بودن اینگونه بانکهای اطلاعاتی بر میگردد. مباحثی مانند مدلهای دادهای نرمال شده، اتصالات و Join جداول، در دنیای NoSQL وجود خارجی ندارند.
ب) Non-schematized/schema free یا بدون اسکیما
مفهوم مهم و مشترک دیگری که در بین بانکهای اطلاعاتی NoSQL وجود دارد، بدون اسکیما بودن اطلاعات آنها است. به این معنا که با حرکت از رکورد یک به رکورد دو، ممکن است با دو ساختار دادهای متفاوت مواجه شوید.
ج) Eventual consistency یا عاقبت یک دست شدن
عاقبت یک دست شدن، به معنای دریافت دستوری از شما و نحوه پاسخ دادن به آن (یا حتی پاسخ ندادن به آن) از طرف بانک اطلاعاتی NoSQL است. برای مثال، زمانیکه یک رکورد جدید را اضافه میکنید، یا اطلاعات موجودی را به روز رسانی خواهید کرد، اغلب بانکهای اطلاعاتی NoSQL این دستور را بسیار سریع دریافت و پردازش خواهند کرد. اما تفاوت است بین دریافت پیام و پردازش واقعی آن در اینجا.
اکثر بانکهای اطلاعاتی NoSQL، پردازش و اعمال واقعی دستورات دریافتی را با یک تاخیر انجام میدهند. به این ترتیب میتوان خیلی سریع به بانک اطلاعاتی اعلام کرد که چه میخواهیم و بانک اطلاعاتی بلافاصله مجددا کنترل را به شما بازخواهد گرداند. اما اعمال و انتشار واقعی این دستور، مدتی زمان خواهد برد.
د) Open source یا منبع باز بودن
اغلب بانکهای اطلاعاتی NoSQL موجود، منبع باز هستند که علاوه بر بهره بردن از مزایای اینگونه پروژهها، استفاده کنندگان سورس باز دیگری را نیز ترغیب به استفاده از آنها کردهاند.
ه) Distributed یا توزیع شده
هرچند امکان پیاده سازی توزیع شده بانکهای اطلاعاتی رابطهای نیز وجود دارد، اما نیاز به تنظیمات قابل توجهی برای حصول این امر میباشد. در دنیای NoSQL، توزیع شده بودن جزئی از استاندارد تهیه اینگونه بانکهای اطلاعاتی است و بر اساس این مدل ذهنی شکل گرفتهاند. به این معنا که اطلاعات را میتوان بین چندین سیستم تقسیم کرد، که حتی این سیستمها ممکن است فواصل جغرافیایی قابل توجهی نیز با یکدیگر داشته باشند.
و) Web scale یا مناسب برای برنامههای تحت وب پر کاربر
امروزه بسیاری از کمپانیهای بزرگ اینترنتی، برای مدیریت تعداد بالایی از کاربران همزمان خود، مانند فیسبوک، یاهو، گوگل، Linkedin، مایکروسافت و غیره، نیاز به بانکهای اطلاعاتی پیدا کردهاند که باید در مقابل این حجم عظیم درخواستها و همچنین اطلاعاتی که دارند، بسیار بسیار سریع پاسخ دهند. به همین جهت بانکهای اطلاعاتی NoSQL ابداع شدهاند تا بتوان برای این نوع سناریوها پاسخی را ارائه داد.
و نکته مهم دیگر اینجا است که خود این کمپانیهای بزرگ اینترنتی، بزرگترین توسعه دهندههای بانکهای اطلاعاتی NoSQL نیز هستند.
نحوه مدیریت یکپارچگی اطلاعات در بانکهای اطلاعاتی NoSQL
مدیریت یکپارچگی اطلاعات بانکهای اطلاعاتی NoSQL به علت ذات و طراحی توزیع شده آنها، با نحوه مدیریت یکپارچگی اطلاعات بانکهای اطلاعاتی رابطهای متفاوت است. اینجا است که تئوری خاصی به نام CAP مطرح میشود که شامل یکپارچگی یا Consistency به همراه Availability یا دسترسی پذیری (همیشه برقرار بودن) و partition tolerance یا توزیع پذیری است. در تئوری CAP مطرح میشود که هر بانک اطلاعاتی خاص، تنها دو مورد از سه مورد مطرح شده را میتواند با هم پوشش دهد.
به این ترتیب بانکهای اطلاعاتی رابطهای عموما دو مورد C و P یا یکپارچگی (Consistency) و partition tolerance یا میزان تحمل تقسیم شدن اطلاعات را ارائه میدهند. اما بانکهای اطلاعاتی NoSQL از این تئوری، تنها دو مورد A و P را پوشش میدهند (دسترسی پذیری و توزیع پذیری مطلوب).
بنابراین مفهومی به نام ACID که در بانکهای اطلاعاتی رابطهای ضامن یکپارچگی اطلاعات آنها است، در دنیای NoSQL وجود خارجی ندارد. کلمه ACID مخفف موارد ذیل است:
Atomicity، Consistency، Isolation و Durability
ACID در بانکهای اطلاعاتی رابطهای تضمین شده است. در این نوع سیستمها، با ایجاد تراکنشها، مباحث ایزوله سازی و یکپارچگی اطلاعات به نحو مطلوبی مدیریت میگردد؛ اما دنیای NoSQL، دسترسی پذیری را به یکپارچگی ترجیح داده است و به همین جهت پیشتر مطرح شد که مفهوم «Eventual consistency یا عاقبت یک دست شدن» در این نوع بانکهای اطلاعاتی در پشت صحنه بکار گرفته میشود. یک مثال دنیای واقعی از عاقبت یک دست شدن اطلاعات را حتما در مباحث DNS مطالعه کردهاید. زمانیکه یک رکورد DNS اضافه میشود یا به روز خواهد شد، اعمال این دستورات در سراسر دنیا به یکباره و همزمان نیست. هرچند اعمال این اطلاعات جدید در یک نود شبکه ممکن است آنی باشد، اما پخش و توزیع آن در سراسر سرورهای DNS دنیا، مدتی زمان خواهد برد (گاهی تا یک روز یا بیشتر).
به همین جهت است که بانکهای اطلاعاتی رابطهای در حجمهای عظیم اطلاعات و تعداد کاربران همزمان بالا، کند عمل میکنند. حجم اطلاعات بالا است، مدتی زمان خواهد برد تا تغییرات اعمال شوند، و چون مفهوم ACID در این نوع بانکهای اطلاعاتی تضمین شده است، کاربران باید مدتی منتظر بمانند و نمونهای از آنها را با dead lockهای شایع، احتمالا پیشتر بررسی یا تجربه کردهاید. در مقابل، بانکهای اطلاعاتی NoSQL بجای یکپارچگی، دسترسی پذیری را اولویت اول خود میدانند و نه یکپارچگی اطلاعات را. در یک بانک اطلاعاتی NoSQL، دستور ثبت اطلاعات دریافت میشود (این مرحله آنی است)، اما اعمال نهایی آن آنی نیست و مدتی زمان خواهد برد تا تمام اطلاعات در کلیه سرورها یک دست شوند.
نحوه مدیریت Indexing اطلاعات در بانکهای اطلاعاتی NoSQL
اغلب بانکهای اطلاعاتی NoSQL تنها بر اساس اطلاعات کلیدهای اصلی جداول آنها index میشوند (البته نام خاصی به نام «جدول»، بسته به نوع بانک اطلاعاتی NoSQL ممکن است متفاوت باشد، اما منظور ظرف دربرگیرنده تعدادی رکورد است در اینجا). این ایندکس نیز از نوع clustered است. به این معنا که اطلاعات به صورت فیزیکی، بر همین مبنا ذخیره و مرتب خواهند شد.
یک مثال: بانک اطلاعاتی NoSQL خاصی به نام Hbase که بر فراز Hadoop distributed file system طراحی شده است، دقیقا به همین روش عمل میکند. این فایل سیستم، تنها از روش Append only برای ذخیره سازی اطلاعات استفاده میکند و در آن مفهوم دسترسی اتفاقی یا random access پیاده سازی نشده است. در این حالت، تمام نوشتنها در بافر، لاگ میشوند و در بازههای زمانی متناوب و مشخصی سبب باز تولید فایلهای موجود و مرتب سازی مجدد آنها از ابتدا خواهند شد. دسترسی به این اطلاعات پس از تکمیل نوشتن، به علت مرتب سازی فیزیکی که صورت گرفته، بسیار سریع است. همچنین مصرف کننده سیستم نیز چون بلافاصله پس از ثبت اطلاعات در بافر سیستم، کنترل را به دست میگیرد، احساس کار با سیستمی را خواهد داشت که بسیار سریع است.
به علاوه Indexهای دیگری نیز وجود دارند که بر اساس کلیدهای اصلی جداول تولید نمیشوند و به آنها ایندکسهای ثانویه یا secondary indexes نیز گفته میشود و تنها تعداد محدودی از بانکهای اطلاعاتی NoSQL از آنها پشتیبانی میکنند. این مساله هم از اینجا ناشی میشود که با توجه به بدون اسکیما بودن جداول بانکهای اطلاعاتی NoSQL، چگونه میتوان اطلاعاتی را ایندکس کرد که ممکن است در رکورد دیگری، ساختار متناظر با آن اصلا وجود خارجی نداشته باشد.
نحوه پردازش Queries در بانکهای اطلاعاتی NoSQL
بانکهای اطلاعاتی NoSQL عموما از زبان کوئری خاصی پشتیبانی نمیکنند. در اینجا باید به اطلاعات به شکل فایلهایی که حاوی رکوردها هستند نگاه کرد. به این ترتیب برای پردازش و یافتن اطلاعات درون این فایلها، نیاز به ایجاد برنامههایی است که این فایلها را گشوده و بر اساس منطق خاصی، اطلاعات مورد نظر را استخراج کنند. گاهی از اوقات زبان SQL نیز پشتیبانی میشود ولی آنچنان عمومیت ندارد. الگوریتمی که در این برنامهها بکار گرفته میشود، Map Reduce نام دارد.
Map Reduce به معنای نوشتن کدی است، با دو تابع. اولین تابع اصطلاحا Map step یا مرحله نگاشت نام دارد. در این مرحله کوئری به قسمتهای کوچکتری خرد شده و بر روی سیستمهای توزیع شده به صورت موازی اجرا میشود. مرحله بعد Reduce step نام دارد که در آن، نتیجه دریافتی حاصل از کوئریهای اجرا شده بر روی سیستمهای مختلف، با هم یکی خواهند شد.
این روش برای نمونه در سیستم Hadoop بسیار مرسوم است. Hadoop دارای یک فایل سیستم توزیع شده است (که پیشتر در مورد آن بحث شد) به همراه یک موتور Map Reduce توکار. همچنین رده دیگری از بانکهای اطلاعاتی NoSQL، اصطلاحا Wide column store نام دارند (مانند Hbase) که عموما به همراه Hadoop بکارگرفته میشوند. موتور Map Reduce متعلق به Hadoop بر روی جداول Hbase اجرا میشوند.
به علاوه Amazon web services دارای سرویسی است به نام Elastic map reduce یا EMR که در حقیقت مجموعهی پردازش ابری است که بر مبنای Hadoop کار میکند. این سرویس قادر است با بانکهای اطلاعاتی NoSQL دیگر و یا حتی بانکهای اطلاعاتی رابطهای نیز کار کند.
بنابراین MapReduce، یک بانک اطلاعاتی نیست؛ بلکه یک روش پردازش اطلاعات است که فایلها را به عنوان ورودی دریافت کرده و یک فایل را به عنوان خروجی تولید میکند. از آنجائیکه بسیاری از بانکهای اطلاعاتی NoSQL کار عمدهاشان، ایجاد و تغییر فایلها است، اغلب جداول اطلاعات آنها ورودی و خروجیهای معتبری برای یک موتور Map reduce به حساب میآیند.
در این بین، افزونهای برای Hadoop به نام Hive طراحی شده است که با ارائه HiveSQL، امکان نوشتن کوئریهایی SQL مانند را بر فراز موتورهای Map reduce ممکن میسازد. این افزونه با Hive tables خاص خودش و یا با Hbase سازگار است.
آشنایی مقدماتی با مفاهیمی مانند الگوهای Sharding و Partitioning در بانکهای اطلاعاتی NoSQL
Sharding (شاردینگ تلفظ میشود) یک الگوی تقسیم اطلاعات بر روی چندین سرور است که اساس توزیع شده بودن بانکهای اطلاعاتی NoSQL را تشکیل میدهد. این نوع تقسیم اطلاعات، از کوئریهایی به نام Fan-out پشتیبانی میکند. به این معنا که شما کوئری خود را به نود اصلی ارسال میکنید و سپس به کمک موتورهای Map reduce، این کوئری بر روی سرورهای مختلف اجرا شده و نتیجه نهایی جمع آوری خواهد شد. به این ترتیب تقسیم اطلاعات، صرفا به معنای قرار دادن یک سری فایل بر روی سرورهای مختلف نیست، بلکه هر کدام از این سرورها به صورت مستقل نیز قابلیت پردازش اطلاعات را دارند.
امکان تکثیر و همچنین replication هر کدام از سرورها نیز وجود دارد که قابلیت بازیابی سریع و مقاومت در برابر خرابیها و مشکلات را افزایش میدهند.
از آنجائیکه Shardها را میتوان در سرورهای بسیار متفاوت و گستردهای از لحاظ جغرافیایی قرار داد، هر Shard میتواند همانند مفاهیم CDN نیز عمل کند؛ به این معنا که میتوان Shard مورد نیاز سروری خاص را در محلی نزدیکتر به او قرار داد. به این ترتیب سرعت عملیات افزایش یافته و همچنین بار شبکه نیز کاهش مییابد.
موقع اجرای دستور ALTER DATABASE SQL2014_Demo
ADD FILEGROUP MemFG CONTAINS MEMORY_OPTIMIZED_DATA
GO
این خطا رو میده لطفا راهنمایم کنید
Msg 534, Level 15, State 72, Line 37
'FILEGROUP ... CONTAINS MEMORY_OPTIMIZED_DATA' failed because it is not supported in the edition of this SQL Server instance 'TFS-SERVER'. See books online for more details on feature support in different SQL Server editions.
در مقاله قبل در مورد نحوه ذخیره سازی در حافظه نوشتیم و به user mode و kernel mode اشاراتی کردیم که میتوانید به آن رجوع کنید.
در این سری مقالات قصد داریم به بررسی اجزا و روند کاری موجود در IIS بپردازیم که چگونه IIS کار میکند و شامل چه بخش هایی میشود. مطمئنا آشنایی با این بخشها در روند شناسایی رفتارهای وب اپلیکیشنها و واکنشهای سرور، کمک زیادی به ما خواهد کرد. در اینجا نسخه IIS7 را به عنوان مرجع در نظر گرفتهایم.
وب سرور IIS در عبارت مخفف Internet information services به معنی سرویسهای اطلاعاتی اینترنت میباشد. IIS شامل کامپوننتهای زیادی است که هر کدام ازآنها کار خاصی را انجام میدهند؛ برای مثال گوش دادن به درخواستهای ارسال شده به سرور، مدیریت فرآیندها Process و خواندن فایلهای پیکربندی Configuration؛ این اجزا شامل protocol listener ،Http.sys و WSA و .. میشوند.
Protocol Listeners
این پروتکلها به درخواستهای رسیده گوش کرده و آنها را مورد پردازش قرار میدهند و پاسخی را به درخواست کننده، ارسال میکنند. هر listener بر اساس نوع پروتکل متفاوت هست. به عنوان مثال کلاینتی، درخواست صفحهای را میکند و http listener که به آن Http.sys میگویند به آن پاسخ میدهد. به طور پیش فرض http.sys به درخواستهای http و https گوش فرا میدهد، این کامپوننت از IIS6 اضافه شده است ولی در نسخه 7 از SSL نیز پشتیبانی میکند.
Http.sys یا Hypertext transfer protocol stack
کار این واحد در سه مرحله دریافت درخواست، ارسال آن به واحد پردازش IIS و ارسال پاسخ به کلاینت است؛ قبل از نسخه 6 از Winsock یا windows socket api که یک کامپوننت user-mod بود استفاده میشد ولی Http.sys یک کامپوننت Kernel-mod هست.
Http.sys مزایای زیر را به همراه دارد:
- کش کرنل به صورت پیش فرض بر روی صفحات ایستا فعال شده است؛ نه برای صفحاتی با محتوای پویا که البته این مورد قابل تغییر است که نحوه این تغییر را پایینتر توضیح خواهیم داد.
- اگر آدرس درخواستی شامل کوئری باشد صفحه کش نخواهد شد: http://www.site.info/postarchive.htm?id=25
- برای پاسخ ازمکانیزمهای فشرده سازی پویا استفاده شده باشد مثل gzip کش نخواهد شد
- صفحه درخواست شده صفحه اصلی سایت باشد کش نخواهد شد : http://www.dotnettip.info ولی اگر درخواست بدین صورت باشه http://www.domain.com/default.htm کش خواهد کرد.
- درخواست به صورت ناشناس anonymous نباشد و نیاز به authentication داشته باشد کش نخواهد شد (یعنی در هدر شامل گزینه authorization میباشد).
- درخواست باید از نوع نسخه http1 به بعد باشد.
- اگر درخواست شامل Entity-body باشد کش نخواهد کرد.
- درخواست شامل If-Range/Range header باشد کش نمیشود.
- کل حجم response بییشتر از اندازه تعیین شده باشد کش نخواهد گردید، این اندازه در کلید ریجستری UriMaxUriBytes قرار دارد. اطلاعات بیشتر
- اندازه هدر بیشتر از اندازه تعیین شده باشد که عموما اندازه تعیین شده یک کیلو بایت است.
- کش پر باشد، کش انجام نخواهد گرفت.
برای فعال سازی کش کرنل راهنمای زیر را دنبال کنید:
گزینه output cache را در IIS، فعال کنید و سپس گزینه Add را بزنید. کادر add cache rule که باز شود، از شما میخواهد یکی از دو نوع کش مد کاربر و مد کرنل را انتخاب کنید و مشخص کنید چه نوع فایلهایی (مثلا aspx) از این قوانین پیروری کنند و مکانیزم کش کردن به سه روش جلوگیری از کش کردن، کش زمان دار و کش بر اساس آخرین تغییر فایل انجام گردد.
برای تعیین مقدار سایز کش response که در بالا اشاره کردیم میتوانید در همان پنجره، گزینه edit feature settings را انتخاب کنید.
این قسمت از مطلب که به نقل از مقاله آقای Karol Jarkovsky در این آدرس است یک سری تست هایی با نرم افزار(Web Capacity Analysis Tool (WCAT گرفته است که به نتایج زیر دست پیدا کرده است:
Kernel Cache Disabled 4 clients/160 threads/30 sec 257 req/sec
Kernel Cache Enabled 4 clients/160 threads/30 sec 553 req/sec
همانطور که میبینید نتیجه فعال سازی کش کرنل پاسخ به بیش از دو برابر درخواست در حالت غیرفعال آن است که یک عدد فوق العاده به حساب میاد.
برای اینکه خودتان هم تست کرده باشید در این آدرس برنامه را دانلود کنید و به دنبال فایل request.cfg بگردید و از صحت پارامترهای server و url اطمینان پیدا کنید. در گام بعدی 5 پنجره خط فرمان باز کرده و در یکی از آنها دستور netsh http show cachestate را بنویسید تا تمامی وروردیهای entry که در کش کرنل ذخیره شده اند لیست شوند. البته در اولین تست کش را غیرفعال کنید و به این ترتیب نباید چیزی نمایش داده شود. در همان پنجره فرمان wcctl –a localhost –c config.cfg –s request.cfg را زده تا کنترلر برنامه در وضعیت listening قرار بگیرد. در 4 پنجره دیگر فرمان wcclient localhost از شاخه کلاینت را نوشته تا تست آغاز شود. بعد از انجام تست به شاخه نصب کنترلر WCAT رفته و فایل log را بخوانید و اگر دوباره دستور نمایش کش کرنل را بزنید باید خالی باشد. حالا کش را فعال کنید و دوباره عملیات تست را از سر بگیرید و اگر دستور netsh را ارسال کنید باید کش کرنل دارای ورودی باشد.
برای تغییرات در سطح http.sys میتوانید از ریجستری کمک بگیرید. در اینجا تعداد زیادی از تنظیمات ذخیره شده در ریجستری برای http.sys لیست شده است.
یک نکتهی تکمیلی: اگر از SQLite به همراه بستهی Microsoft.EntityFrameworkCore.Sqlite استفاده میکنید ... موتور SQLite آن، کمی قدیمی است. برای بهروز رسانی موتور خود SQLite، نیاز است بستهی SQLitePCLRaw.bundle_e_sqlite3 را هم به لیست ارجاعات برنامهی خود اضافه کنید (به صورت صریح). برای مثال نگارش 8.0.7 به همراه EF-Core هر چند ارجاعی را به SQLitePCLRaw.bundle_e_sqlite3 دارد، اما از نگارش 2.1.6 آن استفاده میکند که مربوط به سال قبل است.