نمایش قسمتی از صفحه بر اساس وضعیت اعتبارسنجی کاربر
فرض کنید میخواهیم در کامپوننت Shared\LoginDisplay.razor که در قسمت قبل آنرا اضافه کردیم، لینکهای ثبت نام و لاگین را به کاربران غیر اعتبارسنجی شده (هنوز لاگین نکرده) نمایش دهیم و اگر کاربر، اعتبارسنجی شده بود (لاگین کرده بود)، لینک خروج را به او نمایش دهیم. برای این منظور کامپوننت Shared\LoginDisplay.razor را به صورت زیر تغییر میدهیم:
<AuthorizeView> <Authorized> <a href="Identity/Account/Logout">Logout</a> </Authorized> <NotAuthorized> <a href="Identity/Account/Register">Register</a> <a href="Identity/Account/Login">Login</a> </NotAuthorized> </AuthorizeView>
البته اگر برنامه را در همین حالت اجرا کنیم، به استثنای زیر خواهیم رسید:
InvalidOperationException: Authorization requires a cascading parameter of type Task<AuthenticationState>. Consider using CascadingAuthenticationState to supply this. Microsoft.AspNetCore.Components.Authorization.AuthorizeViewCore.OnParametersSetAsync()
بنابراین به فایل BlazorServer.App\App.razor که محل تعریف ریشهی مسیریابی برنامهاست، مراجعه کرده و کامپوننت آنرا با کامپوننت توکار CascadingAuthenticationState محصور میکنیم:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
اکنون اگر برنامه را اجرا کنیم، مشاهده خواهیم کرد که در اولین بار مراجعهی به آن (پیش از لاگین)، لینک به صفحهی خروج، نمایش داده نشدهاست؛ چون آنرا در فرگمنت مخصوص Authorized قرار دادیم:
آزمایش نمایش منوی خروج برنامه
برای آزمایش برنامه، نیاز است ابتدا یک کاربر جدید را ثبت کنیم؛ چون هنوز هیچ کاربری در آن ثبت نشدهاست و همچنین کاربر پیشفرضی را هم به همراه ندارد. در مورد روش ثبت کاربران پیشفرض ASP.NET Core Identity، میتوانید به مطلب «بازنویسی متد مقدار دهی اولیهی کاربر ادمین در ASP.NET Core Identity توسط متد HasData در EF Core» مراجعه کنید و تمام نکات آن، در اینجا هم صادق است (چون پایهی سیستم Identity مورد استفاده، یکی است و هدف ما در اینجا بیشتر بررسی نکات یکپارچه سازی آن با Blazor Server است و نه مرور تمام نکات ریز Identity).
بنابراین ابتدا از منوی بالای صفحه، گزینهی Register را انتخاب کرده و کاربری را ثبت میکنیم. پس از ثبت نام، بلافاصله به منوی جدید زیر میرسیم که در آن گزینههای ورود و ثبت نام، مخفی شدهاند و اکنون گزینهی خروج از سیستم را نمایش میدهد:
بهبود تجربهی کاربری خروج از سیستم
در همین حال که گزینهی خروج نمایش داده شدهاست، اگر بر روی لینک آن کلیک کنیم، ابتدا ما را به صفحهی مجزای logout هدایت میکند. سپس باید در این صفحه، مجددا بر روی لینک logout بالای آن کلیک کنیم. زمانیکه اینکار را انجام دادیم، اکنون صفحهی دیگری را نمایش میدهد که به همراه پیام «خروج موفقیت آمیز از سیستم» است! در این پروسه، کاربر احساس میکند که کاملا از برنامهی اصلی خارج شدهاست و همچنین مراحل طولانی را نیز باید طی کند.
مدیریت این مراحل توسط دو فایل زیر انجام میشوند:
Areas\Identity\Pages\Account\Logout.cshtml
Areas\Identity\Pages\Account\Logout.cshtml.cs
میخواهیم کدهای این دو فایل را به نحوی تغییر دهیم که اگر کاربری بر روی لینک logout برنامهی اصلی کلیک کرد، به صورت خودکار logout شده و سپس مجددا به صفحهی اصلی برنامهی Blazor Server هدایت شود و مجبور نباشد تا مراحل طولانی یاد شده را تکرار کند.
به همین جهت ابتدا فایل Logout.cshtml.cs را حذف میکنیم؛ چون نیازی به آن نداریم. سپس محتوای فایل Logout.cshtml را به صورت زیر تغییر میدهیم:
@page @using Microsoft.AspNetCore.Identity @inject SignInManager<IdentityUser> SignInManager @functions { public async Task<IActionResult> OnGet() { if (SignInManager.IsSignedIn(User)) { <p>You have successfully logged out of the application.</p> await SignInManager.SignOutAsync(); } return Redirect("~/"); } }
نمایش User Claims، در یک برنامهی Blazor Server
سیستم ASP.NET Core Identity، بر اساس User Claims کار میکند؛ اطلاعات بیشتر. پس از استفاده از CascadingAuthenticationState در بالاترین سطح برنامه، اطلاعات آن در سراسر برنامهی Blazor Server هم قابل دسترسی است. برای مثال در کامپوننت Shared\LoginDisplay.razor، به نحو زیر میتوان نام کاربر ثبت نام شده را که یکی از User Claims او است، نمایش داد:
<AuthorizeView> <Authorized> Hello, @context.User.Identity.Name <a href="Identity/Account/Logout">Logout</a> </Authorized>
محدود کردن دسترسی به صفحات برنامه تنها برای کاربران اعتبارسنجی شده
پس از لاگین موفق به سیستم، اکنون میخواهیم دسترسی به صفحات تعریف اتاقها و یا امکانات رفاهی هتل را تنها به کاربران لاگین شده، محدود کنیم. برای اینکار تنها کافی است از ویژگی Authorize استفاده کنیم. برای مثال به کامپوننت Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و یک سطر زیر را به آن اضافه میکنیم:
@attribute [Authorize]
مشکل! با اینکه تمام کامپوننتهای مثال جاری را به ویژگی Authorize مزین کردهایم، اما ... کار نمیکند! و هنوز هم میتوان بدون لاگین به سیستم، به محتوای آنها دسترسی داشت.
برای رفع این مشکل، مجددا نیاز است کامپوننت BlazorServer.App\App.razor را ویرایش کرد:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> @*<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />*@ <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <NotAuthorized> <p>Sorry, you do not have access to this page</p> </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-22.zip
معرفی سرویسهای ارائه شده توسط شرکتهای گوگل، آمازون و مایکروسافت در قالب رایانش ابری - قسمت دوم
سرویسها و اجزای وب سرویس آمازون:
وب سرویسهای آمازون دارای اجزای زیادی میباشند. تعدادی از این سرویسها برای ارائه خدمات پردازشی و تعداد دیگری برای ارائه فضای ذخیرهسازی، عرضه شدهاند. در ادامه گروهی از این سرویسها معرفی میگردد:
- ابر محاسباتی توسعه پذیر آمازون (EC2)
- سرویس صف ساده آمازون(Simple Queue Service): یک صف پیام یا سیستم تراکنش برای برنامههای مبتنی بر اینترنت توزیع شده میباشد. این سرویس تضمین میکند که پیامها حتی در زمانی که مؤلفهای موجود نیست، گم نشود و برای انتقال پیام میان مؤلفههای مختلف که هرکدام کار جداگانهای را انجام میدهند، بسیار مناسب است.
- سرویس
آگاه سازی ساده آمازون(Simple Notification Service): ): وب سرویسی است که میتواند پیام
یک برنامه را منتشر کند و آنها را به برنامهها یا مشترکین دیگر منتقل کند. SNS متدی
را برای راهاندازی فعالیتها ارائه مینماید که برنامهها را قادر میسازد تا در
مورد اطلاعات جدید یا تغییر یافته از آنها نظرسنجی شود یا به روز رسانیها را
انجام دهند.
- سرویس
نظارت ابر آمازون(Amazon Cloud Watch): کنسولی
را فراهم میکند که در آن مصرف منابع، شاخصهای کلیدی عملکرد سایت و نشانگرهای
عملیاتی برای عواملی همچون تقاضای پردازشگر، مصرف دیسک و ورودی و خروجی شبکه را
ارائه میدهد. نتایج
معیارهایی که توسط آن کسب میشود برای فعالسازی قابلیتی به نام Auto Scaling مورد
استفاده قرار میگیرد که به صورت خودکار میتواند یک سایت EC2 را بر مبنای مجموعهای از قوانین
که توسعه دهنده ایجاد میکند، توسعه دهد.
- توازن بار منعطف(Elastic Load Balancing): نمونههای ماشین آمازون(Amazon Machine Image) با استفاده از این قابلیت، دارای امکان توازن بار ترافیکی میشوند. این قابلیت هنگامی که نمونهای دچار شکست میشود آن را کشف کرده و ترافیک را به یک نمونه سالم حتی نمونهای در محیطهای دیگر AWS مسیریابی مجدد میکند.
قیمت گذاری انواع مختلف نمونه ماشین آمازون به سه پارامتر وابسته است. اولین مورد سیستم عامل مورد استفاده است. دومین عامل مرکز دادهای است که در آن قرار گرفته و سومین عامل مدت زمانی است که اجرا میشود. نرخها بر مبنای ساعت محاسبه میشوند. علاوه بر آن مبالغ اضافی نیز بابت موارد زیر اخذ میشود:
- میزان داده
منتقل شده
- آدرسهای IP اختصاصی
- استفاده
سرور اختصاصی مجازی از فضای ذخیرهسازی بلوکی توسعه پذیر آمازون
- استفاده از توازن بار توسعه پذیر برای دو یا چند سرور
- سایر
ویژگیهای مورد نیاز
- نمونه
مبتنی بر تقاضا: نرخ
ساعتی بدون التزام طولانی مدت
-
نمونه رزرو شده: خرید قراردادی هر نمونه با هزینه به مراتب پایینتر به ازای هر ساعت بعد از رزرو اولیه
-
نمونه نقطهای: این متد برای قیمت گذاری بر روی ظرفیت استفاده نشده EC2 بر مبنای قیمت نقطه فعلی است. این قابلیت، قیمتهای بسیار پایین را به همراه خواهد داشت اما در زمانهای مختلف فرق میکند یا در زمانی که ظرفیت مازادی نباشد، در دسترس نخواهد بود.
نوع | موتور محاسبه | حافظه اصلی(GB) | ذخیره سازی(GB) | سکو |
ریز نمونه | تا دو واحد محاسباتی در انفجار بار | 0.613 | EBS | 32 یا 64 بیتی |
نمونه کوچک | یک واحد محاسباتی | 1.7 | 160 | 32 بیتی |
نمونه بزرگ | چهار واحد محاسباتی | 7.5 | 850 | 64 بیتی |
نمونه بسیار بزرگ | هشت واحد محاسباتی | 15 | 1690 | 64 بیتی |
این تقسیم بندی مزایای متعددی را به همراه دارد که نهایتا پیاده سازی و توسعه راحتتر نرم افزارهای بزرگ و پیچیده را ممکن مینماید. از جمله مزایای این معماری میتوان به راحتتر شدن مباحث continuous delivery/deployment، مقیاس پذیری بهتر، تحمل خطا، مهاجرت به (و یا استفاده از) تکنولوژیهای جدید در بخشهای مختلف نرم افزار و ... اشاره نمود.
مهمترین بخش و تصمیمات شما به عنوان یک معمار نرم افزار، هنگام طراحی با استفاده از این معماری، شناسایی بخشهای مختلف کسب و کار، جدا سازی و مرزبندی نمودن آنها و نهایتا طراحی سرویسها و تعیین نحوه همکاری آنها با یکدیگر میباشد. لذا در هنگام استفاده از معماری میکروسرویس، مرکز توجهات باید کسب و کار باشد و نه مسائل تکنیکال و موضوعاتی مانند Docker, Kubernetes , Serverless و ... . (DDD میتواند به شما جهت مرزبندی بخشهای مختلف کسب و کار و شناسایی سرویسها کمک نماید)
تا اینجا متوجه شدیم که میکروسرویس در واقع یک سبک معماری نرم افزار محسوب میگردد و در واقع میکروسرویس (در اینجا و ادامه مقاله، منظور از میکروسرویس، معماری میکروسرویس میباشد) از چندین سرویس مجزا و مستقل تشکیل شدهاست که هر سرویس معمولا مسئولیت بخشی از منطق کسب و کار را بر عهده خواهد داشت.
مشخصات یک سرویس
ساختار یک سرویس
بیایید با دقت به هر یک از بخشهای یک سرویس (با توجه به دیاگرام فوق) نگاه کنیم
هر سرویس احتمالا دارای یک یا چندین API میباشد
- وقایع (Events)
منطق کسب و کار، قلب هر سرویس و دلیل وجود آن سرویس میباشد که API هایی را در قالب عملیات (Opertaions) پیاده سازی و همچنین مواردی را در قالب وقایع (Events) منتشر مینماید. همچنین منطق کسب و کار میتواند بنا بر نیاز خود، عملیات مربوط به سایر سرویسها را فراخوانی و یا در کانالهای ارتباطی (channels) مربوط به وقایع آنها، مشترک (Subscribes) شود و نهایتا دادهها را در دیتابیس خود نگهداری نماید.
نحوه همکاری سرویسها با یکدیگر (Services Collaborations)
با توجه به مفاهیم فوق، زمانی که صحبت از همکاری (collaborate) بین سرویسها میشود، معمولا منظور، ارتباط آنها از طریق APIهای یکدیگر (شامل عملیات و وقایع که پیشتر توضیح داده شد) میباشد (به جای خواندن و نوشتن مستقیم در دیتابیسهای مربوط به یکدیگر میباشد).
همچنین یک سرویس جهت همکاری با دیگر سرویسها میتواند در وقایع منتشر شده (Published events) توسط آنها مشترک (Subscribes) شود. برای مثال سرویس ثبت سفارش احتمالا در وقایع منتشر شده از سوی سرویس رستوران مشترک میشود.
دیتابیس اختصاصی
معمولا هر سرویس دارای یک یا چند دیتابیس میباشد که دیتای اختصاصی مربوط به منطق کسب و کار خود و در مواردی بخشی از دیتای مربوط به سایر سرویسها را در آن(ها) نگهداری میکند. برای مثال اطلاعات سفارشها را هم سرویس ثبت سفارش و هم سرویس رستوران، هر دو نگهداری میکنند و عملا این دیتا ابتدا در سرویس رستوران و سپس در سرویس ثبت سفارش، مجددا نگهداری میشود و به نوعی دیتای فوق Replicate شده و تکراری میباشد. اما به جهت اطمینان از کاهش وابستگی (loose coupling) این تکرار دادهها انجام میشود. در مجموع استفاده از یک دیتابیس مشترک (منظور table مشترک میباشد) بین سرویسها ایدهی بدی میباشد و سرویسها باید از طریق APIهای یکدیگر باهم همکاری نمایند.
نتیجه
در این مقاله عنوان شد که میکروسرویس یک سبک معماری میباشد و در این معماری، نرم افزار و منطق کسب و کار، به چندین سرویس مختلف تقسیم میشود. مشخصات کلیدی که هر سرویس باید در این سبک معماری (microservice architecture) داشته باشد و همچنین ساختار درونی هر سرویس بررسی شد.
در قسمت بعدی این مقاله، در مورد نحوه مستند سازی این سرویسها صحبت میشود. چرا که با زیاد شدن تعداد سرویسها، در صورت عدم وجود یک مستندات مناسب (documents)، ارتباط و هماهنگی تیمها با یکدیگر خود موجب سربار خواهد شد.
مطابق RFC مربوطه، اگر هدر درخواست ارسالی به سرور را کمی تغییر دهیم میتوان بجای شروع از اولین بایت، از بایت مورد نظر شروع به دریافت فایل نمود. (البته این به شرطی است که سرور آنرا پشتیبانی کند)
یعنی نیاز داریم که به هدر ارسالی سطر زیر را اضافه کنیم:
Range: bytes=n-
برای بدست آوردن اندازهی فایل ناقص موجود میتوان از دستور زیر استفاده کرد:
using System.IO;
long brokenLen = new FileInfo(fileNamePath).Length;
سپس اگر شیء webRequest ما به صورت زیر تعریف شده باشد:
HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url);
فقط کافی است سطر زیر را جهت افزودن قابلیت از سرگیری مجدد دریافت فایل به این شیء افزود:
//دانلود از ادامه
webRequest.AddRange((int)brokenLen); //resume
نکته:
اگر علاقمند باشید که ریز فعالیتهای انجام شده توسط فضای نام System.Net را ملاحظه کنید، به فایل config خود (مثلا فایل app.config برنامه)، چند سطر زیر را اضافه کنید:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.diagnostics>
<trace autoflush="true" />
<sources>
<source name="System.Net">
<listeners>
<add name="MyTraceFile"/>
</listeners>
</source>
</sources>
<sharedListeners>
<add
name="MyTraceFile"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="System.Net.trace.log"
/>
</sharedListeners>
<switches>
<add name="System.Net" value="Verbose" />
</switches>
</system.diagnostics>
</configuration>
ملاحظات:
بدیهی است پیاده سازی قابلیت resume نیاز به موارد زیر خواهد داشت:
الف) در نظر گرفتن مسیری پیش فرض برای ذخیره سازی فایلها
ب) پیدا کردن اندازهی فایل موجود بر روی یک سرور و مقایسهی آن با حجم فایل موجود بر روی هارد
امکان پیدا کردن اندازهی یک فایل هم بدون دریافت کامل آن میسر است. خاصیت ContentLength مربوط به شیء HttpWebResponse بیانگر اندازهی یک فایل بر روی سرور است و صد البته پیش از استفاده از این عدد، مقدار StatusCode شیء نامبرده را بررسی کنید. اگر مساوی OK بود، یعنی این عدد معتبر است.
ORM های NHibernate و Entity framework روشهای متفاوتی را برای به روز رسانی کلید خارجی با حداقل رفت و برگشت به دیتابیس ارائه میدهند که در ادامه معرفی خواهند شد.
صورت مساله:
فرض کنید میخواهیم برنامهای را بنویسیم که ریز پرداختهای روزانهی ما را ثبت کند. برای اینکار حداقل به یک جدول گروههای اقلام خریداری شده، یک جدول حسابهای تامین کنندهی مخارج، یک جدول فروشندهها و نهایتا یک جدول صورتحسابهای پرداختی بر اساس جداول ذکر شده نیاز خواهد بود.
الف) بررسی مدل برنامه
در اینجا جهت تعریف ویژگیها یا Attributes تعریف شده در این کلاسها از NHibernate validator استفاده شده (+). مزیت اینکار هم علاوه بر اعتبارسنجی سمت کلاینت (پیش از تبادل اطلاعات با بانک اطلاعاتی)، تولید جداولی با همین مشخصات است. برای مثال Fluent NHibernate بر اساس ویژگی Length تعریف شده با طول حداکثر 120 ، یک فیلد nvarchar با همین طول را ایجاد میکند.
public class Account
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
}
public class Category
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 130, Message = "طول نام باید بین 3 و 130 کاراکتر باشد")]
public virtual string Name { get; set; }
}
public class Payee
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
}
public class Bill
{
public virtual int Id { get; set; }
[NotNull]
public virtual Account Account { get; set; }
[NotNull]
public virtual Category Category { get; set; }
[NotNull]
public virtual Payee Payee { get; set; }
[NotNull]
public virtual decimal Amount { set; get; }
[NotNull]
public virtual DateTime BillDate { set; get; }
[NotNullNotEmpty]
[Length(Min = 1, Max = 500, Message = "طول توضیحات باید بین 1 و 500 کاراکتر باشد")]
public virtual string Description { get; set; }
}
ب) ساختار جداول متناظر (تولید شده به صورت خودکار توسط Fluent NHibernate در اینجا)
در مورد نحوهی استفاده از ویژگی AutoMapping و همچنین تولید خودکار ساختار بانک اطلاعاتی از روی جداول در NHibernate قبلا توضیح داده شده است. البته بدیهی است که ترکیب مقالهی Validation و آشنایی با AutoMapping در اینجا جهت اعمال ویژگیها باید بکار گرفته شود که در همان مقالهی Validation مفصل توضیح داده شده است.
نکتهی مهم database schema تولیدی، کلیدهای خارجی (foreign key) تعریف شده بر روی جدول Bills است (همان AccountId، CategoryId و PayeeId تعریف شده) که به primary key جداول متناظر اشاره میکند.
create table Accounts (
AccountId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (AccountId)
)
create table Bills (
BillId INT IDENTITY NOT NULL,
Amount DECIMAL(19,5) not null,
BillDate DATETIME not null,
Description NVARCHAR(500) not null,
AccountId INT not null,
CategoryId INT not null,
PayeeId INT not null,
primary key (BillId)
)
create table Categories (
CategoryId INT IDENTITY NOT NULL,
Name NVARCHAR(130) not null,
primary key (CategoryId)
)
create table Payees (
PayeeId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (PayeeId)
)
alter table Bills
add constraint fk_Account_Bill
foreign key (AccountId)
references Accounts
alter table Bills
add constraint fk_Category_Bill
foreign key (CategoryId)
references Categories
alter table Bills
add constraint fk_Payee_Bill
foreign key (PayeeId)
references Payees
ج) صفحهی ثبت صورتحسابها
صفحات ثبت گروههای اقلام، حسابها و فروشندهها، نکتهی خاصی ندارند. چون این جداول وابستگی خاصی به جایی نداشته و به سادگی اطلاعات آنها را میتوان ثبت یا به روز کرد.
صفحهی مشکل در این مثال، همان صفحهی ثبت صورتحسابها است که از سه کلید خارجی به سه جدول دیگر تشکیل شده است.
عموما برای طراحی این نوع صفحات، کلیدهای خارجی را با drop down list نمایش میدهند و اگر در جهت سهولت کار کاربر قدم برداشته شود، باید از یک Auto complete drop down list استفاده کرد تا کاربر برنامه جهت یافتن آیتمهای از پیش تعریف شده کمتر سختی بکشد.
اگر از Silverlight یا WPF استفاده شود، امکان بایند یک لیست کامل از اشیاء با تمام خواص مرتبط به آنها وجود دارد (هر رکورد نمایش داده شده در دراپ داون لیست، دقیقا معادل است با یک شیء متناظر با کلاسهای تعریف شده است). اگر از ASP.NET استفاده شود (یعنی یک محیط بدون حالت که پس از نمایش یک صفحه دیگر خبری از لیست اشیاء بایند شده وجود نخواهد داشت و همگی توسط وب سرور جهت صرفه جویی در منابع تخریب شدهاند)، بهتر است datatextfield را با فیلد نام و datavaluefield را با فیلد Id مقدار دهی کرد تا کاربر نهایی، نام را جهت ثبت اطلاعات مشاهده کند و برنامه از Id موجود در لیست جهت ثبت کلیدهای خارجی استفاده نماید.
و نکتهی اصلی هم همینجا است که چگونه؟! چون ما زمانیکه با یک ORM سر و کار داریم، برای ثبت یک رکورد در جدول Bills باید یک وهله از کلاس Bill را ایجاد کرده و خواص آنرا مقدار دهی کنیم. اگر به تعریف کلاس Bill مراجعه کنید، سه خاصیت آن از نوع سه کلاس مجزا تعریف شده است. به به عبارتی با داشتن فقط یک id از رکوردهای این کلاسها باید بتوان سه وهلهی متناظر آنها را از بانک اطلاعاتی خواند و سپس به این خواص انتساب داد:
var newBill = new Bill
{
Account = accountRepository.GetByKey(1),
Amount = 1,
BillDate = DateTime.Now,
Category = categoryRepository.GetByKey(1),
Description = "testestest...",
Payee = payeeRepository.GetByKey(1)
};
- یکبار برای دریافت رکورد متناظر با گروهها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت میشود)
- یکبار برای دریافت رکورد متناظر با فروشندهها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت میشود)
- یکبار برای دریافت رکورد متناظر با حسابها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت میشود)
- یکبار هم ثبت نهایی اطلاعات در بانک اطلاعاتی
متد GetByKey فوق همان متد session.Get استاندارد NHibernate است (چون به primary key ها از طریق drop down list دسترسی داریم، به سادگی میتوان بر اساس متد Get استاندارد ذکر شده عمل کرد).
SQL نهایی تولیدی هم به صورت واضحی این مشکل را نمایش میدهد (4 بار رفت و برگشت؛ سه بار select یکبار هم insert نهایی):
SELECT account0_.AccountId as AccountId0_0_, account0_.Name as Name0_0_
FROM Accounts account0_ WHERE account0_.AccountId=@p0;@p0 = 1 [Type: Int32 (0)]
SELECT category0_.CategoryId as CategoryId2_0_, category0_.Name as Name2_0_
FROM Categories category0_ WHERE category0_.CategoryId=@p0;@p0 = 1 [Type: Int32 (0)]
SELECT payee0_.PayeeId as PayeeId3_0_, payee0_.Name as Name3_0_
FROM Payees payee0_ WHERE payee0_.PayeeId=@p0;@p0 = 1 [Type: Int32 (0)]
INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:48:33 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]
کسانی که قبلا با رویههای ذخیره شده کار کرده باشند (stored procedures) احتمالا الان خواهند گفت؛ ما که گفتیم این روش کند است! سربار زیادی دارد! فقط کافی است یک SP بنویسید و کل عملیات را با یک رفت و برگشت انجام دهید.
اما در ORMs نیز برای انجام این مورد در طی یک حرکت یک ضرب راه حلهایی وجود دارد که در ادامه بحث خواهد شد:
د) پیاده سازی با NHibernate
برای حل این مشکل در NHibernate با داشتن primary key (برای مثال از طریق datavaluefield ذکر شده)، بجای session.Get از session.Load استفاده کنید.
session.Get یعنی همین الان برو به بانک اطلاعاتی مراجعه کن و رکورد متناظر با کلید اصلی ذکر شده را بازگشت بده و یک شیء از آن را ایجاد کن (حالتهای دیگر دسترسی به اطلاعات مانند استفاده از LINQ یا Criteria API یا هر روش مشابه دیگری نیز در اینجا به همین معنا خواهد بود).
session.Load یعنی فعلا دست نگه دار! مگر در جدول نهایی نگاشت شده، اصلا چیزی به نام شیء مثلا گروه وجود دارد؟ مگر این مورد واقعا یک فیلد عددی در جدول Bills بیشتر نیست؟ ما هم که الان این عدد را داریم (به کمک عناصر دراپ داون لیست)، پس لطفا در پشت صحنه یک پروکسی برای ایجاد شیء مورد نظر ایجاد کن (uninitialized proxy to the entity) و سپس عملیات مرتبط را در حین تشکیل SQL نهایی بر اساس این عدد موجود انجام بده. یعنی نیازی به رفت و برگشت به بانک اطلاعاتی نیست. در این حالت اگر SQL نهایی را بررسی کنیم فقط یک سطر زیر خواهد بود (سه select ذکر شده حذف خواهند شد):
INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:58:22 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]
ه) پیاده سازی با Entity framework
Entity framework زمانیکه بانک اطلاعاتی فوق را (به روش database first) به کلاسهای متناظر تبدیل/نگاشت میکند، حاصل نهایی مثلا در مورد کلاس Bill به صورت خلاصه به شکل زیر خواهد بود:
public partial class Bill : EntityObject
{
public global::System.Int32 BillId {set;get;}
public global::System.Decimal Amount {set;get;}
public global::System.DateTime BillDate {set;get;}
public global::System.String Description {set;get;}
public global::System.Int32 AccountId {set;get;}
public global::System.Int32 CategoryId {set;get;}
public global::System.Int32 PayeeId {set;get;}
public Account Account {set;get;}
public Category Category {set;get;}
}