۱۳۹۱/۰۲/۲۹

EF Code First #14


رديابي تغييرات در EF Code first

EF از DbContext براي ذخيره اطلاعات مرتبط با تغييرات موجوديت‌هاي تحت كنترل خود كمك مي‌گيرد. اين نوع اطلاعات توسط Change Tracker API جهت بررسي وضعيت فعلي يك شيء، مقادير اصلي و مقادير تغيير كرده آن در دسترس هستند. همچنين در اينجا امكان بارگذاري مجدد اطلاعات موجوديت‌ها از بانك اطلاعاتي جهت اطمينان از به روز بودن آن‌ها تدارك ديده شده است. ساده‌ترين روش دستيابي به اين اطلاعات، استفاده از متد context.Entry مي‌باشد كه يك وهله از موجوديتي خاص را دريافت كرده و سپس به كمك خاصيت State خروجي آن، وضعيت‌هايي مانند Unchanged يا Modified را مي‌توان به دست آورد. علاوه بر آن خروجي متد context.Entry، داراي خواصي مانند CurrentValues و OriginalValues نيز مي‌باشد. OriginalValues شامل مقادير خواص موجوديت درست در لحظه اولين بارگذاري در DbContext برنامه است. CurrentValues مقادير جاري و تغيير يافته موجوديت را باز مي‌گرداند. به علاوه اين خروجي امكان فراخواني متد GetDatabaseValues را جهت بدست آوردن مقادير جديد ذخيره شده در بانك اطلاعاتي نيز ارائه مي‌دهد. ممكن است در اين بين، خارج از Context جاري، اطلاعات بانك اطلاعاتي توسط كاربر ديگري تغيير كرده باشد. به كمك GetDatabaseValues مي‌توان به اين نوع اطلاعات نيز دست يافت.
حداقل چهار كاربرد عملي جالب را از اطلاعات موجود در Change Tracker API مي‌توان مثال زد كه در ادامه به بررسي آن‌ها خواهيم پرداخت.


كلاس‌هاي مدل مثال جاري

در اينجا يك رابطه many-to-one بين جدول هزينه‌هاي اقلام خريداري شده يك شخص و جدول فروشندگان تعريف شده است:

using System;

namespace EF_Sample09.DomainClasses
{
    public abstract class BaseEntity
    {
        public int Id { get; set; }

        public DateTime CreatedOn { set; get; }
        public string CreatedBy { set; get; }

        public DateTime ModifiedOn { set; get; }
        public string ModifiedBy { set; get; }
    }
}

using System;

namespace EF_Sample09.DomainClasses
{
    public class Bill : BaseEntity
    {
        public decimal Amount { set; get; }        
        public string Description { get; set; }

        public virtual Payee Payee { get; set; }
    }
}

using System.Collections.Generic;

namespace EF_Sample09.DomainClasses
{
    public class Payee : BaseEntity
    {
        public string Name { get; set; }

        public virtual ICollection<Bill> Bills { set; get; }
    }
}


به علاوه همانطور كه ملاحظه مي‌كنيد، اين كلاس‌ها از يك abstract class به نام BaseEntity مشتق شده‌اند. هدف از اين كلاس پايه تنها تامين يك سري خواص تكراري در كلاس‌هاي برنامه است و هدف از آن، مباحث ارث بري مانند TPH، TPT و TPC نيست.
به همين جهت براي اينكه اين كلاس پايه تبديل به يك جدول مجزا و يا سبب يكي شدن تمام كلاس‌ها در يك جدول نشود، تنها كافي است آن‌را به عنوان DbSet معرفي نكنيم و يا مي‌توان از متد Ignore نيز استفاده كرد:

using System.Data.Entity;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
    public class Sample09Context : MyDbContextBase
    {
        public DbSet<Bill> Bills { set; get; }
        public DbSet<Payee> Payees { set; get; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Ignore<BaseEntity>();

            base.OnModelCreating(modelBuilder);
        }
    }
}



