BEGINNERS Budget App .NET 6, ASP.NET C# MVC (FULL PROJECT) - YouTube
Chapters
0:00 - Intro
2:45 - Create Project
3:32 - Examine Files
5:56 - Cleaning Up
8:00 - Create Database
10:24 - Repository
14:50 - View Models
17:35 - List of Transactions
21:25 - Add Transactions
33:40 - Update Transactions
36:45 - Delete Transactions
39:20 - Categories List
43:00 - Insert Categories
46:00 - Update Categories
47:30 - Delete Categories
49:15 - Frontend Validation
49:50 - Transactions Filter
55:00 - Styling
قطعا شرایطی پیش خواهد آمد که شما مجبور شوید دادههایی را به عنوان تنظیمات برنامه در محلی ذخیره کنید و مجددا آنها را فراخوانی کنید. روشهای مختلفی برای این کار وجود دارند که معروفترین و سادهترین راه، استفاده از Settings خود پروژه میباشد. اما این به منزله بهترین راه نیست! در این مطلب قصد داریم تنظیمات برنامه را در یک فایل json، با همان ساختار استانداردش ذخیره و بازیابی کنیم.
حالا برای دسترسی به این کلاس، یک متغیر جنریک را به نام Config ایجاد میکنیم:
همینطور برای خواندن یک فایل جیسون از محلی مشخص، یک متغیر دیگر را به نام filename ایجاد میکنیم:
در این کلاس به 2 متد نیاز داریم. متد اول برای دیسریالایز کردن فایل جیسون و متد دوم برای سریالایز کردن اطلاعات:
بصورت پیشفرض محل خواندن فایل جیسون را در کنار فایل اجرایی exe و با نام AppConfig.json در نظر میگیریم. در صورتی که فایل ما موجود بود، به کمک ReadAllText محتوای فایل جیسون را میخوانیم و در صورتی که خالی نبود، اقدام به دیسریالایز کردن آن میکنیم.
پراپرتی Config را که شامل اطلاعات ما میباشد، در محل موردنظر سریالایز میکنیم.
حالا پراپرتیهای دلخواه خود را ایجاد میکنیم:
دقت کنید که قبل از خواندن تنظیمات، باید ابتدا فایل تنظیمات را دیسریالایز کرده باشید. پس هنگام اجرای پروژه، متد Init را فراخوانی کنید:
و برای ذخیره کردن :
برای اینکار نیاز به سریالایز و دیسریالایز کردن مدل داریم. اگر از دات نت کور استفاده میکنید، کتابخانه توکار جیسون، در فضای نام System.Text.Json از عهده این کار بر میاد و اگر از نسخههای دات نت فریمورک استفاده میکنید، باید پکیج newtonsoft.json را نصب کنید.
برای شروع یک کلاس را به نام GlobalData (یا هر نام دلخواه دیگری) ایجاد کنید.
چون قرار هست این کلاس هر نوع مدلی را برای ما سریالایز و دیسریالایز کند، پس کلاس را بصورت جنریک تعریف کنید.
public abstract class GlobalData<T> where T : GlobalData<T>, new()
public static T Config { get; set; }
private static string _filename { get; set; }
public static void Init(string FileName = "AppConfig.json") { _filename = FileName; if (File.Exists(FileName)) { string json = File.ReadAllText(FileName); Config = (string.IsNullOrEmpty(json) ? new T() : JsonSerializer.Deserialize<T>(json)) ?? new T(); } else { Config = new T(); } }
در متد Save نیز:
public static void Save() { JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true, IgnoreNullValues = true }; string json = JsonSerializer.Serialize(Config, options); File.WriteAllText(_filename, json); }
حالا به سراغ پروژهی دمو میرویم. یک کلاس را ایجاد کرده و از کلاس GlobalData ارث بری میکنیم:
internal class AppConfig : GlobalData<AppConfig>
public string ServerUrl { get; set; } = "https://sub.deltaleech.com"; public bool IsShowNotification { get; set; } = true; public NavigationViewPaneDisplayMode PaneDisplayMode { get; set; } = NavigationViewPaneDisplayMode.Left; public SkinType Skin { get; set; } = SkinType.Default;
protected override void OnStartup(StartupEventArgs e) { GlobalData<AppConfig>.Init(); }
برای خواندن تنظیمات به این صورت عمل کنید:
var skin = GlobalData<AppConfig>.Config.Skin;
GlobalData<AppConfig>.Config.Skin = Skin.Dark; GlobalData<AppConfig>.Save();
دقت داشته باشید که اگر بعد از ذخیره کردن تنظیمات، قصد داشته باشید اطلاعات جدید را دریافت کنید، باید حتما قبل از دریافت اطلاعات، متد Init را یکبار دیگر فراخوانی کنید تا اطلاعات جدید، نمایش داده شود.
ببین دوست من ، ابتدا نگاهی به ساختار dropdown لیستها داشته باش.
مقداری که به سرور ارسال میشه مقدار داخل value هست و چیزی که نمایش داده میشه جای Name قرار میگیره
attr برای ایجاد کردن یک اتریبیوت به یک المان هست و از text هم برای مقداری دهی به خود المان به کار میرود.
مقداری که باید توی dropdown نشان داده بشند چیزی به این صورته :
<option value="value">Name</option>
حالا ما ابن جا میخواهیم این قسمتها رو خودمان درست کنیم.
در یک حلقه each به اضای مقادیری که از سرور گرفتیم loop می زنیم option را میسازیم و به dropdown اضافه میکنیم.
در مورد این قسمت هم باید بگم
.attr("value", data[i].Id).text(data[i].Title)
مثلا برای یک رکورد که داری ای دی 100 و عنوان AmirHossein هست این option اینطوری ساخته میشه:
<option value="100">AmirHossein</option>
تسکهای پس زمینه (Background Job) چیست؟
بطور کلی تسکهای پس زمینه، کارهایی هستند که برنامه باید بصورت خودکار در زمانهای مشخصی آنها را انجام دهد؛ برای مثال شرایطی را در نظر بگیرید که متدی را با حجم زیادی از محاسبات پیچیده دارید که وقتی کاربر درخواست خود را ارسال میکند، شروع به محاسبه میشود و کاربر چارهای جز انتظار نخواهد داشت؛ اما اگر اینکار در زمانی دیگر، قبل از درخواست کاربر محاسبه میشد و صرفا نتیجهاش به کاربر نمایش داده میشد، قطعا تصمیم گیری بهتری نسبت به محاسبهی آنی آن متد، در زمان درخواست کاربر بوده.
در سناریوی دیگری تصور کنید میخواهید هر شب در ساعتی مشخص، خلاصهای از مطالب وبسایتتان را برای کاربران وبسایت ایمیل کنید. در این حالت برنامه باید هر شب در ساعتی خاص اینکار را برای ما انجام دهد و تماما باید این اتفاق بدون دخالت هیچ ارادهی انسانی و بصورت خودکار توسط برنامه انجام گیرد.
همچنین شرایطی از این قبیل، ارسال ایمیل تایید هویت، یک ساعت بعد از ثبت نام، گرفتن بک آپ از اطلاعات برنامه بصورت هفتگی و دیگر این موارد، همه در دستهی تسکهای پس زمینه (Background Job) از یک برنامه قرار دارند.
سؤال : HangFire چیست؟
همانطور که دانستید، تسکهای پس زمینه نیاز به یک سیستم مدیریت زمان دارند که کارها را در زمانهای مشخص شدهای به انجام برساند. HangFire یک پکیج متن باز، برای ایجاد سیستم زمانبندی شدهی کارها است و اینکار را به سادهترین روش، انجام خواهد داد. همچنین HangFire در کنار Quartz که سیستم دیگری جهت پیاده سازی زمانبندی است، از معروفترین پکیجها برای زمانبندی تسکهای پس زمینه بشمار میروند که در ادامه بیشتر به مزایا و معایب این دو میپردازیم.
مقایسه HangFire و Quartz
میتوان گفت این دو پکیج تا حد زیادی شبیه به هم هستند و تفاوت اصلی آنها، در لایههای زیرین و نوع محاسبات زمانی هریک، نهفته است که الگوریتم مختص به خود را برای این محاسبات دارند؛ اما در نهایت یک کار را انجام میدهند.
دیتابیس :
تفاوتی که میتوان از آن نام برد، وجود قابلیت Redis Store در HangFire است که Quratz چنین قابلیتی را از سمت خودش ارائه نداده و برای استفاده از Redis در Quartz باید شخصا این دو را باهم کانفیگ کنید. دیتابیس Redis بخاطر ساختار دیتابیسی که دارد، سرعت و پرفرمنس بالاتری را ارائه میدهد که استفاده از این قابلیت، در پروژههایی با تعداد تسکها و رکوردهای زیاد، کاملا مشهود است. البته این ویژگی در HangFire رایگان نیست و برای داشتن آن از سمت HangFire، لازم است هزینهی آن را نیز پرداخت کنید؛ اما اگر هم نمیخواهید پولی بابتش بپردازید و همچنان از آن استفاده کنید، یک پکیج سورس باز برای آن نیز طراحی شده که از این لینک میتوانید آنرا مشاهده کنید.
ساختار :
پکیج HangFire از ابتدا با دات نت و معماریهای دات نتی توسعه داده شده، اما Quartz ابتدا برای زبان جاوا نوشته شده بود و به نوعی از این زبان، ریلیزی برای دات نت تهیه شد و این موضوع طبعا تاثیرات خودش را داشته و برخی از معماریها و تفکرات جاوایی در آن مشهود است که البته مشکلی را ایجاد نمیکند و محدودیتی نسبت به HangFire از لحاظ کارکرد، دارا نیست. شاید تنها چیزی که میتوان در این باب گفت، DotNet Friendlyتر بودن HangFire است که کار با متدهای آن، آسانتر و به اصطلاح، خوش دستتر است.
داشبورد :
هردو پکیج از داشبورد، پشتیبانی میکنند که میتوانید در این داشبورد و ui اختصاصی که برای نمایش تسکها طراحی شده، تسکهای ایجاد شده را مدیریت کنید. داشبورد HangFire بصورت پیشفرض همراه با آن قرار دارد که بعد از نصب HangFire میتوانید براحتی داشبورد سوار بر آن را نیز مشاهده کنید. اما در Quartz ، داشبورد باید بصورت یک Extension، در پکیجی جدا به آن اضافه شود و مورد استفاده قرار گیرد. در لینکهای 1 و 2، دوتا از بهترین داشبوردها برای Quartz را مشاهده میکنید که در صورت نیاز میتوانید از آنها استفاده کنید.
استفاده از HangFire
1. نصب :
- برای نصب HangFire در یک پروژهی Asp.Net Core لازم است ابتدا پکیجهای مورد نیاز آن را نصب کنید؛ که شامل موارد زیر است:
Install-Package Hangfire.Core Install-Package Hangfire.SqlServer Install-Package Hangfire.AspNetCore
- پس از نصب پکیجها باید تنظیمات مورد نیاز برای پیاده سازی HangFire را در برنامه، اعمال کنیم. این تنظیمات شامل افزودن سرویسها و اینترفیسهای HangFire به برنامه است که اینکار را با افزودن HangFire به متد ConfigureService کلاس Startup انجام خواهیم داد:
public void ConfigureServices(IServiceCollection services) { // Add Hangfire services. services.AddHangfire(configuration => configuration .SetDataCompatibilityLevel(CompatibilityLevel.Version_170) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings() .UseSqlServerStorage(Configuration.GetConnectionString("HangfireConnection"), new SqlServerStorageOptions { CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), QueuePollInterval = TimeSpan.Zero, UseRecommendedIsolationLevel = true, DisableGlobalLocks = true })); // Add the processing server as IHostedService services.AddHangfireServer(); // Add framework services. services.AddMvc(); }
- پکیج HangFire برای مدیریت کار و زمان ، Table هایی دارد که پس از نصب، بر روی دیتابیس برنامهی شما قرار میگیرد. فقط باید دقت داشته باشید ConnectionString دیتابیس خود را در متد AddHangFire مقدار دهی کنید، تا از این طریق دیتابیس برنامه را شناخته و Tableهای مورد نظر را در Schema جدیدی با نام HangFire به آن اضافه کند.
پ ن : HangFire بصورت پیشفرض با دیتابیس SqlServer ارتباط برقرار میکند.
- این پکیج یک داشبورد اختصاصی دارد که در آن لیستی از انواع تسکهای در صف انجام و گزارشی از انجام شدهها را در اختیار ما قرار میدهد. برای تنظیم این داشبورد باید Middleware مربوط به آن و endpoint جدیدی را برای شناسایی مسیر داشبورد HangFire در برنامه، در متد Configure کلاس Startup اضافه کنید :
public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IHostingEnvironment env) { // HangFire Dashboard app.UseHangfireDashboard(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); // HangFire Dashboard endpoint endpoints.MapHangfireDashboard(); }); }
برای اینکه به داشبورد HangFire دسترسی داشته باشید، کافیست پس از نصب و انجام تنظیمات مذکور، برنامه را اجرا کنید و در انتهای Url برنامه، کلمهی "hangfire" را وارد کنید. سپس وارد پنل داشبورد آن خواهید شد.
http://localhost:50255/hangfire
app.UseHangfireDashboard("/mydashboard");
http://localhost:50255/mydashboard
2. داشبورد :
داشبورد HangFire شامل چندین بخش و تب مختلف است که به اختصار هر یک را بررسی خواهیم کرد.
تب Job :
همهی تسکهای تعریف شده، شامل Enqueued, Succeeded, Processing, Failed و... در این تب نشان داده میشوند.
تب Retries :
این تب مربوط به تسکهایی است که در روال زمانبندی و اجرا، به دلایل مختلفی مثل Stop شدن برنامه توسط iis یا Down شدن سرور و یا هر عامل خارجی دیگری، شکست خوردند و در زمانبندی مشخص شده، اجرا نشدند. همچنین قابلیت دوبارهی به جریان انداختن job مورد نظر را در اختیار ما قرار میدهد که از این طریق میتوان تسکهای از دست رفته را مدیریت کرد و دوباره انجام داد.
تب Recurring Jobs :
وقتی شما یک تسک را مانند گرفتن بکاپ از دیتابیس، بصورت ماهانه تعریف میکنید و قرار است در هر ماه، این اتفاق رخ دهد، این مورد یک تسک تکراری تلقی شده و این تب مسئول نشان دادن اینگونه از تسکها میباشد.
تب Servers :
این بخش، سرویسهایی را که HangFire برای محاسبهی زمانبندی از آنها استفاده میکند، نشان میدهد. وقتی متد services.AddHangfireServer را به متد ConfigureService کلاس Startup اضافه میکنید، سرویسهای HangFire جهت محاسبهی زمانبندیها فعال میشوند.
3. امنیت داشبورد :
همانطور که دانستید، داشبورد، اطلاعات کاملی از نوع کارها و زمان اجرای آنها و نام متدها را در اختیار ما قرار میدهد و همچنین اجازهی تغییراتی را مثل حذف یک تسک، یا دوباره به اجرا در آوردن تسکها و یا اجرای سریع تسکهای به موعد نرسیده را به کاربر میدهد. گاهی ممکن است این اطلاعات، شامل محتوایی امنیتی و غیر عمومی باشد که هرکسی در برنامه حق دسترسی به آنها را ندارد. برای مدیریت کردن این امر، میتوانید مراحل زیر را طی کنید :
مرحله اول : یک کلاس را ایجاد میکنیم (مثلا با نام MyAuthorizationFilter) که این کلاس از اینترفیسی با نام IDashboardAuthorizationFilter ارث بری خواهد کرد.
public class MyAuthorizationFilter : IDashboardAuthorizationFilter { public bool Authorize(DashboardContext context) { var httpContext = context.GetHttpContext(); // Allow all authenticated users to see the Dashboard (potentially dangerous). return httpContext.User.Identity.IsAuthenticated; } }
درون این کلاس، متدی با نام Authorize از اینترفیس مربوطه، پیاده سازی میشود که شروط احراز هویت و صدور یا عدم صدور دسترسی را کنترل میکند. این متد، یک خروجی Boolean دارد که اگر هر یک از شروط احراز هویت شما تایید نشد، خروجی false را بر میگرداند. در این مثال، ما برای دسترسی، محدودیت Login بودن را اعمال کردهایم که این را از HttpContext میگیریم.
مرحله دوم : در این مرحله کلاسی را که بعنوان فیلتر احراز هویت برای کاربران ساختهایم، در optionهای middleware پکیج HangFire اضافه میکنیم.
app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = new [] { new MyAuthorizationFilter() } });
app.UseHangfireDashboard("/hangfire", new DashboardOptions { IsReadOnlyFunc = (DashboardContext context) => true });
این گزینه اجازهی هرگونه تغییری را در روند تسکها، از طریق صفحهی داشبورد، از هر کاربری سلب میکند و داشبورد را صرفا به جهت نمایش کارها استفاده میکند نه چیز دیگر.
انواع تسکها در HangFire :
1. تسکهای Fire-And-Forget :
تسکهای Fire-And-Forget زمانبندی خاصی ندارند و بلافاصله بعد از فراخوانی، اجرا میشوند. برای مثال شرایطی را در نظر بگیرید که میخواهید پس از ثبت نام هر کاربر در وبسایت، یک ایمیل خوش آمد گویی ارسال کنید. این عمل یک تسک پس زمینه تلقی میشود، اما زمانبندی خاصی را نیز نمیخواهید برایش در نظر بگیرید. در چنین شرایطی میتوانید از متد Enqueue استفاده کنید و یک تسک Fire-And-Forget را ایجاد کنید تا این تسک صرفا در تسکهای پس زمینهتان نام برده شود و قابل مشاهده باشد.
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = backgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName) { var jobId = _backgroundJobClient.Enqueue(() => SendWelcomeMail(userName)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
2. تسکهای Delayed :
همانطور که از اسم آن پیداست، تسکهای Delayed، تسکهایی هستند که با یک تاخیر در زمان، اجرا خواهند شد. بطور کلی زمانبندی این تسکها به دو دسته تقسیم میشود :
- دسته اول : اجرا پس از تاخیر در زمانی مشخص.
همان شرایط ارسال ایمیل را به کاربرانی که در وبسایتتان ثبت نام میکنند، در نظر بگیرید؛ اما اینبار میخواهید نه بلافاصله، بلکه 10 دقیقه بعد از ثبت نام کاربر، ایمیل خوش آمد گویی را ارسال کنید. در این نوع شما یک تاخیر 10 دقیقهای میخواهید که Delayed Jobها اینکار را برای ما انجام میدهند.
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = backgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName) { var jobId = BackgroundJob.Schedule(() => SendWelcomeMail(userName),TimeSpan.FromMinutes(10)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
همچنین میتوانید از ورودیهای دیگر نوع TimeSpan شامل TimeSpan.FromMilliseconds و TimeSpan.FromSecondsو TimeSpan.FromMinutes و TimeSpan.FromDays برای تنظیم تاخیر در تسکهای خود استفاده کنید.
- دسته دوم : اجرا در زمانی مشخص.
نوع دیگر استفاده از متد Schedule، تنظیم یک تاریخ و زمان مشخصی برای اجرا شدن تسکهای در آن تاریخ و زمان واحد میباشد. برای مثال سناریویی را در نظر بگیرید که دستور اجرا و زمانبندی آن، در اختیار کاربر باشد و کاربر بخواهد یک Reminder، در تاریخ مشخصی برایش ارسال شود که در اینصوررت میتوانید با استفاده از instance دیگری از متد Schedule که ورودی ای از جنس DateTimeOffset را دریافت میکند، تاریخ مشخصی را برای آن اجرا، انتخاب کنید.
public class HomeController : Controller { private readonly IBackgroundJobClient _backgroundJobClient; public HomeController(IBackgroundJobClient backgroundJobClient) { _backgroundJobClient = IBackgroundJobClient; } [HttpPost] [Route("welcome")] public IActionResult Welcome(string userName , DateTime dateAndTime) { var jobId = BackgroundJob.Schedule(() => SendWelcomeMail(userName),DateTimeOffset(dateAndTime)); return Ok($"Job Id {jobId} Completed. Welcome Mail Sent!"); } public void SendWelcomeMail(string userName) { //Logic to Mail the user Console.WriteLine($"Welcome to our application, {userName}"); } }
در این مثال، تاریخ مشخصی را برای اجرای تسکهای خود، از کاربر، در ورودی اکشن متد دریافت کردهایم و به متد Schedule، در غالب DateTimeOffset تعریف شده، پاس میدهیم.
3. تسکهای Recurring :
تسکهای Recurring به تسکهایی گفته میشود که باید در یک بازهی گردشی از زمان اجرا شوند. در یک مثال، بیشتر با آن آشنا خواهیم شد. فرض کنید میخواهید هر هفته، برنامه از اطلاعات دیتابیس موجود بکاپ بگیرد. در اینجا تسکی را دارید که قرار است هر هفته و هربار به تکرر اجرا شود.
public class HomeController : Controller { private readonly IRecurringJobManager _recurringJobManager; public HomeController(IRecurringJobManager recurringJobManager) { _recurringJobManager = recurringJobManager; } [HttpPost] [Route("BackUp")] public IActionResult BackUp(string userName) { _recurringJobManager.AddOrUpdate("test", () => BackUpDataBase(), Cron.Weekly); return Ok(); } public void BackUpDataBase() { // ... } }
برای تنظیم یک Recurring Job باید اینترفیس دیگری را بنام IRecurringJobManager، تزریق کرده و متد AddOrUpdate را استفاده کنید. در ورودی این متد، یک نوع تعریف شده در HangFire بنام Cron دریافت میشود که بازهی گردش در زمان را دریافت میکند که در اینجا بصورت هفتگی است.
انواع دیگر Cron شامل :
- هر دقیقه (Cron.Minutely) :
این Cron هر دقیقه یکبار اجرا خواهد شد.
_recurringJobManager.AddOrUpdate("test", () => job , Cron.Minutely);
- هر ساعت (Cron.Hourly) :
این Cron هر یک ساعت یکبار و بصورت پیشفرض در دقیقه اول هر ساعت اجرا میشود.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Hourly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Hourly(10));
- هر روز (Cron.Daily) :
این Cron بصورت روزانه و در حالت پیشفرض، در اولین ساعت و اولین دقیقهی هر روز اجرا خواهد شد.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Daily);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Daily(3,10));
- هر هفته (Cron.Weekly) :
این Cron هفتگی است. بصورت پیشفرض هر هفته، شنبه در اولین ساعت و در اولین دقیقه، اجرا میشود.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Weekly);
_recurringJobManager.AddOrUpdate("test", () => Job,Cron.Weekly(DayOfWeek.Monday,3,10));
- هر ماه (Cron.Monthly) :
این Cron بصورت ماهانه اولین روز ماه در اولین ساعت روز و در اولین دقیقه ساعت، زمانبندی خود را اعمال میکند.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Monthly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Monthly(10,3,10));
- هر سال (Cron.Yearly) :
و در نهایت این Cron بصورت سالانه و در اولین ماه، روز، ساعت و دقیقه هر سال، وظیفه خود را انجام خواهد داد.
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Yearly);
_recurringJobManager.AddOrUpdate("test", () => Job, Cron.Yearly(2,4,3,10));
در نهایت با استفاده از این Cronها میتوانید انواع مختلفی از Recurring Jobها را بسازید.
4. تسکهای Continuations :
این نوع از تسکها، وابسته به تسکهای دیگری هستند و بطور کلی وقتی استفاده میشوند که ما میخواهیم تسکی را پس از تسک دیگری، با یک زمانبندی، به نسبت زمان اجرای تسک اول، اجرا کنیم. برای مثال میخواهیم 10 دقیقه بعد از ثبت نام کاربر، برای او ایمیل احراز هویت ارسال شود که شبیه اینکار را در تسکهای Delayed انجام داده بودیم. اما همچنین قصد داریم 5 دقیقه بعد از ارسال ایمیل احراز هویت، لینک فرستاده شده را منسوخ کنیم. در این سناریو ما دو زمانبندی داریم؛ اول 10 دقیقه بعد از ثبت نام کاربر و دوم 5 دقیقه بعد از اجرای متد اول.
var stepOne = _backgroundJobClient.Schedule(() => SendAuthorizationEmail(), TimeSpan.FromMinute(10)); _backgroundJobClient.ContinueJobWith(stepOne, () => _backgroundJobClient.Schedule(() => ExpireAuthorizationEmail(), TimeSpan.FromHours(5)));
برای ایجاد یک Continuations Job باید از متد ContinueJobWith در اینترفیس IBackgroundJobClient استفاده کنیم و در ورودی اول این متد، آیدی تسک ایجاد شده قبلی را پاس دهیم.
برخی از نکات و ترفندهای HangFire
1. استفاده از Cron Expression در Recurring Jobها :
بطور کلی، Cron، ساختاری تعریف شده برای تعیین بازههای زمانی است. Cron اختصار یافتهی کلمات Command Run On میباشد که به اجرا شدن یک دستور، در زمان مشخصی اشاره دارد. برای استفاده از آن، ابتدا به تعریف این ساختار میپردازیم :
* * * * * command to be executed - - - - - | | | | | | | | | ----- Day of week (0 - 7) (Sunday=0 or 7) | | | ------- Month (1 - 12) | | --------- Day of month (1 - 31) | ----------- Hour (0 - 23) ------------- Minute (0 - 59)
* * * * *
- فیلد اول (Minute) : در این فیلد باید دقیقهای مشخص از یک ساعت را وارد کنید؛ مانند دقیقه 10 (میتوانید محدودهای را هم تعیین کنید)
- فیلد دوم (Hour) : در این فیلد باید زمان معلومی را با فرمت ساعت وارد کنید؛ مانند ساعت 7 (میتوانید محدودهای را هم تعیین کنید، مانند ساعات 12-7)
- فیلد سوم (Day of Month) : در این فیلد باید یک روز از ماه را وارد کنید؛ مانند روز 15 ام از ماه (میتوانید محدودهای را هم تعیین کنید)
- فیلد چهارم (Month) : در این فیلد باید یک ماه از سال را وارد کنید؛ مثلا ماه 4 ام(آوریل) (میتوانید محدودهای را هم تعیین کنید)
- فیلد پنجم (Day of Week) : در این فیلد باید روزی از روزهای هفته یا محدودهای از آن روزها را تعیین کنید. مانند صفرم هفته که در کشورهای اروپایی و آمریکایی معادل روز یکشنبه است.
همانطور که میبینید، Cronها دسترسی بهتری از تعیین بازههای زمانی مختلف را ارائه میدهند که میتوانید از آن در Recurring متدها بجای ورودیهای Yearly - Monthly - Weekly - Daily - Hourly - Minutely استفاده کنید. در واقع خود این ورودیها نیز متدی تعریف شده در کلاس Cron هستند که با فراخوانی آن، خروجی Cron Expression را میسازند و در درون ورودی متد Recurring قرار میگیرند.
در ادامه مثالی را خواهیم زد تا نیازمندی به Cron Expressionها را بیشتر درک کنید. فرض کنید میخواهید یک زمانبندی داشته باشید که "هر ماه بین بازه 10 ام تا 15 ام، بطور روزانه در ساعت 4:00" اجرا شود. اعمال این زمانبندی با متدهای معمول در کلاس Cron امکان پذیر نیست؛ اما میتوانید با Cron Expression آنرا اعمال کنید که به این شکل خواهد بود:
0 4 10-15 * *
برای ساخت Cron Expressionها وبسایت هایی وجود دارند که کمک میکنند انواع Cron Expressionهای پیچیدهای را طراحی کنیم و با استفاده از آن، زمانبندیهای دقیقتر و جزئیتری را بسازیم. یکی از بهترین وبسایتها برای اینکار crontab.guru است.
پ. ن. برای استفاده از Cron Expression در متدهای Recurring کافی است بجای ورودیهای Yearly - Monthly - Weekly - Daily - Hourly - Minutely ، خود Cron Expression را درون ورودی متد تعریف کنیم :
_recurringJobManager.AddOrUpdate("test", () => job , "0 4 10-15 * *" );
2. متد Trigger :
متد Trigger یک متد برای اجرای آنی تسکهای Recurring است که به کمک آن میتوانید این نوع از تسکها را بدون در نظر گرفتن زمانبندی آنها، در لحظه اجرا کنید و البته تاثیری در دفعات بعدی تکرار نداشته باشد.
RecurringJob.Trigger("some-id");
3. تعیین تاریخ انقضاء برای Recurring Jobها :
گاهی ممکن است در تسکهای Recurring شرایطی پیش آید که برفرض میخواهید کاری را هر ماه انجام دهید، اما این تکرار در پایان همان سال تمام میشود. در اینصورت باید یک Expire Time برای متد Recurring خود تنظیم کنیم تا بعد از 12 ماه تکرار، در تاریخ 140X/12/30 به پایان برسد. HangFire برای متدهای Recurring ورودی با عنوان ExpireTime تعریف نکرده، اما میتوان از طریق ایجاد یک زمانبندی، تاریخ مشخصی را برای حذف کردن متد Recurring تعریف کرد که همانند یک ExpireTime عمل میکند.
_recurringJobManager.AddOrUpdate("test", () => Console.WriteLine("Recurring Job"), Cron.Monthly); _backgroundJobClient.Schedule(() => _recurringJobManager.RemoveIfExists("test"), DateTimeOffset(dateAndTime));
با اجرای این متد، اول کاری برای تکرار در زمانبندی ماهیانه ایجاد میشود و در متد دوم، زمانی برای حذف متد اول مشخص میکند.
در آخر امیدوارم این مقاله برایتان مفید واقع شده باشد. میتوانید فیدبکتان را در قالب کامنت یا یک قهوه برایم ارسال کنید.
قبلا مطالبی در سایت راجع به نوع داده شمارشی یا Enum و همچنین CheckBoxList و RadioButtonList وجود دارد. اما در این مطلب قصد دارم تا یک روش متفاوت را برای تولید و بهره گیری از CheckBoxList با استفاده از نوع دادههای شمارشی برای شما ارائه کنم.
فرض کنید بخواهید به کاربر این امکان را بدهید تا بتواند چندین گزینه را برای یک فیلد انتخاب کند. به عنوان یک مثال ساده فرض کنید گزینه ای از مدل، پارچههای مورد علاقه یک نفر هست. کاربر میتواند چندین پارچه را انتخاب کند. و این فرض را هم بکنید که به لیست پارچهها گزینه دیگری اضافه نخواهد شد. پارچه (Fabric) را مثلا میتوانیم به صورت زیر تقسیم بندی کنیم :
حال فرض کنید View Model زیر فیلدی از نوع نوع داده شمارشی Fabric دارد:
توجه داشته باشید که فیلد Fabric از کلاس MyViewModel باید چند مقدار را در خود نگهداری کند. یعنی میتواند هر کدام از گزینههای Cotton، Silk، Wool، Rayon، Other به صورت جداگانه یا ترکیبی باشد. اما در حال حاضر با توجه به اینکه یک فیلد Enum معمولی فقط میتواند یک مقدار را در خودش ذخیره کند قابلیت ذخیره ترکیبی مقادیر در فیلد Fabric از View Model بالا وجود ندارد.
اما راه حل این مشکل استفاده از پرچم (Flags) در تعریف نوع داده شمارشی هست. با استفاده از پرچم نوع داده شمارشی بالا به صورت زیر باید تعریف شود:
همان طور که میبینید از عبارت [Flags] قبل از تعریف enum استفاده کرده ایم. همچنین هر کدام از مقادیر ممکن این نوع داده شمارشی با توانهایی از 2 تنظیم شده اند. در این صورت یک نمونه از این نوع داده میتواند چندین مقدار را در خودش ذخیره کند.
برای آشنایی بیشتر با این موضوع به کدهای زیر نگاه کنید:
به وسیله عملگر | میتوان چندین مقدار را در یک نمونه از نوع Fabric ذخیره کرد. مثلا متغیر cotWool هم دارای مقدار Fabric.Cotton و هم دارای مقدار Fabric.Wool هست. مقدار عددی معادل متغیر cotWool برابر 5 هست که از جمع مقدار عددی Fabric.Cotton و Fabric.Wool به دست آمده است.
حال فرض کنید فیلد Fabric از View Model ذکر شده (کلاس MyViewModel) را به صورت لیستی از چک باکسها نمایش دهیم. مثل زیر:
برای این کار از پروژه MVC Enum Flags کمک خواهیم گرفت. این پروژه شامل یک Html Helper برای تبدیل یه Enum به یک CheckBoxList و همچنین شامل Model Binder مربوطه هست. البته بعضی از کدهای Html Helper آن احتیاج به تغییر داشت که آنرا انجام دادم ولی بایندر آن بسیار خوب کار میکند.
خوب html helper مربوط به آن به صورت زیر میباشد:
در کدهای بالا از متد الحاقی ToDescription نیز برای تبدیل معادل انگلیسی به فارسی یک مقدار از نوع داده شمارشی استفاده کرده ایم.
برای استفاده از این Html Helper در View کد زیر را مینویسیم:
که باعث تولید خروجی که در تصویر (الف) نشان داده شد میشود. و همچنین مدل بایندر مربوط به آن به صورت زیر هست:
این مدل بایندر را باید به این صورت در متد Application_Start فایل Global.asax فراخوانی کنیم:
مشاهده میکنید که در اینجا دقیقا مشخص کرده ایم که این مدل بایندر برای نوع داده شمارشی Fabric هست. اگر نیاز دارید تا این بایندر برای نوع دادههای شمارشی دیگری نیز به کار رود نیاز هست تا این خط کد را برای هر کدام از آنها تکرار کنید. اما راه حل بهتر این هست که کلاسی به صورت زیر تعریف کنیم و تمامی نوع دادههای شمارشی که باید از بایندر بالا استفاده کنند را در یک پراپرتی آن برگشت دهیم. مثلا بدین صورت:
سپس به متد Application_Start رفته و کد زیر را اضافه میکنیم:
اگر گزینههای پشم و ابریشم مصنوعی را از CheckBoxList تولید شده انتخاب کنیم، بدین صورت:
و سپس فرم را پست کنید، موردی شبیه زیر مشاهده میکنید:
همچنین مقدار عددی معادل در این جا برابر 12 میباشد که از جمع دو مقدار Wool و Rayon به دست آمده است. بدین ترتیب در یک فیلد از مدل، گزینههای انتخابی توسط کاربر قرار گرفته شده اند.
پروژه مربوط به این مثال را از لینک زیر دریافت کنید:
MvcEnumFlagsProjectSample.zip
پی نوشت : پوشههای bin و obj و packages جهت کاهش حجم پروژه از آن حذف شده اند. برای بازسازی پوشه packages لطفا به مطلب بازسازی کامل پوشه packages بستههای NuGet به صورت خودکار مراجعه کنید.
فرض کنید بخواهید به کاربر این امکان را بدهید تا بتواند چندین گزینه را برای یک فیلد انتخاب کند. به عنوان یک مثال ساده فرض کنید گزینه ای از مدل، پارچههای مورد علاقه یک نفر هست. کاربر میتواند چندین پارچه را انتخاب کند. و این فرض را هم بکنید که به لیست پارچهها گزینه دیگری اضافه نخواهد شد. پارچه (Fabric) را مثلا میتوانیم به صورت زیر تقسیم بندی کنیم :
- پنبه (Cotton)
- ابریشم (Silk)
- پشم (Wool)
- ابریشم مصنوعی (Rayon)
- پارچههای دیگر (Other)
با توجه به اینکه دیگر قرار نیست به این لیست گزینه دیگری اضافه شود میتوانیم آنرا به صورت یک نوع داده شمارشی (Enum) تعریف کنیم. مثلا بدین صورت:
public enum Fabric { [Description("پنبه")] Cotton, [Description("ابریشم")] Silk, [Description("پشم")] Wool, [Description("ابریشم مصنوعی")] Rayon, [Description("پارچههای دیگر")] Other }
حال فرض کنید View Model زیر فیلدی از نوع نوع داده شمارشی Fabric دارد:
public class MyViewModel { public Fabric Fabric { get; set; } }
توجه داشته باشید که فیلد Fabric از کلاس MyViewModel باید چند مقدار را در خود نگهداری کند. یعنی میتواند هر کدام از گزینههای Cotton، Silk، Wool، Rayon، Other به صورت جداگانه یا ترکیبی باشد. اما در حال حاضر با توجه به اینکه یک فیلد Enum معمولی فقط میتواند یک مقدار را در خودش ذخیره کند قابلیت ذخیره ترکیبی مقادیر در فیلد Fabric از View Model بالا وجود ندارد.
اما راه حل این مشکل استفاده از پرچم (Flags) در تعریف نوع داده شمارشی هست. با استفاده از پرچم نوع داده شمارشی بالا به صورت زیر باید تعریف شود:
[Flags] public enum Fabric { [Description("پنبه")] Cotton = 1, [Description("ابریشم")] Silk = 2, [Description("پشم")] Wool = 4, [Description("ابریشم مصنوعی")] Rayon = 8, [Description("پارچههای دیگر")] Other = 128 }
برای آشنایی بیشتر با این موضوع به کدهای زیر نگاه کنید:
Fabric cotWool = Fabric.Cotton | Fabric.Wool; int cotWoolValue = (int) cotWool;
حال فرض کنید فیلد Fabric از View Model ذکر شده (کلاس MyViewModel) را به صورت لیستی از چک باکسها نمایش دهیم. مثل زیر:
شکل (الف)
سپس بخواهیم تا کاربر بعد از انتخاب گزینههای مورد نظرش از لیست بالا و پست کردن فرم مورد نظر، بایندر وارد عمل شده و فیلد Fabric را بر اساس گزینه هایی که کاربر انتخاب کرده مقداردهی کند. برای این کار از پروژه MVC Enum Flags کمک خواهیم گرفت. این پروژه شامل یک Html Helper برای تبدیل یه Enum به یک CheckBoxList و همچنین شامل Model Binder مربوطه هست. البته بعضی از کدهای Html Helper آن احتیاج به تغییر داشت که آنرا انجام دادم ولی بایندر آن بسیار خوب کار میکند.
خوب html helper مربوط به آن به صورت زیر میباشد:
public static IHtmlString CheckBoxesForEnumFlagsFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression) { ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData); Type enumModelType = metadata.ModelType; // Check to make sure this is an enum. if (!enumModelType.IsEnum) { throw new ArgumentException("This helper can only be used with enums. Type used was: " + enumModelType.FullName.ToString() + "."); } // Create string for Element. var sb = new StringBuilder(); foreach (Enum item in Enum.GetValues(enumModelType)) { if (Convert.ToInt32(item) != 0) { var ti = htmlHelper.ViewData.TemplateInfo; var id = ti.GetFullHtmlFieldId(item.ToString()); //Derive property name for checkbox name var body = expression.Body as MemberExpression; var propertyName = body.Member.Name; var name = ti.GetFullHtmlFieldName(propertyName); //Get currently select values from the ViewData model TEnum selectedValues = expression.Compile().Invoke(htmlHelper.ViewData.Model); var label = new TagBuilder("label"); label.Attributes["for"] = id; label.Attributes["style"] = "display: inline-block;"; var field = item.GetType().GetField(item.ToString()); // Add checkbox. var checkbox = new TagBuilder("input"); checkbox.Attributes["id"] = id; checkbox.Attributes["name"] = name; checkbox.Attributes["type"] = "checkbox"; checkbox.Attributes["value"] = item.ToString(); if ((selectedValues as Enum != null) && ((selectedValues as Enum).HasFlag(item))) { checkbox.Attributes["checked"] = "checked"; } sb.AppendLine(checkbox.ToString()); // Check to see if DisplayName attribute has been set for item. var displayName = field.GetCustomAttributes(typeof(DisplayNameAttribute), true) .FirstOrDefault() as DisplayNameAttribute; if (displayName != null) { // Display name specified. Use it. label.SetInnerText(displayName.DisplayName); } else { // Check to see if Display attribute has been set for item. var display = field.GetCustomAttributes(typeof(DisplayAttribute), true) .FirstOrDefault() as DisplayAttribute; if (display != null) { label.SetInnerText(display.Name); } else { label.SetInnerText(item.ToDescription()); } } sb.AppendLine(label.ToString()); // Add line break. sb.AppendLine("<br />"); } } return new HtmlString(sb.ToString()); }
public static string ToDescription(this Enum value) { var attributes = (DescriptionAttribute[])value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false); return attributes.Length > 0 ? attributes[0].Description : value.ToString(); }
@Html.CheckBoxesForEnumFlagsFor(x => x.Fabric)
که باعث تولید خروجی که در تصویر (الف) نشان داده شد میشود. و همچنین مدل بایندر مربوط به آن به صورت زیر هست:
public class FlagEnumerationModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException("bindingContext"); if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) { var values = GetValue<string[]>(bindingContext, bindingContext.ModelName); if (values.Length > 1 && (bindingContext.ModelType.IsEnum && bindingContext.ModelType.IsDefined(typeof(FlagsAttribute), false))) { long byteValue = 0; foreach (var value in values.Where(v => Enum.IsDefined(bindingContext.ModelType, v))) { byteValue |= (int)Enum.Parse(bindingContext.ModelType, value); } return Enum.Parse(bindingContext.ModelType, byteValue.ToString()); } else { return base.BindModel(controllerContext, bindingContext); } } return base.BindModel(controllerContext, bindingContext); } private static T GetValue<T>(ModelBindingContext bindingContext, string key) { if (bindingContext.ValueProvider.ContainsPrefix(key)) { ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(key); if (valueResult != null) { bindingContext.ModelState.SetModelValue(key, valueResult); return (T)valueResult.ConvertTo(typeof(T)); } } return default(T); } }
این مدل بایندر را باید به این صورت در متد Application_Start فایل Global.asax فراخوانی کنیم:
ModelBinders.Binders.Add(typeof(Fabric), new FlagEnumerationModelBinder());
مشاهده میکنید که در اینجا دقیقا مشخص کرده ایم که این مدل بایندر برای نوع داده شمارشی Fabric هست. اگر نیاز دارید تا این بایندر برای نوع دادههای شمارشی دیگری نیز به کار رود نیاز هست تا این خط کد را برای هر کدام از آنها تکرار کنید. اما راه حل بهتر این هست که کلاسی به صورت زیر تعریف کنیم و تمامی نوع دادههای شمارشی که باید از بایندر بالا استفاده کنند را در یک پراپرتی آن برگشت دهیم. مثلا بدین صورت:
public class ModelEnums { public static IEnumerable<Type> Types { get { var types = new List<Type> { typeof(Fabric) }; return types; } } }
سپس به متد Application_Start رفته و کد زیر را اضافه میکنیم:
foreach (var type in ModelEnums.Types) { ModelBinders.Binders.Add(type, new FlagEnumerationModelBinder()) }
اگر گزینههای پشم و ابریشم مصنوعی را از CheckBoxList تولید شده انتخاب کنیم، بدین صورت:
شکل (ب)
و سپس فرم را پست کنید، موردی شبیه زیر مشاهده میکنید:
شکل (ج)
همچنین مقدار عددی معادل در این جا برابر 12 میباشد که از جمع دو مقدار Wool و Rayon به دست آمده است. بدین ترتیب در یک فیلد از مدل، گزینههای انتخابی توسط کاربر قرار گرفته شده اند.
پروژه مربوط به این مثال را از لینک زیر دریافت کنید:
MvcEnumFlagsProjectSample.zip
پی نوشت : پوشههای bin و obj و packages جهت کاهش حجم پروژه از آن حذف شده اند. برای بازسازی پوشه packages لطفا به مطلب بازسازی کامل پوشه packages بستههای NuGet به صورت خودکار مراجعه کنید.
مطالب
خواندنیهای 29 تیر
اس کیوال سرور
امنیت
توسعه وب
دات نت فریم ورک
سی و مشتقات
شیرپوینت
لینوکس
مای اس کیوال
متفرقه
محیطهای مجتمع توسعه
مرورگرها
وب سرورها
ویندوز
درحال حاضر، باتوجه به خرده نداشتن مقادیر پولی در ایران، عموما از نوعهای int و bigint برای ذخیره سازی این مقادیر استفاده میشود؛ اما در آینده با احتمال حذف تعدادی از صفرها، نیاز به ثبت خردهها هم ضروری خواهد بود و در اینجا این سؤال مهم مطرح میشود که نوع دادهای مناسب برای انجام اینکار چیست؟ برای نمونه در SQL Server، نوعهای دادهای decimal، money، smallmoney و امثال آن وجود دارند که در این مطلب، تفاوتهای مهم آنها و روش صحیح انتخاب نوع دادهای مناسب مخصوص اینکار را بررسی خواهیم کرد.
مشکل مهم نوع دادهای int جهت ذخیره سازی مقادیر پولی
فرض کنید جدول سادهای را با دو فیلد Id و Price دارید که نوع مبلغ آنرا با توجه به عدم داشتن خرده در واحد پولی، int انتخاب کردهاید:
اگر در این جدول فقط 7 رکورد زیر را ثبت کنیم:
به نظر شما خروجی کوئری سادهی زیر که جهت نمایش جمع مبالغ وارد شده تهیه شده، چیست؟
خروجی آن فقط استثنای زیر است!
عنوان میکند که جمع آن از بازهی اعداد صحیح خارج شدهاست و در سیستمی که نوع مبالغ آنرا int انتخاب کردهاید، دیر یا زود به این مشکل خواهید رسید. فقط کافی است کاربران، یکسالی با آن برنامه کار کنند!
برای حل این مشکل میتوان به صورت موقت، نوع دادهای را به bigint تبدیل کرد و مجددا جمع رکوردها را محاسبه کرد:
یک روش دیگر مواجه شدن با این مساله، عدم انتخاب نوع int برای فیلد Price، از ابتدای کار است.
از نوع دادهای float برای ذخیره سازی مقادیر پولی استفاده نکنید!
هیچگاه نباید از نوع دادهی float برای ذخیره سازی مقادیر پولی استفاده کرد؛ از این جهت که این نوع اعداد، به صورت تقریبی از یک مقدار decimal و به صورت باینری در SQL Server ذخیره میشوند. به همین جهت به محض ذخیره شدن، با عددی غیر دقیق مواجه خواهیم بود. همچنین مقایسهی دقیق این نوع اعداد هم مشکلات خاصی را به همراه دارد.
SQL Server چگونه مقادیر پولی money و small money را ذخیره میکند؟
SQL Server برای کار با مقادیر پولی، دو نوع MONEY و SMALLMONEY را ارائه میدهد که شبیه به نوعهای BIGINT و INT، نیاز به 8 و 4 بایت برای ذخیره سازی دارند. در عمل نوع MONEY شبیه به نوع DECIMAL(19,4) و نوع SMALLMONEY همانند DECIMAL(10,4) رفتار میکند. یعنی نوع MONEY میتواند تا 15 رقم دسیمال پیش از ممیز و 4 رقم اعشار را ذخیره کند و نوع SMALLMONEY تنها میتواند 6 رقم دسیمال و 4 رقم اعشاری را ذخیره کند.
اما ... هرچند نوع دادهی MONEY و DECIMAL(19,4) به ظاهر یکی هستند، اما به نحو متفاوتی بر روی دیسک سخت ذخیره میشوند. برای نمونه فرض کنید که قصد داریم عدد 4,513.19 را یکبار به صورت MONEY و بار دیگر به صورت SMALLMONEY ذخیره کنیم که در نهایت به جدول زیر میرسیم:
همانطور که مشاهده میکنید، نوعهای MONEY و SMALLMONEY، دقیقا همانند BIGINT هشت بایتی و INT، چهار بایتی ذخیره میشوند و عملا در پشت صحنهی SQL Server، اعداد صحیح هستند. اما نوع DECIMAL(19,4) که هرچند شبیه به MONEY عمل میکند، 9 بایتی است.
الگوریتم انتخاب نوع دادهی مناسب ذخیره سازی مقادیر پولی
در فلوچارت زیر که از کتاب «Donald Knuth’s "The Art of Computer Programming – Volume 1".» انتخاب شده، روش مواجه شدن با انواع و اقسام نوعهای دادهای عددی را به خوبی مشخص میکند که آیا عدد در حال ذخیره شدن، خرده دارد یا خیر؟ آیا از 922,337,203,685,477.5807 کوچکتر است یا خیر و امثال آن که در تصمیمگیری نهایی مؤثر هستند:
اعدادی را که در این نمودار مشاهده میکنید، در جدول زیر بهتر توضیح داده شدهاند. به عبارتی چه تفاوتی بین نوع Money و Decimal(19,4) مشابه وجود دارد:
تفاوت مهم نوع Money و Decimal(19,4)، در دقت آنها است
تا اینجا به نظر آنچنان تفاوتی بین نوع Money و Decimal(19,4) وجود ندارد و نوع money اتفاقا یک بایت را کمتر اشغال میکند و کوچکتر است. اما تفاوت اصلی را با مثال زیر بهتر میتوان توضیح داد:
در اینجا جدولی تهیه شده که دو ستون اصلی Mon1 و Dec1 را دارد و مابقی ستونهای آن، محاسباتی هستند:
همانطور که مشاهده میکنید، با ضرب دو عدد دسیمال، مقادیر پیش و پس از ممیز، یعنی precision و scale تغییر کردهاند، اما در مورد money چنین چیزی رخ نداده و ثابت است. برای مثال زمانیکه با یک عدد DECIMAL(4,2) کار میکنیم، اگر آنرا ضربدر همین عدد کنیم، به یک عدد DECIMAL(8,4) خواهیم رسید که البته حداکثر precision ممکن آن در SQL Server عدد 38 است، اما یک چنین تغییری در حین ضرب اعداد از نوع money رخ نمیدهد.
موضوع دقت را با مثال زیر بهتر میتوان بررسی کرد:
فرض کنید جدولی را داریم با دو فیلد از نوع Money و مشابه آن یعنی decimal(19,4) به صورت فوق. اگر رکوردهای زیر را به آن اضافه کنیم:
و سپس سعی کنیم که جمع اعداد وارد شده را محاسبه کنیم:
به نتیجهی زیر میرسیم:
همانطور که مشخص است در حین محاسباتی مانند جمع و منها و محاسبهی sum، تفاوتی بین این نوعها نیست. اما اگر سعی در تقسیم آنها کنیم:
به خروجی زیر میرسیم:
نتیجهی واقعی 0,00009 است که پس از گرد شدن، به 0.0001 مقدار دسیمال میرسیم، اما این دقت در نوع money از دست رفتهاست.
نکتهی مهمی که در اینجا قابل مشاهدهاست، محدود نبودن نتیجهی حاصل، به دقت اعشارها در عدد decimal تعریف شده و scale تعریف شدهی اولیهی آن است. نمونهی دیگر آنرا در مثال زیر میتوانید مشاهده کنید که هرچند عدد دسیمال تعریف شده، فقط 2 رقم اعشاری دارد، اما در حین تقسیم، از این مساله صرفنظر شده و خروجی آن محدود به 2 رقم اعشار نیست؛ برخلاف نوع money که حداکثر 4 رقم ثابت اعشاری را بیشتر نمیتواند داشته باشد:
نتیجهگیری
برای ذخیره سازی مقادیر پولی در SQL Server، اگر سیستم شما OLTP-like است و با اعدادی مانند 1000.24 کار میکنید و حداکثر میخواهید جمع و منهای آنها را محاسبه کنید، انتخاب نوع MONEY و یا SMALLMONEY بسیار مناسب است؛ اما اگر سیستم شما OLAP-like است و در آن اعمال ضرب و تقسیم زیاد رخ میدهد، فقط از نوع Decimal استفاده کنید.
مشکل مهم نوع دادهای int جهت ذخیره سازی مقادیر پولی
فرض کنید جدول سادهای را با دو فیلد Id و Price دارید که نوع مبلغ آنرا با توجه به عدم داشتن خرده در واحد پولی، int انتخاب کردهاید:
CREATE TABLE [Test1]( [Id] [int] IDENTITY(1,1) NOT NULL, [Price] [int] NOT NULL, CONSTRAINT [PK_Test1] PRIMARY KEY CLUSTERED ( [Id] ASC ));
Insert into Test1 values (1000000000),(1000000000),(1000000000),(1000000000),(1000000000),(1000000000),(1000000000)
select sum(price) from Test1
Arithmetic overflow error converting expression to data type int.
برای حل این مشکل میتوان به صورت موقت، نوع دادهای را به bigint تبدیل کرد و مجددا جمع رکوردها را محاسبه کرد:
select sum(cast(price as bigint)) from Test1
از نوع دادهای float برای ذخیره سازی مقادیر پولی استفاده نکنید!
هیچگاه نباید از نوع دادهی float برای ذخیره سازی مقادیر پولی استفاده کرد؛ از این جهت که این نوع اعداد، به صورت تقریبی از یک مقدار decimal و به صورت باینری در SQL Server ذخیره میشوند. به همین جهت به محض ذخیره شدن، با عددی غیر دقیق مواجه خواهیم بود. همچنین مقایسهی دقیق این نوع اعداد هم مشکلات خاصی را به همراه دارد.
DECLARE @f AS FLOAT = '29545428.0211111'; SELECT CAST(@f AS NUMERIC(28, 14)) AS value;
SQL Server چگونه مقادیر پولی money و small money را ذخیره میکند؟
SQL Server برای کار با مقادیر پولی، دو نوع MONEY و SMALLMONEY را ارائه میدهد که شبیه به نوعهای BIGINT و INT، نیاز به 8 و 4 بایت برای ذخیره سازی دارند. در عمل نوع MONEY شبیه به نوع DECIMAL(19,4) و نوع SMALLMONEY همانند DECIMAL(10,4) رفتار میکند. یعنی نوع MONEY میتواند تا 15 رقم دسیمال پیش از ممیز و 4 رقم اعشار را ذخیره کند و نوع SMALLMONEY تنها میتواند 6 رقم دسیمال و 4 رقم اعشاری را ذخیره کند.
اما ... هرچند نوع دادهی MONEY و DECIMAL(19,4) به ظاهر یکی هستند، اما به نحو متفاوتی بر روی دیسک سخت ذخیره میشوند. برای نمونه فرض کنید که قصد داریم عدد 4,513.19 را یکبار به صورت MONEY و بار دیگر به صورت SMALLMONEY ذخیره کنیم که در نهایت به جدول زیر میرسیم:
همانطور که مشاهده میکنید، نوعهای MONEY و SMALLMONEY، دقیقا همانند BIGINT هشت بایتی و INT، چهار بایتی ذخیره میشوند و عملا در پشت صحنهی SQL Server، اعداد صحیح هستند. اما نوع DECIMAL(19,4) که هرچند شبیه به MONEY عمل میکند، 9 بایتی است.
الگوریتم انتخاب نوع دادهی مناسب ذخیره سازی مقادیر پولی
در فلوچارت زیر که از کتاب «Donald Knuth’s "The Art of Computer Programming – Volume 1".» انتخاب شده، روش مواجه شدن با انواع و اقسام نوعهای دادهای عددی را به خوبی مشخص میکند که آیا عدد در حال ذخیره شدن، خرده دارد یا خیر؟ آیا از 922,337,203,685,477.5807 کوچکتر است یا خیر و امثال آن که در تصمیمگیری نهایی مؤثر هستند:
اعدادی را که در این نمودار مشاهده میکنید، در جدول زیر بهتر توضیح داده شدهاند. به عبارتی چه تفاوتی بین نوع Money و Decimal(19,4) مشابه وجود دارد:
تفاوت مهم نوع Money و Decimal(19,4)، در دقت آنها است
تا اینجا به نظر آنچنان تفاوتی بین نوع Money و Decimal(19,4) وجود ندارد و نوع money اتفاقا یک بایت را کمتر اشغال میکند و کوچکتر است. اما تفاوت اصلی را با مثال زیر بهتر میتوان توضیح داد:
CREATE TABLE MoneyTest ( Mon1 money, Mon2 AS Mon1*Mon1, Mon3 AS Mon1*Mon1*Mon1, Dec1 decimal(19,4), Dec2 AS Dec1*Dec1, Dec3 AS Dec1*Dec1*Dec1, MonDec AS Mon1*Dec1, DecMon AS Dec1*Mon1);
همانطور که مشاهده میکنید، با ضرب دو عدد دسیمال، مقادیر پیش و پس از ممیز، یعنی precision و scale تغییر کردهاند، اما در مورد money چنین چیزی رخ نداده و ثابت است. برای مثال زمانیکه با یک عدد DECIMAL(4,2) کار میکنیم، اگر آنرا ضربدر همین عدد کنیم، به یک عدد DECIMAL(8,4) خواهیم رسید که البته حداکثر precision ممکن آن در SQL Server عدد 38 است، اما یک چنین تغییری در حین ضرب اعداد از نوع money رخ نمیدهد.
موضوع دقت را با مثال زیر بهتر میتوان بررسی کرد:
CREATE TABLE [MoneyTest]( [Id] [int] IDENTITY(1,1) NOT NULL, decimalMoney decimal(19,4), moneyMoney money CONSTRAINT [PK_MoneyTest] PRIMARY KEY CLUSTERED ( [Id] ASC ));
INSERT INTO MoneyTest VALUES (12321423442.3456,12321423442.3456), (1111111.1919,1111111.1919)
SELECT * FROM MoneyTest SELECT SUM(decimalMoney) AS [sumDecimal], SUM(moneyMoney) AS [sumMoney] FROM MoneyTest
همانطور که مشخص است در حین محاسباتی مانند جمع و منها و محاسبهی sum، تفاوتی بین این نوعها نیست. اما اگر سعی در تقسیم آنها کنیم:
DECLARE @moneyPer money, @decimalPer decimal(19,4) SET @moneyPer = (SELECT moneyMoney FROM MoneyTest WHERE id = 2)/((SELECT moneyMoney FROM MoneyTest WHERE id = 1)) SET @decimalPer = (SELECT decimalMoney FROM MoneyTest WHERE id = 2)/((SELECT decimalMoney FROM MoneyTest WHERE id = 1)) SELECT @moneyPer AS[moneyPer], @decimalPer AS [decimalPer];
نتیجهی واقعی 0,00009 است که پس از گرد شدن، به 0.0001 مقدار دسیمال میرسیم، اما این دقت در نوع money از دست رفتهاست.
نکتهی مهمی که در اینجا قابل مشاهدهاست، محدود نبودن نتیجهی حاصل، به دقت اعشارها در عدد decimal تعریف شده و scale تعریف شدهی اولیهی آن است. نمونهی دیگر آنرا در مثال زیر میتوانید مشاهده کنید که هرچند عدد دسیمال تعریف شده، فقط 2 رقم اعشاری دارد، اما در حین تقسیم، از این مساله صرفنظر شده و خروجی آن محدود به 2 رقم اعشار نیست؛ برخلاف نوع money که حداکثر 4 رقم ثابت اعشاری را بیشتر نمیتواند داشته باشد:
DECLARE @M MONEY = 1234, @D DECIMAL(6,2) = 1234 SELECT @M/$1000000 AS [MONEY] , @D/$1000000 AS [DECIMAL]
نتیجهگیری
برای ذخیره سازی مقادیر پولی در SQL Server، اگر سیستم شما OLTP-like است و با اعدادی مانند 1000.24 کار میکنید و حداکثر میخواهید جمع و منهای آنها را محاسبه کنید، انتخاب نوع MONEY و یا SMALLMONEY بسیار مناسب است؛ اما اگر سیستم شما OLAP-like است و در آن اعمال ضرب و تقسیم زیاد رخ میدهد، فقط از نوع Decimal استفاده کنید.
DECLARE @dOne DECIMAL(19,4) = 1, @dThree DECIMAL(19,4) = 3, @mOne MONEY = 1, @mThree MONEY = 3 SELECT (@dOne/@dThree) * @dThree AS DecimalResult, (@mOne/@mThree) * @mThree AS MoneyResult
نظرات مطالب
آشنایی با NHibernate - قسمت سوم
با سلام
من در NHibernate 2 از این کلاس استفاده می کردم:( نگاشت، مربوط به جدول tblAhkam است)
public class Ahkam
{
public virtual int Id { get; set; }
public virtual int HDate { get; set; }
public virtual string SepratedDate
{
get
{
return Functions.SepratePersianDate(HDate);//Convert 890221 to 89/02/21
}
}
}
و در لایه BLL تبدیل به Dataset می کردم و استفاده می شد و مشکلی هم وجود نداشت. اما وقتی با NHibernate 3 برنامه رو اجرا کردم به مشکل برخورد و فهمیدم چون فیلد SepratedDate در جدول بانک وجود ندارد باعث خطا شده است. راه حلی وجود دارد؟
من در NHibernate 2 از این کلاس استفاده می کردم:( نگاشت، مربوط به جدول tblAhkam است)
public class Ahkam
{
public virtual int Id { get; set; }
public virtual int HDate { get; set; }
public virtual string SepratedDate
{
get
{
return Functions.SepratePersianDate(HDate);//Convert 890221 to 89/02/21
}
}
}
و در لایه BLL تبدیل به Dataset می کردم و استفاده می شد و مشکلی هم وجود نداشت. اما وقتی با NHibernate 3 برنامه رو اجرا کردم به مشکل برخورد و فهمیدم چون فیلد SepratedDate در جدول بانک وجود ندارد باعث خطا شده است. راه حلی وجود دارد؟
امروزه در بسیاری از محیطهای برنامه نویسی جاوا و اندروید، استفاده از این سیستم رایج است. ولی هنوز دیده میشود عدهای نسبت به آن دید روشنی ندارند و برای آنها ناشناخته است و در حد یک سیستم کانفیگ آن را میشناسند. در این مقاله قصد داریم که مفهوم روشنتری از این سیستم را داشته باشم و بدانیم هدف آن چیست و چگونه کار میکند تا از این به بعد دیگر آن را به چشم یک کانفیگ کنندهی ساده نگاه نکنیم. قبل از هر چیزی بهتر است که با تعدادی از اصطلاحات آن آشنا شویم تا در متن مقاله به مشکل برنخوریم:
DSL یا Domain Specific languages به معنی زبانهایی با دامنه محدود است که برای اهداف خاصی نوشته میشوند و تنها بر روی یک جنبه از هدف تمرکز دارند. این زبانها به شما اجازه نمیدهند که یک برنامه را به طور کامل با آن بنویسید. بلکه به شما اجازه میدهند به هدفی که برای آن نوشته شدهاند، برسید. یکی از این زبانها همان css هست که با آن کار میکنید. این زبان به صورت محدود تنها بر روی یک جنبه و آن، تزئین سازی المانهای وب، تمرکز دارد. در وقع مثل زبان سی شارپ همه منظوره نیست و محدودهای مشخص برای خود دارد. به این نوع از زبانهای DSL، نوع اکسترنال هم میگویند. چون زبانی مستقل برای خود است و به زبان دیگری وابستگی ندارد. ولی در یک زبان اینترنال، وابستگی به زبان دیگری وجود دارد. مثل Fluent Interfaceها که به ما شیوه آسانی از دسترسی به جنبههای یک شیء را میدهد. برای آشنایی هر چه بیشتر با این زبانها و ساختار آن، کتاب Domain Specific languages نوشته آقای مارتین فاولر توصیه میشود.
Groovy یک زبان شیء گرای DSL هست که برای پلتفرم جاوا ساخته شده است. برای اطلاعات بیشتر در مورد این زبان، صفحه ویکی ، میتواند مفید واقع شود.
از دیرباز سیستمهای Ant و Maven وجود داشتند و کار آنها اتوماسیون بعضی اعمال بود. ولی بعد از مدتی سیستم Gradle یا جمع کردن نقاط قوت آنها و افزودن ویژگیهای قدرتمندتری به خود، پا به میدان گذاشت تا راحتی بیشتری را برای برنامه نویس فراهم کند. از ویژگیهای گریدل میتوان داشتن زبان گرووی اشاره کرده که قدرت بیشتری را نسبت به سایر سیستمها داشت و مزیت مهم دیگر این بود که انعطاف بالایی را جهت افزودن پلاگینها داشت و گوگل با استفاده از این قابلیت، پشتیبانی از گریدل را در اندروید استادیو نیز گنجاند تا راحتی بیشتری را در اتوماسیون وظایف سیستمی ایجاد کند. در واقع آنچه شما در سیستم گریدل کار میکنید و اطلاعات خود را با آن کانفیگ میکنید، پلاگینی است که از سمت گوگل در اختیار شما قرار گرفته است و در مواقع خاص این وظایف توسط پلاگینها اجرا میشوند.
گریدل به راحتی از سایت رسمی آن قابل دریافت است و میتوان آن را در پروژههای جاوایی که مدنظر شماست، دریافت کنید و با استفاده از خط فرمان، با آن تعامل کنید. هر چند امروزه اکثر ویراستارهای جاوا از آن پشتیبانی میکنند.
گریدل یک ماهیت توصیفی دارد که شما تنها لازم است اعمالی را برای آن توصیف کنید تا بقیه کارها را انجام دهد. گریدل در پشت صحنه از یک "گراف جهت دار بدون دور" Directed Acycllic Graph یا به اختصار DAG استفاده میکند و طبق آن ترتیب وظایف یا taskها را دانسته و آنها را اجرا میکند. گریدل با این DAG، سه فاز آماده سازی، پیکربندی و اجرا را انجام میدهد.
در این فایل سه پروژه برای گریدل مشخص شدهاند. البته از نگاه Intellij سه ماژول معرفی شدهاند و این فایل برای یک پروژه اختیاری است. گریدل برای پیدا کردن این فایل، از الگوریتمهای متفاوتی استفاده میکند.
الان پروژه dbutiilities در سطح بالاتری از دایرکتوری جاری قرار گرفته است.
در کلوژر buildscript مخازنی را که کتابخانههای نامبرده (وابستگی ها) در این کلوژ قرار میگیرند، معرفی میکنیم. در کلوژر بعدی تنظمیاتی را که برای همه پروژهها اعمال میشوند، انجام میدهیم که در آن به معرفی مخزن jcenter پرداختیم. کتابخانهای که در کلوژر buildscript صدا زدیم، همان پلاگینی است که گوگل برای گریدل منتشر کرده که ما به آن ابزار بیلد میگوییم. حال به سراغ دیگر فایلهای منحصر به فرد هر پروژه بروید. در این فایلها، شما تنظیمات پیشفرضی را میبینید که یکی از آنها نسخه بندی پروژه است. پروژههای وابستهای را که از مخازن باید دریافت شوند، شامل میشود و بسیاری از گزینههای دیگری که برای شما مهیا شده است تا در فاز پیکربندی، وظایف را بسازید.
در مرحله اجرا هم این وظایف را اجرا میکنیم. تمامی این سه عملیات توسط فایل و دستوری به نام gradlew که برگرفته از gradleWrapper میباشد انجام میشود. اگر در ترمینال اندروید استادیو این عبارت را تایپ کنید، میتوانید در ادامه دستور پیامهای مربوط به این عملیات را ببینید و ترتیب اجرای فازها را مشاهده کنید.
بیایید یک task را تعریف کنیم
در ابتدا عبارت task را به عنوان معرفی task میآوریم و سپس نام آن را وارد میکنیم. بعد از آن ما از عبارتهای شیفت چپ>> استفاده کردیم. این عبارت شیفت چپ به این معناست که تسک مربوطه را آخر از همه وظایف اجرا کن که این عمل از طریق اجرای متدی به نام doLast صورت میگیرد. اگر در ترمینال اندروید عبارت زیر را تایپ کنید، متن مورد نظر باید نمایش یابد:
برای نمایش اطلاعات بیشتر میتوانید از فلگ info هم استفاده کنید:
حال شاید بگویید من در بعضی از سایتها یا مستندات و یا پروژههای دیگر دیدهام که عبارت >> را قرار نمیدهند. در این مورد باید گفت که آنها در واقع تسکهای اجرایی نیستند و برای پیکربندی به کار میروند و در فاز پیکربندی هم اجرا میشوند که در ادامه نمونه آن را خواهیم دید.
اگر بخواهید خودتان دستی یک تسک پیکربندی را به یک تسک اجرایی تبدیل کنید، میتوانید متد doLast را صدا بزنید. کد زیر را توسط gradlew اجرا کنید؛ به همراه اطلاعات verbose تا ببینید که هر کدام از پیامها در کدام بخش چاپ میشوند. پیام اول در فاز پیکربندی و پیام دوم در فاز اجرایی چاپ میشوند.
یکی از کارهایی که در یک تسک میتوانید انجام دهید این است که آن را به یک تسک دیگر وابسته کنید. به عنوان مثال ما قصد داریم بعد از تسک mytask1، تسک my task2 اجرا شود و زمان پایان تسک mytask1 را در خروجی نمایش دهیم. برای اینکار باید بین تسکها یک وابستگی ایجاد شود و سپس با متد doLast کد خودمان را اجرایی نماییم. البته توجه داشته باشید که این وابستگیها تنها به تسکهای داخل فایل گریدل انجام میشود و نه تسکهای پلاگینها یا وابستگی هایی که تعریف میکنیم.
سپس تسک شماره دو را صدا میزنیم. در اینجا جون تسک شماره دو به تسک شماره یک وابسته است، ابتدا تسک شماره یک اجرا شده و سپس نوبت تسک شماره دو میشود.
خروجی کار:
در گریدل مخالف doLast یعنی doFirst را نیز داریم ولی عملگر جایگزینی برای آن وجود ندارد و مستقیما باید آن را پیاده سازی کنید. خود گریدل به طور پیش فرض نیز تسکهای آماده ای نیز دارد که میتوانید در مستندات آن بیابید. به عنوان مثال یکی از تسکهای مفید و کاربردی آن تسک کپی کردن هست که از طریق آن میتوانید فایلی یا فایلهایی را از یک مسیر به مسیر دیگر کپی کنید. برای استفاده از چنین تسکهایی، باید تسکهای خود را به شکل زیر به شیوه اکشن بنویسید:
در تسک بالا بعد از اجرای تسک شماره یک، آخرین کاری که انجام میشود این است که فایلهای apk موجود در زیر دایرکتوریهای مسیر from به ریشه اصلی کپی خواهند شد. پس همانطور که میبینید گریدل به راحتی عملیات اتوماسیون را انجام میدهد.
برای نمایش تسکهای موجود میتوانید از گریدل درخواست کنید که لیست تمامی تسکهای موجود را به شما نشان دهد. برای اینکار میتوانید دستور زیر را صدا کنید:
بعد از مدتی تمامی تسکهای موجود به صورت گروه بندی نمایش داده شده و تسکهایی که شما جدیدا اضافه کردید را با عنوان other tasks نمایش خواهد داد:
اگر به تسکهای خود گریدل نگاه کنید برای هر کدام توضیحی هم وجود دارد؛ اگر شما هم قصد دارید توضیحی اضافه کنید از خصوصیت description استفاده کنید:
یکبار دیگر دستور نمایش تسکها را صدا بزنید تا اینبار توضیح اضافه شده نمایش داده شود.
یکی دیگر از نکات جالب در مورد گریدل این است که میتواند برای شما callback ارسال کند. بدین صورت که اگر اتفاقی خاصی افتاد، تسک خاصی را اجرا کند. به عنوان مثال ما در کد پایین تسکی را ایجاد کردهایم که به ما این اجازه را میدهد، هر موقع تسکی در مرحله پیکربندی به بیلد اضافه میشود، تسک ما هم اجرا شود و نام تسک اضافه شده به بیلد را چاپ میکند.
گریدل امکانات دیگری چون بررسی استثناءها و ایجاد استثناءها را هم پوشش میدهد که میتوانید در این صفحه آن را پیگیری کنید.
Gradle Wrapper
گریدل در حال حاضر مرتبا در حال تغییر و به روز رسانی است و اگر بخواهیم مستقیما با گریدل کار کنیم ممکن است که به مشکلاتی که در نسخه بندی است برخورد کنیم. از آنجا که هر پروژهای که روی سیستم شما قرار بگیرد از نسخهای متفاوتی از گریدل استفاده کند، باعث میشود که نتوانید نسخه مناسبی از گریدل را برای سیستم خود دانلود کنید. بدین جهت wrapper ایجاد شد تا دیگر نیازی به نصب گریدل پیدا نکنید. wrapper در هر پروژه میداند که که به چه نسخهای از گریدل نیاز است. پس موقعی که شما دستور gradlew را صدا میزنید در ویندوز فایل gradlew.bat صدا زده شده و یا در لینوکس و مک فایل شِل اسکریپت gradlew صدا زده میشود و wrapper به خوبی میداند که به چه نسخهای از گریدل برای اجرا نیاز دارد و آن را از طریق دانلود فراهم میکند. اگر همینک دایرکتوری والد پروژه اندرویدی خود را نگاه کنید میتوانید این دو فایل را ببینید.
از آنجا که خود اندروید استادیو به ساخت wrapper اقدام میکند، شما راحت هستید. ولی اگر دوست دارید خودتان برای پروژهای wrapper تولید کنید، مراحل زیر را دنبال کنید:
برای ایجاد wrapper توسط خودتان باید گریدل را دانلود و روی سیستم نصب کنید و سپس دستور زیر را صادر کنید:
دستور بالا یک wrapper برای نسخه 2.4 ایجاد میکند.
اگر میخواهید ببینید wrapper که اندروید استادیو شما دارد چه نسخه از گردیل را صدا میزند مسیر را از دایرکتوری پروژه دنبال کنید و فایل زیر را بگشایید:
هنگامی که گوگل قصد آپدیت نسخه گریدل شما را بکند این فایل را باز کرده و نسخه داخل آن را ویرایش میکند.
اینها فقط مختصراتی از آشنایی با نحوه عملکر گریدل برای داشتن دیدی روشنتر نسبت به آن بود. برای آشنایی بیشتر با گریدل، باید مستندات رسمی آن را دنبال کنید.
DSL یا Domain Specific languages به معنی زبانهایی با دامنه محدود است که برای اهداف خاصی نوشته میشوند و تنها بر روی یک جنبه از هدف تمرکز دارند. این زبانها به شما اجازه نمیدهند که یک برنامه را به طور کامل با آن بنویسید. بلکه به شما اجازه میدهند به هدفی که برای آن نوشته شدهاند، برسید. یکی از این زبانها همان css هست که با آن کار میکنید. این زبان به صورت محدود تنها بر روی یک جنبه و آن، تزئین سازی المانهای وب، تمرکز دارد. در وقع مثل زبان سی شارپ همه منظوره نیست و محدودهای مشخص برای خود دارد. به این نوع از زبانهای DSL، نوع اکسترنال هم میگویند. چون زبانی مستقل برای خود است و به زبان دیگری وابستگی ندارد. ولی در یک زبان اینترنال، وابستگی به زبان دیگری وجود دارد. مثل Fluent Interfaceها که به ما شیوه آسانی از دسترسی به جنبههای یک شیء را میدهد. برای آشنایی هر چه بیشتر با این زبانها و ساختار آن، کتاب Domain Specific languages نوشته آقای مارتین فاولر توصیه میشود.
Groovy یک زبان شیء گرای DSL هست که برای پلتفرم جاوا ساخته شده است. برای اطلاعات بیشتر در مورد این زبان، صفحه ویکی ، میتواند مفید واقع شود.
از دیرباز سیستمهای Ant و Maven وجود داشتند و کار آنها اتوماسیون بعضی اعمال بود. ولی بعد از مدتی سیستم Gradle یا جمع کردن نقاط قوت آنها و افزودن ویژگیهای قدرتمندتری به خود، پا به میدان گذاشت تا راحتی بیشتری را برای برنامه نویس فراهم کند. از ویژگیهای گریدل میتوان داشتن زبان گرووی اشاره کرده که قدرت بیشتری را نسبت به سایر سیستمها داشت و مزیت مهم دیگر این بود که انعطاف بالایی را جهت افزودن پلاگینها داشت و گوگل با استفاده از این قابلیت، پشتیبانی از گریدل را در اندروید استادیو نیز گنجاند تا راحتی بیشتری را در اتوماسیون وظایف سیستمی ایجاد کند. در واقع آنچه شما در سیستم گریدل کار میکنید و اطلاعات خود را با آن کانفیگ میکنید، پلاگینی است که از سمت گوگل در اختیار شما قرار گرفته است و در مواقع خاص این وظایف توسط پلاگینها اجرا میشوند.
گریدل به راحتی از سایت رسمی آن قابل دریافت است و میتوان آن را در پروژههای جاوایی که مدنظر شماست، دریافت کنید و با استفاده از خط فرمان، با آن تعامل کنید. هر چند امروزه اکثر ویراستارهای جاوا از آن پشتیبانی میکنند.
گریدل یک ماهیت توصیفی دارد که شما تنها لازم است اعمالی را برای آن توصیف کنید تا بقیه کارها را انجام دهد. گریدل در پشت صحنه از یک "گراف جهت دار بدون دور" Directed Acycllic Graph یا به اختصار DAG استفاده میکند و طبق آن ترتیب وظایف یا taskها را دانسته و آنها را اجرا میکند. گریدل با این DAG، سه فاز آماده سازی، پیکربندی و اجرا را انجام میدهد.
- در مرحله آماده سازی ما به گریدل میگوییم چه پروژه یا پروژههایی نیاز به بیلد شدن دارند. در اندروید استادیو، این مرحله در فایل settings.gradle انجام میشود؛ شما در این فایل مشخص میکنید چه پروژههای نیاز به بیلد شدن توسط گریدل دارند. ساختار این فایل به این شکل است:
include ':ActiveAndroid-master', ':app', ':dbutilities'
-
در اولین مرحله انتظار دارد که فایل settings در دایرکتوری جاری باشد و اگر آن را پیدا کرد آن را مورد استفاده قرار میدهد؛ در غیر اینصورت مرحله بعدی را آغاز میکند.
- در مرحله دوم، در این دایرکتوری به دنبال دایرکتوری به نام master میگردد و اگر در آن هم یافت نکرد مرحله سوم را آغاز میکند.
- در مرحله سوم، جست و جو در دایرکتوری والد انجام میشود
- چنانچه این فایل را در هیچ یک از احتمالات بالا نیابد، همین پروژه جاری را تشخیص خواهد داد.
include ':ActiveAndroid-master', ':app', ':dbutilities' project('dbutilities').projectDir=new File(settingsDir,'../dir1/dir2');
-
در مرحله پیکربندی، وظایف یا taskها را معرفی میکنیم. این عمل پیکربندی توسط فایل build.gradle که برای پروژه اصلی و هر زیر پروژهای که مشخص شدهاند، صورت میگیرد. در این فایل شما میتوانید خواص و متدهایی را تعریف و و ظایفی را مشخص کنید.
در پروژه اصلی، فایل BuildGradle شامل خطوط زیر است:
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.5.0' } } allprojects { repositories { jcenter() } }
در مرحله اجرا هم این وظایف را اجرا میکنیم. تمامی این سه عملیات توسط فایل و دستوری به نام gradlew که برگرفته از gradleWrapper میباشد انجام میشود. اگر در ترمینال اندروید استادیو این عبارت را تایپ کنید، میتوانید در ادامه دستور پیامهای مربوط به این عملیات را ببینید و ترتیب اجرای فازها را مشاهده کنید.
بیایید یک task را تعریف کنیم
task mytask <<{ println ".net tips task in config phase" }
gradlew mytask
gradlew --info mytask
اگر بخواهید خودتان دستی یک تسک پیکربندی را به یک تسک اجرایی تبدیل کنید، میتوانید متد doLast را صدا بزنید. کد زیر را توسط gradlew اجرا کنید؛ به همراه اطلاعات verbose تا ببینید که هر کدام از پیامها در کدام بخش چاپ میشوند. پیام اول در فاز پیکربندی و پیام دوم در فاز اجرایی چاپ میشوند.
task mytask { println ".net tips task in config phase" doLast{ println ".net tips task in exe phase" } }
یکی از کارهایی که در یک تسک میتوانید انجام دهید این است که آن را به یک تسک دیگر وابسته کنید. به عنوان مثال ما قصد داریم بعد از تسک mytask1، تسک my task2 اجرا شود و زمان پایان تسک mytask1 را در خروجی نمایش دهیم. برای اینکار باید بین تسکها یک وابستگی ایجاد شود و سپس با متد doLast کد خودمان را اجرایی نماییم. البته توجه داشته باشید که این وابستگیها تنها به تسکهای داخل فایل گریدل انجام میشود و نه تسکهای پلاگینها یا وابستگی هایی که تعریف میکنیم.
task mytask1 << { println ".net tips is the best" } task mytask2() { dependsOn mytask1 doLast{ Date time=Calendar.getInstance().getTime(); SimpleDateFormat formatter=new SimpleDateFormat("HH:mm:ss , YYYY/MM/dd"); println "mytask1 is done at " + formatter.format(time); } }
gradlew --info mytask2
Executing task ':app:mytask1' (up-to-date check took 0.003 secs) due to: Task has not declared any outputs. خروجی تسک شماره یک .net tips is the best :app:mytask1 (Thread[main,5,main]) completed. Took 0.046 secs. :app:mytask2 (Thread[main,5,main]) started. :app:mytask2 Executing task ':app:mytask2' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs. خروجی تسک شماره دو mytask1 is done at 04:03:09 , 2016/07/07 :app:mytask2 (Thread[main,5,main]) completed. Took 0.075 secs. BUILD SUCCESSFUL
در گریدل مخالف doLast یعنی doFirst را نیز داریم ولی عملگر جایگزینی برای آن وجود ندارد و مستقیما باید آن را پیاده سازی کنید. خود گریدل به طور پیش فرض نیز تسکهای آماده ای نیز دارد که میتوانید در مستندات آن بیابید. به عنوان مثال یکی از تسکهای مفید و کاربردی آن تسک کپی کردن هست که از طریق آن میتوانید فایلی یا فایلهایی را از یک مسیر به مسیر دیگر کپی کنید. برای استفاده از چنین تسکهایی، باید تسکهای خود را به شکل زیر به شیوه اکشن بنویسید:
task mytask(type:Copy) { dependsOn mytask1 doLast{ from('build/apk') { include '**/*.apk' } into '.' } }
برای نمایش تسکهای موجود میتوانید از گریدل درخواست کنید که لیست تمامی تسکهای موجود را به شما نشان دهد. برای اینکار میتوانید دستور زیر را صدا کنید:
gradlew --info tasks
Other tasks ----------- clean jarDebugClasses jarReleaseClasses mytask mytask2 transformResourcesWithMergeJavaResForDebugUnitTest transformResourcesWithMergeJavaResForReleaseUnitTest
task mytask(type:Copy) { description "copy apk files to root directory" dependsOn mytask1 doLast{ from('build/apk') { include '**/*.apk' } into '.' } }
یکی دیگر از نکات جالب در مورد گریدل این است که میتواند برای شما callback ارسال کند. بدین صورت که اگر اتفاقی خاصی افتاد، تسک خاصی را اجرا کند. به عنوان مثال ما در کد پایین تسکی را ایجاد کردهایم که به ما این اجازه را میدهد، هر موقع تسکی در مرحله پیکربندی به بیلد اضافه میشود، تسک ما هم اجرا شود و نام تسک اضافه شده به بیلد را چاپ میکند.
tasks.whenTaskAdded{ task-> println "task is added $task.name" }
گریدل امکانات دیگری چون بررسی استثناءها و ایجاد استثناءها را هم پوشش میدهد که میتوانید در این صفحه آن را پیگیری کنید.
Gradle Wrapper
گریدل در حال حاضر مرتبا در حال تغییر و به روز رسانی است و اگر بخواهیم مستقیما با گریدل کار کنیم ممکن است که به مشکلاتی که در نسخه بندی است برخورد کنیم. از آنجا که هر پروژهای که روی سیستم شما قرار بگیرد از نسخهای متفاوتی از گریدل استفاده کند، باعث میشود که نتوانید نسخه مناسبی از گریدل را برای سیستم خود دانلود کنید. بدین جهت wrapper ایجاد شد تا دیگر نیازی به نصب گریدل پیدا نکنید. wrapper در هر پروژه میداند که که به چه نسخهای از گریدل نیاز است. پس موقعی که شما دستور gradlew را صدا میزنید در ویندوز فایل gradlew.bat صدا زده شده و یا در لینوکس و مک فایل شِل اسکریپت gradlew صدا زده میشود و wrapper به خوبی میداند که به چه نسخهای از گریدل برای اجرا نیاز دارد و آن را از طریق دانلود فراهم میکند. اگر همینک دایرکتوری والد پروژه اندرویدی خود را نگاه کنید میتوانید این دو فایل را ببینید.
از آنجا که خود اندروید استادیو به ساخت wrapper اقدام میکند، شما راحت هستید. ولی اگر دوست دارید خودتان برای پروژهای wrapper تولید کنید، مراحل زیر را دنبال کنید:
برای ایجاد wrapper توسط خودتان باید گریدل را دانلود و روی سیستم نصب کنید و سپس دستور زیر را صادر کنید:
gradle wrapper --gradle-version 2.4
اگر میخواهید ببینید wrapper که اندروید استادیو شما دارد چه نسخه از گردیل را صدا میزند مسیر را از دایرکتوری پروژه دنبال کنید و فایل زیر را بگشایید:
\gradle\wrapper\gradle-wrapper.properties
اینها فقط مختصراتی از آشنایی با نحوه عملکر گریدل برای داشتن دیدی روشنتر نسبت به آن بود. برای آشنایی بیشتر با گریدل، باید مستندات رسمی آن را دنبال کنید.