موجودیتهای زیر را در نظر بگیرید:
public class Customer { public Customer() { Orders = new ObservableCollection<Order>(); } public Guid Id { get; set; } public string Name { get; set; } public string Family { get; set; } public string FullName { get { return Name + " " + Family; } } public virtual IList<Order> Orders { get; set; } }
public class Product { public Product() { } public Guid Id { get; set; } public string Name { get; set; } public int Price { get; set; } } public class OrderDetail { public Guid Id { get; set; } public Guid ProductId { get; set; } public int Count { get; set; } public Guid OrderId { get; set; } public int Price { get; set; } public virtual Order Order { get; set; } public virtual Product Product { get; set; } public string ProductName { get { return Product != null ? Product.Name : string.Empty; } } }
public class Order { public Order() { OrderDetail = new ObservableCollection<OrderDetail>(); } public Guid Id { get; set; } public DateTime Date { get; set; } public Guid CustomerId { get; set; } public virtual Customer Customer { get; set; } public virtual IList<OrderDetail> OrderDetail { get; set; } public string CustomerFullName { get { return Customer == null ? string.Empty : Customer.FullName; } } public int TotalPrice { get { if (OrderDetail == null) return 0; return OrderDetail.Where(orderdetail => orderdetail.Product != null) .Sum(orderdetail => orderdetail.Price*orderdetail.Count); } } }
و نگاشت موجودیت ها:
public class CustomerConfiguration : EntityTypeConfiguration<Customer> { public CustomerConfiguration() { HasKey(c => c.Id); Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } } public class ProductConfiguration : EntityTypeConfiguration<Product> { public ProductConfiguration() { HasKey(p => p.Id); Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } } public class OrderDetailConfiguration : EntityTypeConfiguration<OrderDetail> { public OrderDetailConfiguration() { HasKey(od => od.Id); Property(od => od.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } } public class OrderConfiguration: EntityTypeConfiguration<Order> { public OrderConfiguration() { HasKey(o => o.Id); Property(o => o.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); } }
و برای معرفی موجودیتها به Entity Framwork کلاس StoreDbContext را به صورت زیر تعریف میکنیم:
public class StoreDbContext : DbContext { public StoreDbContext() : base("name=StoreDb") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new CustomerConfiguration()); modelBuilder.Configurations.Add(new OrderConfiguration()); modelBuilder.Configurations.Add(new OrderDetailConfiguration()); modelBuilder.Configurations.Add(new ProductConfiguration()); } public DbSet<Customer> Customers { get; set; } public DbSet<Product> Products { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderDetail> OrderDetails { get; set; } }
جهت مقدار دهی اولیه به database تستی یک DataBaseInitializer به صورت زیر تعریف میکنیم:
public class MyTestDb : DropCreateDatabaseAlways<StoreDbContext> { protected override void Seed(StoreDbContext context) { var customer1 = new Customer { Name = "Vahid", Family = "Nasiri" }; var customer2 = new Customer { Name = "Mohsen", Family = "Jamshidi" }; var customer3 = new Customer { Name = "Mohsen", Family = "Akbari" }; var product1 = new Product {Name = "CPU", Price = 350000}; var product2 = new Product {Name = "Monitor", Price = 500000}; var product3 = new Product {Name = "Keyboard", Price = 30000}; var product4 = new Product {Name = "Mouse", Price = 20000}; var product5 = new Product {Name = "Power", Price = 70000}; var product6 = new Product {Name = "Hard", Price = 250000}; var order1 = new Order { Customer = customer1, Date = new DateTime(2013, 1, 1), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product1, Count = 1, Price = product1.Price}, new OrderDetail {Product = product2, Count = 1, Price = product2.Price}, new OrderDetail {Product = product3, Count = 1, Price = product3.Price}, } }; var order2 = new Order { Customer = customer1, Date = new DateTime(2013, 1, 5), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product1, Count = 2, Price = product1.Price}, new OrderDetail {Product = product3, Count = 4, Price = product3.Price}, } }; var order3 = new Order { Customer = customer1, Date = new DateTime(2013, 1, 9), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product1, Count = 4, Price = product1.Price}, new OrderDetail {Product = product3, Count = 5, Price = product3.Price}, new OrderDetail {Product = product5, Count = 6, Price = product5.Price}, } }; var order4 = new Order { Customer = customer2, Date = new DateTime(2013, 1, 9), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product4, Count = 1, Price = product4.Price}, new OrderDetail {Product = product3, Count = 1, Price = product3.Price}, new OrderDetail {Product = product6, Count = 1, Price = product6.Price}, } }; var order5 = new Order { Customer = customer2, Date = new DateTime(2013, 1, 12), OrderDetail = new List<OrderDetail> { new OrderDetail {Product = product4, Count = 1, Price = product4.Price}, new OrderDetail {Product = product5, Count = 2, Price = product5.Price}, new OrderDetail {Product = product6, Count = 5, Price = product6.Price}, } }; context.Customers.Add(customer3); context.Orders.Add(order1); context.Orders.Add(order2); context.Orders.Add(order3); context.Orders.Add(order4); context.Orders.Add(order5); context.SaveChanges(); }
و در ابتدای برنامه کد زیر را جهت مقداردهی اولیه به Database مان قرار میدهیم:
Database.SetInitializer(new MyTestDb());
در انتها ConnectionString را در App.Config به صورت زیر تعریف میکنیم:
<connectionStrings> <add name="StoreDb" connectionString="Data Source=.\SQLEXPRESS; Initial Catalog=StoreDBTest;Integrated Security = true" providerName="System.Data.SqlClient"/> </connectionStrings>
بسیار خوب، حالا همه چیز محیاست برای اجرای اولین پرس و جو:
using (var context = new StoreDbContext()) { var query = context.Customers; foreach (var customer in query) { Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); } }
پرس و جوی تعریف شده لیست تمام Customerها را باز میگرداند. query فقط یک "عبارت" پرس و جو هست و زمانی اجرا میشود که از آن درخواست نتیجه شود. در مثال بالا این درخواست در اجرای حلقه foreach اتفاق میافتد و درست در این لحظه است که دستور SQL ساخته شده و به Database فرستاده میشود. EF در این حالت تمام دادهها را در یک لحظه باز نمیگرداند بلکه این ارتباط فعال است تا حلقه به پایان برسد و تمام دادهها از database واکشی شود. خروجی به صورت زیر خواهد بود:
Customer Name: Vahid, Customer Family: Nasiri Customer Name: Mohsen, Customer Family: Jamshidi Customer Name: Mohsen, Customer Family: Akbari
نکته: با هر بار درخواست نتیجه از query ، پرس و جوی مربوطه دوباره به database فرستاده میشود که ممکن است مطلوب ما نباشد و باعث افت سرعت شود. برای جلوگیری از تکرار این عمل کافیست با استفاده از متد ToList پرس و جو را در لحظه تعریف به اجرا در آوریم
var customers = context.Customers.ToList();
خط بالا دیگر یک عبارت پرس و جو نخواهد بود بلکه لیست تمام Customer هاست که به یکباره از database بازگشت داده شده است. در ادامه هرجا که از customers استفاده کنیم دیگر پرس و جویی به database فرستاده نخواهد شد.
پرس و جوی زیر مشتریهایی که نام آنها Mohsen هست را باز میگرداند:
private static void Query3() { using (var context = new StoreDbContext()) { var methodSyntaxquery = context.Customers .Where(c => c.Name == "Mohsen"); var sqlSyntaxquery = from c in context.Customers where c.Name == "Mohsen" select c; foreach (var customer in methodSyntaxquery) { Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); } } // Output: // Customer Name: Mohsen, Customer Family: Jamshidi // Customer Name: Mohsen, Customer Family: Akbari }
همانطور که مشاهده میکنید پرس و جو به دو روش Method Syntax و Sql Syntax نوشته شده است.
روش Method Syntax روشی است که از متدهای الحاقی (Extention Method) و عبارتهای لامبدا (Lambda Expersion) برای نوشتن پرس و جو استفاده میشود. اما #C روش Sql Syntax را که همانند دستورات SQL هست، نیز فراهم کرده است تا کسانیکه آشنایی با این روش دارند، از این روش استفاده کنند. در نهایت این روش به Method Syntax تبدیل خواهد شد بنابراین پیشنهاد میشود که از همین روش استفاده شود تا با دست و پنجه نرم کردن با این روش، از مزایای آن در بخشهای دیگر کدنویسی استفاده شود.
اگر به نوع Customers که در DbContext تعریف شده است، دقت کرده باشید، خواهید دید که DbSet میباشد. DbSet کلاس و اینترفیسهای متفاوتی را پیاده سازی کرده است که در ادامه با آنها آشنا خواهیم شد:
- IQueryable<TEntity>, IEnumerable<TEntity>, IQueryable, IEnumerable: که امکان استفاده از متدهای نام آشنای LINQ را برای ما فراهم میکند. البته فراموش نشود که EF از Provider ای با نام LINQ To Entity برای تفسیر پرس و جوی ما و ساخت دستور SQL متناظر آن استفاده میکند. بنابراین تمامی متدهایی که در LINQ To Object استفاده میشوند در اینجا قابل استفاده نیستند. بطور مثال اگر در پرس و جو از LastOrDefault روی Customer استفاده شود در زمان اجرا با خطای زیر مواجه خواهیم شد و در نتیجه در استفاده از این متدها به این مسئله باید دقت شود.
- <IDbSet<TEntity: که دارای متدهای Add, Attach, Create, Find, Remove, Local میباشد و برای بحث ما Find و Local جهت ساخت پرس و جو استفاده میشوند که در ادامه توضیح داده خواهند شد.
- <DbQuery<TEntity: که دارای متدهای AsNoTracking و Include میباشد و در ادامه توضیح داده خواهند شد.
- دادههای موجود در حافظه را بررسی میکند یعنی آنهایی که Load و یا Attach شده اند.
- داده هایی که به DbContext اضافه (Add) ولی هنوز در database درج نشده اند.
- داده هایی که در database هستند ولی هنوز Load نشده اند.
private static void Query4() { using (var context = new StoreDbContext()) { var customer = context.Customers.Find(new Guid("2ee2fd32-e0e9-4955-bace-1995839d4367")); if (customer == null) Console.WriteLine("Customer not found"); else Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); } }
private static void Query5() { using (var context = new StoreDbContext()) { try { var customer1 = context.Customers.Single(c => c.Name == "Unkown"); // Exception: Sequence contains no elements } catch (Exception ex) { Console.WriteLine(ex.Message); } try { var customer2 = context.Customers.Single(c => c.Name == "Mohsen"); // Exception: Sequence contains more than one element } catch (Exception ex) { Console.WriteLine(ex.Message); } var customer3 = context.Customers.SingleOrDefault(c => c.Name == "Unkown"); // customer3 == null var customer4 = context.Customers.Single(c => c.Name == "Vahid"); // customer4 != null } }
private static void Query6() { using (var context = new StoreDbContext()) { try { var customer1 = context.Customers.First(c => c.Name == "Unkown"); // Exception: Sequence contains no elements } catch (Exception ex) { Console.WriteLine(ex.Message); } var customer2 = context.Customers.FirstOrDefault(c => c.Name == "Unknown"); // customer2 == null var customer3 = context.Customers.First(c => c.Name == "Mohsen"); } }
الگوی معکوس سازی کنترل چیست؟
IoC یک الگوی سطح بالا است و به روشهای مختلفی به مسایل متفاوتی جهت معکوس سازی کنترل، قابل اعمال میباشد؛ مانند:
- کنترل اینترفیسهای بین دو سیستم
- کنترل جریان کاری برنامه
- کنترل بر روی ایجاد وابستگیها (جایی که تزریق وابستگیها و DI ظاهر میشوند)
سؤال: بین IoC و DIP چه تفاوتی وجود دارد؟
در DIP (قسمت قبل) به این نتیجه رسیدیم که یک ماژول سطح بالاتر نباید به جزئیات پیاده سازیهای ماژولی سطح پایینتر وابسته باشد. هر دوی اینها باید بر اساس Abstraction با یکدیگر ارتباط برقرار کنند. IoC روشی است که این Abstraction را فراهم میکند. در DIP فقط نگران این هستیم که ماژولهای موجود در لایههای مختلف برنامه به یکدیگر وابسته نباشند اما بیان نکردیم که چگونه.
معکوس سازی اینترفیسها
هدف از معکوس سازی اینترفیسها، استفاده صحیح و معنا دار از اینترفیسها میباشد. به این معنا که صرفا تعریف اینترفیسها به این معنا نیست که طراحی صحیحی در برنامه بکار گرفته شده است و در حالت کلی هیچ معنای خاصی ندارد و ارزشی را به برنامه و سیستم شما اضافه نخواهد کرد.
برای مثال یک مسابقه بوکس را درنظر بگیرید. در اینجا Ali یک بوکسور است. مطابق عادت معمول، یک اینترفیس را مخصوص این کلاس ایجاد کرده، به نام IAli و مسابقه بوکس از آن استفاده خواهد کرد. در اینجا تعریف یک اینترفیس برای Ali، هیچ ارزش افزودهای را به همراه ندارد و متاسفانه عادتی است که در بین بسیاری از برنامه نویسها متداول شده است؛ بدون اینکه علت واقعی آنرا بدانند و تسلطی به الگوهای طراحی برنامه نویسی شیءگرا داشته باشند. صرف اینکه به آنها گفته شده است تعریف اینترفیس خوب است، سعی میکنند برای همه چیز اینترفیس تعریف کنند!
تعریف یک اینترفیس تنها زمانی ارزش خواهد داشت که چندین پیاده سازی از آن ارائه شود. در مثال ما پیاده سازیهای مختلفی از اینترفیس IAli بیمفهوم است. همچنین در دنیای واقعی، در یک مسابقه بوکس، چندین و چند شرکت کننده وجود خواهند داشت. آیا باید به ازای هر کدام یک اینترفیس جداگانه تعریف کرد؟ ضمنا ممکن است اینترفیس IAli متدی داشته باشد به نام ضربه، اینترفیس IVahid متد دیگری داشته باشد به نام دفاع.
کاری که در اینجا جهت طراحی صحیح باید صورت گیرد، معکوس سازی اینترفیسها است. به این ترتیب که مسابقه بوکس است که باید اینترفیس مورد نیاز خود را تعریف کند و آن هم تنها یک اینترفیس است به نام IBoxer. اکنون Ali، Vahid و سایرین باید این اینترفیس را جهت شرکت در مسابقه بوکس پیاده سازی کنند. بنابراین دیگر صرف وجود یک کلاس، اینترفیس مجزایی برای آن تعریف نشده و بر اساس معکوس سازی کنترل است که تعریف اینترفیس IBoxer معنا پیدا کرده است. اکنون IBoxer دارای چندین و چند پیاده سازی خواهد بود. به این ترتیب، تعریف اینترفیس، ارزشی را به سیستم افزوده است.
به این نوع معکوس سازی اینترفیسها، الگوی provider model نیز گفته میشود. برای مثال کلاسی که از چندین سرویس استفاده میکند، بهتر است یک IService را ایجاد کرده و تامین کنندههایی، این IService را پیاده سازی کنند. نمونهای از آن در دنیای دات نت، Membership Provider موجود در ASP.NET است که پیاده سازیهای بسیاری از آن تاکنون تهیه و ارائه شدهاند.
معکوس سازی جریان کاری برنامه
جریان کاری معمول یک برنامه یا Noraml flow، عموما رویهای یا Procedural است؛ به این معنا که از یک مرحله به مرحلهای بعد هدایت خواهد شد. برای مثال یک برنامه خط فرمان را درنظر بگیرید که ابتدا میپرسد نام شما چیست؟ در مرحله بعد مثلا رنگ مورد علاقه شما را خواهد پرسید.
برای معکوس سازی این جریان کاری، از یک رابط کاربری گرافیکی یا GUI استفاده میشود. مثلا یک فرم را درنظر بگیرید که در آن دو جعبه متنی، کار دریافت نام و رنگ را به عهده دارند؛ به همراه یک دکمه ثبت اطلاعات. به این ترتیب بجای اینکه برنامه، مرحله به مرحله کاربر را جهت ثبت اطلاعات هدایت کند، کنترل به کاربر منتقل و معکوس شده است.
معکوس سازی تولید اشیاء
معکوس سازی تولید اشیاء، اصل بحث دوره و سری جاری را تشکیل میدهد و در ادامه مباحث، بیشتر و عمیقتر بررسی خواهد گردید.
روش متداول تعریف و استفاده از اشیاء دیگر درون یک کلاس، وهله سازی آنها توسط کلمه کلیدی new است. به این ترتیب از یک وابستگی به صورت مستقیم درون کدهای کلاس استفاده خواهد شد. بنابراین در این حالت کلاسهای سطح بالاتر به ماژولهای سطح پایین، به صورت مستقیم وابسته میگردند.
برای اینکه این کنترل را معکوس کنیم، نیاز است ایجاد و وهله سازی این اشیاء وابستگی را در خارج از کلاس جاری انجام دهیم. شاید در اینجا بپرسید که چرا؟
اگر با الگوی طراحی شیءگرای Factory آشنا باشید، همان ایده در اینجا مدنظر است:
Button button; switch (UserSettings.UserSkinType) { case UserSkinTypes.Normal: button = new Button(); break; case UserSkinTypes.Fancy: button = new FancyButton(); break; }
حال در این برنامه اگر قرار باشد کار و کنترل محل وهله سازی این دکمهها معکوس نشود، در هر قسمتی از برنامه نیاز است این سوئیچ تکرار گردد (برای مثال در چند ده فرم مختلف برنامه). بنابراین بهتر است محل ایجاد این دکمهها به کلاس دیگری منتقل شود مانند ButtonFactory و سپس از این کلاس در مکانهای مختلف برنامه استفاده گردد:
Button button = ButtonFactory.CreateButton();
بنابراین در مثال فوق، کنترل ایجاد دکمهها به یک کلاس پایه قرار گرفته در خارج از کلاس جاری، معکوس شده است.
انواع معکوس سازی تولید اشیاء
بسیاری شاید تصور کنند که تنها راه معکوس سازی تولید اشیاء، تزریق وابستگیها است؛ اما روشهای چندی برای انجام اینکار وجود دارد:
الف) استفاده از الگوی طراحی Factory (که نمونهای از آنرا در قسمت قبل مشاهده کردید)
ب) استفاده از الگوی Service Locator
Button button = ServiceLocator.Create(IButton.Class)
ج) تزریق وابستگیها
Button button = GetTheButton(); Form1 frm = new Form1(button);
به صورت خلاصه هر زمانیکه تولید و وهله سازی وابستگیهای یک کلاس را به خارج از آن منتقل کردید، کار معکوس سازی تولید وابستگیها انجام شده است.
XMLHttpRequest رابطی است که به شما امکان نقل و انتقالات را از سمت کاربر، به سمت سرور و سپس دریافت پاسخ آن را میدهد. این رابط طوری طراحی شدهاست که دیگر برای این جابجایی نیازی به بارگزاری مجدد کل صفحه نباشد و قسمتی از اطلاعات صفحات به روز شوند، مزاحمتی برای کاربر ایجاد نشود. به همین دلیل از این رابط، در پشت صحنههای عملیات ایجکسی استفاده زیادی میشود. در این مقاله با استفاده از خصوصیتی به نام request.IsAjax بررسی میشود که آیا درخواست رسیده به سرور از نوع ایجکسی است یا خیر. اگر به سورس نوشته شده این متد نگاه دقیقتری بیندازیم، متوجه میشویم کاری که این متد انجام میدهد، در واقع در یک خط خلاصه میشود و آن بررسی هدری برای وجود درخواست از نوع XMLHttpRequest است:
return request.Headers["X-Requested-With"] == "XMLHttpRequest";
یکی از متدهای این رابط، متد ارسال آن (send) میباشد که میتواند رابطی به نام formData را انتقال دهد و این رابط از نوع مجموعهای از کلید و مقدارهاست. این رابط زمانی به کار گرفته میشود که انکدینگ فرم خود را بر روی multipart/form-data قرار داده باشید. این ساختار میتواند توسط دستور for of بررسی گردد. برای آشنایی بیشتر با متدهای آن این صفحه را مطالعه فرمایید.
هنگام ارسال فایل در حالت postback، ما فرم را بر روی multipart قرار میدهیم تا امکان ارسال آن توسط formData مهیا شود. ولی از آنجاکه ما از ایجکس استفاده میکنیم، بهتر است که خودمان مستقیما از این ساختار استفاده کنیم.
بخشی از فرم Html
<div> <label>تصویر</label> <div> <input id="picture" type="file" data-buttonText="انتخاب تصویر"> </div> </div> <div> <label>کد ملی</label> <div> <input id="txtNationalCode" required="" maxlength="10" type="text"> </div> </div> <div> <label>نام</label> <div> <input id="txtName" type="text" maxlength="50" required=""> </div> </div> <div class="form-group"> <div class="col-sm-4 col-sm-offset-2"> <button class="btn btn-primary" id="btnSubmit" type="submit">ذخیره</button> <button class="btn btn-white" id="btnClear" type="submit">لغو</button> </div> </div>
سپس کد جی کوئری زیر را مینویسیم:
var formData = new FormData(); formData.append('FirstName', $("#txtName").val()); formData.append('NationalCode', $("#txtNationalCode").val()); jQuery.each($('#picture')[0].files, function (i, file) { formData.append('picture-'+i, file); }); $.ajax({ type: "POST", dataType: "json", url: address, data: formData, success: function (data) { //..... }, error: function (data) { //...... } });
توجه به این نکته ضروری است و با توجه کدهایی که در نت دیدم و بسیاری از آن حتی به عنوان پاسخ صحیح در نظر گرفته شده بودند این است که شیء FormData شامل هیچ سازندهای نیست و باید با استفاده از متد append آنها را اضافه کنید.
امروز اولین دستورات MDX را خواهیم نوشت. قبل از شروع کار فراموش نکنید موارد زیر را حتما انجام داده باشید :
- نصب پایگاه داده ی Adventure Work DW 2008 و همچنین نصب پایگاه دادهی چند بعدی Adventure Work DW 2008 روی SSAS
- مطاله قسمتهای قبلی برای آشنایی با مفاهیم پایه .
در صورتیکه پیش شرایط فوق را نداشته باشید، احتمالا در ادامه با مشکلاتی مواجه خواهید شد؛ زیرا برای آموزش MDX Query ها از پایگاه دادهی Adventure Work DW 2008 استفاده شده است.
دقت داشته باشید که MDX Query ها تا حدودی شبیه T/SQL میباشند؛ اما مطلقا از نظر مفهومی با هم شباهت ندارند. به عبارت دیگر ما در T/SQL با یک مدل رابطهای سرو کار داریم در حالیکه در MDX ها با یک پایگاه داده چند بعدی کار میکنیم. به بیان دیگر در پایگاه دادههای رابطهای صحبت از جداول، ردیفها، ستونها و ضرب دکارتی مجموعهها میباشد، اما در پایگاه دادههای چند بعدی در خصوص Dimension,Fact,Cube,Tuple و ... صحبت میکنیم. البته ماکروسافت تلاش کردهاست تا حد زیادی Syntax ها شبیه به یکدیگر باشند.
نحوهی نوشتن یک Select در MDX ها به صورت زیر میباشد :
Select {} On Columns , {} On Rows From <Cube_Name> Where <Condition>
در ادامه با اجرای هر کوئری، توضیحات لازم در خصوص آن ارایه میگردد و با پیگیری این آموزشها میتوانید مفاهیم، توابع و ... را در MDX Query ها بیاموزید.
برای اجرای دستورات زیر باید Microsoft SQL Server Management Studio را باز نمایید و به سرویس SSAS متصل شوید. سپس پایگاه دادهی Adventure Works DW 2008R2 را انتخاب نمایید و از Cubes Adventure Works را انتخاب نمایید.
حال دکمهی New Query را در بالای صفحه بزنید ( Ctrl + N )
سپس در صفحهی باز شده میتوانید Cube یا SubCube های آن Cube را انتخاب کرده و کمی پایینتر Measure Group را خواهیم داشت و در انتها Measure ها و Dimension ها قرار گرفتهاند. (در هنگام نوشتن Select میتوان از عمل Drag&Drop برای آسانتر شدن نوشتن MDX Query ها نیز استفاده کنید)
متاسفانه هنوز در IDE مربوط به SQL Server کلیدی برای مرتب سازی دستورات MDX وجود ندارد و البته در نرم افزار هایی مانند SQL Toll Belt هم چنین چیزی قرار داده نشده است . بنابر این توصیه میشود در نوشتن دستورات MDX تمام تلاش خود را بکنید تا دستوراتی مرتب و خوانا را تولید کنید.
با اجرای دستور زیر اولین کوئری خود را در پایگاه دادهی چند بعدی بنویسید (برای اجرا کلید F5 مانند T/SQL کار خواهد کرد.)
Select From [Adventure Works]
شاید تعجب کنید. کوئری فاقد قسمت Projection میباشد! در MDX ها میتوان هیچ سطر یا ستونی را انتخاب نکرد. اما چگونه؟ و خروجی نمایش داده شده چیست؟
برای توضیح مطلب فوق باید در خصوص Default Measure کمی اطلاعات داشته باشید. در هنگام Deploy کردن پروژه در SSAS برای هر Cube یک Measure به عنوان Measure پیش فرض انتخاب شده. بنابر این در صورتیکه هیچ گونه Projection یا Where ایی اعمال نشده باشد، SQL Server به صورت پیش فرض مقدار Mesaure پیش فرض را بدون اعمال هیچ بعدی نمایش میدهد.
خروجی دستور بالا مشابه تصویر زیر میباشد.
حال دستور زیر را اجرا میکنیم :
Select From [Adventure Works] Where [Measures].[Reseller Sales Amount]
تصویر خروجی به صورت زیر میباشد :
شاید باز هم تعجب کنید. نوشتن نام یک شاخص به جای عبارت شرط؟! آیا خروجی عبارات شرطی نباید Boolean باشند؟
خیر. اگر چنین پرسش هایی در ذهن شما ایجاد شده باشد، به دلیل مقایسهی MDX با T/SQL میباشد. در اینجا شرط Where بر روی ردیفهای جدول مدل رابطه ای اعمال نمیشود و عملا بیانگر واکشی اطلاعات از مدل چند بعدی میباشد. با اعمال شرط فوق به SSAS اعلام کرده ایم که خروجی بر اساس شاخص [Measures].[Reseller Sales Amount] باشد. با توجه به این که شاخص انتخاب شده با شاخص پیش فرض یکی میباشد خروجی با حالت قبل تفاوتی نخواهد کرد.
برای درک بهتر، کوئری زیر را اجرا کنید :
Select From [Adventure Works] where [Measures].[Internet Sales Amount]
استفاده از این شرط سبب استفاده نشدن از شاخص پیش فرض می شود . به عبارت دیگر این کوئری دارای سرجمع مبلغ فروش اینترنتی می باشد.
دستور زیر را اجرا کنید :
Select [Measures].[Reseller Sales Amount] on columns From [Adventure Works]
با اعمال یک شاخص خاص در ستون ، عملا فیلترینگ انجام می شود
استفاده از یک دایمنشن در ستون :
دستور زیر را اجرا کنید
Select [Date].[Calendar].[Calendar Year] on columns From [Adventure Works]
خروجی به شکل زیر خواهد بود
همان طور که مشاهده میکنید خروجی دارای چندین ستون میباشد و دارای مقادیری در هر ستون. اما این مقادیر از کجا آمده اند؟
همواره این نکته را به خاطر بسپارید که در صورت عدم ذکر نام یک Measure در کوئری ، SSAS از Measure پیش فرض استفاده میکند. حال کوئری فوق میزان فروش نمایندگان ( Reseller Sales Amount ) را در هر سال نمایش میدهد.
سوال بعدی این میباشد که این سالها از کجا آمده اند؟ خوب برای درک بهتر این مورد میتوانیم مانند تصویر زیر به دایمنشن Date رفته و در ساختار سلسله مراتبی ، اعضای سطح [Date].[Calendar].[Calendar Year] را مشاهده کنیم.
ایجاد سرجمع ستونها :
کوئری زیر را اجرا نمایید
Select {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns From [Adventure Works]
بعد از اجرا
تصویر زیر را خواهید دید :
سوال اول این میباشد که کاربرد {} در انتخاب دایمنشنها چیست؟ در پاسخ میتوان گفت که اگر شاخص ها یا بعد ها ، مرتبط به یک سلسله مراتب باشند آنها را در یک {} قرار می دهیم ولی اگر سلسله مراتب متفاوت باشد، یا بعد و شاخص باشند باید در () قرار بگیرند .
خوب همان طور که مشخص است در ساختار سلسله مراتبی ابتدا سال و بعد یک سطح بالاتر را انتخاب کرده ایم این به معنی نمایش سرجمع در سطح بالاتر از سال میباشد(سرجمع تمامی سال ها).
استفاده از دایمنشن و Measure در سطر و ستون مجرا :
کوئری زیر را اجرا نمایید
Select {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns, [Product].[Product Categories].[Category] on rows From [Adventure Works]
خروجی مشابه شکل زیر میباشد
در مثال فوق از بعدها در ستون و همزمان، نمایش نوع دسته بندی محصولات در ردیفها استفاده شده است. به عبارت دیگر نتیجه عبارت است از فروش نماینگان فروش ( Reseller Sales Amount ) براساس هر سال به تفکیک نوع دسته بندی محصول فروخته شده.
(کسانی که چنین گزارشی را با استفاده از T/SQL نوشته اند، احتمالا از آسانی نوشتن این گزارش توسط MDX ها شگفت زده شده اند.)
قراردادن فیلد سرجمع در ردیف :
برای این منظور کوئری زیر را اجرا نمایید
Select {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns, {[Product].[Product Categories].[Category],[Product].[Product Categories]}on rows From [Adventure Works]
خروجی به صورت زیر میباشد
نحوهی نمایش سرجمع در ردیف، مشابه نمایش سرجمع در ستون میباشد.
استفاده از تابع non empty :
برای حذف ستون هایی که کاملا دارای مقدار null میباشند به صورت زیر عمل میکنیم :
Select non empty {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns , {[Product].[Product Categories].[Category],[Product].[Product Categories]} on rows From [Adventure Works]
خروجی به صورت زیر میباشد:
انتخاب دو دایمنشن در سطر و ستون و مشخص نمودن یک Measure خاص برای کوئری :
برای این کار به صورت زیر عمل خواهیم کرد:
Select {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns, {[Product].[Product Categories].[Category],[Product].[Product Categories]} on rows From [Adventure Works] Where [Measures].[Internet Sales Amount]
در اینجا با اعمال شرط Where عملا از SSAS خواستهایم خروجی برای شاخص مشخص شده واکشی شود.
در بالا میزان فروش اینترنتی برای دسته بندی محصولات و در سالهای مختلف ارائه و همچنین سرجمع ستون و سطر نیز نمایش داده شده است.
در صورتیکه بخواهیم ستون و سطرهایی را که دارای مقدار null در تمامی آن سطر یا ستون میباشند، حذف کنیم به صورت زیر عمل میکنیم:
Select non empty {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns, non empty {[Product].[Product Categories].[Category],[Product].[Product Categories]} on rows From [Adventure Works] Where [Measures].[Internet Sales Amount]
اگر در یک دایمنشن فقط یک سلسله مراتب باشد یا اصلا سلسله مراتبی وجود نداشته باشد، می توان از نام خود دایمنشن استفاده کرد
Select [Sales Channel] on columns From [Adventure Works]
و دقت داشته باشید دایمنشنی که دارای بیش از یک سلسله مراتب باشد، حتما باید در Select مشخص شود که از کدام سلسله مراتب می خواهیم استفاده کنیم .در غیر این صورت با خطا مواجه خواهیم شد.
Select [Product] on columns From [Adventure Works]
استفاده از فیلدهای یک دایمنشن که دارای سلسه مراتب می باشد نیز جایز می باشد
Select [Product].[Category] on columns From [Adventure Works]
Select [Product].[Category].[all] on columns From [Adventure Works] -- Select [Product].[Category].[All] on columns From [Adventure Works] -- Select [Product].[Category].[(all)] on columns From [Adventure Works] -- Select [Product].[Category].[all products] on columns From [Adventure Works]
برای به دست آوردن سرجمع کل روی یک صفت از دایمنشن، باید از سه حالت آخر استفاده کرد. حالت اول خطا دارد و خروجی خالی نمایش داده می شود .
در صورتی که بخواهیم از یک دایمنشن تمامی Member های آن را واکشی کنیم به صورت زیر عمل خواهیم کرد
Select {[Product].[Category].members} on columns From [Adventure Works]
استفاده از Members روی یک خصوصیت در دایمنشن به معنی دریافت سرجمع آن صفت و سپس تک تک اجزای آن صفت میباشد.
اگر از یک صفت واکشی اطلاعات انجام شود در سطح اعضای آن، در آن صورت دیگر سرجمع نمایش داده نمی شود و فقط جمع هر عضو در آن صفت نمایش داده می شود .
Select [Product].[Category].[Category].members -- dimension.hierarchy.level.members on columns From [Adventure Works]
اگر بخواهیم دو ستون را داشته باشیم که هر دو برای یک دایمنشن میباشند باید از {} استفاده کرد . دستور اول خطا خواهد داشت.
Select [Product].[Category].[Category].members,[Product].[Category].[All Products] on columns From [Adventure Works]
در دستور دوم با استفاده از {} خروجی نمایش داده میشود که عبارت است از تمامی اعضای سطح [Product].[Category].[Category]. به همراه سرجمع تمامی محصولات.
Select {[Product].[Category].[Category].members,[Product].[Category].[All Products]} on columns From [Adventure Works]
یک راه کوتاهتر برای انتخاب تمامی اعضا و سرجمع آنها
Select {[Product].[Category].[Category],[Product].[Category]} on columns From [Adventure Works]
می توان از کلمات Members, All X استفاده نکرد.
انتخاب اولین دسته بندی محصول البته این ترتیب بر اساس Key Columns در SSAS می باشد .
Select [Product].[Category].&[1] on columns From [Adventure Works]
انتخاب دقیق یک عضو در خروجی
Select [Product].[Category].[Bikes] on columns From [Adventure Works]
انتخاب دو عضو از یک دایمنشن
Select {[Product].[Category].[Bikes],[Product].[Category].[Clothing]} on columns From [Adventure Works]
واکشی تمامی دسته بندی محصولات بر اساس Measure پیش فرض :
Select [Product].[Product Categories].members on columns From [Adventure Works]
در صورتیکه بخواهیم دو Dimension مختلف را در یک ستون یا سطر بیاوریم باید از Join استفاده کنیم. بنابر این دو دستور زیر با خطا روبرو میشوند
Select [Product].[Product Categories],[Product].[Category] on columns From [Adventure Works] Go Select {[Product].[Product Categories],[Product].[Category]} on columns From [Adventure Works]
تعریف Axis : به هر کدام از ستون یا سطر یک محور یا Axis گفته میشود.
با بررسی مثال فوق به نتایج زیر خواهیم رسید.
1. امکان استفاده از دو سلسله مراتب مختلف از یک دایمنشن در یک Axis وجود ندارد . مگر اینکه آنها را باهمدیگر CrossJoin کنیم .
2. امکان استفاده از دو سلسله مراتب مختلف از یک دایمنشن در دو Axis مختلف وجود دارد .
ترتیب انتخاب Axis ها به صورت زیر میباشد:
1. Columns
2. Rows
برای مشخص شدن موضوع کوئری زیر را اجرا کنید
Select [Product].[Product Categories].members on rows From [Adventure Works]
نمیتوانیم ردیفی را واکشی کنیم بدون اینکه ستونی برای کوئری مشخص کرده باشیم.
البته میتوان ستون خالی ایجاد نماییم مانند مثال زیر :
Select {} on columns, [Product].[Product Categories].members on rows From [Adventure Works]
البته در این صورت خروجی فقط نام دسته بندی محصولات خواهد بود زیرا هیچ ستونی مشخص نشده .
در مقالات بعدی به ادامهی مطالب MDX Query خواهیم پرداخت.
اعتبارسنجی سرویس های WCF
context.Chapters.Add(new Chapter { Title = "آزمایش متن فارسی", Text = "برای نمونه تهیه شدهاست", User = user1.Entity });
کامپایل افزونهی spell fix1
افزونهی spell fix، به همراه هیچکدام از توزیعهای باینری SQLite ارائه نمیشود. ارائهی آن فقط به صورت سورس کد است و باید خودتان آنرا کامپایل کنید!
برای این منظور ابتدا به آدرس https://www.sqlite.org/src/dir?ci=99749d4fd4930ccf&name=ext/misc مراجعه کرده و فایل ext/misc/spellfix.c آنرا دریافت کنید. اگر بر روی لینک spellfix.c کلیک کنید، در نوار ابزار بالای صفحهی بعدی، لینک download آن هم وجود دارد.
سپس به صفحهی دریافت اصلی SQLite یعنی https://www.sqlite.org/download.html مراجعه کرده و بستهی amalgamation آنرا دریافت کنید. این بسته به همراه کدهای اصلی SQLite است که باید در کنار افزونههای آن قرار گیرند تا بتوان این افزونهها را کامپایل کرد. بنابراین پس از دریافت بستهی amalgamation و گشودن آن، فایل spellfix.c را به داخل پوشهی آن کپی کنید:
اکنون نوبت به کامپایل فایل spellfix.c و تبدیل آن به یک dll است تا بتوان آنرا به صورت یک افزونه در برنامه بارگذاری کرد. برای این منظور از هر کامپایلر ++C ای میتوانید استفاده کنید. برای نمونه به آدرس http://www.codeblocks.org/downloads/binaries مراجعه کرده و بستهی codeblocks-20.03mingw-setup.exe را دریافت کنید (بستهای که به همراه mingw است). پس از نصب آن، در مسیر C:\Program Files (x86)\CodeBlocks\MinGW\bin میتوانید کامپایلر چندسکویی gcc را مشاهده کنید. توسط آن میتوان با اجرای دستور زیر، سبب تولید فایل spellfix1.dll شد:
"C:\Program Files (x86)\CodeBlocks\MinGW\bin\gcc.exe" -g -shared -fPIC -Wall D:\path\to\sqlite-amalgamation-3310100\spellfix.c -o spellfix1.dll
روش معرفی افزونههای SQLite به Microsoft.Data.Sqlite
EF Core، از بستهی Microsoft.Data.Sqlite در پشت صحنه برای کار با SQLite استفاده میکند و در اینجا هم برای معرفی افزونهی کامپایل شده، باید ابتدا آنرا به اتصال برقرار شده، معرفی کرد. خود Sqlite در ویندوز، افزونههایش را بر اساس معرفی مستقیم مسیر فایل dll آنها بارگذاری نمیکند. بلکه path ویندوز را برای جستجوی آنها بررسی کرده و در صورتیکه فایل dll ای را افزونه تشخیص داد، آنرا بارگذاری میکند. بنابراین یا باید به صورت دستی مسیر فایل dll تولید شده را به متغیر محیطی path ویندوز اضافه کرد و یا میتوان توسط قطعه کد زیر، آنرا به صورت پویایی معرفی کرد:
using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; namespace EFCoreSQLiteFTS.DataLayer { public static class LoadSqliteExtensions { public static void AddToSystemPath(string extensionsDirectory) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new NotSupportedException("Modifying the path at runtime only works on Windows. On Linux and Mac, set LD_LIBRARY_PATH or DYLD_LIBRARY_PATH before running the app."); } var path = new HashSet<string>(Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)); if (path.Add(extensionsDirectory)) { Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, path)); } } } }
در ادامه پیش از معرفی services.AddDbContext، باید مسیر پوشهی افزونهها را ثبت کرد و سپس UseSqlite را به همراه اتصالی استفاده کرد که توسط متد LoadExtension آن، افزونهی spellfix1 به آن معرفی شدهاست:
LoadSqliteExtensions.AddToSystemPath("path to .dll file"); services.AddDbContext<ApplicationDbContext>((serviceProvider, optionsBuilder) => { var connection = new SqliteConnection(connectionString); connection.Open(); connection.LoadExtension("spellfix1"); // Passing in an already open connection will keep the connection open between requests. optionsBuilder.UseSqlite(connection); });
ایجاد جداول ویژهی spell fix در برنامه
در قسمت اول، با متد createFtsTables آشنا شدیم. اکنون این متد را برای ایجاد جداول کمکی مرتبط با افزونهی spell fix به صورت زیر تکمیل میکنیم:
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"");"); // 'SQLite Error 1: 'no such module: spellfix1'.' --> must be loaded ... // EditCost for unicode support context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_Vocab USING fts5vocab('Chapters_FTS', 'row');"); context.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Chapters_FTS_SpellFix_EditCost(iLang INT, cFrom TEXT, cTo TEXT, iCost INT);"); context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_SpellFix USING spellfix1(edit_cost_table=Chapters_FTS_SpellFix_EditCost);"); }
- همانطور که مشاهده میکنید، ابتدا بر اساس Chapters_FTS یا همان جدول مجازی FTS برنامه، یک جدول مجازی از نوع fts5vocab ایجاد میشود. کار آن استخراج توکنهای FTS و آماده سازی آنها برای استفاده در غلط یاب املایی هستند.
- سپس جدول ویژهی EditCost را مشاهده میکنید. نام آن مهم نیست، اما ساختار آن باید دقیقا به همین صورت باشد. اگر این جدول اختیاری را تهیه کنیم، الگوریتم spellfix1 به utf8 سوئیچ خواهد کرد و برای پردازش متون یونیکد، بدون مشکل کار میکند. بدون آن، جستجوهای فارسی نتایج مطلوبی را به همراه نخواهند داشت.
- در آخر جدول مجازی مرتبط با spellfix1 که از جدول cost_table معرفی شده استفاده میکند، ایجاد شدهاست.
اجرای این دستورات، جداول زیر را ایجاد میکنند (که ساختار آنها استاندارد است و باید مطابق فرمولهای مستندات آنها باشد):
به روز رسانی جدول واژه نامهی غلط یابی برنامه
آخرین جدولی را که ایجاد کردیم، Chapters_FTS_SpellFix است که اطلاعات خودش را از Chapters_FTS_Vocab دریافت میکند:
هر بار که بانک اطلاعاتی را به روز میکنیم، نیاز است اطلاعات این جدول را نیز توسط دستور زیر به روز کرد:
database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS_SpellFix(word, rank) SELECT term, cnt FROM Chapters_FTS_Vocab WHERE term not in (SELECT word from Chapters_FTS_SpellFix_vocab)");
database.ExecuteSqlRaw("INSERT INTO Chapters_FTS_SpellFix(command) VALUES(\"reset\");");
کوئری گرفتن از جدول مجازی Chapters_FTS_SpellFix
تا اینجا افزونهی spellfix1 را کامپایل و به سیستم معرفی کردیم. سپس جداول واژه نامهی آنرا نیز تشکیل دادیم، اکنون نوبت به کوئری گرفتن از آن است. به همین جهت یک موجودیت بدون کلید دیگر را بر اساس ساختار خروجی کوئریهای آن ایجاد کرده:
namespace EFCoreSQLiteFTS.Entities { public class SpellCheck { public string Word { get; set; } public decimal Rank { get; set; } public decimal Distance { get; set; } public decimal Score { get; set; } public decimal Matchlen { get; set; } } }
namespace EFCoreSQLiteFTS.DataLayer { public class ApplicationDbContext : DbContext { //... protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<SpellCheck>().HasNoKey().ToView(null); } //... } }
در آخر، کوئری گرفتن از این جدول، ساختار زیر را دارد:
foreach (var item in context.Set<SpellCheck>().FromSqlRaw( @"SELECT word, rank, distance, score, matchlen FROM Chapters_FTS_SpellFix WHERE word MATCH {0} and top=6", "فارشی")) { Console.WriteLine($"Word: {item.Word}"); Console.WriteLine($"Distance: {item.Distance}"); }
top=6 در این کوئری خاص یعنی 6 رکورد را بازگشت بده.
یک نکته: اگر میخواهید کوئری فوق را توسط برنامهی «DB Browser for SQLite» اجرا کنید، باید از منوی tools آن، گزینهی load extension را انتخاب کرده و فایل dll افزونه را به برنامه معرفی کنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
Ident_Current(‘tabla_name’)
در واقع ما یک سیستمی داریم که شامل مدلی است از دیتاهای ما و از این مدل برای کوئری گرفتن از دیتابیس استفاده میشود، که البته برای بیشتر پروژههای نرم افزاری، معماری درست و ترجیح داده شدهای هم میباشد.
زمانیکه نیازهای پروژه روز به روز افزورده و پیچیدهتر میشود، مدل CRUD بصورت پیوسته از ارزشش کاسته میشود و از آن سادگی اولیهی در درک و خوانایی آن دور خواهد شد.
ذات CQRS بر آن است که شما مدلهای مختلفی را برای خواندن و نوشتن دیتا داشته باشید. الگوی آن چیزی شبیه به تصویر زیر است
ES از برنامه نویسان میخواهد که مدل سنتی CRUD را فراموش کرده و بجای آن تغییراتی را که روی دیتا صورت گرفته، نیز درج نمایند. اینکار به وسیلهی یک دیتابیس Append-only انجام میشود که به نام Event Store شناخته میشود.
در این معماری ما همهی تغییرات روی دیتا را به صورت Serialize Event ذخیره میکنیم که میتواند دوباره در هر زمانی اجرا شده و current state هر objectی را در اختیار بگذارد.
این روش به ما کمک بزرگی میکند تا وضعیت یک object را در گذشته به راحتی پیدا کنیم و از آن میتوان به غیر از فوایدی که دارد، به عنوان یک Logger نیز استفاده نمود. به دلیل اینکه جزء به جزء تغییرات بر روی state سیستم، در آن ثبت شده است. از آنجاییکه دیتا بصورت serialize ذخیره میشود، بارگزاری آن نیز با سرعت بالایی انجام خواهد شد.
public class Movie : AggregateRoot { public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int RunningTimeMinutes { get; set; } public Movie() { } public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes) { //پیاده سازی خواهد شد } }
public class CreateMovieCommand : ICommand { public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int RunningTimeMinutes { get; set; } public CreateMovieCommand(string title, DateTime releaseDate, int runningTime) { Title = title; ReleaseDate = releaseDate; RunningTimeMinutes = runningTime; } }
public class CreateMovieCommandHandler : CommandHandler<CreateMovieCommand> { protected IDomainRepository _repository; public CreateMovieCommandHandler(IDomainRepository repository) { _repository = repository; } public override void Handle(CreateMovieCommand command) { var movie = new Domain.Movie(Guid.NewGuid(), command.Title, command.ReleaseDate, command.RunningTimeMinutes); _repository.Save(movie); } }
public class MovieCreatedEvent : DomainEvent { public Guid MovieId { get { return AggregateRootId; } set { AggregateRootId = value;} } public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int RunningTimeMinutes { get; set; } public MovieCreatedEvent(Guid movieId, string title, DateTime releaseDate, int runningTime) { MovieId = movieId; Title = title; ReleaseDate = releaseDate; RunningTimeMinutes = runningTime; } }
public class Movie : AggregateRoot { public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int RunningTimeMinutes { get; set; } public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes) { Apply(new MovieCreatedEvent(Guid.NewGuid(), title, releaseDate, runningTimeMinutes)); } }
public class MovieEventHandler : IHandleDomainEvents<MovieCreatedEvent> { public void Handle(MovieCreatedEvent createdEvent) { using (MoviesContext entities = new MoviesContext()) { entities.Movies.Add(new Movie() { Id = createdEvent.AggregateRootId, Title = createdEvent.Title, ReleaseDate = createdEvent.ReleaseDate, RunningTimeMinutes = createdEvent.RunningTimeMinutes }); entities.SaveChanges(); } } }
protected void OnMovieCreated(MovieCreatedEvent domainEvent) { Id = domainEvent.AggregateRootId; Title = domainEvent.Title; ReleaseDate = domainEvent.ReleaseDate; RunningTimeMinutes = domainEvent.RunningTimeMinutes; }
- ذاتا پیاده سازی این مدل سخت و دشوار است و از آنجاییکه سادگی در پیاده سازی سیستمهای نرم افزاری، یک اصل مهم محسوب میشود، بنابراین استفاده از این مدل محدود میشود به سیستمهای نرم افزاری که مزیتهای گفته شده در قسمت فوق برایشان حیاتی محسوب شود.
- برای پیاده سازی سیستمی با این مدل احتیاج به تیم توسعهای است که با مفاهیم آن کاملا آشنا باشد.
- هر چند امروزه فضای فیزیکی برای ذخیره سازی دیتا ارزان محسوب میشود، اما به هر حال استفاده از این مدل به همراه ES، حجم زیادی از Disk space را خواهد گرفت.
- همانطور که دیدید برای پیاده سازی یک Insert ساده، حجم زیادی کد نوشته شدهاست. بنابراین تولید اینگونه نرم افزارها به زمان بیشتری نیاز دارد.
بررسی Semantic Search و FTS Table-valued functions
توابع Predicates مختص به FTS مانند Contains و Freetext، تنها ردیفهای متناظر با جستجوی انجام شده را باز میگردانند و رتبهای به نتایج جستجو اعمال نمیگردد. برای مثال، مشخص نیست اولین ردیف بازگشت داده شده بهترین تطابق را با جستجوی انجام شده دارد یا بدترین نتیجهی ممکن است. برای رفع این مشکل FTS table-valued functions معرفی شدهاند. حاصل اینها یک جدول با دو ستون است. ستون اول کلید متناظر با جدول تطابق یافته بوده و ستون دوم، Rank نام دارد که بیانگر میزان مفید بودن و درجهی اعتبار ردیف بازگشت داده شدهاست.
Semantic Search نیز به کمک سه table-valued functions پیاده سازی میشود. همچنین باید دقت داشت که تمام زبانهای پشتیبانی شده توسط FTS در حالت Semantic Search پشتیبانی نمیشوند. برای بررسی این مورد، دو کوئری ذیل را اجرا نمائید:
-- Full text Languages SELECT * FROM sys.fulltext_languages ORDER BY name; -- Semantic Search Languages SELECT * FROM sys.fulltext_semantic_languages ORDER BY name; GO
بررسی table-valued functions مختص به FTS
دو متد ویژهی CONTAINSTABLE و FREETEXTTABLE خروجی از نوع جدول دارند؛ با ستونهایی به نامهای key و rank. اگر قسمت ایجاد کاتالوگ FTS و ایندکس آنرا بخاطر داشته باشید، در حین ایجاد ایندکس FTS میبایستی KEY INDEX PK_Documents را نیز ذکر کرد. کاربرد آن در همین table-valued functions است.
مقدار rank، عددی است بین 0 و 1000 که هر چقدر مقدار آن بیشتر باشد، یعنی نتیجهای نزدیکتر، به عبارت جستجو شده، یافت گردیدهاست. باید دقت داشت که این عدد فقط در زمینهی یک کوئری معنا پیدا میکند و مقایسهی rank دو کوئری مختلف با هم، بیمعنا است.
عملکرد CONTAINSTABLE بسیار شبیه به متد Contains است با این تفاوت که قابلیتهای بیشتری دارد. برای مثال در اینجا میتوان برای قسمتی از جستجو، وزن و اهمیت بیشتری را قائل شد و این حالت تنها زمانی معنا پیدا میکند که خروجی جستجو، دارای rank باشد.
متد FREETEXTTABLE نیز بسیار شبیه به FREETEXT عمل کرده و نسبت به CONTAINSTABLE بسیار سادهتر است. برای نمونه امکان تعریف وزن، formsof، near و غیره در اینجا وجود ندارد. به علاوه عملگرهای منطقی مانند and و or نیز در اینجا کاربردی نداشته و صرفا یک noise word درنظر گرفته میشوند.
چند مثال جهت بررسی عملکرد دو متد CONTAINSTABLE و FREETEXTTABLE
استفاده از متد CONTAINSTABLE
-- Rank with CONTAINSTABLE SELECT D.id, D.title, CT.[RANK], D.docexcerpt FROM CONTAINSTABLE(dbo.Documents, docexcerpt, N'data OR level') AS CT INNER JOIN dbo.Documents AS D ON CT.[KEY] = D.id ORDER BY CT.[RANK] DESC;
این متد ابتدا نام جدول مورد بررسی را دریافت میکند. سپس ستونی که باید جستجو بر روی آن انجام شود و در ادامه عبارت جستجو شونده، مشخص میگردد. اگر این متد را به تنهایی اجرا کنیم:
SELECT * FROM CONTAINSTABLE(dbo.Documents, docexcerpt, N'data OR level')
استفاده از متد FREETEXTTABLE
-- Rank with FREETEXTTABLE SELECT D.id, D.title, FT.[RANK], D.docexcerpt FROM FREETEXTTABLE (dbo.Documents, docexcerpt, N'data level') AS FT INNER JOIN dbo.Documents AS D ON FT.[KEY] = D.id ORDER BY FT.[RANK] DESC;
در اینجا اگر نیاز باشد تا تعداد نتایج را شبیه به کوئریهای top n محدود نمود، میتوان از پارامتر عددی بعدی که برای نمونه به 2 تنظیم شدهاست، استفاده کرد:
-- Rank with FREETEXTTABLE and top_n_by_rank SELECT D.id, D.title, FT.[RANK], D.docexcerpt FROM FREETEXTTABLE (dbo.Documents, docexcerpt, N'data level', 2) AS FT INNER JOIN dbo.Documents AS D ON FT.[KEY] = D.id ORDER BY FT.[RANK] DESC;
تعیین وزن و اهمیت کلمات در حال جستجو
-- Weighted terms SELECT D.id, D.title, CT.[RANK], D.docexcerpt FROM CONTAINSTABLE (dbo.Documents, docexcerpt, N'ISABOUT(data weight(0.8), level weight(0.2))') AS CT INNER JOIN dbo.Documents AS D ON CT.[KEY] = D.id ORDER BY CT.[RANK] DESC;
انجام جستجوهای Proximity
-- Proximity term SELECT D.id, D.title, CT.[RANK] FROM CONTAINSTABLE (dbo.Documents, doccontent, N'NEAR((data, row), 30)') AS CT INNER JOIN dbo.Documents AS D ON CT.[KEY] = D.id ORDER BY CT.[RANK] DESC; GO
بررسی Semantic Search key valued functions
متد SEMANTICKEYPHRASETABLE کار بازگشت واژههای کلیدی آنالیز شده توسط FTS را انجام داده و جدولی حاوی 4 ستون را باز میگرداند. این چهار ستون عبارتند از:
- column_id: شماره ستون واژه کلیدی یافت شدهاست. تفسیر آن نیاز به استفاده از تابع سیستمی COL_NAME دارد (مانند مثال زیر).
- document_key: متناظر است با کلید اصلی جدولی که بر روی آن کوئری گرفته میشود.
- keyphrase: همان واژه کلیدی است.
- score: رتبهی واژه کلیدی است در بین سایر واژههایی که بازگشت داده شده و عددی است بین صفر تا یک.
مثالی از آنرا در ادامه ملاحظه میکنید:
-- Top 100 semantic key phrases SELECT TOP (100) D.id, D.title, SKT.column_id, COL_NAME(OBJECT_ID(N'dbo.Documents'), SKT.column_id) AS column_name, SKT.document_key, SKT.keyphrase, SKT.score FROM SEMANTICKEYPHRASETABLE (dbo.Documents, doccontent) AS SKT INNER JOIN dbo.Documents AS D ON SKT.document_key = D.id ORDER BY SKT.score DESC;
در متد جدولی SEMANTICKEYPHRASETABLE، ابتدا جدول مورد نظر و سپس ستونی که نیاز است واژههای کلیدی آنالیز شدهی آن بازگشت داده شوند، قید میگردند. document_key آن به تنهایی شاید مفید نباشد. به همین جهت join شدهاست به جدول اصلی، تا بتوان رکوردهای متناظر را نیز بهتر تشخیص داد.
به این ترتیب مهمترین واژههای کلیدی ستون doccontent را به همراه درجهی اهمیت و رتبهی آنها، میتوان گزارش گرفت.
متد SEMANTICSIMILARITYTABLE برای یافتن سندهای مشابه با یک سند مشخص بکار میروند؛ چیزی شبیه به گزارش «مقالات مشابه مطلب جاری» در بسیاری از سایتهای ارائهی محتوا. ستونهای خروجی آن عبارتند از:
- source_column_id: شماره ستون منبع انجام کوئری.
- matched_column_id: شماره ستون سند مشابه یافت شده.
- matched_document_key: متناظر است با کلید اصلی جدولی که بر روی آن کوئری گرفته میشود.
- score: رتبهی نسبی سند مشابه یافت شده.
-- Documents that are similar to document 1 SELECT S.source_document_title, SST.matched_document_key, D.title AS matched_document_title, SST.score FROM (SEMANTICSIMILARITYTABLE (dbo.Documents, doccontent, 1) AS SST INNER JOIN dbo.Documents AS D ON SST.matched_document_key = D.id) CROSS JOIN (SELECT title FROM dbo.Documents WHERE id=1) AS S(source_document_title) ORDER BY SST.score DESC;
در این کوئری، اسناد مشابه با سند شماره 1 یافت شدهاند. مبنای جستجو نیز ستون doccontent، جدول dbo.Documents است. از join بر روی matched_document_key و id جدول اصلی، مشخصات سند یافت شده را میتوان استخراج کرد. کار CROSS JOIN تعریف شده، صرفا افزودن یک ستون مشخص به نتیجهی خروجی کوئری است.
همانطور که در تصویر مشخص است، سند شماره 4 بسیار شبیه است به سند شماره 1. در ادامه قصد داریم بررسی کنیم که علت این شباهت چه بودهاست؟
متد SEMANTICSIMILARITYDETAILSTABLE واژههای کلیدی مهم مشترک بین دو سند را بازگشت میدهد (سند منبع و سند مقصد). به این ترتیب میتوان دریافت، چه واژههای کلیدی سبب شدهاند تا این دو سند به هم شبیه باشند. ستونهای خروجی آن عبارتند از:
- keyphrase: واژهی کلیدی
- score: رتبهی نسبی واژهی کلیدی
-- Key phrases that are common across two documents SELECT SSDT.keyphrase, SSDT.score FROM SEMANTICSIMILARITYDETAILSTABLE (dbo.Documents, doccontent, 1, doccontent, 4) AS SSDT ORDER BY SSDT.score DESC;
در کوئری فوق قصد داریم بررسی کنیم چه واژههای کلیدی، سبب مشابهت سندهای شماره 1 و 4 شدهاند و بین آنها مشترک میباشند.