الف) به روز رساني اطلاعات Context در صورتيكه از متد context.Database.ExecuteSqlCommand مستقيما استفاده شود

در قسمت قبل با متد context.Database.ExecuteSqlCommand براي اجراي مستقيم عبارات SQL بر روي بانك اطلاعاتي آشنا شديم. اگر اين متد در نيمه كار يك Context فراخواني شود، به معناي كنار گذاشتن Change Tracker API مي‌باشد؛ زيرا اكنون در سمت بانك اطلاعاتي اتفاقاتي رخ داده‌اند كه هنوز در Context جاري كلاينت منعكس نشده‌اند:

using System;
using System.Data.Entity;
using EF_Sample09.DataLayer.Context;
using EF_Sample09.DomainClasses;

namespace EF_Sample09
{
    class Program
    {
        static void Main(string[] args)
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample09Context, Configuration>());

            using (var db = new Sample09Context())
            {
                var payee = new Payee { Name = "فروشگاه سر كوچه" };
                var bill = new Bill { Amount = 4900, Description = "يك سطل ماست", Payee = payee };
                db.Bills.Add(bill);

                db.SaveChanges();
            }

            using (var db = new Sample09Context())
            {
                var bill1 = db.Bills.Find(1);
                bill1.Description = "ماست";

                db.Database.ExecuteSqlCommand("Update Bills set Description=N'سطل ماست' where id=1");
                Console.WriteLine(bill1.Description);

                db.Entry(bill1).Reload(); //Refreshing an Entity from the Database
                Console.WriteLine(bill1.Description);

                db.SaveChanges();
            }
        }
    }
}

در اين مثال ابتدا دو ركورد به بانك اطلاعاتي اضافه مي‌شوند. سپس توسط متد db.Bills.Find، اولين ركورد جدول Bills بازگشت داده مي‌شود. در ادامه، خاصيت توضيحات آن به روز شده و سپس با استفاده از متد db.Database.ExecuteSqlCommand نيز بار ديگر خاصيت توضيحات اولين ركورد به روز خواهد شد.
اكنون اگر مقدار bill1.Description را بررسي كنيم، هنوز داراي مقدار پيش از فراخواني db.Database.ExecuteSqlCommand مي‌باشد، زيرا تغييرات سمت بانك اطلاعاتي هنوز به Context مورد استفاده منعكس نشده است.
در اينجا براي هماهنگي كلاينت با بانك اطلاعاتي، كافي است متد Reload را بر روي موجوديت مورد نظر فراخواني كنيم.



ب) يكسان سازي ي و ك اطلاعات رشته‌اي دريافتي پيش از ذخيره سازي در بانك اطلاعاتي

