نظرات مطالب
EF Code First #1
DateTime در دات نت value type هست یعنی نال پذیر نیست مگر اینکه Nullable تعریف شود.
در مقالهی قبلی ما بخشی از BootstrapDialog را با استفاده از Reflection پیاده سازی کردیم. دلیل اینکه پیاده سازی کاملی از آن نداشتیم، متغیر بودن مقادیر و پیچیدهتر شدن و طولانی تر شدن کد نویسی آن بود که برای آن کد ارزش زیادی نداشت تا وقت بیشتری صرف شود. ولی در اینجا بخاطر پیچیدگی کمتر، به طور کامل از Reflection استفاده شده است.
شیء BootstrapSwitch یک چک باکس است که با استفاده از جی کوئری و استایلها به یک سوئیچ انیمیشنی زیبا تبدیل شده است که خودم به شخصه علاقه زیادی به استفادهی از آن در پروژههای شخصی پیدا کردهام. غیر از زیبایی، حس خوبی از کارکرد برنامه میدهد.
فایلهای موردنیاز را دانلود کرده و آنها را در ابتدای صفحه و با رعایت ترتیب صدا بزنید:
<script src="//code.jquery.com/jquery-1.11.3.min.js"></script> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script> <link href="~/content/css/bootstrap-switch.min.css" rel=stylesheet"></link> <script src="~/Scripts/bootstrap-switch.min.js"></script>
نکته مهم: فایل css شامل دو نسخه هست که یکی از آن برای Bootstrap2 و دیگری برای نسخه 3 آن است که نسبت به آن نسخه، استایل مناسب را انتخاب کنید.
پروژهی اصلی را دریافت کنید و آن را به solution خود اضافه کنید. پروژه به دو بخش اصلی Controls و Models تقسیم میشود که بخش مدل آن، برای ایجاد ساختارهای آن و در بخش کنترل، برای ترسیم آن به صورت HtmlHelper به کار میرود.
ابتدا قبل از هر چیزی یک شیء از کلاس BootstrapSwitchModel ایجاد کنید و مقادیر دلخواه خود را به خصوصیتهای آن نسبت دهید:
var model=BootstrapSwitchModel(); //وضعیت فعال بودن و غیرفعال بودن سوئیچ model.Checked=true; //اندازه آن model.Size=BootstrapSize..normal; //یک انیمیشن ساده موقع سوئیچ کردن دارد model.Animate=true; //به چک باکس عادی تبدیل میشود model.Disabled=true; //غیرفعال شده و به صورت فقط خواندنی قابل دسترس است model.Readonly=true; //رنگ قعال بودن model.OnColor=BootstrapColor.Success; //رنگ غیرفعال بودن model.OffColor=BootstrapColor.Danger; //متن نمایشی در هنگام فعال بودن model.OnText="On"; //متن نمایشی در حالت عدم انتخاب model.OffText="Off"; //بین دو حالت روشن و خاموش نمایش داده میشود model.label="Public Display"; //تعیین میزان اندازه برچسب بالا model.LabelWidth=100; //سوئیچ به صورت آینه ای معکوس میشود model.Inverse=false; //کلاسی جهت تغییر استایل سوئیچ model.BaseClass="myclass"; //تعیین کلاس برای تگ اصلی پدر model.WrapperClass="wclass"; //فقط یکی از چند سوئیچ میتواند فعال باشد model.RadioAllOff=false; //یک سوئیچ در حالت عادی فقط یکی از //وضعیتها را نمایش میده ولی در این حالت //سوئیچ در ابتدا بین این دو وضعیت گیر کرده است model.Indeterminate=true; //اندازه سمت چپ و راست سوئیچ model.HandleWidth=25;
@{ var model=BootstrapSwitchModel(); ....} @HTML.BootstrapSwitch("id",model);
مطالب
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت اول - ایجاد و به روز رسانی جدول مجازی FTS
SQLite به صورت توکار از full-text search پشتیبانی میکند؛ اما اهمیت آن چیست؟ هدف از full-text search، انجام جستجوهای بسیار سریع، در ستونهای متنی یک جدول بانک اطلاعاتی است. بدون وجود یک چنین قابلیتی، عموما برای انجام اینکار از دستور LIKE استفاده میشود:
کار این کوئری، یافتن ردیفهایی است که در آن واژهی cat وجود دارند. مشکل این روش، عدم استفادهی از ایندکسها و اصطلاحا انجام یک full table scan است. با استفاده از دستور LIKE، باید تک تک ردیفهای بانک اطلاعاتی برای یافتن واژهی مدنظر، اسکن و بررسی شوند و انجام اینکار با بالا رفتن تعداد رکوردهای بانک اطلاعاتی، کندتر و کندتر خواهد شد. برای رفع این مشکل، راه حلی به نام full-text search ارائه شدهاست که کار آن ایندکس کردن تمام ستونهای متنی مدنظر و سپس جستجوی بر روی این ایندکس از پیش آماده شدهاست.
معادل دستور LIKE در کوئری فوق، متد Contains در EF Core است:
بنابراین هدف از این سری، جایگزین کردن متدهای الحاقی Contains ، StartsWith و EndsWith، با روشی بسیار سریعتر است.
یک نکته: کوئری فوق توسط EF Core و به همراه پروایدر SQLite آن، به صورت زیر ترجمه میشود (که آن نیز یک full table scan است):
اما دقیقا دستور Like را به همراه متدهای الحاقی StartsWith و یا EndsWith میتوان مشاهده کرد:
معرفی موجودیتهای مثال این سری
هدف اصلی ما، ایندکس کردن full-text ستونهای متنی عنوان و متن جدول بانک اطلاعاتی متناظر با Chapter است:
ایجاد جدول مجازی Full-text search
زمانیکه عملیات Migration را در EF Core فعال و اجرا میکنیم، دو جدول متناظر با Chapter و User ایجاد میشوند. اما برای کار با full-text search، نیاز به ایجاد جداول دیگری است، تا کار نگهداری ایندکسهای تشکیل شدهی از ستونهای متنی مدنظر ما را انجام دهند. به این نوع جداول در SQLite، جدول مجازی و یا virtual table گفته میشود. یک virtual table در اصل تفاوتی با یک جدول معمولی ندارد. تفاوت در اینجا است که منطق دسترسی به این جدول مجازی از موتور FTS5 مربوط به SQLite باید عبور کند. تاکنون نگارشهای مختلفی از موتور full-text search آن منتشر شدهاند؛ مانند FTS3 ، FTS4 و غیره که آخرین نگارش آن، FTS5 میباشد و به همراه توزیعی که مایکروسافت ارائه میدهد، وجود دارد و نیازی به تنظیمات خاصی ندارد.
در اینجا روش ایجاد یک جدول مجازی جدید Chapters_FTS را مشاهده میکنید:
جدول مجازی، با اجرای دستور CREATE VIRTUAL TABLE ایجاد میشود و USING fts5 آن به معنای استفادهی از موتور full-text search نگارش پنجم آن است. سپس لیست ستونهایی را که میخواهیم ایندکس کنیم، ذکر میشوند؛ مانند Text و Title در اینجا. همانطور که مشاهده میکنید، فقط نام این ستونها قابل تعریف هستند و هیچ نوع اطلاعات اضافهتری را نمیتوان ذکر کرد.
ذکر پارامتر "content="Chapters اختیاری بوده و به این معنا است که نیازی نیست تا اصل دادههای مرتبط با ستونهای ذکر شده نیز ذخیره شوند و آنها را میتوان از جدول Chapters، بازیابی کرد. در این حالت برای برقراری ارتباط بین این جدول مجازی و جدول chapters، پارامتر "content_rowid="Id مقدار دهی شدهاست. content_rowid به primary key جدول content اشاره میکند. ذکر هر دوی این پارامترها اختیاری بوده و در صورت تنظیم، حجم نهایی بانک اطلاعاتی را کاهش میدهند. چون در این حالت دیگری نیازی به ذخیره سازی جداگانهی اصل اطلاعات متناظر با ایندکسهای FTS نیست.
اکنون که با دستور ایجاد جدول مجازی FTS آشنا شدیم، روش ایجاد آن در برنامههای مبتنی بر EF Core نیز دقیقا به همین صورت است:
فقط کافی است در ابتدای اجرای برنامه با استفاده از متد ExecuteSqlRaw، عبارت SQL متناظر با ایجاد جدول مجازی را اجرا کنیم. این یک روش ایجاد این نوع جداول است؛ روش دیگر آن، قرار دادن همین قطعه کد در متد "protected override void Up(MigrationBuilder migrationBuilder)" مربوط به کلاسهای ایجاد شدهی توسط عملیات Migration است.
به روز رسانی اطلاعات جدول مجازی FTS، توسط تریگرها
پس از اجرای دستورCREATE VIRTUAL TABLE فوق، SQLite پنج جدول را به صورت خودکار ایجاد میکند که در تصویر زیر قابل مشاهده هستند:
البته ما مستقیما با این جداول کار نخواهیم کرد و این جداول برای نگهداری اطلاعات ایندکسهای full-text موتور FTS5، توسط خود SQLite نگهداری و مدیریت میشوند.
اما ... نکتهی مهم اینجا است که جدول مجازی Chapters_FTS، هرچند به جدول اصلی Chapters توسط پارامتر content آن متصل شدهاست، اما تغییرات آنرا ردیابی نمیکند. یعنی هر نوع insert/update/delete ای که در جدول اصلی Chapters رخ میدهد، سبب ایندکس شدن اطلاعات جدید آن در جدول مجازی Chapters_FTS نمیشود و برای اینکار باید اطلاعات را مستقیما در جدول Chapters_FTS درج کرد.
روش پیشنهاد شدهی در مستندات رسمی آن، استفاده از تریگرهای پس از درج اطلاعات، پس از حذف اطلاعات و پس از به روز رسانی اطلاعات به صورت زیر است:
در اینجا ابتدا روش ایجاد یک جدول جدید و سپس ایجاد یک جدول مجازی FTS را از روی آن مشاهده میکنید.
در ادامه سه تریگر بر روی جدول اصلی که ما به صورت متداولی با آن در برنامههای خود کار میکنیم، تعریف شدهاند. این تریگرها کار insert اطلاعات را در جدول مجازی ایجاد شده، به صورت خودکار انجام میدهند.
همانطور که مشاهده میکنید، یک rowid نیز در اینجا قابل تعریف است؛ rowid، ستون مخفی یک جدول مجازی FTS است و هرچند در حین ایجاد، آنرا ذکر نمیکنیم، اما جزئی از ساختار آن بوده و قابل کوئری گرفتن است.
نکتهی مهم: به فرمت دستورات به روز رسانی جدول مجازی FTS دقت کنید. حتی در حالت تریگرهای update و یا delete نیز در اینجا دستور insert، مشاهده میشوند. این فرمت دقیقا باید به همین نحو رعایت شود؛ در غیراینصورت اگر از دستورات delete و یا update معمولی بر روی این جدول مجازی استفاده کنید، دفعهی بعدی که برنامه را اجرا میکنید، خطای «این بانک اطلاعاتی تخریب شدهاست» را مشاهده کرده (database disk image is malformed) و دیگر نمیتوانید با فایل بانک اطلاعاتی خود کار کنید.
به روز رسانی اطلاعات جدول مجازی FTS توسط EF Core
الف) یافتن لیست اطلاعات تغییر یافتهی حاصل از اعمال insert/update/delete
هدف از متد GetChangedEntities فوق این است که با استفاده از سیستم tracking، نوع عملیات انجام شده و همچنین اصل موجودیتها را پیش و پس از تغییر، بتوان لیست کرد و سپس بر اساس آنها، جدول مجازی FTS را به روز رسانی نمود.
علت نیاز به نمونهی اصل و سپس تغییر کردهی موجودیتها، به نحوهی تعریف تریگرهای مخصوص به به روز رسانی FTS بر میگردد. اگر دقت کرده باشید در این تریگرها، new.a و همچنین old.a را داریم که برای شبیه سازی آنها دقیقا باید به اطلاعات یک رکورد، در پیش و پس از به روز رسانی آن، دسترسی یافت.
ب) تعریف تریگرهای SQL توسط سیستم tracking؛ به همراه عملیات نرمال سازی اطلاعات
در اینجا نحوهی تعریف متد UpdateChapterFTS را مشاهده میکند که اطلاعات خودش را از متد GetChangedEntities دریافت کرده و سپس یکی یکی آنها را در جدول مجازی FTS، با فرمت مخصوصی که عنوان شد (دقیقا متناظر با فرمت تریگرهای مستندات رسمی FTS)، درج میکند.
همچنین در اینجا متد NormalizeText را نیز مشاهده میکند که بر روی ستونهای متنی اعمال شدهاست. کار آن پاکسازی تگهای یک متن HTML ای است و نگهداری اطلاعات صرفا متنی آن. در اینجا اگر نیاز بود میتوان منطقهای پاکسازی اطلاعات دیگری را نیز اعمال کرد.
اکنون که این اطلاعات به صورت پاکسازی شده در جدول مجازی درج میشوند، زمانیکه بر روی آنها جستجویی صورت میگیرد، دیگر شامل جستجوی بر روی تگهای HTML ای نیست و دقت بسیار بیشتری دارد.
ج) اتصال به سیستم
پس از تعریف متدهای الحاقی GetChangedEntities و UpdateChapterFTS، اکنون روش اتصال آنها به DbContext برنامه، با بازنویسی متد SaveChanges آن است:
از این پس تمام عملیات insert/update/delete برنامه تحت کنترل قرار گرفته و به صورت خودکار سبب به روز رسانی جدول مجازی FTS نیز میشوند.
در قسمت بعدی، روش کوئری گرفتن از این جدول مجازی FTS را بررسی میکنیم.
SELECT Title FROM Book WHERE Desc LIKE '%cat%';
معادل دستور LIKE در کوئری فوق، متد Contains در EF Core است:
var cats = context.Chapters.Where(item => item.Text.Contains("cat")).ToList();
یک نکته: کوئری فوق توسط EF Core و به همراه پروایدر SQLite آن، به صورت زیر ترجمه میشود (که آن نیز یک full table scan است):
SELECT "c"."Text" FROM "Chapters" AS "c" WHERE ('cat' = '') OR (instr("c"."Text", 'cat') > 0)
var cats = context.Chapters.Where(item => item.Text.StartsWith("cat")).ToList(); // SELECT "c"."Text", FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE 'cat%')
var cats = context.Chapters.Where(item => item.Text.EndsWith("cat")).ToList(); // SELECT "c"."Title" FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE '%cat')
معرفی موجودیتهای مثال این سری
هدف اصلی ما، ایندکس کردن full-text ستونهای متنی عنوان و متن جدول بانک اطلاعاتی متناظر با Chapter است:
using System.Collections.Generic; namespace EFCoreSQLiteFTS.Entities { public class User { public int Id { get; set; } public string Name { get; set; } public ICollection<Chapter> Chapters { get; set; } } public class Chapter { public int Id { get; set; } public string Title { get; set; } public string Text { get; set; } public User User { get; set; } public int UserId { get; set; } } }
زمانیکه عملیات Migration را در EF Core فعال و اجرا میکنیم، دو جدول متناظر با Chapter و User ایجاد میشوند. اما برای کار با full-text search، نیاز به ایجاد جداول دیگری است، تا کار نگهداری ایندکسهای تشکیل شدهی از ستونهای متنی مدنظر ما را انجام دهند. به این نوع جداول در SQLite، جدول مجازی و یا virtual table گفته میشود. یک virtual table در اصل تفاوتی با یک جدول معمولی ندارد. تفاوت در اینجا است که منطق دسترسی به این جدول مجازی از موتور FTS5 مربوط به SQLite باید عبور کند. تاکنون نگارشهای مختلفی از موتور full-text search آن منتشر شدهاند؛ مانند FTS3 ، FTS4 و غیره که آخرین نگارش آن، FTS5 میباشد و به همراه توزیعی که مایکروسافت ارائه میدهد، وجود دارد و نیازی به تنظیمات خاصی ندارد.
در اینجا روش ایجاد یک جدول مجازی جدید Chapters_FTS را مشاهده میکنید:
CREATE VIRTUAL TABLE "Chapters_FTS" USING fts5("Text", "Title", content="Chapters", content_rowid="Id")
ذکر پارامتر "content="Chapters اختیاری بوده و به این معنا است که نیازی نیست تا اصل دادههای مرتبط با ستونهای ذکر شده نیز ذخیره شوند و آنها را میتوان از جدول Chapters، بازیابی کرد. در این حالت برای برقراری ارتباط بین این جدول مجازی و جدول chapters، پارامتر "content_rowid="Id مقدار دهی شدهاست. content_rowid به primary key جدول content اشاره میکند. ذکر هر دوی این پارامترها اختیاری بوده و در صورت تنظیم، حجم نهایی بانک اطلاعاتی را کاهش میدهند. چون در این حالت دیگری نیازی به ذخیره سازی جداگانهی اصل اطلاعات متناظر با ایندکسهای FTS نیست.
اکنون که با دستور ایجاد جدول مجازی FTS آشنا شدیم، روش ایجاد آن در برنامههای مبتنی بر EF Core نیز دقیقا به همین صورت است:
private static void createFtsTables(ApplicationDbContext context) { // For SQLite FTS // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too. context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS"" USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");"); }
به روز رسانی اطلاعات جدول مجازی FTS، توسط تریگرها
پس از اجرای دستورCREATE VIRTUAL TABLE فوق، SQLite پنج جدول را به صورت خودکار ایجاد میکند که در تصویر زیر قابل مشاهده هستند:
البته ما مستقیما با این جداول کار نخواهیم کرد و این جداول برای نگهداری اطلاعات ایندکسهای full-text موتور FTS5، توسط خود SQLite نگهداری و مدیریت میشوند.
اما ... نکتهی مهم اینجا است که جدول مجازی Chapters_FTS، هرچند به جدول اصلی Chapters توسط پارامتر content آن متصل شدهاست، اما تغییرات آنرا ردیابی نمیکند. یعنی هر نوع insert/update/delete ای که در جدول اصلی Chapters رخ میدهد، سبب ایندکس شدن اطلاعات جدید آن در جدول مجازی Chapters_FTS نمیشود و برای اینکار باید اطلاعات را مستقیما در جدول Chapters_FTS درج کرد.
روش پیشنهاد شدهی در مستندات رسمی آن، استفاده از تریگرهای پس از درج اطلاعات، پس از حذف اطلاعات و پس از به روز رسانی اطلاعات به صورت زیر است:
-- Create a table. And an external content fts5 table to index it. CREATE TABLE tbl(a INTEGER PRIMARY KEY, b, c); CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a'); -- Triggers to keep the FTS index up to date. CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c); END; CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c); END; CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c); INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c); END;
در ادامه سه تریگر بر روی جدول اصلی که ما به صورت متداولی با آن در برنامههای خود کار میکنیم، تعریف شدهاند. این تریگرها کار insert اطلاعات را در جدول مجازی ایجاد شده، به صورت خودکار انجام میدهند.
همانطور که مشاهده میکنید، یک rowid نیز در اینجا قابل تعریف است؛ rowid، ستون مخفی یک جدول مجازی FTS است و هرچند در حین ایجاد، آنرا ذکر نمیکنیم، اما جزئی از ساختار آن بوده و قابل کوئری گرفتن است.
نکتهی مهم: به فرمت دستورات به روز رسانی جدول مجازی FTS دقت کنید. حتی در حالت تریگرهای update و یا delete نیز در اینجا دستور insert، مشاهده میشوند. این فرمت دقیقا باید به همین نحو رعایت شود؛ در غیراینصورت اگر از دستورات delete و یا update معمولی بر روی این جدول مجازی استفاده کنید، دفعهی بعدی که برنامه را اجرا میکنید، خطای «این بانک اطلاعاتی تخریب شدهاست» را مشاهده کرده (database disk image is malformed) و دیگر نمیتوانید با فایل بانک اطلاعاتی خود کار کنید.
به روز رسانی اطلاعات جدول مجازی FTS توسط EF Core
روش تعریف تریگرهای یاد شده، مستقل از EF Core بوده و راسا توسط خود بانک اطلاعاتی مدیریت میشود. بنابراین فقط کافی است دستور CREATE TRIGGER را به همان نحوی که عنوان شد، توسط متد ExecuteSqlRaw اجرا کنیم تا جزئی از ساختار بانک اطلاعاتی شوند؛ اما ... این روش برای برنامههایی با متنهای پیچیده کارآیی ندارد. برای مثال فرض کنید اطلاعات اصلی شما با فرمت HTML است. ایندکس ایجاد شده، تگهای HTML را حذف نمیکند و آنها را نیز ایندکس میکند که نه تنها سبب بالا رفتن حجم بانک اطلاعاتی میشود، بلکه زمانیکه ما قصد جستجویی را بر روی اطلاعات HTML ای داریم، اساسا کاری به تگهای آن نداشته و هدف اصلی ما، متنهای درج شدهی در آن است. نمونهی دیگر آن داشتن اطلاعاتی با «اعراب» است و یا شاید نیاز به یکدست سازی ی و ک فارسی وجود داشته باشد. به این نوع عملیات، «نرمال سازی متن» گفته میشود و با روش تریگرهای فوق قابل تعریف و مدیریت نیست. به همین جهت میتوان از روش پیشنهادی زیر استفاده کرد:
using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace EFCoreSQLiteFTS.DataLayer { public static class EFChangeTrackerExtensions { public static List<(EntityState State, TEntity NewEntity, TEntity OldEntity)> GetChangedEntities<TEntity>(this DbContext dbContext) where TEntity : class, new() { if (!dbContext.ChangeTracker.AutoDetectChangesEnabled) { // ChangeTracker.Entries() only calls `Try`DetectChanges() behind the scene. dbContext.ChangeTracker.DetectChanges(); } return dbContext.ChangeTracker.Entries<TEntity>() .Where(IsEntityChanged) .Select(entityEntry => (entityEntry.State, entityEntry.Entity, createWithValues<TEntity>(entityEntry.OriginalValues))) .ToList(); } private static bool IsEntityChanged(EntityEntry entry) { return entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted || entry.References.Any(r => r.TargetEntry?.Metadata.IsOwned() == true && IsEntityChanged(r.TargetEntry)); } private static T createWithValues<T>(PropertyValues values) where T : new() { var entity = new T(); foreach (var prop in values.Properties) { var value = values[prop.Name]; if (value is PropertyValues) { throw new NotSupportedException("nested complex object"); } else { prop.PropertyInfo.SetValue(entity, value); } } return entity; } } }
علت نیاز به نمونهی اصل و سپس تغییر کردهی موجودیتها، به نحوهی تعریف تریگرهای مخصوص به به روز رسانی FTS بر میگردد. اگر دقت کرده باشید در این تریگرها، new.a و همچنین old.a را داریم که برای شبیه سازی آنها دقیقا باید به اطلاعات یک رکورد، در پیش و پس از به روز رسانی آن، دسترسی یافت.
ب) تعریف تریگرهای SQL توسط سیستم tracking؛ به همراه عملیات نرمال سازی اطلاعات
using System.Collections.Generic; using System.Data; using System.Text.RegularExpressions; using EFCoreSQLiteFTS.Entities; using Microsoft.EntityFrameworkCore; namespace EFCoreSQLiteFTS.DataLayer { public static class FtsNormalizer { private static readonly Regex _htmlRegex = new Regex("<[^>]*>", RegexOptions.Compiled); public static string NormalizeText(this string text) { if (string.IsNullOrWhiteSpace(text)) { return string.Empty; } // Remove html tags text = _htmlRegex.Replace(text, string.Empty); // TODO: add other normalizers here, such as `remove diacritics`, `fix Persian Ye-Ke` and so on ... return text; } } public static class UpdateFtsTriggers { public static void UpdateChapterFTS( this DbContext context, List<(EntityState State, Chapter NewEntity, Chapter OldEntity)> changedChapters) { var database = context.Database; try { database.BeginTransaction(IsolationLevel.ReadCommitted); foreach (var (State, NewEntity, OldEntity) in changedChapters) { var chapterNew = NewEntity; var chapterOld = OldEntity; var normalizedNewText = chapterNew.Text.NormalizeText(); var normalizedOldText = chapterOld.Text.NormalizeText(); var normalizedNewTitle = chapterNew.Title.NormalizeText(); var normalizedOldTitle = chapterOld.Title.NormalizeText(); switch (State) { case EntityState.Added: if (shouldSkipAddedChapter(chapterNew)) { continue; } database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});", chapterNew.Id, normalizedNewText, normalizedNewTitle); break; case EntityState.Modified: if (shouldSkipModifiedChapter(chapterNew, chapterOld)) { continue; } // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error! database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title) VALUES('delete', {0}, {1}, {2}); ", chapterOld.Id, normalizedOldText, normalizedOldTitle); database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});", chapterNew.Id, normalizedNewText, normalizedNewTitle); break; case EntityState.Deleted: // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error! database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title) VALUES('delete', {0}, {1}, {2}); ", chapterOld.Id, normalizedOldText, normalizedOldTitle); break; } } } finally { database.CommitTransaction(); } } private static bool shouldSkipAddedChapter(Chapter chapterNew) { // TODO: add your logic to avoid indexing this item return false; } private static bool shouldSkipModifiedChapter(Chapter chapterNew, Chapter chapterOld) { // TODO: add your logic to avoid indexing this item return chapterNew.Text == chapterOld.Text && chapterNew.Title == chapterOld.Title; } } }
همچنین در اینجا متد NormalizeText را نیز مشاهده میکند که بر روی ستونهای متنی اعمال شدهاست. کار آن پاکسازی تگهای یک متن HTML ای است و نگهداری اطلاعات صرفا متنی آن. در اینجا اگر نیاز بود میتوان منطقهای پاکسازی اطلاعات دیگری را نیز اعمال کرد.
اکنون که این اطلاعات به صورت پاکسازی شده در جدول مجازی درج میشوند، زمانیکه بر روی آنها جستجویی صورت میگیرد، دیگر شامل جستجوی بر روی تگهای HTML ای نیست و دقت بسیار بیشتری دارد.
ج) اتصال به سیستم
پس از تعریف متدهای الحاقی GetChangedEntities و UpdateChapterFTS، اکنون روش اتصال آنها به DbContext برنامه، با بازنویسی متد SaveChanges آن است:
namespace EFCoreSQLiteFTS.DataLayer { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } public DbSet<Chapter> Chapters { get; set; } public DbSet<User> Users { get; set; } public override int SaveChanges() { var changedChapters = this.GetChangedEntities<Chapter>(); this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again. var result = base.SaveChanges(); this.ChangeTracker.AutoDetectChangesEnabled = true; this.UpdateChapterFTS(changedChapters); return result; } } }
در قسمت بعدی، روش کوئری گرفتن از این جدول مجازی FTS را بررسی میکنیم.
به صورت پیش فرض، Rx هر بار تنها یک مقدار را بررسی میکند. اما گاهی از اوقات نیاز است تا در هربار، بیشتر از یک مقدار دریافت و پردازش شوند. برای این منظور Rx متدهای الحاقی ویژهای را به نامهای Buffer ،Scan و Window تدارک دیدهاست تا بتواند از یک توالی، چندین توالی را تولید کند (توالی توالیها = Sequence of sequences).
متد Scan
فرض کنید قصد دارید تعدادی عدد را با هم جمع بزنید. برای اینکار عموما عدد اول با عدد دوم جمع زده شده و سپس حاصل آن با عدد سوم جمع زده خواهد شد و به همین ترتیب تا آخر توالی. کار متد Scan نیز دقیقا به همین نحو است. هربار که قرار است توالی پردازش شود، حاصل عملیات مرحلهی قبل را در اختیار مصرف کننده قرار میدهد.
در مثال ذیل، قصد داریم حاصل جمع اعداد موجود در آرایهای را بدست بیاوریم:
با این خروجی
در اولین بار اجرای متد Subscribe، کار مقدار دهی accumulator با اولین عنصر آرایه صورت میگیرد.
در دفعات بعدی، مقدار این accumulator با عدد جاری جمع زده شده و حاصل این عملیات در تکرار آتی، مجددا توسط accumulator قابل دسترسی خواهد بود.
یک نکته: اگر علاقمند باشیم که مقدار اولیهی accumulator، اولین عنصر توالی نباشد، میتوان آنرا توسط پارامتر seed متد Scan مقدار دهی کرد:
متد Buffer
متد بافر، کار تقسیم یک توالی را به توالیهای کوچکتر، بر اساس زمان، یا تعداد عنصر مشخص شده، انجام میدهد. برای مثال در برنامههای دسکتاپ شاید نیازی نباشد تا به ازای هر عنصر توالی، یکبار رابط کاربری را به روز کرد. عموما بهتر است تا تعداد مشخصی از عناصر یکجا پردازش شده و نتیجهی این پردازش به تدریج نمایش داده شود.
در اینجا نحوهی استفاده از متد بافر را به همراه مشخص کردن تعداد اعضای بافر ملاحظه میکنید. هربار که onNext متد Subscribe فراخوانی شود، 10 عنصر از توالی را در اختیار خواهیم داشت (بجای یک عنصر حالت متداول بافر نشده).
به این ترتیب میتوان فشار حجم اطلاعات ورودی با فرکانس بالا را کنترل کرد و در نتیجه از منابع موجود بهتر استفاده نمود. برای مثال اگر میخواهید عملیات bulk insert را انجام دهید، میتوان بر اساس یک batch size مشخص، گروه گروه اطلاعات را به بانک اطلاعاتی اضافه کرد تا فشار کار کاهش یابد.
همینکار را بر اساس زمان نیز میتوان انجام داد:
در مثال فوق هر 2 ثانیه یکبار، مجموعهای از عناصر به متد onNext ارسال خواهند شد.
متد Window
متد Window نیز دقیقا همان پارامترهای متد بافر را قبول میکند. با این تفاوت که هربار، یک توالی obsevable را به متد onNext ارسال میکند.
نوع numbers پارامتر onNext، در حین بکارگیری متد بافر در مثالهای فوق، IList of int است. اما اگر متدهای Buffer را تبدیل به متد Window کنیم، اینبار نوع numbers، معادل IObservable of int خواهد شد.
چه زمانی باید از Buffer استفاده کرد و چه زمانی از Window؟
در متد بافر، به ازای هر توالی که به پارامتر onNext ارسال میشود، یکبار وهلهی جدیدی از توالی مدنظر در حافظه ایجاد و ارسال خواهد شد. در متد Window صرفا اشارهگرهایی به این توالی را در اختیار داریم؛ بنابراین مصرف حافظهی کمتری را شاهد خواهیم بود. متد Window بسیار مناسب است برای اعمال aggregation. مثلا اگر نیاز است جمع، میانگین، حداقل و حداکثر عناصر دریافتی محاسبه شوند، بهتر است از متد Window استفاده شود که نهایتا قابلیت استفاده از متدهای الحاقی Sum و Max و Min را به همراه دارد. با این تفاوت که حاصل اینها نیز یک IObservable است که باید Subscribe آنرا برای دریافت نتیجه فراخوانی کرد:
در این حالت متد Window، برخلاف متد Buffer، توالی numbers را هربار کش نمیکند و به این ترتیب میتوان به مصرف حافظهی کمتری رسید.
کاربردهای دنیای واقعی
در اینجا دو مثال از بکارگیری متد Buffer را جهت پردازش مجموعههای عظیمی از اطلاعات و نمایش همزمان آنها در رابط کاربری ملاحظه میکنید.
مثال اول: فرض کنید قصد دارید تمام فایلهای درایو C خود را توسط یک TreeView نمایش دهید. در این حالت نباید رابط کاربری برنامه در حالت هنگ به نظر برسد. همچنین به علت زیاد بودن تعداد فایلها و نمایش همزمان آنها در UI، نباید CPU Usage برنامه تا حدی باشد که در کار سایر برنامهها اخلال ایجاد کند. در این مثالها با استفاده از Rx و متد بافر آن، هربار مثلا 1000 آیتم را بافر کرده و سپس یکجا در TreeView نمایش میدهند. به این ترتیب دو شرط یاد شده محقق میشوند.
The Rx Framework By Example
مثال دوم: خواندن تعداد زیادی رکورد از بانک اطلاعاتی به همراه نمایش همزمان آنها در UI بدون اخلالی در کار سیستم و همچنین هنگ کردن برنامه.
Using Reactive Extensions for Streaming Data from Database
متد Scan
فرض کنید قصد دارید تعدادی عدد را با هم جمع بزنید. برای اینکار عموما عدد اول با عدد دوم جمع زده شده و سپس حاصل آن با عدد سوم جمع زده خواهد شد و به همین ترتیب تا آخر توالی. کار متد Scan نیز دقیقا به همین نحو است. هربار که قرار است توالی پردازش شود، حاصل عملیات مرحلهی قبل را در اختیار مصرف کننده قرار میدهد.
در مثال ذیل، قصد داریم حاصل جمع اعداد موجود در آرایهای را بدست بیاوریم:
var sequence = new[] { 12, 3, -4, 7 }.ToObservable(); var runningSum = sequence.Scan((accumulator, value) => { Console.WriteLine("accumulator {0}", accumulator); Console.WriteLine("value {0}", value); return accumulator + value; }); runningSum.Subscribe(result => Console.WriteLine("result {0}\n", result));
result 12 accumulator 12 value 3 result 15 accumulator 15 value -4 result 11 accumulator 11 value 7 result 18
در دفعات بعدی، مقدار این accumulator با عدد جاری جمع زده شده و حاصل این عملیات در تکرار آتی، مجددا توسط accumulator قابل دسترسی خواهد بود.
یک نکته: اگر علاقمند باشیم که مقدار اولیهی accumulator، اولین عنصر توالی نباشد، میتوان آنرا توسط پارامتر seed متد Scan مقدار دهی کرد:
var runningSum = sequence.Scan(seed: 10, accumulator: (accumulator, value) =>
متد Buffer
متد بافر، کار تقسیم یک توالی را به توالیهای کوچکتر، بر اساس زمان، یا تعداد عنصر مشخص شده، انجام میدهد. برای مثال در برنامههای دسکتاپ شاید نیازی نباشد تا به ازای هر عنصر توالی، یکبار رابط کاربری را به روز کرد. عموما بهتر است تا تعداد مشخصی از عناصر یکجا پردازش شده و نتیجهی این پردازش به تدریج نمایش داده شود.
var sequence = Enumerable.Range(1, 200) .ToObservable() .Buffer(count: 10); sequence.Subscribe(onNext: numbers => { Console.WriteLine(numbers.Sum()); });
به این ترتیب میتوان فشار حجم اطلاعات ورودی با فرکانس بالا را کنترل کرد و در نتیجه از منابع موجود بهتر استفاده نمود. برای مثال اگر میخواهید عملیات bulk insert را انجام دهید، میتوان بر اساس یک batch size مشخص، گروه گروه اطلاعات را به بانک اطلاعاتی اضافه کرد تا فشار کار کاهش یابد.
همینکار را بر اساس زمان نیز میتوان انجام داد:
var sequence = Enumerable.Range(1, 200) .ToObservable() .Buffer(timeSpan: TimeSpan.FromSeconds(2));
متد Window
متد Window نیز دقیقا همان پارامترهای متد بافر را قبول میکند. با این تفاوت که هربار، یک توالی obsevable را به متد onNext ارسال میکند.
نوع numbers پارامتر onNext، در حین بکارگیری متد بافر در مثالهای فوق، IList of int است. اما اگر متدهای Buffer را تبدیل به متد Window کنیم، اینبار نوع numbers، معادل IObservable of int خواهد شد.
var sequence = Enumerable.Range(1, 200) .ToObservable() .Window(timeSpan: TimeSpan.FromSeconds(2)); sequence.Subscribe(onNext: numbers => { numbers.Subscribe(onNext: number => Console.WriteLine(number)); });
چه زمانی باید از Buffer استفاده کرد و چه زمانی از Window؟
در متد بافر، به ازای هر توالی که به پارامتر onNext ارسال میشود، یکبار وهلهی جدیدی از توالی مدنظر در حافظه ایجاد و ارسال خواهد شد. در متد Window صرفا اشارهگرهایی به این توالی را در اختیار داریم؛ بنابراین مصرف حافظهی کمتری را شاهد خواهیم بود. متد Window بسیار مناسب است برای اعمال aggregation. مثلا اگر نیاز است جمع، میانگین، حداقل و حداکثر عناصر دریافتی محاسبه شوند، بهتر است از متد Window استفاده شود که نهایتا قابلیت استفاده از متدهای الحاقی Sum و Max و Min را به همراه دارد. با این تفاوت که حاصل اینها نیز یک IObservable است که باید Subscribe آنرا برای دریافت نتیجه فراخوانی کرد:
var sequence = Enumerable.Range(1, 200) .ToObservable() .Window(10); sequence.Subscribe(onNext: numbers => { numbers.Sum().Subscribe(onNext: number => Console.WriteLine(number)); });
کاربردهای دنیای واقعی
در اینجا دو مثال از بکارگیری متد Buffer را جهت پردازش مجموعههای عظیمی از اطلاعات و نمایش همزمان آنها در رابط کاربری ملاحظه میکنید.
مثال اول: فرض کنید قصد دارید تمام فایلهای درایو C خود را توسط یک TreeView نمایش دهید. در این حالت نباید رابط کاربری برنامه در حالت هنگ به نظر برسد. همچنین به علت زیاد بودن تعداد فایلها و نمایش همزمان آنها در UI، نباید CPU Usage برنامه تا حدی باشد که در کار سایر برنامهها اخلال ایجاد کند. در این مثالها با استفاده از Rx و متد بافر آن، هربار مثلا 1000 آیتم را بافر کرده و سپس یکجا در TreeView نمایش میدهند. به این ترتیب دو شرط یاد شده محقق میشوند.
The Rx Framework By Example
مثال دوم: خواندن تعداد زیادی رکورد از بانک اطلاعاتی به همراه نمایش همزمان آنها در UI بدون اخلالی در کار سیستم و همچنین هنگ کردن برنامه.
Using Reactive Extensions for Streaming Data from Database
مطالب دورهها
شی گرایی در #F
برنامه نویسی شی گرای سومین نسل از الگوهای اصلی برنامه نویسی است. در توضیحات فصل اول گفته شد که #F یک زبان تابع گرا است ولی این بدان معنی نیست که #F از مفاهیمی نظیر کلاس و یا interface پشتیبانی نکند. برعکس در #F امکان تعریف کلاس و interface و هم چنین پیاده سازی مفاهیم شی گرایی وجود دارد.
*با توجه به این موضوع که فرض است دوستان با مفاهیم شی گرایی آشنایی دارند از توضیح و تشریح این مفاهیم خودداری میکنم.
Classes
کلاس چارچوبی از اشیا است برای نگهداری خواص(Properties) و رفتار ها(Methods) و رخدادها(Events). کلاس پایه ایترین مفهوم در برنامه نویسی شی گراست. ساختار کلی تعربف کلاس در #F به صورت زیر است:
همان طور که در ساختار بالا میبینید مفاهیم access-modifier و inherit و constructor هم در #F وجود دارد.
انواع access-modifier در #F
کلاس بالا دارای یک سازنده است که دو پارامتر ورودی میگیرد. کلمه end به معنای انتهای کلاس است. برای استفاده کلاس باید به صورت زیر عمل کنید:
توابع و خواص در کلاس ها
برای تعریف خاصیت در #F باید از کلمه کلیدی member استفاده کنید. در مثال بعدی برای کلاس بالا تابع و خاصیت تعریف خواهیم کرد.
کلاس بالا دارای سه خاصیت به نامهای Number و Name و Amount است و دو تابع به نامهای Deposit و Withdraw دارد. اما x استفاده شده قبل از هر member به معنی this در #C است. در #F شما برای اشاره به شناسههای یک محدوده خودتون باید یک نام رو برای اشاره گر مربوطه تعیین کنید.
استفاده از کلمه do
در #F زمانی که قصد داشته باشیم در بعد از وهله سازی از کلاس و فراخوانی سازنده، عملیات خاصی انجام شود(مثل انجام برخی عملیات متداول در سازندههای کلاسهای دات نت) باید از کلمه کلیدی do به همراه یک بلاک از کد استفاده کنیم.
یک مثال در این زمینه:
در مثال بالا دو عبارت do یکی به صورت static و دیگری به صورت غیر static تعریف شده اند. استفاده از do به صورت غیر static این امکان را به ما میدهد که بتوانیم به تمام شناسهها و توابع تعریف شده در کلاس استفاده کنیم ولی do به صورت static فقط به خواص و توابع از نوع static در کلاس دسترسی دارد.
خروجی مثال بالا:
خواص static:
برای تعریف خواص به صورت استاتیک مانند #C از کلمه کلیدی static استفاده کنید.مثالی در این زمینه:
SomeStaticMethod به صورت استاتیک تعریف شده در حالی که x.Prop به صورت غیر استاتیک. دسترسی به متدها یا خواص static باید بدون وهله سازی از کلاس انجام بگیرد در غیر این صورت با خطای کامپایلر روبرو خواهید شد.
روش استفاده درست:
متدهای get , set در خاصیت ها:
همانند #C و سایر زبانهای دات نت امکان تعریف متدهای get و set برای خاصیتهای یک کلاس وجود دارد.
ساختار کلی:
مثالی در این زمینه:
کد متناظر در #C:
یا به صورت:
Interface ها
اینترفیس به تمامی خواص و توابع عمومی اشئایی که آن را پیاده سازی کرده اند اشاره میکند. (توضیحات بیشتر (^ ) و (^ ))ساختار کلی برای تعریف آن به صورت زیر است:
مثال:
استفاده از حرف I برای شروع نام اینترفیس طبق قوانین تعریف شده (اختیاری) برای نام گذاری است.
نکته: در هنگام تعریف توابع و خاصیت در interfaceها باید از کلمه abstract استفاده کنیم. هر کلاسی که از یک یا چند تا اینترفیس ارث ببرد باید تمام خواص و توابع اینتریسها را پیاده سازی کند. در مثال بعدی کلاس SomeClass1 اینترفیس بالا را پیاده سازی میکند. دقت کنید که کلمه this توسط من به عنوان اشاره گر به اشیای کلاس تعیین شده و شما میتونید از هر کلمه یا حرف دیگری استفاده کنید.
نکته مهم: اگر قصد فراخوانی متد Print را در کلاس بالا دارید نمیتونید به صورت مستقیم متد بالا را فراخوانی کنید. بلکه حتما باید کلاس به
اینترفیس مربوطه cast شود.
روش نادرست:
روش درست:
برای عملیات cast ازاستفاده کنید.
در مثال بعدی کلاسی خواهیم داشت که از سه اینترفیس ارث میبرد. در نتیجه باید تمام متدهای هر سه اینترفیس را پیاده سازی کند.
فراخوانی این متدها نیز به صورت زیر خواهد بود:
کلاسهای Abstract
#F از کلاسهای abstract هم پشتیبانی میکند. اگر با کلاسهای abstract در #C آشنایی ندارید میتونید مطالب مورد نظر رو در (^ ) و (^ ) مطالعه کنید. به صورت خلاصه کلاسهای abstract به عنوان کلاسهای پایه در برنامه نویسی شی گرا استفاده میشوند. این کلاسها دارای خواص و متدهای پیاده سازی شده و نشده هستند. خواص و متد هایی که در کلاس پایه abstract پیاده سازی نشده اند باید توسط کلاس هایی که از این کلاس پایه ارث میبرند حتما پیاده سازی شوند.
ساختار کلی تعریف کلاسهای abstract:
در #F برای این که مشخص کنیم که یک کلاس abstract است حتما باید [<AbstractClass>] در بالای کلاس تعریف شود.
کلاس بالا تعریفی از کلاس abstract است که سه خصوصیت abstract دارد (برای
تعیین خصوصیتها و متد هایی که در کلاس پایه پیاده سازی نمیشوند از کلمه
کلیدی abstract در هنگام تعریف آنها استفاده میکنیم). حال دو کلاس ایجاد میکنیم که این کلاس پایه را پیاده سازی کنند.
#1 کلاس اول
#2 کلاس دوم
Structures
structureها در #F دقیقا معال struct در #C هستند. توضیحات بیشتر درباره struct در #C (^ ) و (^ )). اما به طور خلاصه باید ذکر کنم که strucureها تقریبا دارای مفهوم کلاس هستند با اندکی تفاوت که شامل موارد زیر است:
یک نکته مهم هنگام کار با structها در #F این است که امکان استفاده از let
و Binding در structها وجود ندارد. به جای آن باید از val استفاده کنید.
تفاوت اصلی بین val و let در این است که هنگام تعریف
شناسه با val امکان مقدار دهی اولیه به شناسه وجود ندارد. در مثال بالا
مقادیر برای x و y و z برابر 0.0 است که توسط کامپایلر انجام میشود. در
ادامه یک struct به همراه سازنده تعریف میکنیم:
توسط سازنده struct بالا مقادیر اولیه x و y دریافت میشود به متغیرهای متناظر انتساب میشود.
در پایان یک مثال مشترک رو در #C و #F پیاده سازی میکنیم:
*با توجه به این موضوع که فرض است دوستان با مفاهیم شی گرایی آشنایی دارند از توضیح و تشریح این مفاهیم خودداری میکنم.
Classes
کلاس چارچوبی از اشیا است برای نگهداری خواص(Properties) و رفتار ها(Methods) و رخدادها(Events). کلاس پایه ایترین مفهوم در برنامه نویسی شی گراست. ساختار کلی تعربف کلاس در #F به صورت زیر است:
type [access-modifier] type-name [type-params] [access-modifier] ( parameter-list ) [ as identifier ] = [ class ] [ inherit base-type-name(base-constructor-args) ] [ let-bindings ] [ do-bindings ] member-list ... [ end ] type [access-modifier] type-name1 ... and [access-modifier] type-name2 ... ...
انواع access-modifier در #F
- public : دسترسی برای تمام فراخوانها امکان پذیر است
- internal : دسترسی برای تمام فراخوان هایی که در همین assembly هستند امکان پذیر است
- private : دسترسی فقط برای فراخوانهای موجود در همین ماژول امکان پذیر است
نکته : protected access modifier در #F پشتیبانی نمیشود.
مثالی از تعریف کلاس:
type Account(number : int, name : string) = class let mutable amount = 0m end
let myAccount = new Account(123456, "Masoud")
برای تعریف خاصیت در #F باید از کلمه کلیدی member استفاده کنید. در مثال بعدی برای کلاس بالا تابع و خاصیت تعریف خواهیم کرد.
type Account(number : int, name: string) = class let mutable amount = 0m member x.Number = number member x.Name= name member x.Amount = amount member x.Deposit(value) = amount <- amount + value member x.Withdraw(value) = amount <- amount - value end
open System type Account(number : int, name: string) = class let mutable amount = 0m member x.Number = number member x.Name= name member x.Amount = amount member x.Deposit(value) = amount <- amount + value member x.Withdraw(value) = amount <- amount - value end
let masoud= new Account(12345, "Masoud") let saeed = new Account(67890, "Saeed") let transfer amount (source : Account) (target : Account) = source.Withdraw amount target.Deposit amount let printAccount (x : Account) = printfn "x.Number: %i, x.Name: %s, x.Amount: %M" x.Number x.Name x.Amount let main() = let printAccounts() = [masoud; saeed] |> Seq.iter printAccount printfn "\nInializing account" homer.Deposit 50M marge.Deposit 100M printAccounts() printfn "\nTransferring $30 from Masoud to Saeed" transfer 30M masoud saeed
printAccounts() printfn "\nTransferring $75 from Saeed to Masoud" transfer 75M saeed masoud printAccounts() main()
در #F زمانی که قصد داشته باشیم در بعد از وهله سازی از کلاس و فراخوانی سازنده، عملیات خاصی انجام شود(مثل انجام برخی عملیات متداول در سازندههای کلاسهای دات نت) باید از کلمه کلیدی do به همراه یک بلاک از کد استفاده کنیم.
open System open System.Net type Stock(symbol : string) = class let mutable _symbol = String.Empty do //کد مورد نظر در این جا نوشته میشود end
open System type MyType(a:int, b:int) as this = inherit Object() let x = 2*a let y = 2*b do printfn "Initializing object %d %d %d %d %d %d" a b x y (this.Prop1) (this.Prop2) static do printfn "Initializing MyType." member this.Prop1 = 4*x member this.Prop2 = 4*y override this.ToString() = System.String.Format("{0} {1}", this.Prop1, this.Prop2) let obj1 = new MyType(1, 2)
خروجی مثال بالا:
Initializing MyType. Initializing object 1 2 2 4 8 16
برای تعریف خواص به صورت استاتیک مانند #C از کلمه کلیدی static استفاده کنید.مثالی در این زمینه:
type SomeClass(prop : int) = class member x.Prop = prop static member SomeStaticMethod = "This is a static method" end
let instance = new SomeClass(5);; instance.SomeStaticMethod;; output: stdin(81,1): error FS0191: property 'SomeStaticMethod' is static.
SomeClass.SomeStaticMethod;; (* invoking static method *)
همانند #C و سایر زبانهای دات نت امکان تعریف متدهای get و set برای خاصیتهای یک کلاس وجود دارد.
ساختار کلی:
member alias.PropertyName with get() = some-value and set(value) = some-assignment
type MyClass() = class let mutable num = 0 member x.Num with get() = num and set(value) = num <- value end;;
public int Num { get{return num;} set{num=value;} }
type MyClass() = class let mutable num = 0 member x.Num with get() = num and set(value) = if value > 10 || value < 0 then raise (new Exception("Values must be between 0 and 10")) else num <- value end
Interface ها
اینترفیس به تمامی خواص و توابع عمومی اشئایی که آن را پیاده سازی کرده اند اشاره میکند. (توضیحات بیشتر (^ ) و (^ ))ساختار کلی برای تعریف آن به صورت زیر است:
type type-name = interface inherits-decl member-defns end
type IPrintable = abstract member Print : unit -> unit
نکته: در هنگام تعریف توابع و خاصیت در interfaceها باید از کلمه abstract استفاده کنیم. هر کلاسی که از یک یا چند تا اینترفیس ارث ببرد باید تمام خواص و توابع اینتریسها را پیاده سازی کند. در مثال بعدی کلاس SomeClass1 اینترفیس بالا را پیاده سازی میکند. دقت کنید که کلمه this توسط من به عنوان اشاره گر به اشیای کلاس تعیین شده و شما میتونید از هر کلمه یا حرف دیگری استفاده کنید.
type SomeClass1(x: int, y: float) = interface IPrintable with member this.Print() = printfn "%d %f" x y
روش نادرست:
let instance = new SomeClass1(10,20) instance.Print//فراخوانی این متد باعث ایجاد خطای کامپایلری میشود.
let instance = new SomeClass1(10,20) let instanceCast = instance :> IPrintable// استفاده از (<:) برای عملیات تبدیل کلاس به اینترفیس instanceCast.Print
در مثال بعدی کلاسی خواهیم داشت که از سه اینترفیس ارث میبرد. در نتیجه باید تمام متدهای هر سه اینترفیس را پیاده سازی کند.
type Interface1 = abstract member Method1 : int -> int type Interface2 = abstract member Method2 : int -> int type Interface3 = inherit Interface1 inherit Interface2 abstract member Method3 : int -> int type MyClass() = interface Interface3 with member this.Method1(n) = 2 * n member this.Method2(n) = n + 100 member this.Method3(n) = n / 10
let instance = new MyClass() let instanceToCast = instance :> Interface3 instanceToCast.Method3 10
#F از کلاسهای abstract هم پشتیبانی میکند. اگر با کلاسهای abstract در #C آشنایی ندارید میتونید مطالب مورد نظر رو در (^ ) و (^ ) مطالعه کنید. به صورت خلاصه کلاسهای abstract به عنوان کلاسهای پایه در برنامه نویسی شی گرا استفاده میشوند. این کلاسها دارای خواص و متدهای پیاده سازی شده و نشده هستند. خواص و متد هایی که در کلاس پایه abstract پیاده سازی نشده اند باید توسط کلاس هایی که از این کلاس پایه ارث میبرند حتما پیاده سازی شوند.
ساختار کلی تعریف کلاسهای abstract:
[<AbstractClass>] type [ accessibility-modifier ] abstract-class-name = [ inherit base-class-or-interface-name ] [ abstract-member-declarations-and-member-definitions ] abstract member member-name : type-signature
[<AbstractClass>] type Shape(x0 : float, y0 : float) = let mutable x, y = x0, y0 let mutable rotAngle = 0.0 abstract Area : float with get abstract Perimeter : float with get abstract Name : string with get
#1 کلاس اول
type Square(x, y,SideLength) = inherit Shape(x, y)
override this.Area = this.SideLength * this.SideLength override this.Perimeter = this.SideLength * 4. override this.Name = "Square"
type Circle(x, y, radius) = inherit Shape(x, y)
let PI = 3.141592654 member this.Radius = radius override this.Area = PI * this.Radius * this.Radius override this.Perimeter = 2. * PI * this.Radius
structureها در #F دقیقا معال struct در #C هستند. توضیحات بیشتر درباره struct در #C (^ ) و (^ )). اما به طور خلاصه باید ذکر کنم که strucureها تقریبا دارای مفهوم کلاس هستند با اندکی تفاوت که شامل موارد زیر است:
- structureها از نوع مقداری هستند و این بدین معنی است مستقیما درون پشته ذخیره میشوند.
- ارجاع به structureها از نوع ارجاع با مقدار است بر خلاف کلاسها که از نوع ارجاع به منبع هستند.(^ )
- structureها دارای خواص ارث بری نیستند.
- عموما از structure برای ذخیره مجموعه ای از دادهها با حجم و اندازه کم استفاده میشود.
ساختار کلی تعریف structure
[ attributes ] type [accessibility-modifier] type-name = struct type-definition-elements end //یا به صورت زیر [ attributes ] [<StructAttribute>] type [accessibility-modifier] type-name = type-definition-elements
type Point3D = struct val x: float val y: float val z: float end
type Point2D = struct val X: float val Y: float new(x: float, y: float) = { X = x; Y = y } end
در پایان یک مثال مشترک رو در #C و #F پیاده سازی میکنیم:
در مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت چهارم - اعتبارسنجی ورودیها» مشاهده کردیم که Angular در روش فرمهای مبتنی بر قالبها، تنها از 4 روش بومی اعتبارسنجی مرورگرها مانند ذکر ویژگی required برای فیلدهای اجباری، ویژگیهای minlength و maxlength برای تعیین حداقل و حداکثر تعداد حروف مجاز قابل ورود در یک فیلد و از pattern برای کار با عبارات با قاعده پشتیبانی میکند. برای بهبود این وضعیت در این مطلب قصد داریم روش تهیه اعتبارسنجهای سفارشی مخصوص حالت فرمهای مبتنی بر قالبها را بررسی کنیم.
تدارک مقدمات مثال این قسمت
این مثال، در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه میکنیم:
همچنین به فایل app.module.ts مراجعه کرده و CustomValidatorsModule را بجای CustomValidatorsRoutingModule در قسمت imports معرفی میکنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت نام یک کاربر را اضافه خواهیم کرد:
که اینکار سبب به روز رسانی فایل custom-validators.module.ts و افزوده شدن UserRegisterComponent به قسمت declarations آن میشود.
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف میکنیم:
با این محتوا:
در طراحی فرم HTML ایی آن نیاز است این موارد رعایت شوند:
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمهی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمهی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.
تعریف اعتبارسنج سفارشی ایمیلها
هرچند میتوان اعتبارسنجی ایمیلها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوهی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه میدهیم:
این دستور علاوه بر ایجاد فایل جدید email-validator.directive.ts و تکمیل ساختار ابتدایی آن، کار به روز رسانی custom-validators.module.ts را نیز انجام میدهد. در این حالت به صورت خودکار قسمت declarations این ماژول با EmailValidatorDirective مقدار دهی میشود.
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده میکنید:
توضیحات تکمیلی:
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آنرا همانند ویژگیهای HTML، به فیلد ورودی اضافه کرد:
- روش تعریف selector آن اندکی متفاوت است:
در اینجا مطابق https://angular.io/guide/styleguide#style-02-08 توصیه شدهاست که:
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شدهاست:
این مساله در جهت مشخص کردن سفارشی بودن این دایرکتیو و همچنین کاهش احتمال تکرار نامها توصیه شدهاست.
ب) در اینجا formControlName، formControl و ngModel قید شدهی در کنار نام selector این دایرکتیو را نیز مشاهده میکنید. وجود آنها به این معنا است که کلاس این دایرکتیو، به المانهایی که به آنها ویژگی appEmailValidator اضافه شدهاست و همچنین آن المانها از یکی از سه نوع ذکر شده هستند، اعمال میشود و در سایر موارد بیاثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا میتوان نوشت:
- پس از آن قسمت providers را مشاهده میکنید:
کار قسمت multi آن این است که EmailValidatorDirective (یا همان کلاس جاری) را به لیست NG_VALIDATORS توکار (اعتبارسنجهای توکار مبتنی بر قالبها) اضافه میکند و سبب بازنویسی هیچ موردی نخواهد شد. بنابراین وجود این قسمت در جهت تکمیل تامین کنندههای توکار Angular ضروری است.
- سپس پیاده سازی اینترفیس توکار Validator را مشاهده میکنید:
این اینترفیس جزو مجموعهی فرمهای مبتنی بر قالبها است و از آن جهت نوشتن اعتبارسنجهای سفارشی میتوان استفاده کرد.
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
کار این متد این است که المانی را که appEmailValidator به آن اعمال شدهاست، به عنوان پارامتر متد validate در اختیار کلاس جاری قرار میدهد. به این ترتیب میتوان برای مثال به مقدار آن دسترسی یافت و سپس منطق سفارشی را پیاده سازی و یک خروجی key/value را بازگشت داد.
برای مثال در اینجا مقدار فیلد ایمیل element.value توسط عبارت باقاعدهی نوشته شده بررسی میشود. اگر با این الگو انطباق داشته باشد، نال بازگشت داده میشود (اعلام عدم وجود مشکلی در اعتبارسنجی) و اگر خیر، یک شیء key/value دلخواه را میتوان بازگشت داد.
- اکنون که این دایرکتیو جدید طراحی و ثبت شدهاست (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آنرا به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آنرا به صورت خودکار مدیریت کند:
نحوهی طراحی خروجی متد validate
هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشدهاست و همینقدر که این خروجی یک شیء key/value باشد، کفایت میکند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
و یا اگر اعتبارسنج استاندارد minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه میدهد:
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد؛ برای نمونه در اینجا appEmailValidator (یک کلید و نام دلخواه است و هیچ الزامی ندارد که با نام selector این دایرکتیو یکی باشد)، به true تنظیم شدهاست:
بنابراین شرط تامین نوع خروجی، برقرار است. علت true بودن آن نیز مورد ذیل است:
در اینجا اگر false را بازگشت دهیم، هرچند email.errors دارای کلید جدید appEmailValidator شدهاست، اما ngIf سبب رندر خطای اعتبارسنجی «ایمیل وارد شده معتبر نیست.» به علت false بودن نتیجهی نهایی، نمیشود. یا حتی میتوان بجای true یک رشته و یا یک شیء با توضیحات بیشتری را نیز تنظیم کرد؛ چون value این key/value به any تنظیم شدهاست و هر چیزی را میپذیرد.
از دیدگاه اعتبارسنج فرمهای مبتنی بر قالبها، همینقدر که آرایهی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیدهاست و اعتبارسنجی المان را شکست خورده ارزیابی میکند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه میدهد (بر اساس نیازهای نمایشی برنامه).
تهیه اعتبارسنج سفارشی مقایسهی کلمات عبور با یکدیگر
در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمهی عبور و تائید کلمهی عبور را مشاهده میکنید:
Angular به همراه اعتبارسنج توکاری برای بررسی یکی بودن این دو نیست. به همین جهت نمونهی سفارشی آنرا همانند EmailValidatorDirective فوق تهیه میکنیم. ابتدا یک دایرکتیو جدید را به نام EqualValidator به ماژول custom-validators اضافه میکنیم:
که سبب ایجاد فایل جدید equal-validator.directive.ts و به روز رسانی قسمت declarations فایل custom-validators.module.ts با EqualValidatorDirective نیز میشود.
در ادامه کدهای کامل آنرا در ذیل مشاهده میکنید:
توضیحات تکمیلی:
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شدهاست تا بتوان selector آنرا به المانهای HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شدهاست تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شدهاست؛ اما چون در اینجا میخواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کنندهی مقدار آنرا نیز افزودهایم:
به این ترتیب زمانیکه قرار است فیلد کلمهی عبور را تعریف کنیم، ابتدا ویژگی appValidateEqual یا همان selector این اعتبارسنج به آن اضافه شدهاست تا کار فعال سازی ابتدایی صورت گیرد:
سپس Attribute یا ویژگی به نام compare-to نیز تعریف شدهاست. این compare-to همان نامی است که به Attribute@ نسبت داده شدهاست. سپس مقداری که به این ویژگی نسبت داده میشود، توسط خاصیت compareToControl دریافت خواهد شد.
در اینجا محدودیتی هم از لحاظ تعداد ویژگیها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند میتوان ویژگیهای بیشتری را به سازندهی آن نسبت داد.
یک نکته: میتوان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج میگردد.
- در ابتدای این اعتبارسنج، نحوهی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده میکنید:
در اینجا element.value مقدار المان یا کنترل HTML جاری است که appValidateEqual به آن اعمال شدهاست.
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت میشود، میتوان به کنترل دوم، توسط element.root.get دسترسی یافت.
- در ادامهی کار، مقایسهی سادهای را مشاهده میکنید:
اگر کنترل دوم یافت شد و همچنین مقدار آن با مقدار کنترل جاری یکی نبود، همان شیء key/value مورد انتظار متد validate، در جهت اعلام شکست اعتبارسنجی بازگشت داده میشود.
- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفتهاست:
اعتبارسنج تعریف شده، فقط به کنترلی که هم اکنون در حال کار با آن هستیم اعمال میشود. اگر پیشتر کلمهی عبوری را وارد کرده باشیم و سپس به فیلد تائید آن مراجعه کنیم، وضعیت اعتبارسنجی فیلد کلمهی عبور قبلی به حالت غیرمعتبر تنظیم شدهاست. اما پس از تکمیل فیلد تائید کلمهی عبور، هرچند وضعیت فیلد جاری معتبر است، اما هنوز وضعیت فیلد قبلی غیرمعتبر میباشد. برای رفع این مشکل، ابتدا کلید دلخواه appValidateEqual را از آن حذف میکنیم (همان کلیدی است که پیشتر در صورت مساوی نبودن مقدار فیلدها بازگشت داده شدهاست). حذف این کلید سبب نال شدن آرایهی errors یک شیء نمیشود و همانطور که پیشتر عنوان شد، Angular تنها به همین مورد توجه میکند. بنابراین در ادامه کار، setErrors یا تنظیم آرایهی errors به نال هم انجام شدهاست. در اینجا است که Angular فیلد دوم را نیز معتبر ارزیابی خواهد کرد.
تکمیل کامپوننت فرم ثبت نام کاربران
اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا میکند:
در اینجا تنها کار مهمی که انجام شدهاست، ارائهی خاصیت عمومی مدل، جهت استفادهی از آن در قالب HTML ایی این کامپوننت است. بنابراین به فایل user-register.component.html مراجعه کرده و آنرا نیز به صورت ذیل تکمیل میکنیم:
ابتدای فرم
در اینجا novalidate اضافه شدهاست تا اعتبارسنجی توکار مرورگرها با اعتبارسنجی سفارشی فرم جاری تداخل پیدا نکند. همچنین توسط یک template reference variable به وهلهای از فرم دسترسی یافته و آنرا به متد submitForm کامپوننت ارسال کردهایم.
تکمیل قسمت ورود نام کاربری
اعتبارسنجی فیلد نام کاربری شامل سه قسمت بررسی errors.required، errors.minlength و errors.maxlength است.
تکمیل قسمت ورود ایمیل
در اینجا نحوهی استفادهی از دایرکتیو جدید appEmailValidator را ملاحظه میکنید. این دایرکتیو ابتدا به المان فوق متصل و سپس نتیجهی آن در قسمت ngIf، برای نمایش خطای متناظری بررسی شدهاست.
تکمیل قسمتهای ورود کلمهی عبور و تائید آن
در اینجا نحوهی اعمال دایرکتیو جدید appValidateEqual و همچنین ویژگی compare-to آنرا به فیلدهای کلمهی عبور و تائید آن مشاهده میکنید.
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شدهاست و سبب نمایش خطای اعتبارسنجی متناسبی میشود.
تکمیل انتهای فرم
در اینجا بررسی میشود که آیا فرم معتبر است یا خیر. اگر خیر، دکمهی submit آن غیرفعال میشود و برعکس.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
و در دومی دستورات ذیل را اجرا کنید:
اکنون میتوانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
تدارک مقدمات مثال این قسمت
این مثال، در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه میکنیم:
>ng g m CustomValidators -m app.module --routing
>ng g c CustomValidators/user-register
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف میکنیم:
>ng g cl CustomValidators/user
export class User { constructor( public username: string = "", public email: string = "", public password: string = "", public confirmPassword: string = "" ) {} }
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمهی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمهی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.
تعریف اعتبارسنج سفارشی ایمیلها
هرچند میتوان اعتبارسنجی ایمیلها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوهی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه میدهیم:
>ng g d CustomValidators/EmailValidator -m custom-validators.module
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده میکنید:
import { Directive } from "@angular/core"; import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms"; @Directive({ selector: "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]", providers: [ { provide: NG_VALIDATORS, useExisting: EmailValidatorDirective, multi: true } ] }) export class EmailValidatorDirective implements Validator { validate(element: AbstractControl): { [key: string]: any } { const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; const valid = emailRegex.test(element.value); return valid ? null : { appEmailValidator: true }; } }
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آنرا همانند ویژگیهای HTML، به فیلد ورودی اضافه کرد:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email">
- روش تعریف selector آن اندکی متفاوت است:
selector: "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شدهاست:
"apps": [ { // ... "prefix": "app",
ب) در اینجا formControlName، formControl و ngModel قید شدهی در کنار نام selector این دایرکتیو را نیز مشاهده میکنید. وجود آنها به این معنا است که کلاس این دایرکتیو، به المانهایی که به آنها ویژگی appEmailValidator اضافه شدهاست و همچنین آن المانها از یکی از سه نوع ذکر شده هستند، اعمال میشود و در سایر موارد بیاثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا میتوان نوشت:
selector: "[appEmailValidator]"
- پس از آن قسمت providers را مشاهده میکنید:
providers: [ { provide: NG_VALIDATORS, useExisting: EmailValidatorDirective, multi: true }
- سپس پیاده سازی اینترفیس توکار Validator را مشاهده میکنید:
export class EmailValidatorDirective implements Validator {
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
validate(element: AbstractControl): { [key: string]: any }
validate(element: AbstractControl): { [key: string]: any } { const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; const valid = emailRegex.test(element.value); return valid ? null : { appEmailValidator: true }; }
- اکنون که این دایرکتیو جدید طراحی و ثبت شدهاست (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آنرا به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آنرا به صورت خودکار مدیریت کند:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email">
نحوهی طراحی خروجی متد validate
هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشدهاست و همینقدر که این خروجی یک شیء key/value باشد، کفایت میکند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
{ required:true }
{ minlength : { requiredLength : 3, actualLength : 1 } }
{ appEmailValidator: true }
<div class="alert alert-danger" *ngIf="email.errors.appEmailValidator"> The entered email is not valid. </div>
از دیدگاه اعتبارسنج فرمهای مبتنی بر قالبها، همینقدر که آرایهی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیدهاست و اعتبارسنجی المان را شکست خورده ارزیابی میکند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه میدهد (بر اساس نیازهای نمایشی برنامه).
تهیه اعتبارسنج سفارشی مقایسهی کلمات عبور با یکدیگر
در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمهی عبور و تائید کلمهی عبور را مشاهده میکنید:
public password: string = "", public confirmPassword: string = ""
>ng g d CustomValidators/EqualValidator -m custom-validators.module
در ادامه کدهای کامل آنرا در ذیل مشاهده میکنید:
import { Directive, Attribute } from "@angular/core"; import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms"; @Directive({ selector: "[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]", providers: [ { provide: NG_VALIDATORS, useExisting: EqualValidatorDirective, multi: true } ] }) export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {} validate(element: AbstractControl): { [key: string]: any } { const selfValue = element.value; const otherControl = element.root.get(this.compareToControl); console.log("EqualValidatorDirective", { thisControlValue: selfValue, otherControlValue: otherControl ? otherControl.value : null }); if (otherControl && selfValue !== otherControl.value) { return { appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject. }; } if ( otherControl && otherControl.errors && selfValue === otherControl.value ) { delete otherControl.errors["appValidateEqual"]; if (!Object.keys(otherControl.errors).length) { otherControl.setErrors(null); } } return null; } }
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شدهاست تا بتوان selector آنرا به المانهای HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شدهاست تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شدهاست؛ اما چون در اینجا میخواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کنندهی مقدار آنرا نیز افزودهایم:
export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {}
<input #password="ngModel" required type="password" class="form-control" appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
در اینجا محدودیتی هم از لحاظ تعداد ویژگیها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند میتوان ویژگیهای بیشتری را به سازندهی آن نسبت داد.
یک نکته: میتوان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج میگردد.
- در ابتدای این اعتبارسنج، نحوهی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده میکنید:
export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {} validate(element: AbstractControl): { [key: string]: any } { const selfValue = element.value; const otherControl = element.root.get(this.compareToControl); console.log("EqualValidatorDirective", { thisControlValue: selfValue, otherControlValue: otherControl ? otherControl.value : null });
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت میشود، میتوان به کنترل دوم، توسط element.root.get دسترسی یافت.
- در ادامهی کار، مقایسهی سادهای را مشاهده میکنید:
if (otherControl && selfValue !== otherControl.value) { return { appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject. }; }
- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفتهاست:
if (otherControl && otherControl.errors && selfValue === otherControl.value) { delete otherControl.errors["appValidateEqual"]; if (!Object.keys(otherControl.errors).length) { otherControl.setErrors(null); } } return null;
تکمیل کامپوننت فرم ثبت نام کاربران
اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا میکند:
import { NgForm } from "@angular/forms"; import { User } from "./../user"; import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-user-register", templateUrl: "./user-register.component.html", styleUrls: ["./user-register.component.css"] }) export class UserRegisterComponent implements OnInit { model = new User(); constructor() {} ngOnInit() {} submitForm(form: NgForm) { console.log(this.model); console.log(form.value); } }
ابتدای فرم
<div class="container"> <h3>Registration Form</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate>
تکمیل قسمت ورود نام کاربری
<div class="form-group" [class.has-error]="username.invalid && username.touched"> <label class="control-label">User Name</label> <input #username="ngModel" required maxlength="8" minlength="4" type="text" class="form-control" name="username" [(ngModel)]="model.username"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-info"> errors: {{ username.errors | json }} </div> <div class="alert alert-danger" *ngIf="username.errors.required"> username is required. </div> <div class="alert alert-danger" *ngIf="username.errors.minlength"> username should be minimum {{username.errors.minlength.requiredLength}} characters. </div> <div class="alert alert-danger" *ngIf="username.errors.maxlength"> username should be max {{username.errors.maxlength.requiredLength}} characters. </div> </div> </div>
تکمیل قسمت ورود ایمیل
<div class="form-group" [class.has-error]="email.invalid && email.touched"> <label class="control-label">Email</label> <input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email"> <div *ngIf="email.invalid && email.touched"> <div class="alert alert-info"> errors: {{ email.errors | json }} </div> <div class="alert alert-danger" *ngIf="email.errors.required"> email is required. </div> <div class="alert alert-danger" *ngIf="email.errors.appEmailValidator"> The entered email is not valid. </div> </div> </div>
تکمیل قسمتهای ورود کلمهی عبور و تائید آن
<div class="form-group" [class.has-error]="password.invalid && password.touched"> <label class="control-label">Password</label> <input #password="ngModel" required type="password" class="form-control" appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-info"> errors: {{ password.errors | json }} </div> <div class="alert alert-danger" *ngIf="password.errors.required"> password is required. </div> <div class="alert alert-danger" *ngIf="password.errors.appValidateEqual"> Password mismatch. Please complete the confirmPassword . </div> </div> </div> <div class="form-group" [class.has-error]="confirmPassword.invalid && confirmPassword.touched"> <label class="control-label">Retype password</label> <input #confirmPassword="ngModel" required type="password" class="form-control" appValidateEqual compare-to="password" name="confirmPassword" [(ngModel)]="model.confirmPassword"> <div *ngIf="confirmPassword.invalid && confirmPassword.touched"> <div class="alert alert-info"> errors: {{ confirmPassword.errors | json }} </div> <div class="alert alert-danger" *ngIf="confirmPassword.errors.required"> confirmPassword is required. </div> <div class="alert alert-danger" *ngIf="confirmPassword.errors.appValidateEqual"> Password mismatch. </div> </div> </div>
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شدهاست و سبب نمایش خطای اعتبارسنجی متناسبی میشود.
تکمیل انتهای فرم
<button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button> </form> </div>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
مثال زیر را به عنوانی نمونهای از کاربرد LINQ to XML برای خواندن فیدهای RSS که اساسا به فرمت XML هستند میتوان ارائه داد.
ابتدا کد کامل مثال را در نظر بگیرید:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace LinqToRSS
{
public static class LanguageExtender
{
public static string SafeValue(this XElement input)
{
return (input == null) ? string.Empty : input.Value;
}
public static DateTime SafeDateValue(this XElement input)
{
return (input == null) ? DateTime.MinValue : DateTime.Parse(input.Value);
}
}
public class RssEntry
{
public string Title { set; get; }
public string Description { set; get; }
public string Link { set; get; }
public DateTime PublicationDate { set; get; }
public string Author { set; get; }
public string BlogName { set; get; }
public string BlogAddress { set; get; }
}
public class Rss
{
static XElement selectDate(XElement date1, XElement date2)
{
return date1 ?? date2;
}
public static List<RssEntry> GetEntries(string feedUrl)
{
//applying namespace in an XElement
XName xn = XName.Get("{http://purl.org/dc/elements/1.1/}creator");//{namespace}root
XName xn2 = XName.Get("{http://purl.org/dc/elements/1.1/}date");
var feed = XDocument.Load(feedUrl);
if (feed.Root == null) return null;
var items = feed.Root.Element("channel").Elements("item");
var feedQuery =
from item in items
select new RssEntry
{
Title = item.Element("title").SafeValue(),
Description = item.Element("description").SafeValue(),
Link = item.Element("link").SafeValue(),
PublicationDate =
selectDate(item.Element(xn2), item.Element("pubDate")).SafeDateValue(),
Author = item.Element(xn).SafeValue(),
BlogName = item.Parent.Element("title").SafeValue(),
BlogAddress = item.Parent.Element("link").SafeValue()
};
return feedQuery.ToList();
}
}
class Program
{
static void Main(string[] args)
{
List<RssEntry> entries = Rss.GetEntries("http://weblogs.asp.net/aspnet-team/rss.aspx");
if (entries != null)
foreach (var item in entries)
Console.WriteLine(item.Title);
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
توضیحات:
1- در این مثال فقط جهت سهولت بیان آن در یک صفحه، تمامی کلاسهای تعریف شده در یک فایل آورده شدند. این روش صحیح نیست و باید به ازای هر کلاس یک فایل جدا در نظر گرفته شود.
2- کلاس LanguageExtender از قابلیت extension methods سی شارپ 3 استفاده میکند. به این صورت کلاس XElement دات نت بسط یافته و دو متد به آن اضافه میشود که به سادگی در کدهای خود میتوان از آنها استفاده کرد. هدف آن هم بررسی نال بودن یک آیتم دریافتی و ارائهی حاصلی امن برای این مورد است.
3- کلاس RssEntry به جهت استفاده در خروجی کوئری LINQ تعریف شد. میخواهیم خروجی نهایی، یک لیست جنریک از نوع RssEntry باشد.
4- متد اصلی برنامه، GetEntries است. این متد آدرس اینترنتی یک فید را دریافت کرده و پس از آنالیز، آنرا به صورت یک لیست بر میگرداند.
<?xml version="1.0" encoding="UTF-8" ?>
<?xml-stylesheet type="text/xsl" href="http://weblogs.asp.net/utility/FeedStylesheets/rss.xsl" media="screen"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:wfw="http://wellformedweb.org/CommentAPI/">
<channel>
<title>Latest Microsoft Blogs</title>
<link>http://weblogs.asp.net/aspnet-team/default.aspx</link>
<description />
<dc:language>en</dc:language>
<generator>CommunityServer 2007 SP1 (Build: 20510.895)</generator>
<item>
<title>Comments on my recent benchmarks.</title>
<link>http://misfitgeek.com/blog/aspnet/comments-on-my-recent-benchmarks/</link>
<pubDate>Mon, 10 Aug 2009 23:33:59 GMT</pubDate>
<guid isPermaLink="false">c06e2b9d-981a-45b4-a55f-ab0d8bbfdc1c:7166225</guid>
<dc:creator>Misfit Geek: msft</dc:creator>
<slash:comments>0</slash:comments>
<wfw:commentRss xmlns:wfw="http://wellformedweb.org/CommentAPI/">http://weblogs.asp.net/aspnet-team/rsscomments.aspx?PostID=7166225</wfw:commentRss>
<comments>http://misfitgeek.com/blog/aspnet/comments-on-my-recent-benchmarks/#comments</comments>
<description>Overall I’ve been pretty impressed ...</description>
<category domain="http://weblogs.asp.net/aspnet-team/archive/tags/ASP.NET/default.aspx">ASP.NET</category>
</item>
</channel>
</rss>
var items = feed.Root.Element("channel").Elements("item");
XName.Get("{mynamespace}root");
//or
XName.Get("root", "mynamespace");
اگر SQL Server و MySQL بر روی سیستم شما نصب است، روشی ساده برای انتقال اطلاعات بین این دو وجود دارد که نیازی به دخالت هیچ نوع برنامهی جانبی نداشته و با امکانات موجود قابل مدیریت است.
ایجاد یک Linked server
برای اینکه SQL Server را به MySQL متصل کنیم میتوان بین این دو یک Linked server تعریف کرد و سپس دسترسی به بانکهای اطلاعاتی MySQL همانند یک بانک اطلاعاتی محلی SQL Server خواهد شد که شرح آن در ادامه ذکر میشود.
ابتدا نیاز است تا درایور ODBC مربوط به MySQL دریافت و نصب شود. آنرا میتوانید از اینجا دریافت کنید : (+)
سپس management studio را گشوده و در قسمت Server objects ، بر روی گزینهی Linked servers کلیک راست نمائید. از منوی ظاهر شده، گزینهی New linked server را انتخاب کنید:
در ادامه، باید تنظیمات زیر را در صفحهی باز شده وارد کرد:
در قسمت Linked server و Product name ، نام دلخواهی را وارد کنید.
Provider انتخابی باید از نوع Microsoft OLE DB Provider for ODBC Drivers باشد.
مهمترین تنظیم آن، قسمت Provider string است که باید به صورت زیر وارد شود (در غیر اینصورت کار نمیکند):
DRIVER={MySQL ODBC 5.1 Driver}; SERVER=localhost; DATABASE=testdb; USER=root; PASSWORD=mypass; OPTION=3;PORT=3306; CharSet=UTF8;
پس از انجام این تنظیمات بر روی دکمهی Ok کلیک کنید تا Linked server ساخته شود:
اگر لیست بانکهای اطلاعاتی را مشاهده نمودید، یعنی اتصال به درستی برقرار شده است.
تنظیمات ثانویه:
تا اینجا اس کیوال سرور به MySQL متصل شده است، اما برای استفاده بهینه از امکانات موجود نیاز است تا یک سری تغییرات دیگر را هم اعمال کرد.
تنظیم MSDASQL Provider :
در همان قسمت Linked provider ، ذیل قسمت Providers ، گزینهی MSDASQL را انتخاب کرده و بر روی آن کلیک راست نمائید. سپس صفحهی خواص آنرا انتخاب کنید تا بتوان تنظیمات زیر را به آن اعمال کرد. این پروایدر جهت اتصال به MySQL مورد استفاده قرار میگیرد.
فعال سازی RPC :
برای اینکه بتوان از طریق SQL Server رکوردی را در یکی از جداول بانکهای اطلاعاتی MySQL متصل شده ثبت نمود، میتوان از دستور زیر استفاده کرد:
EXECUTE('insert into testdb.testtable(f1,f1) values(1,''data'')') at mysql
اینجا testdb نام بانک اطلاعاتی اتصالی MySQL است و testTable هم نام جدول مورد نظر. MySQL ایی که در آخر عبارت ذکر شده همان نام linked server ایی است که پیشتر تعریف کردیم.
به محض سعی در اجرای این کوئری خطای زیر ظاهر میشود:
Server 'mysql' is not configured for RPC.
برای رفع این مشکل، مجددا به صفحهی خواص همان liked server ایجاد شده مراجعه کنید. در قسمت Server options دو گزینه مرتبط به RPC باید فعال شوند:
و اکنون برای کوئری گرفتن از اطلاعات ثبت شده هم از عبارت زیر میتوان استفاده کرد:
SELECT * FROM OPENQUERY(mysql, 'SELECT * FROM testdb.testtable')
در این کوئری، MySQL نام Linked server ثبت شده است و testdb هم یکی از بانکهای اطلاعاتی MySQL مورد نظر.
انتقال تمام اطلاعات یک جدول از بانک اطلاعاتی MySQL به SQL Server
پس از برقراری اتصال، اکنون import کامل یک جدول MySQL به SQL Server به سادگی اجرای کوئری زیر میباشد:
SELECT * INTO MyDb.dbo.testtable FROM openquery(MYSQL, 'SELECT * FROM testdb.testtable')
در این کوئری، MySQL همان Linked server تعریف شده است. MyDB نام بانک اطلاعاتی موجود در SQL Server جاری است و testtable هم جدولی است که قرار است اطلاعات testdb.testtable بانک اطلاعاتی MySQL به آن وارد شود.
با اطلاعات فارسی هم (در سمت SQL Server) مشکلی ندارد. همانطور که مشخص است، در اطلاعات provider string ذکر شده، مقدار charset به utf8 تنظیم شده و همچنین اگر نوع collation فیلدهای تعریف شده در MySQL نیز به utf8_persian_ci تنظیم شده باشد، با مشکل ثبت اطلاعات فارسی به صورت ???? مواجه نخواهید شد.
نکته:
اگر بانک اطلاعاتی MySQL شما بر روی local host نصب نیست، جهت فعال سازی دسترسی ریموت به آن، میتوان به یکی از نکات زیر مراجعه کرد و سپس این اطلاعات جدید باید در همان قسمت provider string مرتبط با تعریف linked server وارد شوند:
مطالب مشابه:
هیچکدام از این روشها قابل استفاده نبودند چون provider string صحیحی را نهایتا تولید نمیکنند. همچنین تمام این روشها مبتنی است بر ایجاد DSN در کنترل پنل که اصلا نیازی به آن نیست و اضافی است.
درحال حاضر، باتوجه به خرده نداشتن مقادیر پولی در ایران، عموما از نوعهای 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