اشتراکها
مطالب
پیاده سازی 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 را بررسی میکنیم.
UI-Router ابزاری برای مسیریابی در AngularJS است که این امکان را برایتان فراهم میکند تا بخشهای برنامه رابط کاربریتان را به شکل یک ماشین حالت ساماندهی کنید. برخلاف سرویس route$ که بر اساس مسیریابی URLها ساماندهی شده و کار میکند، UI-Router بر اساس حالتها کار میکند، که این حالتها میتوانند در صورت لزوم مسیریابی هم داشته باشند.
UI-Router یکی از افزونههای مجموعه Angular-ui، و پاراگراف بالا معرفی آن در صفحه خانگیش است (تقریبا!). این افزونه جزئیات مفصلی دارد و در این مطلب تنها به معرفی آن خواهم پرداخت (بر اساس مطالب صفحه خانگیش). پیش از ادامه پیشنهاد میکنم اگر مطالب زیر را نخواندهاید ابتدا آنها را مرور کنید:
برای استفاده از UI-Router باید:
- فایل جاوا اسکریپت آن را دانلود کنید (released یا minified).
- در صفحه اصلی برنامهتان پس از include کردن فایل اصلی AngularJS فایل angular-ui-router.js (یا angular-ui-router.min.js) را include کنید.
- 'ui.router' را به لیست وابستگیهای ماژول اصلی اضافه کنید.
<!doctype html> <html ng-app="myApp"> <head> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script> <script src="js/angular-ui-router.min.js"></script> <script> var myApp = angular.module('myApp', ['ui.router']); // For Component users, it should look like this: // var myApp = angular.module('myApp', [require('angular-ui-router')]); </script> ... </head> <body> ... </body> </html>
حالتها و viewهای تو در تو
قابلیت اصلی UI-Router امکان تعریف حالتها و vieweهای تو در تو است. در مطلب مسیریابی در AngularJs #بخش اول دایرکتیو ng-view معرفی شده است. هنگام استفاده از سرویس route$ با این دایرکتیو میتوان محل مورد نظر برای بارگذاری محتویات مربوط به مسیرها را مشخص کرد. دایرکتیو ui-view در UI-Router همین نقش را دارد. فرض کنید این کد فایل index.html باشد:<!-- index.html --> <body> <div ui-view></div> <!-- We'll also add some navigation: --> <a ui-sref="state1">State 1</a> <a ui-sref="state2">State 2</a> </body>
در ادامه برای هر کدام از حالتها یک template اضافه میکنیم:
فایل state1.html:
<!-- partials/state1.html --> <h1>State 1</h1> <hr/> <a ui-sref="state1.list">Show List</a> <div ui-view></div>
<!-- partials/state2.html --> <h1>State 2</h1> <hr /> <a ui-sref="state2.list">Show List</a> <div ui-view></div>
دو نکته قابل توجه در این templateها وجود دارد. اول اینکه همانطور که میبینید templateها خود شامل تگی با دایرکتیو ui-view هستند. و دوم مقدار دایرکتیو ui-sref است که به صورت state1.list و state2.list آمده است. این جدا سازی با نقطه نشان دهنده سلسله مراتب حالتهاست. یعنی حالتهای state1 و state2 هرکدام حالت فرزندی به نام list دارند. در ادامه وقتی حالتها و مسیریابی را در ()app.config تعریف کنیم این مسائل از هالهای از ابهام که در آن هستند خارج میشوند! فعلا بیاید با راهنمای UI-Router پیش برویم و فایلهای template حالتهای فرزند را تعریف کنیم. templateهایی که قرار است در ui-view پدرانشان بارگذاری شوند:
<!-- partials/state1.list.html --> <h3>List of State 1 Items</h3> <ul> <li ng-repeat="item in items">{{ item }}</li> </ul>
<!-- partials/state2.list.html --> <h3>List of State 2 Things</h3> <ul> <li ng-repeat="thing in things">{{ thing }}</li> </ul>
myApp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { // // For any unmatched url, redirect to /state1 $urlRouterProvider.otherwise("/state1"); // // Now set up the states $stateProvider .state('state1', { url: "/state1", templateUrl: "partials/state1.html" }) .state('state1.list', { url: "/list", templateUrl: "partials/state1.list.html", controller: function($scope) { $scope.items = ["A", "List", "Of", "Items"]; } }) .state('state2', { url: "/state2", templateUrl: "partials/state2.html" }) .state('state2.list', { url: "/list", templateUrl: "partials/state2.list.html", controller: function($scope) { $scope.things = ["A", "Set", "Of", "Things"]; } }) }]);
خصوصیت url مشخص کننده مسیر حالت است. این خصوصیت همان مقداریست که به عنوان پارامتر اول به ()routeProvider.when$ پاس میشد. در این پارامتر میشود متغیرهای url را هم به همان ترتیب تعریف کرد. مثلا اگر حالت state1 در آدرسش یک پارامتر id داشته باشد میشود آن را به این ترتیب تعریف کرد:
.state('state1', { url: "/state1/:id", templateUrl: "partials/state1.html" })
برای خواندن مقدار این متغیر باید از stateParams$ استفاده کرد:
$stateParams.id
.state('list', { parent: "state1", url: "/list", templateUrl: "partials/state1.list.html", controller: function($scope) { $scope.items = ["A", "List", "Of", "Items"]; } }) .state('list', { parent: "state2", url: "/list", templateUrl: "partials/state2.list.html", controller: function($scope) { $scope.items = ["A", "List", "Of", "Items"]; } })
تا اینجای کار، اگر آدرس "state1/" وارد شود، فایل "partials/state1.html" در "ui-view" فایل "index.html" بارگذاری خواهد شد. اگر آدرس "state1/list/" وارد شود، ابتدا فایل "partials/state1.html" در "ui-view" فایل "index.html" بارگذاری شده، سپس فایل "partials/state1.list.html" در "ui-view" آمده در فایل فایل "partials/state1.html" بارگذاری میشود. این همان امکان حالتها و viewهای تو در تو است که UI-Router فراهم میکند.
اینجا میتوانید خروجی کدهای بالا را مشاهده کنید.
اگر مستقیما url یک حالت فرزند وارد شود، یا به عبارت دیگر، اگر بخواهیم مستقیما برنامه به حالتی که فرزند حالت دیگر است برود، UI-Router برنامه را ابتدا به حالت پدر، و پس از آن به حالت فرزند خواهد برد. حالت فرزند دو چیز را از حالت پدر به ارث میبرد:
- وابستگیهای فراهم شده در حالت پدر به وسیله "resolve"
- دادههای سفارشی مشخص شده در خصوصیت data حالت پدر
.state('state1', { url: "/state1", templateUrl: "partials/state1.html", data:{ foodata: 'addorder' } })
$state.current.data.foodata
Viewهای نامگذاری شده و چندگانه
یکی دیگر از قابلیتهای کاربردی UI-Router امکان داشتن چند ui-view در هر template است (استفاده همزمان از این قابلیت و حالتهای تو در تو، امکان مدیریت واسط کاربری را به خوبی فراهم میکند). برای توضیح این قابلیت، با راهنمای UI-Router همراه شویم:
1. دستورالعمل برپایی UI-Router که در بالا آمده را اجرا کنید.
2. یک یا چند ui-view به برنامهتان اضافه کنید و آنها را نامگذاری کنید:
<!-- index.html --> <body> <div ui-view="viewA"></div> <div ui-view="viewB"></div> <!-- Also a way to navigate --> <a ui-sref="route1">Route 1</a> <a ui-sref="route2">Route 2</a> </body>
3. حالتهای برنامهتان را در روال config ماژول تعریف کنید:
myApp.config(function ($stateProvider) { $stateProvider .state('index', { url: "", views: { "viewA": { template: "index.viewA" }, "viewB": { template: "index.viewB" } } }) .state('route1', { url: "/route1", views: { "viewA": { template: "route1.viewA" }, "viewB": { template: "route1.viewB" } } }) .state('route2', { url: "/route2", views: { "viewA": { template: "route2.viewA" }, "viewB": { template: "route2.viewB" } } }) });
4. خروجی کدهای بالا را اینجا مشاهده کنید.
چند نکته
UI-Router جزئیات فراوانی دارد و آنچه آمد تنها پرده برداری از آن بود. دلم میخواست میتوانستم بیش از این آن را معرفی کنم، اما متاسفانه این روزها وقت آزاد کافی ندارم. در انتها میخواهم به چند نکته اشاره کنم:
روش controller as
برای استفاده از روش controller as در UI-Router باید به این ترتیب عمل کنید:
.state('list', { parent: "state1", url: "/list", templateUrl: "partials/state1.list.html", controller: "state1ListController as listCtrl1" } }) .state('list', { parent: "state2", url: "/list", templateUrl: "partials/state2.list.html", controller: "state2ListController as listCtrl2" } })
حالتهای انتزاعی
حالت انتزاعی حالتی است که url ندارد و در نتیجه برنامه نمیتواند در آن حالت قرار گیرد. حالتهای انتزاعی بسیار به درد خور هستند! مثلا فرض کنید چند حالت دارید که اشتراکاتی با هم دارند (همه باید در template مشابهی بارگذاری شود، یا وابستگیهای یکسانی دارند، یا حتی سطح دسترسی یکسان). با تعریف یک حالت انتزاعی و جمع کردن همه وابستگیها در آن، و تعریف حالتهای مورد نظرتان به عنوان فرزندان حالت انتزاعی، میتوانید اشتراکات حالتهای برنامه را سادهتر مدیریت کنید.
حساسیت به حروف بزرگ و کوچک
در سرویس route$ با مقداردهی خصوصیت caseInsensitiveMatch میتوانستیم مشخص کنیم که بزرگ و کوچک بودن حروف در تطبیق آدرس صفحه با پارامتر route در نظر گرفته بشود یا نه. خودمانیش اینکه url به حروف بزرگ و کوچک حساس باشد یا نه. متاسفانه در UI-Router از این امکان خبری نیست (البته فعلا) و آدرسهای تعریف شده به حروف بزگ و کوچک حساس هستند.
اینجا روشی برای حل این مشکل پیشنهاد شده، به این ترتیب که همه urlهای وارد شده به حروف کوچک تبدیل شود (راستش من این راه حل را نمیپسندم!).
چند روز قبل هم تغییراتی در کد UI-Router داده شده که امکان حساس نبودن به حروف کوچک و بزرگ فراهم شود. این تغییر هنوز در نسخه نهایی فایل UI-Router نیامده است. هرچند اگر بیاید هم آنچه تا امروز (23 اسفند 92) انجام شده مشکل را حل نمیکند.
اگر شما هم مثل من میخواهید کلا آدرسها به حروف بزرگ و کوچک حساس نباشند، و فرصت حل کردن اساسی مشکل را هم ندارید به این ترتیب عمل کنید:
- در فایل "angular-ui-router.js" عبارت "new RegExp(compiled)" را پیدا کرده و آن را به "RegExp(compiled, 'i')" تبدیل کنید. و یا در "angular-ui-router.min.js" (هرکدام از فایلها که استفاده میکنید) عبارت new RegExp(o) را پیدا کرده و آن را به new RegExp(o, "i") تبدیل کنید. همین؛ صدایش را هم در نیاورید!
اشتراکها
خبرنامه هفتگی NET Stacks.
اشتراکها
شرکت در نظرسنجی رسمی NuGet
اشتراکها
جشن 25 سالگی Windows 95
اشتراکها
مطالب مفید در مورد CQRS
Welcome to the Edument CQRS and Intentful Testing Starter Kit project page. On this page, you will find links to our open sourced CQRS Starter Kit on GitHub as well as tutorials to get you started.
Quivr, your second brain, utilizes the power of GenerativeAI to store and retrieve unstructured information. Think of it as Obsidian, but turbocharged with AI capabilities.
اشتراکها
وب سایت FeedSpot
اشتراکها