يكي از الزامات برنامه‌هاي فارسي، يكسان سازي ي و ك دريافتي از كاربر است. براي اين منظور بايد پيش از فراخواني متد SaveChanges نهايي،‌ مقادير رشته‌اي كليه موجوديت‌ها را يافته و به روز رساني كرد:

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using EF_Sample09.DataLayer.Toolkit;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
    public class MyDbContextBase : DbContext
    {
        public void RejectChanges()
        {
            foreach (var entry in this.ChangeTracker.Entries())
            {
                switch (entry.State)
                {
                    case EntityState.Modified:
                        entry.State = EntityState.Unchanged;
                        break;

                    case EntityState.Added:
                        entry.State = EntityState.Detached;
                        break;
                }
            }
        }

        public override int SaveChanges()
        {
            applyCorrectYeKe();
            auditFields();
            return base.SaveChanges();
        }

        private void applyCorrectYeKe()
        {
            //پيدا كردن موجوديت‌هاي تغيير كرده
            var changedEntities = this.ChangeTracker
                                      .Entries()
                                      .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);

            foreach (var item in changedEntities)
            {
                if (item.Entity == null) continue;

                //يافتن خواص قابل تنظيم و رشته‌اي اين موجوديت‌ها
                var propertyInfos = item.Entity.GetType().GetProperties(
                    BindingFlags.Public | BindingFlags.Instance
                    ).Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));

                var pr = new PropertyReflector();

                //اعمال يكپارچگي نهايي
                foreach (var propertyInfo in propertyInfos)
                {
                    var propName = propertyInfo.Name;
                    var val = pr.GetValue(item.Entity, propName);
                    if (val != null)
                    {
                        var newVal = val.ToString().Replace("ی", "ي").Replace("ک", "ك");
                        if (newVal == val.ToString()) continue;
                        pr.SetValue(item.Entity, propName, newVal);
                    }
                }
            }
        }

        private void auditFields()
        {
            // var auditUser = User.Identity.Name; // in web apps
            var auditDate = DateTime.Now;
            foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
            {
                // Note: You must add a reference to assembly : System.Data.Entity
                switch (entry.State)
                {
                    case EntityState.Added:
                        entry.Entity.CreatedOn = auditDate;
                        entry.Entity.ModifiedOn = auditDate;
                        entry.Entity.CreatedBy = "auditUser";
                        entry.Entity.ModifiedBy = "auditUser";
                        break;

                    case EntityState.Modified:
                        entry.Entity.ModifiedOn = auditDate;
                        entry.Entity.ModifiedBy = "auditUser";
                        break;
                }
            }
        }
    }
}


اگر به كلاس Context مثال جاري كه در ابتداي بحث معرفي شد دقت كرده باشيد به اين نحو تعريف شده است (بجاي DbContext از MyDbContextBase مشتق شده):
public class Sample09Context : MyDbContextBase
علت هم اين است كه يك سري كد تكراري را كه مي‌توان در تمام Contextها قرار داد، بهتر است در يك كلاس پايه تعريف كرده و سپس از آن ارث بري كرد.
تعاريف كامل كلاس MyDbContextBase را در كدهاي فوق ملاحظه مي‌كنيد.
در اينجا كار با تحريف متد SaveChanges شروع مي‌شود. سپس در متد applyCorrectYeKe كليه موجوديت‌هاي تحت نظر ChangeTracker كه تغيير كرده باشند يا به آن اضافه شده‌ باشند، يافت شده و سپس خواص رشته‌اي آن‌ها جهت يكساني سازي ي و ك، بررسي مي‌شوند.


ج) ساده‌تر سازي به روز رساني فيلدهاي بازبيني يك ركورد مانند DateCreated، DateLastUpdated و امثال آن بر اساس وضعيت جاري يك موجوديت

در كلاس MyDbContextBase فوق، كار متد auditFields، مقدار دهي خودكار خواص تكراري تاريخ ايجاد، تاريخ به روز رساني، شخص ايجاد كننده و شخص تغيير دهنده يك ركورد است. به كمك ChangeTracker مي‌توان به موجوديت‌هايي از نوع كلاس پايه BaseEntity دست يافت. در اينجا اگر entry.State آن‌ها مساوي EntityState.Added بود، هر چهار خاصيت ياد شده به روز مي‌شوند. اگر حالت موجوديت جاري، EntityState.Modified بود، تنها خواص مرتبط با تغييرات ركورد به روز خواهند شد.
به اين ترتيب ديگر نيازي نيست تا در حين ثبت يا ويرايش اطلاعات برنامه نگران اين چهار خاصيت باشيم؛ زيرا به صورت خودكار مقدار دهي خواهند شد.


د) پياده سازي قابليت لغو تغييرات در برنامه

علاوه بر اين‌ها در كلاس MyDbContextBase، متد RejectChanges نيز تعريف شده است تا بتوان در صورت نياز، حالت موجوديت‌هاي تغيير كرده يا اضافه شده را به حالت پيش از عمليات، بازگرداند.