لاگ گیری از تغییرات رکورد های جداول
تشخیص تفاوت بین IIS سرورهای مختلف
I commonly hear the phrase “The web application worked in the pre-production environment and now is encountering issues in production and the server’s configuration are identical!” when I appear onsite to help assist with the resolution of the issues. Upon further investigation, an IIS module has not been installed on the production server, or the configuration is different for an application pool setting between the pre-production and production environments. This is a very common scenario I encounter in the field and here are some suggestions on how to determine differences between IIS servers in an IIS farm environment or between servers in different environments, such as pre-production and production. Keeping server configuration and content synchronized is always a challenge and I hope these suggestions help out.
استفاده از WinML در NET 5 .
WinML is a high-performance, reliable API for deploying hardware-accelerated ML (Machine Learning) inferences on Windows devices. Since its introduction, many developers started using this technology to develop UWP applications that leverage artificial intelligence. Throughout this blog post, we’ll understand how you can leverage WinML on a simple .NET5 Console app.
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت اول - ایجاد و به روز رسانی جدول مجازی 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
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 را بررسی میکنیم.