نوعهای ارجاعی (Reference Types) در #C، همیشه نالپذیر بودهاند؛ در مقابل نوعهای مقداری (value types) مانند DateTime که برای نالپذیر کردن آنها باید یک علامت سؤال را در حین تعریف نوع آنها ذکر کرد تا تبدیل به یک نوع نالپذیر شود (DateTime? Created). بنابراین عنوانی مانند «نوعهای ارجاعی نالنپذیر» شاید آنچنان مفهوم نباشد.
خالق Null در زبانهای برنامه نویسی، آنرا یک اشتباه چند میلیارد دلاری میداند! و به عنوان یک توسعه دهندهی دات نت، غیرممکن است که در حین اجرای برنامههای خود تابحال به null reference exception برخورد نکرده باشید. هدف از ارائهی قابلیت جدید «نوعهای ارجاعی نالنپذیر» در C# 8.0، مقابلهی با یک چنین مشکلاتی است و خصوصا غنی سازی IDEها برای ارائهی اخطارهایی پیش از کامپایل برنامه، در مورد قسمتهایی از کد که ممکن است سبب بروز null reference exception شوند.
فعالسازی «نوعهای ارجاعی نالنپذیر»
قابلیت «نوعهای ارجاعی نالنپذیر» به صورت پیشفرض غیرفعال است. برای فعالسازی آن میتوان فایل csproj را به صورت زیر، با افزودن خاصیت NullableContextOptions، ویرایش کرد:
یک نکته: در نگارشهای بعدی NET Core SDK. و همچنین ویژوال استودیو (از نگارش 16.2.0 به بعد)، خاصیت NullableContextOptions به صرفا Nullable تغییر نام یافته و ساده شدهاست. بنابراین اگر در این نگارشها به خطاهای ذیل برخوردید:
صرفا به معنای استفادهی از نام قدیمی این ویژگی است که باید به Nullable تغییر پیدا کند:
اما در زمان نگارش این مطلب که 3.0.100-preview5-011568 در دسترس است، فعلا همان نام قدیمی NullableContextOptions کار میکند.
تغییر ماهیت نوعهای ارجاعی #C با فعالسازی NullableContextOptions
در #C ای که ما میشناسیم، رشتهها قابلیت پذیرش نال را دارند و همچنین ذکر آنها به صورت nullable بیمعنا است. اما پس از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر، اکنون عکس آن رخ میدهد. رشتهها نالنپذیر میشوند؛ اما میتوان در صورت نیاز، آنها را nullable نیز تعریف کرد.
یک مثال: بررسی تاثیر فعالسازی NullableContextOptions بر روی یک پروژه
کلاس زیر را در نظر بگیرید:
با فعالسازی خاصیت NullableContextOptions، بلافاصله اخطار زیر در IDE ظاهر میشود (اگر ظاهر نشد، یکبار پروژه را بسته و مجددا بارگذاری کنید):
در این کلاس، دو سازنده وجود دارند که یکی MiddleName را دریافت میکند و دیگری خیر. در اینجا کامپایلر تشخیص دادهاست که چون در سازندهی اولی که MiddleName را دریافت نمیکند، مقدار پیشفرض خاصیت MiddleName، نال خواهد بود و همچنین ما NullableContextOptions را نیز فعال کردهایم، بنابراین این خاصیت دیگر به صورت معمول و متداول یک نوع ارجاعی نالپذیر عمل نمیکند و دیگر نمیتوان نال را به عنوان مقدار پیشفرض آن، به آن نسبت داد. به همین جهت اخطار فوق ظاهر شدهاست.
برای رفع این مشکل:
به کامپایلر اعلام میکنیم: «میدانیم که MiddleName میتواند نال هم باشد» و آنرا در این زمینه راهنمایی میکنیم:
پس از این تغییر، اخطار فوق که ذیل سازندهی اول کلاس Person ظاهر شده بود، محو میشود. اما اکنون مجددا کامپایلر، در جائیکه میخواهیم از آن استفاده کنیم:
اخطارهایی را صادر میکند:
در اینجا در متد محلی (local function) تعریف شده، سعی در دسترسی به خاصیت MiddleName وجود دارد و اکنون با تغییر جدیدی که اعمال کردیم، به صورت نالپذیر تعریف شدهاست.
همچنین در سطر بعدی آن نیز نتیجهی نهایی middleName، مورد استفاده قرار گرفتهاست که آن نیز مشکلدار تشخیص داده شدهاست.
مشکل اولین سطر را به این صورت میتوانیم برطرف کنیم:
در اینجا بجای ذکر صریح نوع string، از var استفاده شدهاست. پیشتر با ذکر صریح نوع string، آنرا یک رشتهی نالنپذیر تعریف کرده بودیم. اما اکنون چون person.MiddleName نالپذیر تعریف شدهاست، var نیز به صورت خودکار به این رشتهی نالپذیر اشاره میکند.
اما مشکل سطر دوم هنوز باقی است:
علت اینجا است که متغیر middleName نیز اکنون ممکن است مقدار نال را داشته باشد. برای رفع این مشکل میتوان از اپراتور .? استفاده کرد و سپس اگر مقدار نهایی این عبارت نال بود، مقدار صفر را بازگشت میدهیم:
هدف از این قابلیت و ویژگی کامپایلر، کمک کردن به توسعه دهندهها جهت نوشتن کدهایی امنتر و مقاومتر به null reference exceptionها است.
امکان خاموش و روشن کردن ویژگی نوعهای ارجاعی نالنپذیر به صورت موضعی
زمانیکه خاصیت NullableContextOptions را فعال میکنیم، بر روی کل پروژه تاثیر میگذارد. برای مثال اگر یک چنین قابلیتی را بر روی پروژههای قدیمی خود فعال کنید، با صدها اخطار مواجه خواهید شد. به همین جهت است که این ویژگی حتی با فعالسازی C# 8.0 و انتخاب آن، به صورت پیشفرض غیرفعال است. بنابراین برای اینکه بتوان پروژههای قدیمی را قدم به قدم و سر فرصت، «مقاومتر» کرد، میتوان تعیین کرد که کدام قسمت، تحت تاثیر این ویژگی قرار بگیرد و کدام قسمتها خیر:
در اینجا میتوان با استفاده از compiler directive جدید nullable# به کامپایلر اعلام کرد که از این قسمت صرفنظر کن. مقدار آن میتواند disable و یا enable باشد.
مجبور ساختن خود به «مقاوم سازی» برنامه
اگر NullableContextOptions را فعال کنید، کامپایلر صرفا یکسری اخطار را در مورد مشکلات احتمالی صادر میکند؛ اما برنامه هنوز کامپایل میشود. برای اینکه خود را مقید به «مقاوم سازی» برنامه کنیم، میتوانیم با فعالسازی ویژگی TreatWarningsAsErrors در فایل csprj، این اخطارها را تبدیل به خطای کامپایلر کرده و از کامپایل برنامه جلوگیری کنیم:
البته TreatWarningsAsErrors تمام اخطارهای برنامه را تبدیل به خطا میکند. اگر میخواهید انتخابیتر عمل کنید، میتوان از خاصیت WarningsAsErrors استفاده کرد:
آیا اگر برنامهای با C# 7.0 کامپایل شود، کتابخانههای تهیه شدهی با C# 8.0 را میتواند استفاده کند؟
پاسخ: بله. از دیدگاه برنامههای قدیمی، کتابخانههای تهیه شدهی با C# 8.0، تفاوتی با سایر کتابخانه ندارند. آنها نوعهای نالپذیر جدید را مانند ?string مشاهده نمیکنند؛ آنها فقط string را مشاهده میکنند و روش کار کردن با آنها نیز همانند قبل است. بدیهی است در این حالت از مزایای کامپایلر C# 8.0 در تشخیص زود هنگام مشکلات برنامه محروم خواهند بود؛ اما عملکرد برنامه تفاوتی نمیکند.
وضعیت برنامهی C# 8.0 ای که از کتابخانههای C# 7.0 و یا قبل از آن استفاده میکند، چگونه خواهد بود؟
چون کتابخانههای قدیمیتر از مزایای کامپایلر C# 8.0 استفاده نمیکنند، خروجیهای آن بدون بروز خطایی توسط کامپایلر C# 8.0 استفاده میشوند؛ چون حجم اخطارهای صادر شدهی در این حالت بیش از حد خواهد بود. یعنی این بررسیهای کامپایلر صرفا برای کتابخانههای جدید فعال هستند و نه برای کتابخانههای قدیمی.
مهارتهای مواجه شدن با اخطارهای ناشی از فعالسازی NullableContextOptions
در مثالی که بررسی شد، یک نمونه از روشهای مواجه شدن با اخطارهای ناشی از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر را بررسی کردیم. در ادامه روشهای تکمیلی دیگری را بررسی میکنیم.
1- هرجائیکه قرار است متغیر ارجاعی نالپذیر باشد، آنرا صراحتا اعلام کنید.
این مثال را پیشتر بررسی کردیم. با فعالسازی ویژگی نوعهای ارجاعی نالنپذیر، ماهیت آنها نیز تغییر میکند و دیگر نمیتوان به آنها null را انتساب داد. اگر نیاز است حتما اینکار صورت گیرد، آنها را توسط ? به صورت nullable تعریف کنید.
نمونهی دیگر آن مثال زیر است:
در اینجا Address یک نوع ارجاعی نالپذیر است. بنابراین حاصل Address?.Country میتواند نال باشد و به Country نالنپذیر قابل انتساب نیست. برای رفع این مشکل کافی است دقیقا مشخص کنیم که این رشته نیز نالپذیر است:
البته در این حالت باید به مثال زیر دقت داشت:
چون node در اینجا توسط var تعریف شدهاست، دقیقا نوع this را که non-nullable است، پیدا میکند. بنابراین بعدها نمیتوان به آن null را انتساب داد. اگر چنین موردی نیاز بود، باید صریحا نوع آنرا بدو امر، nullable تعریف کرد؛ چون هنوز امکان تعریف ?var میسر نیست:
2- نوعهای خود را مقدار دهی اولیه کنید.
در مثال زیر:
در این حالت چون خاصیت Name، در سازندهی کلاس مقدار دهی اولیه نشدهاست، یک اخطار صادر میشود که بیانگر احتمال نال بودن آن است. یک روش مواجه شدن با این مشکل، تعریف آن به صورت یک خاصیت نالپذیر است:
یا یک استثناء را صادر کنید:
به این ترتیب کامپایلر میداند که اگر نام دریافتی نال بود، دقیقا باید چگونه رفتار کند.
البته در این حالت برای مقدار دهی اولیهی Name، حتما نیاز به تعریف یک سازندهاست و در این حالت کدهایی را که از سازندهی پیشفرض استفاده کرده بودند (مانند new Person { Name = "Vahid" })، باید تغییر دهید.
راهحل دیگر، مقدار دهی اولیهی این خواص بدون تعریف یک سازندهی اضافی است:
برای مثال میتوان از مقادیر خالی زیر برای مقدار دهی اولیهی رشتهها، آرایهها و مجموعهها استفاده کرد:
یا حتی میتوان اشیاء دیگر را نیز به صورت زیر مقدار دهی اولیه کرد:
البته در این حالت باید مفهوم فلسفی «خالی بودن» را پیش خودتان تفسیر و تعریف کنید که دقیقا مقصود از یک آدرس خالی چیست؟ به همین جهت شاید تعریف این شیء به صورت nullable بهتر باشد.
خالق Null در زبانهای برنامه نویسی، آنرا یک اشتباه چند میلیارد دلاری میداند! و به عنوان یک توسعه دهندهی دات نت، غیرممکن است که در حین اجرای برنامههای خود تابحال به null reference exception برخورد نکرده باشید. هدف از ارائهی قابلیت جدید «نوعهای ارجاعی نالنپذیر» در C# 8.0، مقابلهی با یک چنین مشکلاتی است و خصوصا غنی سازی IDEها برای ارائهی اخطارهایی پیش از کامپایل برنامه، در مورد قسمتهایی از کد که ممکن است سبب بروز null reference exception شوند.
فعالسازی «نوعهای ارجاعی نالنپذیر»
قابلیت «نوعهای ارجاعی نالنپذیر» به صورت پیشفرض غیرفعال است. برای فعالسازی آن میتوان فایل csproj را به صورت زیر، با افزودن خاصیت NullableContextOptions، ویرایش کرد:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> </PropertyGroup> </Project>
CS8632: The annotation for nullable reference types should only be used in code within a ‘#nullable’ context. CS8627: A nullable type parameter must be known to be a value-type or non-nullable reference type. Consider adding a ‘class’, ‘struct’ or type constraint.
<PropertyGroup> <LangVersion>preview</LangVersion> <Nullable>enable</Nullable> </PropertyGroup>
تغییر ماهیت نوعهای ارجاعی #C با فعالسازی NullableContextOptions
در #C ای که ما میشناسیم، رشتهها قابلیت پذیرش نال را دارند و همچنین ذکر آنها به صورت nullable بیمعنا است. اما پس از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر، اکنون عکس آن رخ میدهد. رشتهها نالنپذیر میشوند؛ اما میتوان در صورت نیاز، آنها را nullable نیز تعریف کرد.
یک مثال: بررسی تاثیر فعالسازی NullableContextOptions بر روی یک پروژه
کلاس زیر را در نظر بگیرید:
public class Person { public string FirstName { get; set; } public string MiddleName { get; set; } public string LastName { get; set; } public Person(string first, string last) => (FirstName, LastName) = (first, last); public Person(string first, string middle, string last) => (FirstName, MiddleName, LastName) = (first, middle, last); public override string ToString() => $"{FirstName} {MiddleName} {LastName}"; }
در این کلاس، دو سازنده وجود دارند که یکی MiddleName را دریافت میکند و دیگری خیر. در اینجا کامپایلر تشخیص دادهاست که چون در سازندهی اولی که MiddleName را دریافت نمیکند، مقدار پیشفرض خاصیت MiddleName، نال خواهد بود و همچنین ما NullableContextOptions را نیز فعال کردهایم، بنابراین این خاصیت دیگر به صورت معمول و متداول یک نوع ارجاعی نالپذیر عمل نمیکند و دیگر نمیتوان نال را به عنوان مقدار پیشفرض آن، به آن نسبت داد. به همین جهت اخطار فوق ظاهر شدهاست.
برای رفع این مشکل:
به کامپایلر اعلام میکنیم: «میدانیم که MiddleName میتواند نال هم باشد» و آنرا در این زمینه راهنمایی میکنیم:
public string? MiddleName { get; set; }
public static class NullableReferenceTypes { //#nullable enable // Toggle to enable public static string Exemplify() { var vahid = new Person("Vahid", "N"); var length = GetLengthOfMiddleName(vahid); return $"{vahid.FirstName}'s middle name has {length} characters in it."; static int GetLengthOfMiddleName(Person person) { string middleName = person.MiddleName; return middleName.Length; } } }
در اینجا در متد محلی (local function) تعریف شده، سعی در دسترسی به خاصیت MiddleName وجود دارد و اکنون با تغییر جدیدی که اعمال کردیم، به صورت نالپذیر تعریف شدهاست.
همچنین در سطر بعدی آن نیز نتیجهی نهایی middleName، مورد استفاده قرار گرفتهاست که آن نیز مشکلدار تشخیص داده شدهاست.
مشکل اولین سطر را به این صورت میتوانیم برطرف کنیم:
var middleName = person.MiddleName;
اما مشکل سطر دوم هنوز باقی است:
علت اینجا است که متغیر middleName نیز اکنون ممکن است مقدار نال را داشته باشد. برای رفع این مشکل میتوان از اپراتور .? استفاده کرد و سپس اگر مقدار نهایی این عبارت نال بود، مقدار صفر را بازگشت میدهیم:
static int GetLengthOfMiddleName(Person person) { var middleName = person.MiddleName; return middleName?.Length ?? 0; }
امکان خاموش و روشن کردن ویژگی نوعهای ارجاعی نالنپذیر به صورت موضعی
زمانیکه خاصیت NullableContextOptions را فعال میکنیم، بر روی کل پروژه تاثیر میگذارد. برای مثال اگر یک چنین قابلیتی را بر روی پروژههای قدیمی خود فعال کنید، با صدها اخطار مواجه خواهید شد. به همین جهت است که این ویژگی حتی با فعالسازی C# 8.0 و انتخاب آن، به صورت پیشفرض غیرفعال است. بنابراین برای اینکه بتوان پروژههای قدیمی را قدم به قدم و سر فرصت، «مقاومتر» کرد، میتوان تعیین کرد که کدام قسمت، تحت تاثیر این ویژگی قرار بگیرد و کدام قسمتها خیر:
public static class NullableReferenceTypes { #nullable disable // Toggle to enable
مجبور ساختن خود به «مقاوم سازی» برنامه
اگر NullableContextOptions را فعال کنید، کامپایلر صرفا یکسری اخطار را در مورد مشکلات احتمالی صادر میکند؛ اما برنامه هنوز کامپایل میشود. برای اینکه خود را مقید به «مقاوم سازی» برنامه کنیم، میتوانیم با فعالسازی ویژگی TreatWarningsAsErrors در فایل csprj، این اخطارها را تبدیل به خطای کامپایلر کرده و از کامپایل برنامه جلوگیری کنیم:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> </Project>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
آیا اگر برنامهای با C# 7.0 کامپایل شود، کتابخانههای تهیه شدهی با C# 8.0 را میتواند استفاده کند؟
پاسخ: بله. از دیدگاه برنامههای قدیمی، کتابخانههای تهیه شدهی با C# 8.0، تفاوتی با سایر کتابخانه ندارند. آنها نوعهای نالپذیر جدید را مانند ?string مشاهده نمیکنند؛ آنها فقط string را مشاهده میکنند و روش کار کردن با آنها نیز همانند قبل است. بدیهی است در این حالت از مزایای کامپایلر C# 8.0 در تشخیص زود هنگام مشکلات برنامه محروم خواهند بود؛ اما عملکرد برنامه تفاوتی نمیکند.
وضعیت برنامهی C# 8.0 ای که از کتابخانههای C# 7.0 و یا قبل از آن استفاده میکند، چگونه خواهد بود؟
چون کتابخانههای قدیمیتر از مزایای کامپایلر C# 8.0 استفاده نمیکنند، خروجیهای آن بدون بروز خطایی توسط کامپایلر C# 8.0 استفاده میشوند؛ چون حجم اخطارهای صادر شدهی در این حالت بیش از حد خواهد بود. یعنی این بررسیهای کامپایلر صرفا برای کتابخانههای جدید فعال هستند و نه برای کتابخانههای قدیمی.
مهارتهای مواجه شدن با اخطارهای ناشی از فعالسازی NullableContextOptions
در مثالی که بررسی شد، یک نمونه از روشهای مواجه شدن با اخطارهای ناشی از فعالسازی ویژگی نوعهای ارجاعی نالنپذیر را بررسی کردیم. در ادامه روشهای تکمیلی دیگری را بررسی میکنیم.
1- هرجائیکه قرار است متغیر ارجاعی نالپذیر باشد، آنرا صراحتا اعلام کنید.
string name = null; // ERROR string? name = null; // OK!
نمونهی دیگر آن مثال زیر است:
public class Person { public Address? Address { get; set; }; public string Country => Address?.Country; // ERROR! }
public class Person { public Address? Address { get; set; }; public string? Country => Address?.Country; // OK! }
البته در این حالت باید به مثال زیر دقت داشت:
var node = this; // Initialize non-nullable variable while (node != null) { node = null; // ERROR! }
Node? node = this; // Initialize nullable variable while (node != null) { node = null; // OK! }
2- نوعهای خود را مقدار دهی اولیه کنید.
در مثال زیر:
public class Person { public string Name { get; set; } // ERROR! }
public class Person { public string? Name { get; set; } }
یا یک استثناء را صادر کنید:
public class Person { public string Name { get; set; } public Person(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } }
البته در این حالت برای مقدار دهی اولیهی Name، حتما نیاز به تعریف یک سازندهاست و در این حالت کدهایی را که از سازندهی پیشفرض استفاده کرده بودند (مانند new Person { Name = "Vahid" })، باید تغییر دهید.
راهحل دیگر، مقدار دهی اولیهی این خواص بدون تعریف یک سازندهی اضافی است:
public class Person { public string Name { get; set; } = string.Empty; // -or- public string Name { get; set; } = ""; }
String.Empty Array.Empty<T>() Enumerable.Empty<T>()
public class Person { public Address Address { get; set; } = new Address(); }
عموما برنامههای بزرگ NET.، به چندین زیر پروژه شکسته میشوند تا مدیریت آنها سادهتر شود. مهمترین مشکلی که در این حالت پس از مدتی بروز میکند، هماهنگ نگه داشتن شماره نگارشهای ارجاعات NuGet این پروژهها است و همچنین به روز رسانی مکرر و هر بارهی تمام این فایلهای csproj. به همین جهت ایدهی مدیریت مرکزی شماره نگارشهای ارجاعات پروژههای NuGet قرار است به نگارش بعدی آن اضافه شود که البته هم اکنون نیز قسمتی از آن در NET Core SDK 3.1.300. به بعد، قابل استفادهاست که جزئیات آنرا در ادامه مرور میکنیم.
ایجاد فایل جدید Directory.Packages.props
زمانیکه قرار است شماره نگارشهای بستههای NuGet مختلف مورد استفادهی در برنامه، به صورت مرکزی مدیریت شوند، نیاز به یک مخزن ثبت آنها نیز میباشد. به همین جهت یک فایل جدید را به نام Directory.Packages.props در کنار فایل sln پروژهی خود ایجاد کنید (در ریشهی اصلی پروژه)؛ با این محتوای فرضی:
برای تشکیل این فایل، فایلهای csproj مختلف موجود در solution جاری را یافته و سپس PackageReferenceهای آنها را به فایل props فوق کپی کنید؛ با یک تفاوت مهم: بجای PackageReference اینبار از نام PackageVersion استفاده میشود.
تغییرات مورد نیاز در فایلهای پروژههای موجود
در ادامه مجددا به تمام فایلهای csproj خود مراجعه کرده و ویژگی Version را از آنها حذف کنید؛ مانند:
از این پس دیگر هیچکدام از فایلهای پروژهی شما نباید به همراه قید صریح شماره نگارش بستههای مورد استفاده باشند؛ در غیر اینصورت در حین Build پروژه، خطای زیر را دریافت خواهید کرد:
همچنین اگر دقت کرده باشید، ویژگی جدید ManagePackageVersionsCentrally نیز به این فایل پروژه و سایر فایلهای پروژه نیز باید اضافه شود. حالت پیشفرض آن false است.
یک نکته: میتوان ویژگی ManagePackageVersionsCentrally را نیز به صورت سراسری به فایل Directory.Packages.props اضافه کرد تا به صورت خودکار به تمام فایلهای csproj موجود، اعمال شود:
نحوهی افزودن بستههای جدید
قابلیتی که تا اینجا معرفی شد، در NET Core SDK 3.1.300. به بعد قابل دسترسی و استفادهاست (و پس از این تغییرات، برنامه بدون مشکل کامپایل میشود)؛ اما هنوز NET Core CLI. برای افزودن خودکار بستههای جدید NuGet به این سیستم، به روز رسانی نشدهاست. یعنی فعلا اگر خواستید بستهی جدیدی را اضافه کنید باید ابتدا به صورت دستی PackageVersion آنرا در فایل Directory.Packages.props ثبت کنید و سپس PackageReference بدون شمارهی نگارش را نیز به پروژهی مدنظر خود به صورت دستی اضافه کنید.
برای مطالعه بیشتر
مستندات رسمی آن
وضعیت پیاده سازی آن
ایجاد فایل جدید Directory.Packages.props
زمانیکه قرار است شماره نگارشهای بستههای NuGet مختلف مورد استفادهی در برنامه، به صورت مرکزی مدیریت شوند، نیاز به یک مخزن ثبت آنها نیز میباشد. به همین جهت یک فایل جدید را به نام Directory.Packages.props در کنار فایل sln پروژهی خود ایجاد کنید (در ریشهی اصلی پروژه)؛ با این محتوای فرضی:
<Project> <ItemGroup> <PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="3.1.8" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.8" /> </ItemGroup> </Project>
تغییرات مورد نیاز در فایلهای پروژههای موجود
در ادامه مجددا به تمام فایلهای csproj خود مراجعه کرده و ویژگی Version را از آنها حذف کنید؛ مانند:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Localization.Abstractions" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" /> </ItemGroup> </Project>
error NU1008: Projects that use central package version management should not define the version on the PackageReference items
همچنین اگر دقت کرده باشید، ویژگی جدید ManagePackageVersionsCentrally نیز به این فایل پروژه و سایر فایلهای پروژه نیز باید اضافه شود. حالت پیشفرض آن false است.
یک نکته: میتوان ویژگی ManagePackageVersionsCentrally را نیز به صورت سراسری به فایل Directory.Packages.props اضافه کرد تا به صورت خودکار به تمام فایلهای csproj موجود، اعمال شود:
<Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> </PropertyGroup> <ItemGroup> <PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="3.1.8" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.8" /> </ItemGroup> </Project>
نحوهی افزودن بستههای جدید
قابلیتی که تا اینجا معرفی شد، در NET Core SDK 3.1.300. به بعد قابل دسترسی و استفادهاست (و پس از این تغییرات، برنامه بدون مشکل کامپایل میشود)؛ اما هنوز NET Core CLI. برای افزودن خودکار بستههای جدید NuGet به این سیستم، به روز رسانی نشدهاست. یعنی فعلا اگر خواستید بستهی جدیدی را اضافه کنید باید ابتدا به صورت دستی PackageVersion آنرا در فایل Directory.Packages.props ثبت کنید و سپس PackageReference بدون شمارهی نگارش را نیز به پروژهی مدنظر خود به صورت دستی اضافه کنید.
برای مطالعه بیشتر
مستندات رسمی آن
وضعیت پیاده سازی آن
در نهایت Repository شما وابستهاست به سرویس DbContext از نوع IDispoable (برای تشکیل یک سرویس، چندین سرویس به صورت خودکار توسط IoC Container وهله سازی میشوند). بنابراین برای Dispose صحیح وابستگیهای تو در توی آنها حتما نیاز است که یک Scope را ایجاد کنید؛ وگرنه این سرویسها و منابع آنها (مانند اتصال گشوده شدهی به بانک اطلاعاتی) تا آخر طول عمر برنامه در حافظه باقی خواهند ماند.
نظرات مطالب
EF Code First #12
طراحی EF Code first مبتنی است بر روان بودن و سادگی؛ هر چند پشت صحنه سادهای ندارد. هر وهله از DbContext به صورت خودکار یک تراکنش را تشکیل میدهد و در زمان بسته شدن و dispose، این تراکنش را commit میکند (همچنین متد SaveChanges در پشت صحنه از تراکنشها بهره میگیرد). هر استثنایی این بین رخ دهد، تراکنش rollback خواهد شد. به همین جهت الگوی واحد کار مطرح شده تا تعداد وهلههای زیادی از DbContext هربار ایجاد نشوند و کل عملیات در یک DbContext یا یک تراکنش انجام شود.
نظرات مطالب
EF Code First #1
با سلام
تشکیل خودکار بانک اطلاعاتی و جداول برای من انجام نمیشود. در واقع چون دیتابیس مورد نظر که در Connection String نام برده شده وجود ندارد، برنامه من اصلا به دیتابیس کانکت نمیشود و با خطای زیر هنگام اجرای متد SaveChanges مواجه میشوم:
لطفا راهنمایی بفرمائید
با تشکر
تشکیل خودکار بانک اطلاعاتی و جداول برای من انجام نمیشود. در واقع چون دیتابیس مورد نظر که در Connection String نام برده شده وجود ندارد، برنامه من اصلا به دیتابیس کانکت نمیشود و با خطای زیر هنگام اجرای متد SaveChanges مواجه میشوم:
Cannot open database "EFTest" requested by the login. The login failed.
با تشکر
در مورد طراحی Self Referencing Entities پیشتر مطلبی را در این سایت مطالعه کردهاید .
یک مثال دیگر آن میتواند نظرات چند سطحی در یک سایت باشند. نحوه تعریف آن با مطالبی که در قسمت هشتم عنوان شود تفاوتی نمیکند؛ اما ... زمانیکه نوبت به نمایش آن فرا میرسد، چند نکته اضافی را باید درنظر گرفت. ابتدا مثال کامل زیر را در نظر بگیرید:
در مثال فوق کلاس نظرات به صورت خود ارجاع دهنده (خاصیت Reply به همین کلاس اشاره میکند) تعریف شده است تا کاربران بتوانند تا هر چند سطح لازم، به یک نظر خاص، پاسخ دهند.
در اینجا یک چنین جدولی با اطلاعاتی که ملاحظه میکنید تشکیل خواهند شد:
یک نظر ارائه شده و سپس دو نظر تو در توی دیگر برای این نظر ثبت شده است.
اولین نکته اضافهتری که نسبت به قسمت هشتم قابل ملاحظه است، تعریف خاصیت جدید Children به نحو زیر میباشد:
این خاصیت تاثیری در نحوه تشکیل جدول ندارد. علت تعریف آن به توانمندی EF در پرکردن خودکار آن بر میگردد.
اگر به کوئری نوشته شده در متد RunTests دقت کنید، ابتدا یک ToList نوشته شده است. این مورد سبب میشود که تمام رکوردهای مرتبط دریافت شوند. مثلا در اینجا سه رکورد دریافت میشود. سپس برای اینکه حالت درختی آن حفظ شود، در مرحله بعد ریشهها فیلتر میشوند (مواردی که reply آنها null است). سپس این مورد تبدیل به list خواهد شد. اینبار اگر خروجی را بررسی کنیم، به ظاهر فقط یک رکورد است اما ... به نحو زیبایی توسط EF به شکل یک ساختار درختی، بدون نیاز به کدنویسی خاصی، منظم شده است:
سؤال:
برای نمایش این اطلاعات درختی و تو در تو در یک برنامه وب چکار باید کرد؟
تا اینجا که توانستیم اطلاعات را به نحو صحیحی توسط EF مرتب کنیم، برای نمایش آنها در یک برنامه ASP.NET MVC میتوان از یک TreeViewHelper سورس باز استفاده کرد.
البته کد آن در اصل برای استفاده از EF Code first طراحی نشده و نیاز به اندکی تغییر به نحو زیر دارد تا با EF هماهنگ شود (متد ToList و Count موجود در سورس اصلی آن باید به نحو زیر حذف و اصلاح شوند):
یک مثال دیگر آن میتواند نظرات چند سطحی در یک سایت باشند. نحوه تعریف آن با مطالبی که در قسمت هشتم عنوان شود تفاوتی نمیکند؛ اما ... زمانیکه نوبت به نمایش آن فرا میرسد، چند نکته اضافی را باید درنظر گرفت. ابتدا مثال کامل زیر را در نظر بگیرید:
using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; namespace EFGeneral { public class BlogComment { public int Id { set; get; } [MaxLength] public string Body { set; get; } public virtual BlogComment Reply { set; get; } public int? ReplyId { get; set; } public ICollection<BlogComment> Children { get; set; } } public class MyContext : DbContext { public DbSet<BlogComment> BlogComments { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Self Referencing Entity modelBuilder.Entity<BlogComment>() .HasOptional(x => x.Reply) .WithMany(x => x.Children) .HasForeignKey(x => x.ReplyId) .WillCascadeOnDelete(false); base.OnModelCreating(modelBuilder); } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { var comment1 = new BlogComment { Body = "نظر من این است که" }; var comment12 = new BlogComment { Body = "پاسخی به نظر اول", Reply = comment1 }; var comment121 = new BlogComment { Body = "پاسخی به پاسخ به نظر اول", Reply = comment12 }; context.BlogComments.Add(comment121); base.Seed(context); } } public static class Test { public static void RunTests() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var ctx = new MyContext()) { var list = ctx.BlogComments //.where ... .ToList() // fills the childs list too .Where(x => x.Reply == null) // for TreeViewHelper .ToList(); if (list.Any()) { } } } } }
در مثال فوق کلاس نظرات به صورت خود ارجاع دهنده (خاصیت Reply به همین کلاس اشاره میکند) تعریف شده است تا کاربران بتوانند تا هر چند سطح لازم، به یک نظر خاص، پاسخ دهند.
در اینجا یک چنین جدولی با اطلاعاتی که ملاحظه میکنید تشکیل خواهند شد:
یک نظر ارائه شده و سپس دو نظر تو در توی دیگر برای این نظر ثبت شده است.
اولین نکته اضافهتری که نسبت به قسمت هشتم قابل ملاحظه است، تعریف خاصیت جدید Children به نحو زیر میباشد:
public class BlogComment { // ... public ICollection<BlogComment> Children { get; set; } }
اگر به کوئری نوشته شده در متد RunTests دقت کنید، ابتدا یک ToList نوشته شده است. این مورد سبب میشود که تمام رکوردهای مرتبط دریافت شوند. مثلا در اینجا سه رکورد دریافت میشود. سپس برای اینکه حالت درختی آن حفظ شود، در مرحله بعد ریشهها فیلتر میشوند (مواردی که reply آنها null است). سپس این مورد تبدیل به list خواهد شد. اینبار اگر خروجی را بررسی کنیم، به ظاهر فقط یک رکورد است اما ... به نحو زیبایی توسط EF به شکل یک ساختار درختی، بدون نیاز به کدنویسی خاصی، منظم شده است:
سؤال:
برای نمایش این اطلاعات درختی و تو در تو در یک برنامه وب چکار باید کرد؟
تا اینجا که توانستیم اطلاعات را به نحو صحیحی توسط EF مرتب کنیم، برای نمایش آنها در یک برنامه ASP.NET MVC میتوان از یک TreeViewHelper سورس باز استفاده کرد.
البته کد آن در اصل برای استفاده از EF Code first طراحی نشده و نیاز به اندکی تغییر به نحو زیر دارد تا با EF هماهنگ شود (متد ToList و Count موجود در سورس اصلی آن باید به نحو زیر حذف و اصلاح شوند):
private void AppendChildren(TagBuilder parentTag, T parentItem, Func<T, IEnumerable<T>> childrenProperty) { var children = childrenProperty(parentItem); if (children == null || !children.Any()) { return; } //...
در قسمت قبل، دکمهی new movie را برای کاربران وارد نشدهی به سیستم، از صفحهی نمایش لیست فیلمها، مخفی کردیم. اما ... اگر آدرس http://localhost:3000/movies/new مستقیما در مرورگر وارد شود، هنوز هم برای عموم کاربران قابل دسترسی است.
روش محافظت از مسیریابیهای تعریف شدهی در برنامه
شبیه به روشی را که در قسمت قبل، برای انتقال شیء user، به مسیریابی کامپوننت Movies استفاده کردیم:
در اینجا نیز میتوان برای محافظت از یک مسیریابی، استفاده کرد. به همین جهت به app.js مراجعه کرده و مسیریابی فعلی کامپوننت MovieForm را:
به صورت زیر تغییر میدهیم:
اینبار نیز بجای ویژگی component، از ویژگی render استفاده میکنیم تا بتوان در اینجا به صورت پویا، کدنویسی کرد. ابتدا بررسی میکنیم که آیا کاربر جاری تنظیم شدهاست؟ اگر خیر، او را به صفحهی لاگین هدایت میکنیم؛ در غیراینصورت، همان کامپوننت MovieForm را به همراه تمام props مرتبط با آن، بازگشت میدهیم.
اکنون اگر این تغییرات را ذخیره کرده و در حالت Logout، مسیر http://localhost:3000/movies/new را مستقیما درخواست دهیم، به صفحهی لاگین هدایت خواهیم شد.
ایجاد کامپوننتی با قابلیت استفادهی مجدد، برای محافظت از مسیریابیها
هرچند روشی که تا اینجا برای محافظت از مسیریابیها معرفی شد، بدون مشکل کار میکند، اما اگر قرار باشد برای تمام مسیریابیهای اینگونه، استفاده شود، به تکرار بیش از اندازهی کدهای یکسانی خواهیم رسید. به همین جهت میتوان این منطق را تبدیل به یک کامپوننت با قابلیت استفادهی مجدد کرد؛ تا دیگر نیازی به تکرار این if/elseها نباشد. برای این منظور، فایل جدید src\components\common\protectedRoute.jsx را ایجاد میکنیم. کامپوننت جدید protectedRoute را هم در پوشهی common قرار دادهایم؛ چون وابستگی به دومین این برنامه نداشته و میتواند در سایر برنامه نیز مورد استفاده قرار گیرد. سپس با استفاده از میانبرهای imrc و sfc، یک کامپوننت تابعی بدون حالت را به نام ProtectedRoute ایجاد کرده و در آن، همان کامپوننت اصلی Route را بازگشت میدهیم. بنابراین هر زمانیکه از ProtectedRoute استفاده شود، خروجی آن، همان کامپوننت استاندارد Route خواهد بود که اینبار قرار است از وضعیت کاربر جاری وارد شدهی به سیستم، مطلع باشد. به همین جهت در اولین قدم، همان قطعه کد Route فوق را که به همراه if/else نوشتیم، از فایل app.js کپی کرده و به اینجا، داخل متد رندر کامپوننت، منتقل میکنیم. سپس شروع میکنیم به متغیر کردن عباراتی که در آن به صورت صریح و ثابت، مقدار دهی شدهاند تا به یک کامپوننت با قابلیت استفادهی مجدد برسیم:
- در ابتدا بجای ذکر props بعنوان پارامتر این کامپوننت، از طریق Object Destructuring، خواصی را که قرار است به صورت props دریافت کنیم، مشخص کردهایم. مزیت اینکار، مشخص شدن اینترفیس این کامپوننت به نحو واضحی است. برای مثال بجای ذکر مقدار ویژگی path، به صورت یک رشتهی ثابت، آنرا از طریق یک متغیر دریافت میکنیم.
- در این کامپوننت نیاز است اطلاعات کاربر جاری وارد شدهی به سیستم در دسترس باشد. یا میتوان آنرا به عنوان یکی از خواص props دریافت کرد و یا همانند این مثال، امکان دریافت مستقیم آن از authService نیز وجود دارد.
- در ادامه اگر CurrentUser مقدار دهی نشده باشد، کامپوننت Redirect را که کاربر را به صفحهی لاگین هدایت میکند، بازگشت میدهیم. در غیراینصورت نیاز است یک کامپوننت را بجای برای مثال MovieForm، بازگشت دهیم. علت استفادهی از component: Component این است که React انتظار دارد، کامپوننتها با نام بزرگ شروع شوند. به همین جهت خاصیت component را از props دریافت کرده و آنرا به Component تغییر نام میدهیم.
- زمانیکه از کامپوننت Route استاندارد استفاده میشود، یا از ویژگی component آن استفاده میشود و یا از ویژگی render آن که یک تابع است، تا بتوان داخل آن، کدهای پویایی را درج کرد. به همین جهت ممکن است که مقدار متغیر کامپوننت دریافت شده، نال باشد. بنابراین در اینجا بررسی میشود که آیا Component، مقدار دهی شدهاست یا خیر؟ اگر بله، همان کامپوننت را به همراه props آن بازگشت میدهیم. در غیراینصورت، متد render مقدار دهی شده را به همراه props ارسالی به آن، بازگشت خواهیم داد.
- علت وجود پارامتر rest نیز این است که این کامپوننت علاوه بر ویژگیهایی که تاکنون پیش بینی کردهایم، ممکن است در آینده ویژگیهای دیگری را نیز نیاز داشته باشد. به همین جهت مابقی آنها را توسط {rest...}، به صورت خودکار در اینجا درج میکنیم. برای نمونه در اینجا ذکر path={path} را مشاهده نمیکنید؛ چون توسط همان {rest...} به صورت خودکار تامین میشود.
اکنون به app.js بازگشته و کدهای قبلی را با این کامپوننت جدید ProtectedRoute، جایگزین میکنیم:
اینبار نحوهی تعریف ProtectedRoute، همانند نحوهی تعریف کامپوننت Route استاندارد است؛ با این تفاوت که این کامپوننت در پشت صحنه، از وضعیت کاربر جاری سیستم مطلع است و بر اساس آن واکنش نشان میدهد.
مدیریت بازگشت کاربران، پس از لاگین به سیستم
پس از خروج از برنامه، اگر سعی در ویرایش یکی از فیلمهای موجود کنیم، به صفحهی لاگین هدایت خواهیم شد. پس از لاگین موفق، مجددا به ریشهی سایت بازگشت داده میشویم و نه به صفحهای که پیش از لاگین، مدنظر کاربر بودهاست. برای رفع این مشکل نیاز است بتوان به آدرس قبلی درخواستی، دسترسی یافت و این مورد توسط سیستم مسیریابی، به کامپوننتها به صورت خودکار تزریق میشود. برای مثال اگر در کامپوننت ProtectedRoute، مقدار شیء props دریافتی را لاگ کنیم:
و سپس بر روی لینک به مشاهدهی جزئیات و ویرایش یک فیلم کلیک کنیم، تصویر زیر حاصل میشود:
همانطور که مشخص است، شیء location دریافتی از props، به همراه اطلاعات آدرسی است که پیش از هدایت خودکار به صفحهی لاگین، درخواست کرده بودیم. به همین جهت یک چنین تنظیمی، در تعاریف کامپوننت ProtectedRoute درنظر گرفته شدهاند:
در کامپوننت Redirect، مقدار to میتواند یک رشته و یا یک شیء باشد. اگر حالت انتساب یک شیء را انتخاب کردیم، خاصیت pathname آن مانند قبل است و مکان نهایی Redirect را مشخص میکند. اما کار خاصیت state آن، ارسال اطلاعاتی اضافی است به کامپوننتی که قرار است کار Redirect به آن صورت گیرد. برای مثال در تنظیم فوق، شیء ای که دارای خاصیت from و با مقدار props.location است، به صورت خودکار به کامپوننت مقصد ارسال میشود.
اکنون که این شیء، به کامپوننت لاگین، پس از Redirect خودکار ارسال میشود، نیاز است به src\components\loginForm.jsx مراجعه کرده و تغییرات زیر را اعمال کنیم:
در اینجا خاصیت state، از شیء location تزریق شدهی به props این کامپوننت، استخراج میشود. سپس با مقدار دهی window.location به from.pathname آن، کار هدایت کاربر را پس از لاگین موفق، به آدرس قبلی مدنظر او، انجام میدهیم.
تا اینجا اگر برنامه را ذخیره کرده، از سیستم خارج شویم و سعی در ویرایش اولین رکورد موجود در لیست فیلمها کنیم، ابتدا به صفحهی لاگین هدایت میشویم. پس از لاگین موفق، اینبار بجای مشاهدهی ریشهی سایت که در اینجا به لیست فیلمها تنظیم شده، دقیقا صفحهی ویرایش جزئیات اولین فیلم را مشاهده خواهیم کرد.
عدم نمایش مجدد صفحهی لاگین، به کاربران وارد شدهی به سیستم
آخرین تغییری را که در اینجا اعمال خواهیم کرد، رفع مشکل امکان مشاهدهی مجدد صفحهی لاگین، با وارد کردن مستقیم آدرس آن در مرورگر، پس از ورود موفقیت آمیز به سیستم است. برای این منظور، ابتدای متد رندر کامپوننت فرم لاگین را به صورت زیر تغییر میدهیم تا اگر کاربر، پیشتر به سیستم وارد شده بود، به صورت خودکار به ریشهی سایت هدایت شده و مجددا فرم لاگین برای او رندر نشود:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-29-backend.zip و sample-29-frontend.zip
روش محافظت از مسیریابیهای تعریف شدهی در برنامه
شبیه به روشی را که در قسمت قبل، برای انتقال شیء user، به مسیریابی کامپوننت Movies استفاده کردیم:
<Route path="/movies" render={props => <Movies {...props} user={this.state.currentUser} />} />
<Route path="/movies/:id" component={MovieForm} />
<Route path="/movies/:id" render={props => { if (!this.state.currentUser) { return <Redirect to="/login" />; } return <MovieForm {...props} />; }} />
اکنون اگر این تغییرات را ذخیره کرده و در حالت Logout، مسیر http://localhost:3000/movies/new را مستقیما درخواست دهیم، به صفحهی لاگین هدایت خواهیم شد.
ایجاد کامپوننتی با قابلیت استفادهی مجدد، برای محافظت از مسیریابیها
هرچند روشی که تا اینجا برای محافظت از مسیریابیها معرفی شد، بدون مشکل کار میکند، اما اگر قرار باشد برای تمام مسیریابیهای اینگونه، استفاده شود، به تکرار بیش از اندازهی کدهای یکسانی خواهیم رسید. به همین جهت میتوان این منطق را تبدیل به یک کامپوننت با قابلیت استفادهی مجدد کرد؛ تا دیگر نیازی به تکرار این if/elseها نباشد. برای این منظور، فایل جدید src\components\common\protectedRoute.jsx را ایجاد میکنیم. کامپوننت جدید protectedRoute را هم در پوشهی common قرار دادهایم؛ چون وابستگی به دومین این برنامه نداشته و میتواند در سایر برنامه نیز مورد استفاده قرار گیرد. سپس با استفاده از میانبرهای imrc و sfc، یک کامپوننت تابعی بدون حالت را به نام ProtectedRoute ایجاد کرده و در آن، همان کامپوننت اصلی Route را بازگشت میدهیم. بنابراین هر زمانیکه از ProtectedRoute استفاده شود، خروجی آن، همان کامپوننت استاندارد Route خواهد بود که اینبار قرار است از وضعیت کاربر جاری وارد شدهی به سیستم، مطلع باشد. به همین جهت در اولین قدم، همان قطعه کد Route فوق را که به همراه if/else نوشتیم، از فایل app.js کپی کرده و به اینجا، داخل متد رندر کامپوننت، منتقل میکنیم. سپس شروع میکنیم به متغیر کردن عباراتی که در آن به صورت صریح و ثابت، مقدار دهی شدهاند تا به یک کامپوننت با قابلیت استفادهی مجدد برسیم:
import React from "react"; import { Route, Redirect } from "react-router-dom"; import * as auth from "../../services/authService"; const ProtectedRoute = ({ path, component: Component, render, ...rest }) => { return ( <Route {...rest} render={props => { if (!auth.getCurrentUser()) return ( <Redirect to={{ pathname: "/login", state: { from: props.location } }} /> ); return Component ? <Component {...props} /> : render(props); }} /> ); }; export default ProtectedRoute;
- در این کامپوننت نیاز است اطلاعات کاربر جاری وارد شدهی به سیستم در دسترس باشد. یا میتوان آنرا به عنوان یکی از خواص props دریافت کرد و یا همانند این مثال، امکان دریافت مستقیم آن از authService نیز وجود دارد.
- در ادامه اگر CurrentUser مقدار دهی نشده باشد، کامپوننت Redirect را که کاربر را به صفحهی لاگین هدایت میکند، بازگشت میدهیم. در غیراینصورت نیاز است یک کامپوننت را بجای برای مثال MovieForm، بازگشت دهیم. علت استفادهی از component: Component این است که React انتظار دارد، کامپوننتها با نام بزرگ شروع شوند. به همین جهت خاصیت component را از props دریافت کرده و آنرا به Component تغییر نام میدهیم.
- زمانیکه از کامپوننت Route استاندارد استفاده میشود، یا از ویژگی component آن استفاده میشود و یا از ویژگی render آن که یک تابع است، تا بتوان داخل آن، کدهای پویایی را درج کرد. به همین جهت ممکن است که مقدار متغیر کامپوننت دریافت شده، نال باشد. بنابراین در اینجا بررسی میشود که آیا Component، مقدار دهی شدهاست یا خیر؟ اگر بله، همان کامپوننت را به همراه props آن بازگشت میدهیم. در غیراینصورت، متد render مقدار دهی شده را به همراه props ارسالی به آن، بازگشت خواهیم داد.
- علت وجود پارامتر rest نیز این است که این کامپوننت علاوه بر ویژگیهایی که تاکنون پیش بینی کردهایم، ممکن است در آینده ویژگیهای دیگری را نیز نیاز داشته باشد. به همین جهت مابقی آنها را توسط {rest...}، به صورت خودکار در اینجا درج میکنیم. برای نمونه در اینجا ذکر path={path} را مشاهده نمیکنید؛ چون توسط همان {rest...} به صورت خودکار تامین میشود.
اکنون به app.js بازگشته و کدهای قبلی را با این کامپوننت جدید ProtectedRoute، جایگزین میکنیم:
import ProtectedRoute from "./components/common/protectedRoute"; // ... <ProtectedRoute path="/movies/:id" component={MovieForm} />
مدیریت بازگشت کاربران، پس از لاگین به سیستم
پس از خروج از برنامه، اگر سعی در ویرایش یکی از فیلمهای موجود کنیم، به صفحهی لاگین هدایت خواهیم شد. پس از لاگین موفق، مجددا به ریشهی سایت بازگشت داده میشویم و نه به صفحهای که پیش از لاگین، مدنظر کاربر بودهاست. برای رفع این مشکل نیاز است بتوان به آدرس قبلی درخواستی، دسترسی یافت و این مورد توسط سیستم مسیریابی، به کامپوننتها به صورت خودکار تزریق میشود. برای مثال اگر در کامپوننت ProtectedRoute، مقدار شیء props دریافتی را لاگ کنیم:
return ( <Route {...rest} render={props => { console.log(props);
همانطور که مشخص است، شیء location دریافتی از props، به همراه اطلاعات آدرسی است که پیش از هدایت خودکار به صفحهی لاگین، درخواست کرده بودیم. به همین جهت یک چنین تنظیمی، در تعاریف کامپوننت ProtectedRoute درنظر گرفته شدهاند:
<Redirect to={{ pathname: "/login", state: { from: props.location } }} />
اکنون که این شیء، به کامپوننت لاگین، پس از Redirect خودکار ارسال میشود، نیاز است به src\components\loginForm.jsx مراجعه کرده و تغییرات زیر را اعمال کنیم:
doSubmit = async () => { try { const { data } = this.state; await auth.login(data.username, data.password); const { state } = this.props.location; window.location = state ? state.from.pathname : "/"; } catch (ex) { //...
تا اینجا اگر برنامه را ذخیره کرده، از سیستم خارج شویم و سعی در ویرایش اولین رکورد موجود در لیست فیلمها کنیم، ابتدا به صفحهی لاگین هدایت میشویم. پس از لاگین موفق، اینبار بجای مشاهدهی ریشهی سایت که در اینجا به لیست فیلمها تنظیم شده، دقیقا صفحهی ویرایش جزئیات اولین فیلم را مشاهده خواهیم کرد.
عدم نمایش مجدد صفحهی لاگین، به کاربران وارد شدهی به سیستم
آخرین تغییری را که در اینجا اعمال خواهیم کرد، رفع مشکل امکان مشاهدهی مجدد صفحهی لاگین، با وارد کردن مستقیم آدرس آن در مرورگر، پس از ورود موفقیت آمیز به سیستم است. برای این منظور، ابتدای متد رندر کامپوننت فرم لاگین را به صورت زیر تغییر میدهیم تا اگر کاربر، پیشتر به سیستم وارد شده بود، به صورت خودکار به ریشهی سایت هدایت شده و مجددا فرم لاگین برای او رندر نشود:
import { Redirect } from "react-router-dom"; //... render() { if (auth.getCurrentUser()) return <Redirect to="/" />;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-29-backend.zip و sample-29-frontend.zip
مطالب دورهها
تراکنشها در RavenDB
پیش از شروع به بحث در مورد تراکنشها و نحوه مدیریت آنها در RavenDB، نیاز است با مفهوم ACID آشنا شویم.
ACID چیست؟
ACID از 4 قاعده تشکیل شده است (Atomic, Consistent, Isolated, and Durable) که با کنار هم قرار دادن آنها یک تراکنش مفهوم پیدا میکند:
الف) Atomic: به معنای همه یا هیچ
اگر تراکنشی از چندین تغییر تشکیل میشود، همهی آنها باید با موفقیت انجام شوند، یا اینکه هیچکدام از تغییرات نباید فرصت اعمال نهایی را بیابند.
برای مثال انتقال مبلغ X را از یک حساب، به حسابی دیگر درنظر بگیرید. در این حالت X ریال از حساب شخص کسر و X ریال به حساب شخص دیگری واریز خواهد شد. اگر موجودی حساب شخص، دارای X ریال نباشد، نباید مبلغی از این حساب کسر شود. مرحله اول شکست خورده است؛ بنابراین کل عملیات لغو میشود. همچنین اگر حساب دریافت کننده بسته شده باشد نیز نباید مبلغی از حساب اول کسر گردد و در این حالت نیز کل تراکنش باید برگشت بخورد.
ب) Consistent یا یکپارچه
در اینجا consistency علاوه بر اعمال قیود، به معنای اطلاعاتی است که بلافاصله پس از پایان تراکنشی از سیستم قابل دریافت و خواندن است.
ج) Isolated: محصور شده
اگر چندین تراکنش در یک زمان با هم در حال اجرا باشند، نتیجه نهایی با حالتی که تراکنشها یکی پس از دیگری اجرا میشوند باید یکی باشد.
د) Durable: ماندگار
اگر سیستم پایان تراکنشی را اعلام میکند، این مورد به معنای 100 درصد نوشته شدن اطلاعات در سخت دیسک باید باشد.
مراحل چهارگانه ACID در RavenDB به چه نحوی وجود دارند؟
RavebDB از هر دو نوع تراکنشهای implicit و explicit پشتیبانی میکند. Implicit به این معنا است که در حین استفاده معمول از RavenDB (و بدون انجام تنظیمات خاصی)، به صورت خودکار مفهوم تراکنشها وجود داشته و اعمال میشوند. برای نمونه به متد ذیل توجه نمائید:
در این متد مراحل ذیل رخ میدهند:
- از document store ایی که پیشتر تدارک دیده شده، جهت بازکردن یک سشن استفاده شده است.
- به سشن صراحتا عنوان شده است که از Optimistic Concurrency استفاده کند. در این حالت RavenDB اطمینان حاصل میکند که اکانتهای بارگذاری شده توسط متدهای Load، تا زمان فراخوانی SaveChanges تغییر پیدا نکردهاند (و در غیراینصورت یک استثناء را صادر میکند).
- دو اکانت بر اساس Id آنها از بانک اطلاعاتی واکشی میشوند.
- موجودی یکی تقلیل یافته و موجودی دیگر، افزایش مییابد.
- متد SaveChanges بر روی شیء سشن فراخوانی شده است. تا زمانیکه این متد فراخوانی نشده است، کلیه تغییرات در حافظه نگهداری میشوند و به سرور ارسال نخواهند شد. فراخوانی آن سبب کامل شدن تراکنش و ارسال اطلاعات به سرور میگردد.
بنابراین شیء سشن بیانگر یک atomic transaction ماندگار و محصور شده است (سه جزء ACID تاکنون محقق شدهاند). محصور شده بودن آن به این معنا است که:
الف) هر تغییری که در سشن اعمال میشود، تا پیش از فراخوانی متد SaveChanges از دید سایر تراکنشها مخفی است.
ب) اگر دو تراکنش همزمان رخ دهند، تغییرات هیچکدام بر روی دیگری اثری ندارد.
اما Consistency یا یکپارچگی در RavenDB بستگی دارد به نحوهی خواندن اطلاعات و این مورد با دنیای رابطهای اندکی متفاوت است که در ادامه جزئیات آنرا بیشتر بررسی خواهیم کرد.
عاقبت یک دست شدن یا eventual consistency
درک Consistency مفهوم ACID در RavenDB بسیار مهم است و عدم آشنایی با نحوه عملکرد آن میتواند مشکلساز شود. در دنیای بانکهای اطلاعاتی رابطهای، برنامه نویسها به «immediate consistency» عادت دارند (یکپارچگی آنی). به این معنا که هرگونه تغییری در بانک اطلاعاتی، پس از پایان تراکنش، بلافاصله در اختیار کلیه خوانندگان سیستم قرار میگیرد. در RavenDB و خصوصا دنیای NoSQL، این یکپارچگی آنی دنیای رابطهای، به «eventual consistency» تبدیل میشود (عاقبت یکدست شدن). عاقبت یک دست شدن در RavenDB به این معنا است که اگر تغییری به یک سند اعمال گردیده و ذخیره شود؛ کوئری انجام شده بر روی این اطلاعات تغییر یافته ممکن است «stale data» باز گرداند. واژه stale در RavenDB به این معنا است که هنوز اطلاعاتی در دیتابیس موجود هستند که جهت تکمیل ایندکسها پردازش نشدهاند. به این مورد در قسمت بررسی ایندکسها در RavenDB اشاره شد.
در RavenDB یک سری تردهای پشت صحنه، مدام مشغول به کار هستند و بدون کند کردن عملیات سیستم، کار ایندکس کردن اطلاعات را انجام میدهند. هر زمانیکه اطلاعاتی را ذخیره میکنیم، بلافاصله این تردها تغییرات را تشخیص داده و ایندکسها را به روز رسانی میکنند. همچنین باید درنظر داشت که RavenDB جزو معدود بانکهای اطلاعاتی است که خودش را بر اساس نحوه استفاده شما ایندکس میکند! (نمونهای از آنرا در قسمت ایندکسهای پویای حاصل از کوئریهای LINQ پیشتر مشاهده کردهاید)
نکته مهم
در RavenDB اگر از کوئریهای LINQ استفاده کنیم، ممکن است به علت اینکه هنوز تردهای پشت صحنهی ایندکس سازی اطلاعات، کارشان تمام نشده است، تمام اطلاعات یا آخرین اطلاعات را دریافت نکنیم (که به آن stale data گفته میشود). هر آنچه که ایندکس شده است دریافت میگردد (مفهوم عاقبت یک دست شدن ایندکسها). اما اگر نیاز به یکپارچگی آنی داشتیم، متد Load یک سشن، مستقیما به بانک اطلاعاتی مراجعه میکند و اطلاعات بازگشت داده شده توسط آن هیچگاه احتمال stale بودن را ندارند.
بنابراین برای نمایش اطلاعات یا گزارشگیری، از کوئریهای LINQ استفاده کنید. RavenDB خودش را بر اساس کوئری شما ایندکس خواهد کرد و نهایتا به کوئریهایی فوق العاده سریعی در طول کارکرد سیستم خواهیم رسید. اما در صفحه ویرایش اطلاعات بهتر است از متد Load استفاده گردد تا نیاز به مفهوم immediate consistency یا یکپارچگی آنی برآورده شود.
تنظیمات خاص کار با ایندکس سازها برای انتظار جهت اتمام کار آنها
عنوان شد که اگر ایندکس سازهای پشت صحنه هنوز کارشان تمام نشده است، در حین کوئری گرفتن، هر آنچه که ایندکس شده بازگشت داده میشود.
در اینجا میتوان به RavenDB گفت که تا چه زمانی میتواند یک کوئری را جهت دریافت اطلاعات نهایی به تاخیر بیندازد. برای اینکار باید اندکی کوئریهای LINQ آنرا سفارشی سازی کنیم:
توسط امکانات آماری کوئریهای LINQ در RavenDB مطابق کدهای فوق، میتوان دریافت که آیا اطلاعات دریافت شده stale است یا خیر.
همچنین زمان انتظار تا پایان کار ایندکس ساز را نیز توسط متد Customize به نحو ذیل میتوان تنظیم کرد:
به علاوه میتوان کلیه کوئریهای یک documentStore را وارد به صبر کردن تا پایان کار ایندکس سازی کرد (متد Customize پیش فرضی را با WaitForNonStaleResultsAsOfLastWrite مقدار دهی و اعمال میکند):
این مورد در برنامههای وب توصیه نمیشود چون کل سیستم در حین آغاز کار با آن بر اساس یک documentStore سینگلتون باید کار کند و همین مساله صبر کردنها، با بالا رفتن حجم اطلاعات و تعداد کاربران، پاسخ دهی سیستم را تحت تاثیر قرار خواهد داد. به علاوه این تنظیم خاص بر روی کوئریهای پیشرفته Map/Reduce کار نمیکند. در این نوع کوئریهای ویژه، برای صبر کردن تا پایان کار ایندکس شدن، میتوان از روش زیر استفاده کرد:
مقابله با تداخلات همزمانی
با تنظیم session.Advanced.UseOptimisticConcurrency = true، اگر سندی که در حال ویرایش است، در این حین توسط کاربر دیگری تغییر کرده باشد، استثنای ConcurrencyException صادر خواهد شد. همچنین این استثناء در صورتیکه شخصی قصد بازنویسی سند موجودی را داشته باشد نیز صادر خواهد شد (شخصی بخواهد سندی را با ID سند موجودی ذخیره کند). اگر از optimistic concurrency استفاده نشود، آخرین ترد نویسنده یا به روز کننده اطلاعات، برنده خواهد شد و اطلاعات نهایی موجود در بانک اطلاعاتی متعلق به او و حاصل بازنویسی آن ترد است.
optimistic concurrency به زبان ساده به معنای به خاطر سپردن شماره نگارش یک سند است، زمانیکه آنرا بارگذاری میکنیم و سپس ارسال آن به سرور، زمانیکه قصد ذخیره آنرا داریم. در SQL Server اینکار توسط RowVersion انجام میشود. در بانکهای اطلاعاتی سندگرا چون تمایل به استفاده از HTTP در آنها زیاد است (مانند RavenDB) از مکانیزمی به نام E-Tag برای این منظور کمک گرفته میشود. هر زمانیکه تغییری به یک سند اعمال میشود، E-Tag آن به صورت خودکار افزایش خواهد یافت.
برای مثال فرض کنید کاربری سندی را با E-Tag مساوی 2 بارگذاری کرده است. قبل از اینکه این کاربر در صفحه ویرایش اطلاعات کارش با این سند خاتمه یابد، کاربر دیگری در شبکه، این سند را ویرایش کرده است و اکنون E-Tag آن مثلا مساوی 6 است. در این زمان اگر کاربر یک سعی به ذخیره سازی اطلاعات نماید، چون E-Tag سند او با E-Tag سند موجود در سرور دیگر یکی نیست، با استثنای ConcurrencyException متوقف خواهد شد.
مشکل! در برنامههای بدون حالت وب، چون پس از نمایش صفحه ویرایش اطلاعات، سشن RavenDB نیز بلافاصله Dispose خواهد شد، این E-Tag را از دست خواهیم داد. همچنین باید دقت داشت که سشن RavenDB به هیچ عنوان نباید در طول عمر یک برنامه باز نگهداشته شود و برای طول عمری کوتاه طراحی شده است. راه حلی که برای آن درنظر گرفته شده است، ذخیره سازی این E-Tag در بار اول دریافت آن از سشن میباشد. برای این منظور تنها کافی است خاصیتی را به نام Etag با ویژگی JsonIgnore (که سبب عدم ذخیره سازی آن در بانک اطلاعاتی خواهد شد) تعریف کنیم:
اکنون زمانیکه سندی را از بانک اطلاعاتی دریافت میکنیم، با استفاده از متد session.Advanced.GetEtagFor، میتوان این Etag واقعی را دریافت کرد و ذخیره نمود:
و برای استفاده از آن ابتدا باید UseOptimisticConcurrency به true تنظیم شده و سپس در متد Store این Etag دریافتی از سرور را مشخص نمائیم:
تراکنشهای صریح
همانطور که عنوان شد، به صورت ضمنی کلیه سشنها، یک واحد کار را تشکیل داده و با پایان آنها، تراکنش خاتمه مییابد. اگر به هر علتی قصد تغییر این رفتار ضمنی پیش فرض را دارید، امکان تعریف صریح تراکنشهای نیز وجود دارد:
باید دقت داشت که پایان یک تراکنش، یک non-blocking asynchronous call است و مباحث stale data که پیشتر در مورد آن بحث شد، برقرار هستند.
ACID چیست؟
ACID از 4 قاعده تشکیل شده است (Atomic, Consistent, Isolated, and Durable) که با کنار هم قرار دادن آنها یک تراکنش مفهوم پیدا میکند:
الف) Atomic: به معنای همه یا هیچ
اگر تراکنشی از چندین تغییر تشکیل میشود، همهی آنها باید با موفقیت انجام شوند، یا اینکه هیچکدام از تغییرات نباید فرصت اعمال نهایی را بیابند.
برای مثال انتقال مبلغ X را از یک حساب، به حسابی دیگر درنظر بگیرید. در این حالت X ریال از حساب شخص کسر و X ریال به حساب شخص دیگری واریز خواهد شد. اگر موجودی حساب شخص، دارای X ریال نباشد، نباید مبلغی از این حساب کسر شود. مرحله اول شکست خورده است؛ بنابراین کل عملیات لغو میشود. همچنین اگر حساب دریافت کننده بسته شده باشد نیز نباید مبلغی از حساب اول کسر گردد و در این حالت نیز کل تراکنش باید برگشت بخورد.
ب) Consistent یا یکپارچه
در اینجا consistency علاوه بر اعمال قیود، به معنای اطلاعاتی است که بلافاصله پس از پایان تراکنشی از سیستم قابل دریافت و خواندن است.
ج) Isolated: محصور شده
اگر چندین تراکنش در یک زمان با هم در حال اجرا باشند، نتیجه نهایی با حالتی که تراکنشها یکی پس از دیگری اجرا میشوند باید یکی باشد.
د) Durable: ماندگار
اگر سیستم پایان تراکنشی را اعلام میکند، این مورد به معنای 100 درصد نوشته شدن اطلاعات در سخت دیسک باید باشد.
مراحل چهارگانه ACID در RavenDB به چه نحوی وجود دارند؟
RavebDB از هر دو نوع تراکنشهای implicit و explicit پشتیبانی میکند. Implicit به این معنا است که در حین استفاده معمول از RavenDB (و بدون انجام تنظیمات خاصی)، به صورت خودکار مفهوم تراکنشها وجود داشته و اعمال میشوند. برای نمونه به متد ذیل توجه نمائید:
public void TransferMoney(string fromAccountNumber, string toAccountNumber, decimal amount) { using(var session = Store.OpenSession()) { session.Advanced.UseOptimisticConcurrency = true; var fromAccount = session.Load<Account>("Accounts/" + fromAccountNumber); var toAccount = session.Load<Account>("Accounts/" + toAccountNumber); fromAccount.Balance -= amount; toAccount.Balance += amount; session.SaveChanges(); } }
- از document store ایی که پیشتر تدارک دیده شده، جهت بازکردن یک سشن استفاده شده است.
- به سشن صراحتا عنوان شده است که از Optimistic Concurrency استفاده کند. در این حالت RavenDB اطمینان حاصل میکند که اکانتهای بارگذاری شده توسط متدهای Load، تا زمان فراخوانی SaveChanges تغییر پیدا نکردهاند (و در غیراینصورت یک استثناء را صادر میکند).
- دو اکانت بر اساس Id آنها از بانک اطلاعاتی واکشی میشوند.
- موجودی یکی تقلیل یافته و موجودی دیگر، افزایش مییابد.
- متد SaveChanges بر روی شیء سشن فراخوانی شده است. تا زمانیکه این متد فراخوانی نشده است، کلیه تغییرات در حافظه نگهداری میشوند و به سرور ارسال نخواهند شد. فراخوانی آن سبب کامل شدن تراکنش و ارسال اطلاعات به سرور میگردد.
بنابراین شیء سشن بیانگر یک atomic transaction ماندگار و محصور شده است (سه جزء ACID تاکنون محقق شدهاند). محصور شده بودن آن به این معنا است که:
الف) هر تغییری که در سشن اعمال میشود، تا پیش از فراخوانی متد SaveChanges از دید سایر تراکنشها مخفی است.
ب) اگر دو تراکنش همزمان رخ دهند، تغییرات هیچکدام بر روی دیگری اثری ندارد.
اما Consistency یا یکپارچگی در RavenDB بستگی دارد به نحوهی خواندن اطلاعات و این مورد با دنیای رابطهای اندکی متفاوت است که در ادامه جزئیات آنرا بیشتر بررسی خواهیم کرد.
عاقبت یک دست شدن یا eventual consistency
درک Consistency مفهوم ACID در RavenDB بسیار مهم است و عدم آشنایی با نحوه عملکرد آن میتواند مشکلساز شود. در دنیای بانکهای اطلاعاتی رابطهای، برنامه نویسها به «immediate consistency» عادت دارند (یکپارچگی آنی). به این معنا که هرگونه تغییری در بانک اطلاعاتی، پس از پایان تراکنش، بلافاصله در اختیار کلیه خوانندگان سیستم قرار میگیرد. در RavenDB و خصوصا دنیای NoSQL، این یکپارچگی آنی دنیای رابطهای، به «eventual consistency» تبدیل میشود (عاقبت یکدست شدن). عاقبت یک دست شدن در RavenDB به این معنا است که اگر تغییری به یک سند اعمال گردیده و ذخیره شود؛ کوئری انجام شده بر روی این اطلاعات تغییر یافته ممکن است «stale data» باز گرداند. واژه stale در RavenDB به این معنا است که هنوز اطلاعاتی در دیتابیس موجود هستند که جهت تکمیل ایندکسها پردازش نشدهاند. به این مورد در قسمت بررسی ایندکسها در RavenDB اشاره شد.
در RavenDB یک سری تردهای پشت صحنه، مدام مشغول به کار هستند و بدون کند کردن عملیات سیستم، کار ایندکس کردن اطلاعات را انجام میدهند. هر زمانیکه اطلاعاتی را ذخیره میکنیم، بلافاصله این تردها تغییرات را تشخیص داده و ایندکسها را به روز رسانی میکنند. همچنین باید درنظر داشت که RavenDB جزو معدود بانکهای اطلاعاتی است که خودش را بر اساس نحوه استفاده شما ایندکس میکند! (نمونهای از آنرا در قسمت ایندکسهای پویای حاصل از کوئریهای LINQ پیشتر مشاهده کردهاید)
نکته مهم
در RavenDB اگر از کوئریهای LINQ استفاده کنیم، ممکن است به علت اینکه هنوز تردهای پشت صحنهی ایندکس سازی اطلاعات، کارشان تمام نشده است، تمام اطلاعات یا آخرین اطلاعات را دریافت نکنیم (که به آن stale data گفته میشود). هر آنچه که ایندکس شده است دریافت میگردد (مفهوم عاقبت یک دست شدن ایندکسها). اما اگر نیاز به یکپارچگی آنی داشتیم، متد Load یک سشن، مستقیما به بانک اطلاعاتی مراجعه میکند و اطلاعات بازگشت داده شده توسط آن هیچگاه احتمال stale بودن را ندارند.
بنابراین برای نمایش اطلاعات یا گزارشگیری، از کوئریهای LINQ استفاده کنید. RavenDB خودش را بر اساس کوئری شما ایندکس خواهد کرد و نهایتا به کوئریهایی فوق العاده سریعی در طول کارکرد سیستم خواهیم رسید. اما در صفحه ویرایش اطلاعات بهتر است از متد Load استفاده گردد تا نیاز به مفهوم immediate consistency یا یکپارچگی آنی برآورده شود.
تنظیمات خاص کار با ایندکس سازها برای انتظار جهت اتمام کار آنها
عنوان شد که اگر ایندکس سازهای پشت صحنه هنوز کارشان تمام نشده است، در حین کوئری گرفتن، هر آنچه که ایندکس شده بازگشت داده میشود.
در اینجا میتوان به RavenDB گفت که تا چه زمانی میتواند یک کوئری را جهت دریافت اطلاعات نهایی به تاخیر بیندازد. برای اینکار باید اندکی کوئریهای LINQ آنرا سفارشی سازی کنیم:
RavenQueryStatistics stats; var results = session.Query<Product>() .Statistics(out stats) .Where(x => x.Price > 10) .ToArray(); if (stats.IsStale) { // Results are known to be stale }
همچنین زمان انتظار تا پایان کار ایندکس ساز را نیز توسط متد Customize به نحو ذیل میتوان تنظیم کرد:
RavenQueryStatistics stats; var results = session.Query<Product>() .Statistics(out stats) .Where(x => x.Price > 10) .Customize(x => x.WaitForNonStaleResults(TimeSpan.FromSeconds(5))) .ToArray();
documentStore.Conventions.DefaultQueryingConsistency = ConsistencyOptions.QueryYourWrites;
while (documentStore.DatabaseCommands.GetStatistics().StaleIndexes.Length != 0) { Thread.Sleep(10); }
مقابله با تداخلات همزمانی
با تنظیم session.Advanced.UseOptimisticConcurrency = true، اگر سندی که در حال ویرایش است، در این حین توسط کاربر دیگری تغییر کرده باشد، استثنای ConcurrencyException صادر خواهد شد. همچنین این استثناء در صورتیکه شخصی قصد بازنویسی سند موجودی را داشته باشد نیز صادر خواهد شد (شخصی بخواهد سندی را با ID سند موجودی ذخیره کند). اگر از optimistic concurrency استفاده نشود، آخرین ترد نویسنده یا به روز کننده اطلاعات، برنده خواهد شد و اطلاعات نهایی موجود در بانک اطلاعاتی متعلق به او و حاصل بازنویسی آن ترد است.
optimistic concurrency به زبان ساده به معنای به خاطر سپردن شماره نگارش یک سند است، زمانیکه آنرا بارگذاری میکنیم و سپس ارسال آن به سرور، زمانیکه قصد ذخیره آنرا داریم. در SQL Server اینکار توسط RowVersion انجام میشود. در بانکهای اطلاعاتی سندگرا چون تمایل به استفاده از HTTP در آنها زیاد است (مانند RavenDB) از مکانیزمی به نام E-Tag برای این منظور کمک گرفته میشود. هر زمانیکه تغییری به یک سند اعمال میشود، E-Tag آن به صورت خودکار افزایش خواهد یافت.
برای مثال فرض کنید کاربری سندی را با E-Tag مساوی 2 بارگذاری کرده است. قبل از اینکه این کاربر در صفحه ویرایش اطلاعات کارش با این سند خاتمه یابد، کاربر دیگری در شبکه، این سند را ویرایش کرده است و اکنون E-Tag آن مثلا مساوی 6 است. در این زمان اگر کاربر یک سعی به ذخیره سازی اطلاعات نماید، چون E-Tag سند او با E-Tag سند موجود در سرور دیگر یکی نیست، با استثنای ConcurrencyException متوقف خواهد شد.
مشکل! در برنامههای بدون حالت وب، چون پس از نمایش صفحه ویرایش اطلاعات، سشن RavenDB نیز بلافاصله Dispose خواهد شد، این E-Tag را از دست خواهیم داد. همچنین باید دقت داشت که سشن RavenDB به هیچ عنوان نباید در طول عمر یک برنامه باز نگهداشته شود و برای طول عمری کوتاه طراحی شده است. راه حلی که برای آن درنظر گرفته شده است، ذخیره سازی این E-Tag در بار اول دریافت آن از سشن میباشد. برای این منظور تنها کافی است خاصیتی را به نام Etag با ویژگی JsonIgnore (که سبب عدم ذخیره سازی آن در بانک اطلاعاتی خواهد شد) تعریف کنیم:
public class Person { public string Id { get; set; } [JsonIgnore] public Guid? Etag { get; set; } public string Name { get; set; } }
public Person Get(string id) { var person = session.Load<Person>(id); person.Etag = session.Advanced.GetEtagFor(person); return person; }
public void Update(Person person) { session.Advanced.UseOptimisticConcurrency = true; session.Store(person, person.Etag, person.Id); session.SaveChanges(); person.Etag = session.Advanced.GetEtagFor(person); }
تراکنشهای صریح
همانطور که عنوان شد، به صورت ضمنی کلیه سشنها، یک واحد کار را تشکیل داده و با پایان آنها، تراکنش خاتمه مییابد. اگر به هر علتی قصد تغییر این رفتار ضمنی پیش فرض را دارید، امکان تعریف صریح تراکنشهای نیز وجود دارد:
using (var transaction = new TransactionScope()) { using (var session1 = store.OpenSession()) { session1.Store(new Account()); session1.SaveChanges(); } using (var session2 = store.OpenSession()) { session2.Store(new Account()); session2.SaveChanges(); } transaction.Complete(); }
نظرات اشتراکها