۱۳۹۱/۰۳/۰۳

در مورد ادامه ...


در حال تهيه يك سيستم blogging هستم كه بشود آن‌را چند كاربره يا چند نويسنده‌اي كرد. اين سيستم جديد تا يك ماه ديگر جايگزين سيستم وصله پينه‌اي جاري خواهد شد (متن يكجا، css لينك شده از يك سايت ديگر، syntax highlighting از يك سايت ديگر، عكس‌ها در يك سرور مجزا، كامنت‌ها كلا در يك سرور ديگر (ديسكاس) بجز هاست بلاگر، لينك‌هاي روزانه در يك سايت ثالث، فايل‌هاي مثال‌ها در سروري ديگر! هاست اصلي بلاگ (بلاگر) هم كلا در ايران فيلتر است!). البته همين سيستم وصله پينه‌اي 4 سال است كه داره كار مي‌كنه!
به همين دليل براي پيدا كردن وقت جهت بازنويسي سيستم، اين سايت تا يك ماه ديگر در حالت تعليق خواهد بود.
در اين بين اگر علاقمند به مشاركت در بلاگ جديد (با همين آدرس با همين عنوان) به عنوان نويسنده هستيد، يك ايميل به من ارسال كنيد (vahid_nasiri در ياهو). موضوع سايت مشخص است. اهداف آن هم به همين ترتيب. فقط فني است. تنها مطالب فني قرار است ارسال شود. پس از آماده شدن سايت جديد، يك اكانت نويسنده مطلب براي شما ارسال خواهد شد. با تشكر از همكاري شما.

۱۳۹۱/۰۲/۳۰

EF Code First #15


EF Code first و بانك‌هاي اطلاعاتي متفاوت

در آخرين قسمت از سري EF Code first بد نيست نحوه استفاده از بانك‌هاي اطلاعاتي ديگري را بجز SQL Server نيز بررسي كنيم. در اينجا كلاس‌هاي مدل و كدهاي مورد استفاده نيز همانند قسمت 14 است و تنها به ذكر تفاوت‌ها و نكات مرتبط اكتفاء خواهد شد.


حالت كلي پشتيباني از بانك‌هاي اطلاعاتي مختلف توسط EF Code first

EF Code first با كليه پروايدرهاي تهيه شده براي ADO.NET 3.5 كه پشتيباني از EF را لحاظ كرده باشند،‌ به خوبي كار مي‌كند. پروايدرهاي مخصوص ADO.NET 4.0، تنها سه گزينه DeleteDatabase/CreateDatabase/DatabaseExists را نسبت به نگارش قبلي بيشتر دارند و EF Code first ويژگي‌هاي بيشتري را طلب نمي‌كند.
بنابراين اگر حين استفاده از پروايدر ADO.NET مخصوص بانك اطلاعاتي خاصي با پيغام «CreateDatabase is not supported by the provider» مواجه شديد، به اين معنا است كه اين پروايدر براي دات نت 4 به روز نشده است. اما به اين معنا نيست كه با EF Code first كار نمي‌كند. فقط بايد يك ديتابيس خالي از پيش تهيه شده را به برنامه معرفي كنيد تا مباحث Database Migrations به خوبي كار كنند؛ يا اينكه كلا مي‌توانيد Database Migrations را خاموش كرده (متد Database.SetInitializer را با پارامتر نال فراخواني كنيد) و فيلدها و جداول را دستي ايجاد كنيد.


استفاده از EF Code first با SQLite

براي استفاده از SQLite در دات نت ابتدا نياز به پروايدر ADO.NET آن است: «مكان دريافت درايور‌هاي جديد SQLite مخصوص دات نت»
ضمن اينكه به نكته «استفاده از اسمبلي‌هاي دات نت 2 در يك پروژه دات نت 4» نيز بايد دقت داشت.
و يكي از بهترين management studio هايي كه براي آن تهيه شده: «SQLite Manager»
پس از دريافت پروايدر آن، ارجاعي را به اسمبلي System.Data.SQLite.dll به برنامه اضافه كنيد.
سپس فايل كانفيگ برنامه را به نحو زير تغيير دهيد:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  </configSections>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>

  <connectionStrings>
    <clear/>
    <add name="Sample09Context"         
         connectionString="Data Source=CodeFirst.db"
         providerName="System.Data.SQLite"/>
  </connectionStrings>
</configuration>

همانطور كه ملاحظه مي‌كنيد، تفاوت آن با قبل، تغيير connectionString و providerName است.
اكنون اگر همان برنامه قسمت قبل را اجرا كنيم به خطاي زير برخواهيم خورد:
«The given key was not present in the dictionary»
در اين مورد هم توضيح داده شد. سه گزينه DeleteDatabase/CreateDatabase/DatabaseExists در پروايدر جاري SQLite براي دات نت وجود ندارد. به همين جهت نياز است فايل «CodeFirst.db» ذكر شده در كانكشن استرينگ را ابتدا دستي درست كرد.
براي مثال از افزونه SQLite Manager استفاده كنيد. ابتدا يك بانك اطلاعاتي خالي را درست كرده و سپس دستورات زير را بر روي بانك اطلاعاتي اجرا كنيد تا دو جدول خالي را ايجاد كند (در برگه Execute sql افزونه SQLite Manager):

CREATE TABLE [Payees](
 [Id] [integer] PRIMARY KEY AUTOINCREMENT NOT NULL,
 [Name] [text] NULL,
 [CreatedOn] [datetime] NOT NULL,
 [CreatedBy] [text] NULL,
 [ModifiedOn] [datetime] NOT NULL,
 [ModifiedBy] [text] NULL
);

CREATE TABLE [Bills](
 [Id] [integer] PRIMARY KEY AUTOINCREMENT NOT NULL,
 [Amount] [float](18, 2) NOT NULL,
 [Description] [text] NULL,
 [CreatedOn] [datetime] NOT NULL,
 [CreatedBy] [text] NULL,
 [ModifiedOn] [datetime] NOT NULL,
 [ModifiedBy] [text] NULL,
 [Payee_Id] [integer] NULL
);

سپس سطر زير را نيز به ابتداي برنامه اضافه كنيد:

Database.SetInitializer<Sample09Context>(null);

به اين ترتيب database migrations خاموش مي‌شود و اكنون برنامه بدون مشكل كار خواهد كرد.
فقط بايد به يك سري نكات مانند نوع داده‌ها در بانك‌هاي اطلاعاتي مختلف دقت داشت. براي مثال integer در اينجا از نوع Int64 است؛ بنابراين در برنامه نيز بايد به همين ترتيب تعريف شود تا نگاشت‌ها به درستي انجام شوند.

در كل تنها مشكل پروايدر فعلي SQLite عدم پشتيباني از مباحث database migrations است. اين مورد را خاموش كرده و تغييرات ساختار بانك اطلاعاتي را به صورت دستي به بانك اطلاعاتي اعمال كنيد. بدون مشكل كار خواهد كرد.

البته اگر به دنبال پروايدري تجاري با پشتيباني از آخرين نگارش EF Code first هستيد، گزينه زير نيز مهيا است:
http://devart.com/dotconnect/sqlite/
براي مثال اگر علاقمند به استفاده از حالت تشكيل بانك اطلاعاتي SQLite در حافظه هستيد (با رشته اتصالي ويژه Data Source=:memory:;Version=3;New=True;)،‌ فعلا تنها گزينه مهيا استفاده از پروايدر تجاري فوق است؛ زيرا مبحث Database Migrations را به خوبي پشتيباني مي‌كند.



استفاده از EF Code first با SQL Server CE

قبلا در مورد «استفاده از SQL-CE به كمك NHibernate» مطلبي را در اين سايت مطالعه كرده‌ايد. سه مورد اول آن با EF Code first يكي است و تفاوتي نمي‌كند (يك سري بحث عمومي مشترك است). البته با يك تفاوت؛ در اينجا EF Code first قادر است يك بانك اطلاعاتي خالي SQL Server CE را به صورت خودكار ايجاد كند و نيازي نيست تا آن‌را دستي ايجاد كرد. مباحث database migrations و به روز رساني خودكار ساختار بانك اطلاعاتي نيز در اينجا پشتيباني مي‌شود.
براي استفاده از آن ابتدا ارجاعي را به اسمبلي System.Data.SqlServerCe.dll قرار گرفته در مسير Program Files\Microsoft SQL Server Compact Edition\v4.0\Desktop اضافه كنيد.
سپس رشته اتصالي به بانك اطلاعاتي و providerName را به نحو زير تغيير دهيد:

<connectionStrings>
    <clear/>
    <add name="Sample09Context"
         connectionString="Data Source=mydb.sdf;Password=1234;Encrypt Database=True"
         providerName="System.Data.SqlServerCE.4.0"/>
</connectionStrings>


بدون نياز به هيچگونه تغييري در كدهاي برنامه، همين مقدار تغيير در تنظيمات ابتدايي برنامه براي كار با SQL Server CE كافي است.
ضمنا مشكلي هم با فيلد Identity در آخرين نگارش EF Code first وجود ندارد؛ برخلاف حالت database first آن كه پيشتر اين اجازه را نمي‌داد و خطاي «Server-generated keys and server-generated values are not supported by SQL Server Compact» را ظاهر مي‌كرد.



استفاده از EF Code first با MySQL

براي استفاده از EF Code first با MySQL (نگارش 5 به بعد البته) ابتدا نياز است پروايدر مخصوص ADO.NET آن‌را دريافت كرد: (^)
كه از EF نيز پشتيباني مي‌كند. پس از نصب آن، ارجاعي را به اسمبلي MySql.Data.dll قرار گرفته در مسير Program Files\MySQL\MySQL Connector Net 6.5.4\Assemblies\v4.0 به پروژه اضافه نمائيد.
سپس رشته اتصالي و providerName را به نحو زير تغيير دهيد:

<connectionStrings>
    <clear/>
    <add name="Sample09Context"
         connectionString="Datasource=localhost; Database=testdb2; Uid=root; Pwd=123;"
         providerName="MySql.Data.MySqlClient"/>
</connectionStrings>

<system.data>
    <DbProviderFactories>
      <remove invariant="MySql.Data.MySqlClient"/>
      <add name="MySQL Data Provider"
           invariant="MySql.Data.MySqlClient"
           description=".Net Framework Data Provider for MySQL"
           type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.5.4.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
    </DbProviderFactories>
</system.data>

همانطور كه مشاهده مي‌كنيد در اينجا شماره نگارش دقيق پروايدر مورد استفاده نيز ذكر شده است. براي مثال اگر چندين پروايدر روي سيستم نصب است، با مقدار دهي DbProviderFactories مي‌توان از نگارش مخصوصي استفاده كرد.

با اين تغييرات پس از اجراي برنامه قسمت قبل، به خطاي زير برخواهيم خورد:
The given key was not present in the dictionary

توضيحات اين مورد با قسمت SQLite يكي است؛ به عبارتي نياز است بانك اطلاعاتي testdb را دستي درست كرد. همچنين جداول و فيلدها را نيز بايد دستي ايجاد كرد و database migrations را نيز بايد خاموش كرد (پارامتر Database.SetInitializer را به نال مقدار دهي كنيد).
براي اين منظور يك ديتابيس خالي را ايجاد كرده و سپس دو جدول زير را به آن اضافه كنيد:

CREATE TABLE IF NOT EXISTS `bills` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `Amount` float DEFAULT NULL,
  `Description` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
  `CreatedOn` datetime NOT NULL,
  `CreatedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
  `ModifiedOn` datetime NOT NULL,
  `ModifiedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
  `Payee_Id` int(11) NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `payees` (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `Name` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
  `CreatedOn` datetime NOT NULL,
  `CreatedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
  `ModifiedOn` datetime NOT NULL,
  `ModifiedBy` varchar(400) CHARACTER SET utf8 COLLATE utf8_persian_ci NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_persian_ci AUTO_INCREMENT=1 ;

پس از اين تغييرات، برنامه بدون مشكل اجرا خواهد شد (ايجاد بانك اطلاعاتي خالي به همراه ايجاد ساختار جداول و خاموش كردن database migrations كه توسط اين پروايدر پشتيباني نمي‌شود).

به علاوه پروايدر تجاري ديگري هم در سايت devart.com براي MySQL و EF Code first مهيا است كه مباحث database migrations را به خوبي مديريت مي‌كند.


مشكل!
اگر به همين نحو برنامه را اجرا كنيم، فيلدهاي يونيكد فارسي ثبت شده در MySQL با «??????? ?? ????» مقدار دهي خواهند شد و تنظيم CHARACTER SET utf8 COLLATE utf8_persian_ci نيز كافي نبوده است (اين مورد با SQLite يا نگارش‌هاي مختلف SQL Server بدون مشكل كار مي‌كند و نياز به تنظيم اضافه‌تري ندارد):

ALTER TABLE `bills` DEFAULT CHARACTER SET utf8 COLLATE utf8_persian_ci

براي رفع اين مشكل توصيه شده است كه CharSet=UTF8 را به رشته اتصالي به بانك اطلاعاتي اضافه كنيم. اما در اين حالت خطاي زير ظاهر مي‌شود:
The provider did not return a ProviderManifestToken string
اين مورد فقط به اشتباه بودن تعاريف رشته اتصالي بر مي‌گردد؛‌ يا عدم پشتيباني از تنظيم اضافه‌اي كه در رشته اتصالي ذكر شده است.
مقدار صحيح آن دقيقا مساوي CHARSET=utf8 است (با همين نگارش و رعايت كوچكي و بزرگي حروف؛ مهم!):

<connectionStrings>
    <clear/>
    <add name="Sample09Context"
         connectionString="Datasource=localhost; Database=testdb; Uid=root; Pwd=123;CHARSET=utf8"
         providerName="MySql.Data.MySqlClient"/>
</connectionStrings>


به اين ترتيب، مشكل ثبت عبارات يونيكد فارسي برطرف مي‌شود (البته جدول هم بهتر است به DEFAULT CHARACTER SET utf8 COLLATE utf8_persian_ci تغيير پيدا كند؛ مطابق دستور Alter ايي كه در بالا ذكر شد).

۱۳۹۱/۰۲/۲۹

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 نيز تعريف شده است تا بتوان در صورت نياز، حالت موجوديت‌هاي تغيير كرده يا اضافه شده را به حالت پيش از عمليات، بازگرداند.



۱۳۹۱/۰۲/۲۷

EF Code First #13


استفاده مستقيم از عبارات SQL در EF Code first

طراحي اكثر ORMهاي موجود به نحوي است كه برنامه نهايي شما را مستقل از بانك اطلاعاتي كنند و اين پروايدر نهايي است كه معادل‌هاي صحيح بسياري از توابع توكار بانك اطلاعاتي مورد استفاده را در اختيار EF قرار مي‌دهد. براي مثال در يك بانك اطلاعاتي تابعي به نام substr تعريف شده، در بانك اطلاعاتي ديگري همين تابع substring نام دارد. اگر برنامه را به كمك كوئري‌هاي LINQ تهيه كنيم، نهايتا پروايدر نهايي مخصوص بانك اطلاعاتي مورد استفاده است كه اين معادل‌ها را در اختيار EF قرار مي‌دهد و برنامه بدون مشكل كار خواهد كرد. اما يك سري از موارد شايد معادلي در ساير بانك‌هاي اطلاعاتي نداشته باشند؛ براي مثال رويه‌هاي ذخيره شده يا توابع تعريف شده توسط كاربر. امكان استفاده از يك چنين توانايي‌هايي نيز با اجراي مستقيم عبارات SQL در EF Code first پيش بيني شده و بديهي است در اين حالت برنامه به يك بانك اطلاعاتي خاص گره خواهد خورد؛ همچنين مزيت استفاده از كوئري‌هاي Strongly typed تحت نظر كامپايلر را نيز از دست خواهيم داد. به علاوه بايد به يك سري مسايل امنيتي نيز دقت داشت كه در ادامه بررسي خواهند شد.


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

در مثال جاري قصد داريم نحوه استفاده از رويه‌هاي ذخيره شده و توابع تعريف شده توسط كاربر مخصوص SQL Server را بررسي كنيم. در اينجا كلاس‌هاي پزشك و بيماران او، كلاس‌هاي مدل برنامه را تشكيل مي‌دهند:

using System.Collections.Generic;

namespace EF_Sample08.DomainClasses
{
    public class Doctor
    {
        public int Id { set; get; }
        public string Name { set; get; }

        public virtual ICollection<Patient> Patients { set; get; }
    }
}

namespace EF_Sample08.DomainClasses
{
    public class Patient
    {
        public int Id { set; get; }
        public string Name { set; get; }

        public virtual Doctor Doctor { set; get; }
    }
}

كلاس Context برنامه به نحو زير تعريف شده:

using System.Data.Entity;
using EF_Sample08.DomainClasses;

namespace EF_Sample08.DataLayer.Context
{
    public class Sample08Context : DbContext
    {
        public DbSet<Doctor> Doctors { set; get; }
        public DbSet<Patient> Patients { set; get; }
    }
}

و اينبار كلاس DbMigrationsConfiguration تعريف شده اندكي با مثال‌هاي قبلي متفاوت است:

using System.Data.Entity.Migrations;
using EF_Sample08.DomainClasses;
using System.Collections.Generic;

namespace EF_Sample08.DataLayer.Context
{
    public class Configuration : DbMigrationsConfiguration<Sample08Context>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(Sample08Context context)
        {
            addData(context);
            addSP(context);
            addFn(context);
            base.Seed(context);
        }

        private static void addData(Sample08Context context)
        {
            var patient1 = new Patient { Name = "p1" };
            var patient2 = new Patient { Name = "p2" };
            var doctor1 = new Doctor { Name = "doc1", Patients = new List<Patient> { patient1, patient2 } };
            context.Doctors.Add(doctor1);
        }

        private static void addFn(Sample08Context context)
        {
            context.Database.ExecuteSqlCommand(
                @"IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[FindDoctorPatientsCount]') 
                   AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
                      DROP FUNCTION [dbo].[FindDoctorPatientsCount]");
            context.Database.ExecuteSqlCommand(
                @"CREATE FUNCTION FindDoctorPatientsCount(@Doctor_Id INT)
                    RETURNS INT
                    BEGIN
                     RETURN 
                     (
                         SELECT COUNT(*)
                         FROM   Patients
                         WHERE  Doctor_Id = @Doctor_Id
                     );
                    END");
        }

        private static void addSP(Sample08Context context)
        {
            context.Database.ExecuteSqlCommand(
                @"IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[FindDoctorsStartWith]')
                  AND type in (N'P', N'PC'))
                       DROP PROCEDURE [dbo].[FindDoctorsStartWith]
                 ");
            context.Database.ExecuteSqlCommand(
                @"CREATE PROCEDURE FindDoctorsStartWith(@name NVARCHAR(400))
                  AS
                 SELECT *
                 FROM   Doctors
                 WHERE  [Name] LIKE @name + '%'");
        }
    }
}

در اينجا از متد Seed علاوه بر مقدار دهي اوليه جداول، براي تعريف يك رويه ذخيره شده به نام FindDoctorsStartWith و يك تابع سفارشي به نام FindDoctorPatientsCount نيز استفاده شده است. متد context.Database.ExecuteSqlCommand مستقيما يك عبارت SQL را بر روي بانك اطلاعاتي اجرا مي‌كند.

در ادامه كدهاي كامل برنامه نهايي را ملاحظه مي‌كنيد:
using System;
using System.Data;
using System.Data.Entity;
using System.Data.Objects.SqlClient;
using System.Data.SqlClient;
using System.Linq;
using EF_Sample08.DataLayer.Context;
using EF_Sample08.DomainClasses;

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

            using (var db = new Sample08Context())
            {
                runSp(db);
                runFn(db);
                usingSqlFunctions(db);
            }
        }

        private static void usingSqlFunctions(Sample08Context db)
        {
            var doctorsWithNumericNameList = db.Doctors.Where(x => SqlFunctions.IsNumeric(x.Name) == 1).ToList();
            if (doctorsWithNumericNameList.Any())
            {
                //do something
            }
        }

        private static void runFn(Sample08Context db)
        {
            var doctorIdParameter = new SqlParameter
            {
                ParameterName = "@doctor_id",
                Value = 1,
                SqlDbType = SqlDbType.Int
            };
            var patientsCount = db.Database.SqlQuery<int>("select dbo.FindDoctorPatientsCount(@doctor_id)", doctorIdParameter).FirstOrDefault();
            Console.WriteLine(patientsCount);
        }

        private static void runSp(Sample08Context db)
        {
            var nameParameter = new SqlParameter
            {
                ParameterName = "@name",
                Value = "doc",
                Direction = ParameterDirection.Input,
                SqlDbType = SqlDbType.NVarChar
            };
            var doctors = db.Database.SqlQuery<Doctor>("exec FindDoctorsStartWith @name", nameParameter).ToList();
            if (doctors.Any())
            {
                foreach (var item in doctors)
                {
                    Console.WriteLine(item.Name);
                }
            }
        }
    }
}

توضيحات

همانطور كه ملاحظه مي‌كنيد، براي اجراي مستقيم يك عبارت SQL صرفنظر از اينكه يك رويه ذخيره شده است يا يك تابع و يا يك كوئري معمولي، بايد از متد db.Database.SqlQuery استفاده كرد. خروجي اين متد از نوع IEnumerable است و اين توانايي را دارد كه ركوردهاي بازگشت داده شده از بانك اطلاعاتي را به خواص يك كلاس به صورت خودكار نگاشت كند.
پارامتر اول متد db.Database.SqlQuery، عبارت SQL مورد نظر است. پارامتر دوم آن بايد توسط وهله‌هايي از كلاس SqlParameter مقدار دهي شود. به كمك SqlParameter نام پارامتر مورد استفاده، مقدار و نوع آن مشخص مي‌گردد. همچنين Direction آن نيز براي استفاده از رويه‌هاي ذخيره شده ويژه‌اي كه داراي پارامتري از نوع out هستند درنظر گرفته شده است.

چند نكته

- در متد runSp فوق، متد الحاقي ToList را حذف كرده و برنامه را اجرا كنيد. بلافاصله پيغام خطاي «The SqlParameter is already contained by another SqlParameterCollection.» ظاهر خواهد شد. علت هم اين است كه با بكارگيري متد ToList، تمام عمليات يكبار انجام شده و نتيجه بازگشت داده مي‌شود اما اگر به صورت مستقيم از خروجي IEnumerable آن استفاده كنيم، در حلقه foreach تعريف شده، ممكن است اين فراخواني چندبار انجام شود. به همين جهت ذكر متد ToList در اينجا ضروري است.

- عنوان شد كه در اينجا بايد به مسايل امنيتي دقت داشت. بديهي است امكان نوشتن يك چنين كوئري‌هايي نيز وجود دارد:

db.Database.SqlQuery<Doctor>("exec FindDoctorsStartWith "+ txtName.Text, nameParameter).ToList()

در اين حالت به ظاهر مشغول به استفاده از رويه‌هاي ذخيره شده‌اي هستيم كه عنوان مي‌شود در برابر حملات تزريق SQL در امان هستند، اما چون در كدهاي ما به نحو ناصحيحي با جمع زدن رشته‌ها مقدار دهي شده است، برنامه و بانك اطلاعاتي ديگر در امان نخواهند بود. بنابراين در اين حالت استفاده از پارامترها را نبايد فراموش كرد.
زمانيكه از كوئري‌هاي LINQ استفاده مي‌شود تمام اين مسايل توسط EF مديريت خواهد شد. اما اگر قصد داريد مستقيما عبارات SQL را فراخواني كنيد، تامين امنيت برنامه به عهده خودتان خواهد بود.

- در متد usingSqlFunctions از SqlFunctions.IsNumeric استفاده شده است. اين مورد مختص به SQL Server است و امكان استفاده از توابع توكار ويژه SQL Server را در كوئري‌هاي LINQ برنامه فراهم مي‌سازد. براي مثال متدالحاقي از پيش تعريف شده‌اي به نام IsNumeric به صورت مستقيم در دسترس نيست، اما به كمك كلاس SqlFunctions اين تابع و بسياري از توابع ديگر توكار SQL Server قابل استفاده خواهند بود.
اگر علاقمند هستيد كه ليست اين توابع را مشاهده كنيد، در ويژوال استوديو بر روي SqlFunctions كليك راست كرده و گزينه Go to definition را انتخاب كنيد.


۱۳۹۱/۰۲/۲۶

EF Code First #12


پياده سازي الگوي Context Per Request در برنامه‌هاي مبتني بر EF Code first

در طراحي برنامه‌هاي چند لايه مبتني بر EF مرسوم نيست كه در هر كلاس و متدي كه قرار است از امكانات آن استفاده كند، يكبار DbContext و كلاس مشتق شده از آن وهله سازي شوند؛ به اين ترتيب امكان انجام امور مختلف در طي يك تراكنش از بين مي‌رود. براي حل اين مشكل الگويي مطرح شده است به نام Session/Context Per Request و يا به اشتراك گذاري يك Unit of work در لايه‌هاي مختلف برنامه در طي يك درخواست، كه در ادامه يك پياده سازي آن‌را با هم مرور خواهيم كرد.
البته اين سشن با سشن ASP.NET يكي نيست. در NHibernate معادل DbContextايي كه در اينجا ملاحظه مي‌كنيد، Session نام دارد.


اهميت بكارگيري الگوي Unit of work و به اشتراك گذاري آن در طي يك درخواست

در الگوي واحد كار يا همان DbContext در اينجا، تمام درخواست‌هاي رسيده به آن، در صف قرار گرفته و تمام آن‌ها در پايان كار، به بانك اطلاعاتي اعمال مي‌شوند. براي مثال زمانيكه شيءايي را به يك وهله از DbContext اضافه/حذف مي‌كنيم، يا در ادامه مقدار خاصيتي را تغيير مي‌دهيم، هيچكدام از اين تغييرات تا زمانيكه متد SaveChanges فراخواني نشود، به بانك اطلاعاتي اعمال نخواهند شد. اين مساله مزاياي زير را به همراه خواهد داشت:

الف) كارآيي بهتر
در اينجا از يك كانكشن باز شده، حداكثر استفاده صورت مي‌گيرد. چندين و چند عمليات در طي يك batch به بانك اطلاعاتي اعمال مي‌گردند؛ بجاي اينكه براي اعمال هركدام، يكبار اتصال جداگانه‌اي به بانك اطلاعاتي باز شود.

ب) بررسي مسايل همزماني
استفاده از يك الگوي واحد كار، امكان بررسي خودكار تمام تغييرات انجام شده بر روي يك موجوديت را در متدها و لايه‌هاي مختلف ميسر كرده و به اين ترتيب مسايل مرتبط با ConcurrencyMode عنوان شده در قسمت‌هاي قبل به نحو بهتري قابل مديريت خواهند بود.

ج) استفاده صحيح از تراكنش‌ها
الگوي واحد كار به صورت خودكار از تراكنش‌ها استفاده مي‌كند. اگر در حين فراخواني متد SaveChanges مشكلي رخ دهد، كل عمليات Rollback خواهد شد و تغييري در بانك اطلاعاتي رخ نخواهد داد. بنابراين استفاده از يك تراكنش در حين چند عمليات ناشي از لايه‌هاي مختلف برنامه، منطقي‌تر است تا اينكه هر كدام، در تراكنشي جدا مشغول به كار باشند.


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

در مثالي كه در اين قسمت بررسي خواهيم كرد، از كلاس‌هاي مدل گروه محصولات كمك گرفته شده است:

using System.Collections.Generic;

namespace EF_Sample07.DomainClasses
{
    public class Category
    {
        public int Id { get; set; }
        public virtual string Name { get; set; }
        public virtual string Title { get; set; }

        public virtual ICollection<Product> Products { get; set; }
    }
}

using System.ComponentModel.DataAnnotations;

namespace EF_Sample07.DomainClasses
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }        

        [ForeignKey("CategoryId")]
        public virtual Category Category { get; set; }
        public int CategoryId { get; set; }
    }
}


در كلاس Product، يك خاصيت اضافي به نام CategoryId اضافه شده است كه توسط ويژگي ForeignKey، به عنوان كليد خارجي جدول معرفي خواهد شد. از اين خاصيت در برنامه‌هاي ASP.NET براي مقدار دهي يك كليد خارجي توسط يك DropDownList پر شده با ليست گروه‌ها، استفاده خواهيم كرد.



پياده سازي الگوي واحد كار

همانطور كه در قسمت قبل نيز ذكر شد، DbContext در EF Code first بر اساس الگوي واحد كار تهيه شده است، اما براي به اشتراك گذاشتن آن بين لايه‌هاي مختلف برنامه نياز است يك لايه انتزاعي را براي آن تهيه كنيم، تا بتوان آن‌را به صورت خودكار توسط كتابخانه‌هاي Dependency Injection يا به اختصار DI در زمان نياز به استفاده از آن‌، به كلاس‌هاي استفاده كننده تزريق كنيم. كتابخانه‌ي DI ايي كه در اين قسمت مورد استفاده قرار مي‌گيرد، كتابخانه معروف StructureMap است. براي دريافت آن مي‌توانيد از Nuget استفاده كنيد؛ يا از صفحه اصلي آن در Github : (^).
اينترفيس پايه الگوي واحد كار ما به شرح زير است:

using System.Data.Entity;
using System;

namespace EF_Sample07.DataLayer.Context
{
    public interface IUnitOfWork
    {
        IDbSet<TEntity> Set<TEntity>() where TEntity : class;
        int SaveChanges();
    }
}


براي استفاده اوليه آن، تنها تغييري كه در برنامه حاصل مي‌شود به نحو زير است:

using System.Data.Entity;
using EF_Sample07.DomainClasses;

namespace EF_Sample07.DataLayer.Context
{
    public class Sample07Context : DbContext, IUnitOfWork
    {
        public DbSet<Category> Categories { set; get; }
        public DbSet<Product> Products { set; get; }

        #region IUnitOfWork Members
        public new IDbSet<TEntity> Set<TEntity>() where TEntity : class
        {
            return base.Set<TEntity>();
        }
        #endregion
    }
}

توضيحات:
با كلاس Context در قسمت‌هاي قبل آشنا شده‌ايم. در اينجا به معرفي كلاس‌هايي خواهيم پرداخت كه در معرض ديد EF Code first قرار خواهند گرفت.
DbSetها هم معرف الگوي Repository ‌هستند. كلاس Sample07Context، معرفي الگوي واحد كار يا Unit of work برنامه است.
براي اينكه بتوانيم تعاريف كلاس‌هاي سرويس برنامه را مستقل از تعريف كلاس Sample07Context كنيم، يك اينترفيس جديد را به نام IUnitOfWork به برنامه اضافه كرده‌ايم.
در اينجا كلاس Sample07Context پياده سازي كننده اينترفيس IUnitOfWork خواهد بود (اولين تغيير).
دومين تغيير هم استفاده از متد base.Set مي‌باشد. به اين ترتيب به سادگي مي‌توان به DbSetهاي مختلف در حين كار با IUnitOfWork دسترسي پيدا كرد. به عبارتي ضرورتي ندارد به ازاي تك تك DbSetها يكبار خاصيت جديدي را به اينترفيس IUnitOfWork اضافه كرد. به كمك استفاده از امكانات Generics مهيا، اينبار
uow.Set<Product>

معادل همان db.Products سابق است؛ در حالتيكه از Sample07Context به صورت مستقيم استفاده شود.
همچنين نيازي به پياده سازي متد SaveChanges نيست؛ زيرا پياده سازي آن در كلاس DbContext قرار دارد.


استفاده از الگوي واحد كار در كلاس‌هاي لايه سرويس برنامه

using EF_Sample07.DomainClasses;
using System.Collections.Generic;

namespace EF_Sample07.ServiceLayer
{
    public interface ICategoryService
    {
        void AddNewCategory(Category category);
        IList<Category> GetAllCategories();
    }
}

using EF_Sample07.DomainClasses;
using System.Collections.Generic;

namespace EF_Sample07.ServiceLayer
{
    public interface IProductService
    {
        void AddNewProduct(Product product);
        IList<Product> GetAllProducts();
    }
}

لايه سرويس برنامه را با دو اينترفيس جديد شروع مي‌كنيم. هدف از اين اينترفيس‌ها، ارائه پياده سازي‌هاي متفاوت، به ازاي ORMهاي مختلف است. براي مثال در كلاس‌هاي زير كه نام آن‌ها با Ef شروع شده است، پياده سازي خاص Ef Code first را تدارك خواهيم ديد. اين پياده سازي، قابل انتقال به ساير ORMها نيست چون نه پياده سازي يكساني را از مباحث LINQ ارائه مي‌دهند و نه متدهاي الحاقي همانندي را به همراه دارند و نه اينكه مباحث نگاشت كلاس‌هاي آن‌ها به جداول مختلف يكي است:

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;

namespace EF_Sample07.ServiceLayer
{
    public class EfCategoryService : ICategoryService
    {
        IUnitOfWork _uow;
        IDbSet<Category> _categories;
        public EfCategoryService(IUnitOfWork uow)
        {
            _uow = uow;
            _categories = _uow.Set<Category>();
        }

        public void AddNewCategory(Category category)
        {
            _categories.Add(category);
        }

        public IList<Category> GetAllCategories()
        {
            return _categories.ToList();
        }
    }
}

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;

namespace EF_Sample07.ServiceLayer
{
    public class EfProductService : IProductService
    {
        IUnitOfWork _uow;
        IDbSet<Product> _products;
        public EfProductService(IUnitOfWork uow)
        {
            _uow = uow;
            _products = _uow.Set<Product>();
        }

        public void AddNewProduct(Product product)
        {
            _products.Add(product);
        }

        public IList<Product> GetAllProducts()
        {
            return _products.Include(x => x.Category).ToList();
        }
    }
}


توضيحات:
همانطور كه ملاحظه مي‌كنيد در هيچكدام از كلاس‌هاي سرويس برنامه، وهله سازي مستقيمي از الگوي واحد كار وجود ندارد. اين لايه از برنامه اصلا نمي‌داند كه كلاسي به نام Sample07Context وجود خارجي دارد يا خير.
همچنين لايه اضافي ديگري را به نام Repository جهت مخفي سازي سازوكار EF به برنامه اضافه نكرده‌ايم. اين لايه شايد در نگاه اول برنامه را مستقل از ORM جلوه دهد اما در عمل قابل انتقال نيست و سبب تحميل سربار اضافي بي موردي به برنامه مي‌شود؛ ORMها ويژگي‌هاي يكساني را ارائه نمي‌دهند. حتي در حالت استفاده از LINQ، پياده سازي‌هاي يكساني را به همراه ندارند.
بنابراين اگر قرار است برنامه مستقل از ORM كار كند، نياز است لايه استفاده كننده از سرويس برنامه، با دو اينترفيس IProductService و ICategoryService كار كند و نه به صورت مستقيم با پياده سازي آن‌ها. به اين ترتيب هر زمان كه لازم شد، فقط بايد پياده سازي‌هاي كلاس‌هاي سرويس را تغيير داد؛ باز هم برنامه نهايي بدون نياز به تغييري كار خواهد كرد.

تا اينجا به معماري پيچيده‌اي نرسيده‌ايم و اصطلاحا over-engineering صورت نگرفته است. يك اينترفيس بسيار ساده IUnitOfWork به برنامه اضافه شده؛ در ادامه اين اينترفيس به كلاس‌هاي سرويس برنامه تزريق شده است (تزريق وابستگي در سازنده كلاس). كلاس‌هاي سرويس ما «مي‌دانند» كه EF وجود خارجي دارد و سعي نكرده‌ايم توسط لايه اضافي ديگري آن‌را مخفي كنيم. شيوه كار با IDbSet تعريف شده دقيقا همانند روال متداولي است كه با EF Code first كار مي‌شود و بسيار طبيعي جلوه مي‌كند.


استفاده از الگوي واحد كار و كلاس‌هاي سرويس تهيه شده در يك برنامه كنسول ويندوزي

در ادامه براي وهله سازي اينترفيس‌هاي سرويس و واحد كار برنامه، از كتابخانه StructureMap كه ياد شد، استفاده خواهيم كرد. بنابراين، تمام برنامه‌هاي نهايي ارائه شده در اين قسمت، ارجاعي را به اسمبلي StructureMap.dll نياز خواهند داشت.
كدهاي برنامه كنسول مثال جاري را در ادامه ملاحظه خواهيد كرد:

using System.Collections.Generic;
using System.Data.Entity;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer;
using StructureMap;

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

            HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
            
            ObjectFactory.Initialize(x =>
            {
                x.For<IUnitOfWork>().CacheBy(InstanceScope.Hybrid).Use<Sample07Context>();
                x.For<ICategoryService>().Use<EfCategoryService>();
            });

            var uow = ObjectFactory.GetInstance<IUnitOfWork>();
            var categoryService = ObjectFactory.GetInstance<ICategoryService>();

            var product1 = new Product { Name = "P100", Price = 100 };
            var product2 = new Product { Name = "P200", Price = 200 };
            var category1 = new Category
            {
                Name = "Cat100",
                Title = "Title100",
                Products = new List<Product> { product1, product2 }
            };
            categoryService.AddNewCategory(category1);

            uow.SaveChanges();            
        }
    }
}

در اينجا بيشتر هدف، معرفي نحوه استفاده از StructureMap است.
ابتدا توسط متد ObjectFactory.Initialize مشخص مي‌كنيم كه اگر برنامه نياز به اينترفيس IUnitOfWork داشت، لطفا كلاس Sample07Context را وهله سازي كرده و مورد استفاده قرار بده. اگر ICategoryService مورد استفاده قرار گرفت، وهله مورد نظر بايد از كلاس EfCategoryService تامين شود.
توسط ObjectFactory.GetInstance نيز مي‌توان به وهله‌اي از اين كلاس‌ها دست يافت و نهايتا با فراخواني uow.SaveChanges مي‌توان اطلاعات را ذخيره كرد.

چند نكته:
- به كمك كتابخانه StructureMap، تزريق IUnitOfWork به سازنده كلاس EfCategoryService به صورت خودكار انجام مي‌شود. اگر به كدهاي فوق دقت كنيد ما فقط با اينترفيس‌ها مشغول به كار هستيم، اما وهله‌سازي‌ها در پشت صحنه انجام مي‌شود.
- حين معرفي IUnitOfWork از متد CacheBy با پارامتر InstanceScope.Hybrid استفاده شده است. اين enum مقادير زير را مي‌تواند بپذيرد:

public enum InstanceScope
{
        PerRequest = 0,
        Singleton = 1,
        ThreadLocal = 2,
        HttpContext = 3,
        Hybrid = 4,
        HttpSession = 5,
        HybridHttpSession = 6,
        Unique = 7,
        Transient = 8,
}

براي مثال اگر در برنامه‌اي نياز داشتيد يك كلاس به صورت Singleton عمل كند، فقط كافي است نحوه كش شدن آن‌را تغيير دهيد.
حالت PerRequest در برنامه‌هاي وب كاربرد دارد (و حالت پيش فرض است). با انتخاب آن وهله سازي كلاس مورد نظر به ازاي هر درخواست رسيده انجام خواهد شد.
در حالت ThreadLocal، به ازاي هر Thread، وهله‌اي متفاوت در اختيار مصرف كننده قرار مي‌گيرد.
با انتخاب حالت HttpContext، به ازاي هر HttpContext ايجاد شده، كلاس معرفي شده يكبار وهله سازي مي‌گردد.
حالت Hybrid تركيبي است از حالت‌هاي HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد كرد در غيراينصورت به ThreadLocal سوئيچ مي‌كند.


استفاده از الگوي واحد كار و كلاس‌هاي سرويس تهيه شده در يك برنامه ASP.NET MVC

يك برنامه خالي ASP.NET MVC را آغاز كنيد. سپس يك HomeController جديد را نيز به آن اضافه نمائيد و كدهاي آن‌را مطابق اطلاعات زير تغيير دهيد:

using System.Web.Mvc;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer;
using EF_Sample07.DataLayer.Context;
using System.Collections.Generic;

namespace EF_Sample07.MvcAppSample.Controllers
{
    public class HomeController : Controller
    {
        IProductService _productService;
        ICategoryService _categoryService;
        IUnitOfWork _uow;
        public HomeController(IUnitOfWork uow, IProductService productService, ICategoryService categoryService)
        {
            _productService = productService;
            _categoryService = categoryService;
            _uow = uow;
        }

        [HttpGet]
        public ActionResult Index()
        {
            var list = _productService.GetAllProducts();
            return View(list);
        }

        [HttpGet]
        public ActionResult Create()
        {
            ViewBag.CategoriesList = new SelectList(_categoryService.GetAllCategories(), "Id", "Name");
            return View();
        }

        [HttpPost]
        public ActionResult Create(Product product)
        {
            if (this.ModelState.IsValid)
            {
                _productService.AddNewProduct(product);
                _uow.SaveChanges();
            }
            return RedirectToAction("Index");
        }

        [HttpGet]
        public ActionResult CreateCategory()
        {
            return View();
        }

        [HttpPost]
        public ActionResult CreateCategory(Category category)
        {
            if (this.ModelState.IsValid)
            {
                _categoryService.AddNewCategory(category);
                _uow.SaveChanges();
            }
            return RedirectToAction("Index");
        }
    }
}

نكته مهم اين كنترلر، تزريق وابستگي‌ها در سازنده كلاس كنترلر است؛ به اين ترتيب كنترلر جاري نمي‌داند كه با كدام پياده سازي خاصي از اين اينترفيس‌ها قرار است كار كند.
اگر برنامه را به همين نحو اجرا كنيم، موتور ASP.NET MVC ايراد خواهد گرفت كه يك كنترلر بايد داراي سازنده‌اي بدون پارامتر باشد تا من بتوانم به صورت خودكار وهله‌اي از آن‌را ايجاد كنم. براي رفع اين مشكل از كتابخانه StructureMap براي تزريق خودكار وابستگي‌ها كمك خواهيم گرفت:

using System;
using System.Data.Entity;
using System.Web.Mvc;
using System.Web.Routing;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap;

namespace EF_Sample07.MvcAppSample
{
    // Note: For instructions on enabling IIS6 or IIS7 classic mode, 
    // visit http://go.microsoft.com/?LinkId=9394801

    public class MvcApplication : System.Web.HttpApplication
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

        }

        protected void Application_Start()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
            HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
            initStructureMap();
        }

        private static void initStructureMap()
        {
            ObjectFactory.Initialize(x =>
            {
                x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context());
                x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>();
                x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>();
            });
            //Set current Controller factory as StructureMapControllerFactory
            ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory());
        }

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
        }
    }

    public class StructureMapControllerFactory : DefaultControllerFactory
    {
        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            return ObjectFactory.GetInstance(controllerType) as Controller;
        }
    }
}

توضيحات:
كدهاي فوق متعلق به كلاس Global.asax.cs هستند. در اينجا در متد Application_Start، متد initStructureMap فراخواني شده است.
با پياده سازي ObjectFactory.Initialize در كدهاي برنامه كنسول معرفي شده آشنا شديم. اينبار فقط حالت كش شدن كلاس Context برنامه را HttpContextScoped قرار داده‌ايم تا به ازاي هر درخواست رسيده يك بار الگوي واحد كار وهله سازي شود.
نكته مهمي كه در اينجا اضافه شده‌است، استفاده از متد ControllerBuilder.Current.SetControllerFactory مي‌باشد. اين متد نياز به وهله‌اي از نوع DefaultControllerFactory دارد كه نمونه‌اي از آن‌را در كلاس StructureMapControllerFactory مشاهده مي‌كنيد. به اين ترتيب در زمان وهله سازي خودكار يك كنترلر، اينبار StructureMap وارد عمل شده و وابستگي‌هاي برنامه را مطابق تعاريف ObjectFactory.Initialize ذكر شده، به سازنده كلاس كنترلر تزريق مي‌كند.
همچنين در متد Application_EndRequest با فراخواني ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects از نشتي اتصالات به بانك اطلاعاتي جلوگيري خواهيم كرد. چون وهله الگوي كار برنامه HttpScoped تعريف شده، در پايان يك درخواست به صورت خودكار توسط StructureMap پاكسازي مي‌شود و به نشتي منابع نخواهيم رسيد.


استفاده از الگوي واحد كار و كلاس‌هاي سرويس تهيه شده در يك برنامه ASP.NET Web forms

در يك برنامه ASP.NET Web forms نيز مي‌توان اين مباحث را پياده سازي كرد:

using System;
using System.Data.Entity;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap;

namespace EF_Sample07.WebFormsAppSample
{
    public class Global : System.Web.HttpApplication
    {
        private static void initStructureMap()
        {
            ObjectFactory.Initialize(x =>
            {
                x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context());
                x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>();
                x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>();

                x.SetAllProperties(y=>
                {
                    y.OfType<IUnitOfWork>();
                    y.OfType<ICategoryService>();
                    y.OfType<IProductService>();
                });
            });
        }

        void Application_Start(object sender, EventArgs e)
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
            HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

            initStructureMap();
        }

        void Application_EndRequest(object sender, EventArgs e)
        {
            ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects();
        }

در اينجا كدهاي كلاس Global.asax.cs را ملاحظه مي‌كنيد. توضيحات آن با قسمت ASP.NET MVC آنچنان تفاوتي ندارد و يكي است. البته منهاي تعاريف SetAllProperties كه جديد است و در ادامه به علت اضافه كردن آن‌ها خواهيم رسيد.
در ASP.NET Web forms برخلاف ASP.NET MVC نياز است كار وهله سازي اينترفيس‌ها را به صورت دستي انجام دهيم. براي اين منظور و كاهش كدهاي تكراري برنامه مي‌توان يك كلاس پايه را به نحو زير تعريف كرد:

using System.Web.UI;
using StructureMap;

namespace EF_Sample07.WebFormsAppSample
{
    public class BasePage : Page
    {
        public BasePage()
        {
            ObjectFactory.BuildUp(this);
        }
    }
}

سپس براي استفاده از آن خواهيم داشت:

using System;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer;

namespace EF_Sample07.WebFormsAppSample
{
    public partial class AddProduct : BasePage
    {
        public IUnitOfWork UoW { set; get; }
        public IProductService ProductService { set; get; }
        public ICategoryService CategoryService { set; get; }

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                bindToCategories();
            }
        }

        private void bindToCategories()
        {
            ddlCategories.DataTextField = "Name";
            ddlCategories.DataValueField = "Id";
            ddlCategories.DataSource = CategoryService.GetAllCategories();
            ddlCategories.DataBind();
        }

        protected void btnAdd_Click(object sender, EventArgs e)
        {
            var product = new Product
            {
                Name = txtName.Text,
                Price = int.Parse(txtPrice.Text),
                CategoryId = int.Parse(ddlCategories.SelectedItem.Value)
            };
            ProductService.AddNewProduct(product);
            UoW.SaveChanges();
            Response.Redirect("~/Default.aspx");
        }
    }
}


اينبار وابستگي‌هاي كلاس افزودن محصولات، به صورت خواصي عمومي تعريف شده‌اند. اين خواص عمومي توسط متد SetAllProperties كه در فايل global.asax.cs معرفي شدند، بايد يكبار تعريف شوند (مهم!).
سپس اگر دقت كرده باشيد، اينبار كلاس AddProduct از BasePage ما ارث بري كرده است. در سازند كلاس BasePage، با فراخواني متد ObjectFactory.BuildUp، تزريق وابستگي‌ها به خواص عمومي كلاس جاري صورت مي‌گيرد.
در ادامه نحوه استفاده از اين اينترفيس‌ها را جهت مقدار دهي يك DropDownList يا ذخيره سازي اطلاعات يك محصول مشاهده مي‌كنيد. در اينجا نيز كار با اينترفيس‌ها انجام شده و كلاس جاري دقيقا نمي‌داند كه با چه وهله‌اي مشغول به كار است. تنها در زمان اجرا است كه توسط StructureMap ، به ازاي هر اينترفيس معرفي شده، وهله‌اي مناسب بر اساس تعاريف فايل Global.asax.cs در اختيار برنامه قرار مي‌گيرد.

كدهاي كامل مثال‌هاي اين سري را از آدرس زير هم مي‌توانيد دريافت كنيد: (^)

۱۳۹۱/۰۲/۲۴

EF Code First #11


استفاده از الگوي Repository اضافي در EF Code first؛‌ آري يا خير؟!

اگر در ويژوال استوديو، اشاره‌گر ماوس را بر روي تعريف DbContext قرار دهيم، راهنماي زير ظاهر مي‌شود:

A DbContext instance represents a combination of the Unit Of Work and Repository patterns such that 
it can be used to query from a database and group together changes that will then be written back to 
the store as a unit.  DbContext is conceptually similar to ObjectContext.

در اينجا تيم EF صراحتا عنوان مي‌كند كه DbContext در EF Code first همان الگوي Unit Of Work را پياده سازي كرده و در داخل كلاس‌ مشتق شده از آن، DbSet‌ها همان Repositories هستند (فقط نام‌ها تغيير كرده‌اند؛ اصول يكي است).
به عبارت ديگر با نام بردن صريح از اين الگوها، مقصود زير را دنبال مي‌كنند:
لطفا بر روي اين لايه Abstraction ايي كه ما تهيه ديده‌ايم، يك لايه Abstraction ديگر را ايجاد نكنيد!
«لايه Abstraction ديگر» يعني پياده سازي الگوهاي Unit Of Work و Repository جديد، برفراز الگوهاي Unit Of Work و Repository توكار موجود!
كار اضافه‌اي كه در بسياري از سايت‌ها مشاهده مي‌شود و ... متاسفانه اكثر آن‌ها هم اشتباه هستند! در ذيل روش‌هاي تشخيص پياده سازي‌هاي نادرست الگوي Repository را بر خواهيم شمرد:
1) قرار دادن متد Save تغييرات نهايي انجام شده، در داخل كلاس Repository
متد Save بايد داخل كلاس Unit of work تعريف شود نه داخل كلاس Repository. دقيقا همان كاري كه در EF Code first به درستي انجام شده. متد SaveChanges توسط DbContext ارائه مي‌شود. علت هم اين است كه در زمان Save ممكن است با چندين Entity و چندين جدول مشغول به كار باشيم. حاصل يك تراكنش، بايد نهايتا ذخيره شود نه اينكه هر كدام از اين‌ها، تراكنش خاص خودشان را داشته باشند.
2) نداشتن دركي از الگوي Unit of work
به Unit of work به شكل يك تراكنش نگاه كنيد. در داخل آن با انواع و اقسام موجوديت‌ها از كلاس‌ها و جداول مختلف كار شده و حاصل عمليات، به بانك اطلاعاتي اعمال مي‌گردد. پياده سازي‌هاي اشتباه الگوي Repository، تمام امكانات را در داخل همان كلاس Repository قرار مي‌دهند؛ كه اشتباه است. اين نوع كلاس‌ها فقط براي كار با يك Entity بهينه شده‌اند؛ در حاليكه در دنياي واقعي، اطلاعات ممكن است از دو Entity مختلف دريافت و نتيجه محاسبات مفروضي به Entity سوم اعمال شود. تمام اين عمليات يك تراكنش را تشكيل مي‌دهد، نه اينكه هر كدام، تراكنش مجزاي خود را داشته باشند.
3) وهله سازي از DbContext به صورت مستقيم داخل كلاس Repository
4) Dispose اشياء DbContext داخل كلاس Repository
هر بار وهله سازي DbContext مساوي است با باز شدن يك اتصال به بانك اطلاعاتي و همچنين از آنجائيكه راهنماي ذكر شده فوق را در مورد DbContext مطالعه نكرده‌اند، زمانيكه در يك متد با سه وهله از سه Repository موجوديت‌هاي مختلف كار مي‌كنيد، سه تراكنش و سه اتصال مختلف به بانك اطلاعاتي گشوده شده است. اين مورد ذاتا اشتباه است و سربار بالايي را نيز به همراه دارد.
ضمن اينكه بستن DbContext در يك Repository، امكان اعمال كوئري‌هاي بعدي LINQ را غيرممكن مي‌كند. به ظاهر يك شيء IQueryable در اختيار داريم كه مي‌توان بر روي آن انواع و اقسام كوئري‌هاي LINQ را تعريف كرد اما ... در اينجا با LINQ to Objects كه بر روي اطلاعات موجود در حافظه كار مي‌كند سر و كار نداريم. اتصال به بانك اطلاعاتي با بستن DbContext قطع شده، بنابراين كوئري LINQ بعدي شما كار نخواهد كرد.
همچنين در EF نمي‌توان يك Entity را از يك Context به Context‌ ديگري ارسال كرد. در پياده سازي صحيح الگوي Repository (دقيقا همان چيزي كه در EF Code first به صورت توكار وجود دارد)، Context بايد بين Repositories كه در اينجا فقط نامش DbSet تعريف شده، به اشتراك گذاشته شود. علت هم اين است كه EF از Context براي رديابي تغييرات انجام شده بر روي موجوديت‌ها استفاده مي‌كند (همان سطح اول كش كه در قسمت‌هاي قبل به آن اشاره شد). اگر به ازاي هر Repository يكبار وهله سازي DbContext انجام شود، هر كدام كش جداگانه خاص خود را خواهند داشت.
5) عدم امكان استفاده از تنها يك DbConetext به ازاي يك Http Request
هنگاميكه وهله سازي DbContext به داخل يك Repository منتقل مي‌شود و الگوي واحد كار رعايت نمي‌گردد، امكان به اشتراك گذاري آن بين Repositoryهاي تعريف شده وجود نخواهد داشت. اين مساله در برنامه‌هاي وب سبب كاهش كارآيي مي‌گردد (باز و بسته شدن بيش از حد اتصال به بانك اطلاعاتي در حاليكه مي‌شد تمام اين عمليات را با يك DbContext انجام داد).

نمونه‌اي از اين پياده سازي اشتباه را در اينجا مي‌توانيد پيدا كنيد. متاسفانه شبيه به همين پياده سازي، در پروژه MVC Scaffolding نيز بكارگرفته شده است.


چرا تعريف لايه ديگري بر روي لايه Abstraction موجود در EF Code first اشتباه است؟

يكي از دلايلي كه حين تعريف الگوي Repository دوم بر روي لايه موجود عنوان مي‌شود، اين است:
«به اين ترتيب به سادگي مي‌توان ORM مورد استفاده را تغيير داد» چون پياده سازي استفاده از ORM، در پشت اين لايه مخفي شده و ما هر زمان كه بخواهيم به ORM ديگري كوچ كنيم، فقط كافي است اين لايه را تغيير دهيم و نه كل برنامه‌ را.
ولي سؤال اين است كه هرچند اين مساله از هزار فرسنگ بالاتر درست است، اما واقعا تابحال ديده‌ايد كه پروژه‌اي را با يك ORM شروع كنند و بعد سوئيچ كنند به ORM ديگري؟!
ضمنا براي اينكه واقعا لايه اضافي پياده سازي شده انتقال پذير باشد، شما بايد كاملا دست و پاي ORM موجود را بريده و توانايي‌هاي در دسترس آن را به سطح نازلي كاهش دهيد تا پياده سازي شما قابل انتقال باشد. براي مثال يك سري از قابليت‌هاي پيشرفته و بسيار جالب در NH هست كه در EF نيست و برعكس. آيا واقعا مي‌توان به همين سادگي ORM مورد استفاده را تغيير داد؟ فقط در يك حالت اين امر ميسر است: از قابليت‌هاي پيشرفته ابزار موجود استفاده نكنيم و از آن در سطحي بسيار ساده و ابتدايي كمك بگيريم تا از قابليت‌هاي مشترك بين ORMهاي موجود استفاده شود.
ضمن اينكه مباحث نگاشت كلاس‌ها به جداول را چكار خواهيد كرد؟ EF راه و روش خاص خودش را دارد، NH چندين و چند روش خاص خودش را دارد! اين‌ها به اين سادگي قابل انتقال نيستند كه شخصي عنوان كند: «هر زمان كه علاقمند بوديم، ORM مورد استفاده را مي‌شود عوض كرد!»

دليل دومي كه براي تهيه لايه اضافه‌تري بر روي DbContext عنوان مي‌كنند اين است:
«با استفاده از الگوي Repository نوشتن آزمون‌هاي واحد ساده‌تر مي‌شود». زمانيكه برنامه بر اساس Interfaceها كار مي‌كند مي‌توان آن‌ها را بجاي اشاره به بانك اطلاعاتي، به نمونه‌اي موجود در حافظه، در زمان آزمون تغيير داد.
اين مورد در حالت كلي درست است اما .... نه در مورد بانك‌هاي اطلاعاتي!
زمانيكه در يك آزمون واحد، پياده سازي جديدي از الگوي Interface مخزن ما تهيه مي‌شود و اينبار بجاي بانك اطلاعاتي با يك سري شيء قرارگرفته در حافظه سروكار داريم، آيا موارد زير را هم مي‌توان به سادگي آزمايش كرد؟
ارتباطات بين جداول‌را، cascade delete، فيلدهاي identity، فيلدهاي unique، كليدهاي تركيبي، نوع‌هاي خاص تعريف شده در بانك اطلاعاتي و مسايلي از اين دست.
پاسخ: خير! تغيير انجام شده، سبب كار برنامه با اطلاعات موجود در حافظه خواهد شد، يعني LINQ to Objects.
شما در حالت استفاده از LINQ to Objects آزادي عمل فوق العاده‌اي داريد. مي‌توانيد از انواع و اقسام متدها حين تهيه كوئري‌هاي LINQ استفاده كنيد كه هيچكدام معادلي در بانك اطلاعاتي نداشته و ... به ظاهر آزمون واحد شما پاس مي‌شود؛ اما در عمل بر روي يك بانك اطلاعاتي واقعي كار نخواهد كرد.
البته شايد شخصي عنوان كه بله مي‌شود تمام اين‌ها نيازمندي‌ها را در حالت كار با اشياء درون حافظه هم پياده سازي كرد ولي ... در نهايت پياده سازي آن بسيار پيچيده و در حد پياده سازي يك بانك اطلاعاتي واقعي خواهد شد كه واقعا ضرورتي ندارد.

و پاسخ صحيح در اينجا و اين مساله خاص اين است:
لطفا در حين كار با بانك‌هاي اطلاعاتي مباحث mocking را فراموش كنيد. بجاي SQL Server، رشته اتصالي و تنظيمات برنامه را به SQL Server CE تغيير داده و آزمايشات خود را انجام دهيد. پس از پايان كار هم بانك اطلاعاتي را delete كنيد. به اين نوع آزمون‌ها اصطلاحا integration tests گفته مي‌شود. لازم است برنامه با يك بانك اطلاعاتي واقعي تست شود و نه يك سري شيء ساده قرار گرفته در حافظه كه هيچ قيدي همانند شرايط كار با يك بانك اطلاعاتي واقعي، بر روي آ‌ن‌ها اعمال نمي‌شود.
ضمنا بايد درنظر داشت بانك‌هاي اطلاعاتي كه تنها در حافظه كار كنند نيز وجود دارند. براي مثال SQLite حالت كار كردن صرفا در حافظه را پشتيباني مي‌كند. زمانيكه آزمون واحد شروع مي‌شود، يك بانك اطلاعاتي واقعي را در حافظه تشكيل داده و پس از پايان كار هم ... اثري از اين بانك اطلاعاتي باقي نخواهد ماند و براي اين نوع كارها بسيار سريع است.


نتيجه گيري:
حين استفاده از EF code first، الگوي واحد كار، همان DbContext است و الگوي مخزن، همان DbSetها. ضرورتي به ايجاد يك لايه محافظ اضافي بر روي اين‌ها وجود ندارد.
در اينجا بهتر است يك لايه اضافي را به نام مثلا Service ايجاد كرد و تمام اعمال كار با EF را به آن منتقل نمود. سپس در قسمت‌هاي مختلف برنامه مي‌توان از متدهاي اين لايه استفاده كرد. به عبارتي در فايل‌هاي Code behind برنامه شما نبايد كدهاي EF مشاهده شوند. يا در كنترلرهاي MVC نيز به همين ترتيب. اين‌ها مصرف كننده نهايي لايه سرويس ايجاد شده خواهند بود.
همچنين بجاي نوشتن آزمون‌هاي واحد، به Integration tests سوئيچ كنيد تا بتوان برنامه را در شرايط كار با يك بانك اطلاعاتي واقعي تست كرد.


براي مطالعه بيشتر:

۱۳۹۱/۰۲/۲۳

EF Code First #10


حين كار با ORMهاي پيشرفته، ويژگي‌هاي جالب توجهي در اختيار برنامه نويس‌ها قرار مي‌گيرد كه در زمان استفاده از كلاس‌هاي متداول SQLHelper از آن‌ها خبري نيست؛ مانند:
الف) Deferred execution
ب) Lazy loading
ج) Eager loading

نحوه بررسي SQL نهايي توليدي توسط EF

براي توضيح موارد فوق، نياز به مشاهده خروجي SQL نهايي حاصل از ORM است و همچنين شمارش تعداد بار رفت و برگشت به بانك اطلاعاتي. بهترين ابزاري را كه براي اين منظور مي‌توان پيشنهاد داد، برنامه EF Profiler است. براي دريافت آن مي‌توانيد به اين آدرس مراجعه كنيد: (^) و (^)

پس از وارد كردن نام و آدرس ايميل، يك مجوز يك ماهه آزمايشي، به آدرس ايميل شما ارسال خواهد شد.
زمانيكه اين فايل را در ابتداي اجراي برنامه به آن معرفي مي‌كنيد، محل ذخيره سازي نهايي آن جهت بازبيني بعدي، مسير MyUserName\Local Settings\Application Data\EntityFramework Profiler خواهد بود.

استفاده از اين برنامه هم بسيار ساده است:
الف) در برنامه خود، ارجاعي را به اسمبلي HibernatingRhinos.Profiler.Appender.dll كه در پوشه برنامه EFProf موجود است، اضافه كنيد.
ب) در نقطه آغاز برنامه، متد زير را فراخواني نمائيد:
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

نقطه آغاز برنامه مي‌تواند متد Application_Start برنامه‌هاي وب، در متد Program.Main برنامه‌هاي ويندوزي كنسول و WinForms و در سازنده كلاس App برنامه‌هاي WPF باشد.
ج) برنامه EFProf را اجرا كنيد.

مزاياي استفاده از اين برنامه
1) وابسته به بانك اطلاعاتي مورد استفاده نيست. (برخلاف براي مثال برنامه معروف SQL Server Profiler كه فقط به همراه SQL Server ارائه مي‌شود)
2) خروجي SQL نمايش داده شده را فرمت كرده و به همراه Syntax highlighting نيز هست.
3) كار اين برنامه صرفا به لاگ كردن SQL توليدي خلاصه نمي‌شود. يك سري از Best practices را نيز به شما گوشزد مي‌كند. بنابراين اگر نياز داريد سيستم خود را بر اساس ديدگاه يك متخصص بررسي كنيد (يك Code review ارزشمند)، اين ابزار مي‌تواند بسيار مفيد باشد.
4) مي‌تواند كوئري‌هاي سنگين و سبك را به خوبي تشخيص داده و گزارشات آماري جالبي را به شما ارائه دهد.
5) مي‌تواند دقيقا مشخص كند، كوئري را كه مشاهده مي‌كنيد از طريق كدام متد در كدام كلاس صادر شده است و دقيقا از چه سطري.
6) امكان گروه بندي خودكار كوئري‌هاي صادر شده را بر اساس DbContext مورد استفاده به همراه دارد.
و ...

استفاده از اين برنامه حين كار با EF «الزامي» است! (البته نسخه‌هاي NH و ساير ORMهاي ديگر آن نيز موجود است و اين مباحث در مورد تمام ORMهاي پيشرفته صادق است)
مدام بايد بررسي كرد كه صفحه جاري چه تعداد كوئري را به بانك اطلاعاتي ارسال كرده و به چه نحوي. همچنين آيا مي‌توان با اعمال اصلاحاتي، اين وضع را بهبود بخشيد. بنابراين عدم استفاده از اين برنامه حين كار با ORMs، همانند راه رفتن در خواب است! ممكن است تصور كنيد برنامه دارد به خوبي كار مي‌كند اما ... در پشت صحنه فقط صفحه جاري برنامه، 100 كوئري را به بانك اطلاعاتي ارسال كرده، در حاليكه شما تنها نياز به يك كوئري داشته‌ايد.


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

كلاس‌هاي مدل مثال جاري از يك دپارتمان كه داراي تعدادي كارمند مي‌باشد، تشكيل شده است. ضمنا هر كارمند تنها در يك دپارتمان مي‌تواند مشغول به كار باشد و رابطه many-to-many نيست :

using System.Collections.Generic;

namespace EF_Sample06.Models
{
    public class Department
    {
        public int DepartmentId { get; set; }
        public string Name { get; set; }

        //Creates Employee navigation property for Lazy Loading (1:many)
        public virtual ICollection<Employee> Employees { get; set; }
    }
}

namespace EF_Sample06.Models
{
    public class Employee
    {
        public int EmployeeId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }        

        //Creates Department navigation property for Lazy Loading
        public virtual Department Department { get; set; }
    }
}

نگاشت دستي اين كلاس‌ها هم ضرورتي ندارد، زيرا قراردادهاي توكار EF Code first را رعايت كرده و EF در اينجا به سادگي مي‌تواند primary key و روابط one-to-many را بر اساس navigation properties تعريف شده، تشخيص دهد.

در اينجا كلاس Context برنامه به شرح زير است:

using System.Data.Entity;
using EF_Sample06.Models;

namespace EF_Sample06.DataLayer
{
    public class Sample06Context : DbContext
    {
        public DbSet<Department> Departments { set; get; }
        public DbSet<Employee> Employees { set; get; }
    }
}


و تنظيمات ابتدايي نحوه به روز رساني و آغاز بانك اطلاعاتي نيز مطابق كدهاي زير مي‌باشد:

using System.Collections.Generic;
using System.Data.Entity.Migrations;
using EF_Sample06.Models;

namespace EF_Sample06.DataLayer
{
    public class Configuration : DbMigrationsConfiguration<Sample06Context>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(Sample06Context context)
        {
            var employee1 = new Employee { FirstName = "f name1", LastName = "l name1" };
            var employee2 = new Employee { FirstName = "f name2", LastName = "l name2" };
            var employee3 = new Employee { FirstName = "f name3", LastName = "l name3" };
            var employee4 = new Employee { FirstName = "f name4", LastName = "l name4" };

            var dept1 = new Department { Name = "dept 1", Employees = new List<Employee> { employee1, employee2 } };
            var dept2 = new Department { Name = "dept 2", Employees = new List<Employee> { employee3 } };
            var dept3 = new Department { Name = "dept 3", Employees = new List<Employee> { employee4 } };

            context.Departments.Add(dept1);
            context.Departments.Add(dept2);
            context.Departments.Add(dept3);
            base.Seed(context);
        }
    }
}

نكته: تهيه خروجي XML از نگاشت‌هاي خودكار تهيه شده

اگر علاقمند باشيد كه پشت صحنه نگاشت‌هاي خودكار EF Code first را در يك فايل XML جهت بررسي بيشتر ذخيره كنيد، مي‌توان از متد كمكي زير استفاده كرد:

void ExportMappings(DbContext context, string edmxFile)
{
     var settings = new XmlWriterSettings { Indent = true };
     using (XmlWriter writer = XmlWriter.Create(edmxFile, settings))
     {
         System.Data.Entity.Infrastructure.EdmxWriter.WriteEdmx(context, writer);
     }
}

بهتر است پسوند فايل XML توليدي را edmx قيد كنيد تا بتوان آن‌را با دوبار كليك بر روي فايل، در ويژوال استوديو نيز مشاهده كرد:

using (var db = new Sample06Context())
{
     ExportMappings(db, "mappings.edmx");
}



الف) بررسي Deferred execution يا بارگذاري به تاخير افتاده

براي توضيح مفهوم Deferred loading/execution بهترين مثالي را كه مي‌توان ارائه داد، صفحات جستجوي تركيبي در برنامه‌ها است. براي مثال يك صفحه جستجو را طراحي كرده‌ايد كه حاوي دو تكست باكس دريافت FirstName و LastName كاربر است. كنار هر كدام از اين تكست باكس‌ها نيز يك چك‌باكس قرار دارد. به عبارتي كاربر مي‌تواند جستجويي تركيبي را در اينجا انجام دهد. نحوه پياده سازي صحيح اين نوع مثال‌ها در EF Code first به چه نحوي است؟

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample06.DataLayer;
using EF_Sample06.Models;

namespace EF_Sample06
{
    class Program
    {
        static IList<Employee> FindEmployees(string fName, string lName, bool byName, bool byLName)
        { 
            using (var db = new Sample06Context())
            {
                IQueryable<Employee> query = db.Employees.AsQueryable();

                if (byLName)
                {
                    query = query.Where(x => x.LastName == lName);
                }

                if (byName)
                {
                    query = query.Where(x => x.FirstName == fName);
                }

                return query.ToList();
            }
        }

        static void Main(string[] args)
        {
            // note: remove this line if you received : create database is not supported by this provider.
            HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

            Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample06Context, Configuration>());

            var list = FindEmployees("f name1", "l name1", true, true);
            foreach (var item in list)
            {
                Console.WriteLine(item.FirstName);
            }
        }
    }
}

نحوه صحيح اين نوع پياده سازي تركيبي را در متد FindEmployees مشاهده مي‌كنيد. نكته مهم آن، استفاده از نوع IQueryable و متد AsQueryable است و امكان تركيب كوئري‌ها با هم.
به نظر شما با فراخواني متد FindEmployees به نحو زير كه هر دو شرط آن توسط كاربر انتخاب شده است، چه تعداد كوئري به بانك اطلاعاتي ارسال مي‌شود؟

var list = FindEmployees("f name1", "l name1", true, true);

شايد پاسخ دهيد كه سه بار : يكبار در متد db.Employees.AsQueryable و دوبار هم در حين ورود به بدنه شرط‌هاي ياد شده و اينجا است كه كساني كه قبلا با رويه‌هاي ذخيره شده كار كرده باشند، شروع به فرياد و فغان مي‌كنند كه ما قبلا اين مسايل رو با يك SP در يك رفت و برگشت مديريت مي‌كرديم!
پاسخ صحيح: «فقط يكبار»! آن‌هم تنها در زمان فراخواني متد ToList و نه قبل از آن.
براي اثبات اين مدعا نياز است به خروجي SQL لاگ شده توسط EF Profiler مراجعه كرد:

SELECT [Extent1].[EmployeeId]              AS [EmployeeId],
       [Extent1].[FirstName]               AS [FirstName],
       [Extent1].[LastName]                AS [LastName],
       [Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM   [dbo].[Employees] AS [Extent1]
WHERE  ([Extent1].[LastName] = 'l name1' /* @p__linq__0 */)
       AND ([Extent1].[FirstName] = 'f name1' /* @p__linq__1 */)


IQueryable قلب LINQ است و تنها بيانگر يك عبارت (expression) از ركوردهايي مي‌باشد كه مد نظر شما است و نه بيشتر. براي مثال زمانيكه يك IQueryable را همانند مثال فوق فيلتر مي‌كنيد، هنوز چيزي از بانك اطلاعاتي يا منبع داده‌اي دريافت نشده است. هنوز هيچ اتفاقي رخ نداده است و هنوز رفت و برگشتي به منبع داده‌اي صورت نگرفته است. به آن بايد به شكل يك expression builder نگاه كرد و نه ليستي از اشياء فيلتر شده‌ي ما. به اين مفهوم، deferred execution (اجراي به تاخير افتاده) نيز گفته مي‌شود.
كوئري LINQ شما تنها زماني بر روي بانك اطلاعاتي اجرا مي‌شود كه كاري بر روي آن صورت گيرد مانند فراخواني متد ToList، فراخواني متد First يا FirstOrDefault و امثال آن. تا پيش از اين فقط به شكل يك عبارت در برنامه وجود دارد و نه بيشتر.
اطلاعات بيشتر: «تفاوت بين IQueryable و IEnumerable در حين كار با ORMs»



ب) بررسي Lazy Loading يا واكشي در صورت نياز

در مطلب جاري اگر به كلاس‌هاي مدل برنامه دقت كنيد، تعدادي از خواص به صورت virtual تعريف شده‌اند. چرا؟
تعريف يك خاصيت به صورت virtual، پايه و اساس lazy loading است و به كمك آن، تا به اطلاعات شيءايي نياز نباشد، وهله سازي نخواهد شد. به اين ترتيب مي‌توان به كارآيي بيشتري در حين كار با ORMs رسيد. براي مثال در كلاس‌هاي فوق، اگر تنها نياز به دريافت نام يك دپارتمان هست، نبايد حين وهله سازي از شيء دپارتمان، شيء ليست كارمندان مرتبط با آن نيز وهله سازي شده و از بانك اطلاعاتي دريافت شوند. به اين وهله سازي با تاخير، lazy loading گفته مي‌شود.
Lazy loading پياده سازي ساده‌اي نداشته و مبتني است بر بكارگيري AOP frameworks يا كتابخانه‌هايي كه امكان تشكيل اشياء Proxy پويا را در پشت صحنه فراهم مي‌كنند. علت virtual تعريف كردن خواص رابط نيز به همين مساله بر مي‌گردد، تا اين نوع كتابخانه‌ها بتوانند در نحوه تعريف اينگونه خواص virtual در زمان اجرا، در پشت صحنه دخل و تصرف كنند. البته حين استفاده از EF يا انواع و اقسام ORMs ديگر با اين نوع پيچيدگي‌ها روبرو نخواهيم شد و تشكيل اشياء Proxy در پشت صحنه انجام مي‌شوند.

يك مثال: قصد داريم اولين دپارتمان ثبت شده در حين آغاز برنامه را يافته و سپس ليست كارمندان آن‌را نمايش دهيم:

using (var db = new Sample06Context())
{
    var dept1 = db.Departments.Find(1);
    if (dept1 != null)
    {
        Console.WriteLine(dept1.Name);
        foreach (var item in dept1.Employees)
        {
             Console.WriteLine(item.FirstName);
        }
    }
}



رفتار يك ORM جهت تعيين اينكه آيا نياز است براي دريافت اطلاعات بين جداول Join صورت گيرد يا خير، واكشي حريصانه و غيرحريصانه را مشخص مي‌سازد.
در حالت واكشي حريصانه به ORM خواهيم گفت كه لطفا جهت دريافت اطلاعات فيلدهاي جداول مختلف، از همان ابتداي كار در پشت صحنه، Join هاي لازم را تدارك ببين. در حالت واكشي غيرحريصانه به ORM خواهيم گفت به هيچ عنوان حق نداري Join ايي را تشكيل دهي. هر زماني كه نياز به اطلاعات فيلدي از جدولي ديگر بود بايد به صورت مستقيم به آن مراجعه كرده و آن مقدار را دريافت كني.
به صورت خلاصه برنامه نويس در حين كار با ORM هاي پيشرفته نيازي نيست Join بنويسد. تنها بايد ORM را طوري تنظيم كند كه آيا اينكار را حتما خودش در پشت صحنه انجام دهد (واكشي حريصانه)، يا اينكه خير، به هيچ عنوان SQL هاي توليدي در پشت صحنه نبايد حاوي Join باشند (lazy loading).

در مثال فوق به صورت خودكار دو كوئري به بانك اطلاعاتي ارسال مي‌گردد:

SELECT [Limit1].[DepartmentId] AS [DepartmentId],
       [Limit1].[Name]         AS [Name]
FROM   (SELECT TOP (2) [Extent1].[DepartmentId] AS [DepartmentId],
                       [Extent1].[Name]         AS [Name]
        FROM   [dbo].[Departments] AS [Extent1]
        WHERE  [Extent1].[DepartmentId] = 1 /* @p0 */) AS [Limit1]


SELECT [Extent1].[EmployeeId]              AS [EmployeeId],
       [Extent1].[FirstName]               AS [FirstName],
       [Extent1].[LastName]                AS [LastName],
       [Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM   [dbo].[Employees] AS [Extent1]
WHERE  ([Extent1].[Department_DepartmentId] IS NOT NULL)
       AND ([Extent1].[Department_DepartmentId] = 1 /* @EntityKeyValue1 */)

يكبار زمانيكه قرار است اطلاعات دپارتمان‌ يك (db.Departments.Find) دريافت شود. تا اين لحظه خبري از جدول Employees نيست. چون lazy loading فعال است و فقط اطلاعاتي را كه نياز داشته‌ايم فراهم كرده است.
زمانيكه برنامه به حلقه مي‌رسد، نياز است اطلاعات dept1.Employees را دريافت كند. در اينجا است كه كوئري دوم، به بانك اطلاعاتي صادر خواهد شد (بارگذاري در صورت نياز).


ج) بررسي Eager Loading يا واكشي حريصانه

حالت lazy loading بسيار جذاب به نظر مي‌رسد؛ براي مثال مي‌توان خواص حجيم يك جدول را به جدول مرتبط ديگري منتقل كرد. مثلا فيلد‌هاي متني طولاني يا اطلاعات باينري فايل‌هاي ذخيره شده، تصاوير و امثال آن. به اين ترتيب تا زمانيكه نيازي به اينگونه اطلاعات نباشد، lazy loading از بارگذاري آن‌ها جلوگيري كرده و سبب افزايش كارآيي برنامه مي‌شود.
اما ... همين lazy loading در صورت استفاده نا آگاهانه مي‌تواند سرور بانك اطلاعاتي را در يك برنامه چندكاربره از پا درآورد! نيازي هم نيست تا شخصي به سايت شما حمله كند. مهاجم اصلي همان برنامه نويس كم اطلاع است!
اينبار مثال زير را درنظر بگيريد كه بجاي دريافت اطلاعات يك شخص، مثلا قصد داريم، اطلاعات كليه دپارتمان‌ها را توسط يك Grid نمايش دهيم (فرقي نمي‌كند برنامه وب يا ويندوز باشد؛ اصول يكي است):

using (var db = new Sample06Context())
{
      foreach (var dept in db.Departments)
      {
           Console.WriteLine(dept.Name);
           foreach (var item in dept.Employees)
           {
                Console.WriteLine(item.FirstName);
            }
        }
}
يك نكته: اگر سعي كنيم كد فوق را اجرا كنيم به خطاي زير برخواهيم خورد:

There is already an open DataReader associated with this Command which must be closed first

براي رفع اين مشكل نياز است گزينه MultipleActiveResultSets=True را به كانكشن استرينگ اضافه كرد:

<connectionStrings>
    <clear/>
    <add
       name="Sample06Context"
       connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true;MultipleActiveResultSets=True;"
       providerName="System.Data.SqlClient"
      />
</connectionStrings>

سؤال: به نظر شما در دو حلقه تو در توي فوق چندبار رفت و برگشت به بانك اطلاعاتي صورت مي‌گيرد؟ با توجه به اينكه در متد Seed ذكر شده در ابتداي مطلب، تعداد ركوردها مشخص است.
پاسخ: 7 بار!


و اينجا است كه عنوان شد استفاده از EF Profiler در حين توسعه برنامه‌هاي مبتني بر ORM «الزامي» است! اگر از اين نكته اطلاعي نداشتيد، بهتر است يكبار تمام صفحات گزارش‌گيري برنامه‌هاي خود را كه حاوي يك Grid هستند، توسط EF Profiler بررسي كنيد. اگر در اين برنامه پيغام خطاي n+1 select را دريافت كرديد، يعني در حال استفاده ناصحيح از امكانات lazy loading مي‌باشيد.

آيا مي‌توان اين وضعيت را بهبود بخشيد؟ زمانيكه كار ما گزارشگيري از اطلاعات با تعداد ركوردهاي بالا است، استفاده ناصحيح از ويژگي Lazy loading مي‌تواند به شدت كارآيي بانك اطلاعاتي را پايين بياورد. براي حل اين مساله در زمان‌هاي قديم (!) بين جداول join مي‌نوشتند؛ الان چطور؟
در EF متدي به نام Include جهت Eager loading اطلاعات موجوديت‌هاي مرتبط به هم درنظر گرفته شده است كه در پشت صحنه همينكار را انجام مي‌دهد:

using (var db = new Sample06Context())
{
      foreach (var dept in db.Departments.Include(x => x.Employees))
      {
           Console.WriteLine(dept.Name);
           foreach (var item in dept.Employees)
           {
              Console.WriteLine(item.FirstName);
           }
       }
}

همانطور كه ملاحظه مي‌كنيد اينبار به كمك متد Include، نسبت به واكشي حريصانه Employees اقدام كرده‌ايم. اكنون اگر برنامه را اجرا كنيم، فقط يك رفت و برگشت به بانك اطلاعاتي انجام خواهد شد و كار Join نويسي به صورت خودكار توسط EF مديريت مي‌گردد:

SELECT [Project1].[DepartmentId]            AS [DepartmentId],
       [Project1].[Name]                    AS [Name],
       [Project1].[C1]                      AS [C1],
       [Project1].[EmployeeId]              AS [EmployeeId],
       [Project1].[FirstName]               AS [FirstName],
       [Project1].[LastName]                AS [LastName],
       [Project1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM   (SELECT [Extent1].[DepartmentId]            AS [DepartmentId],
               [Extent1].[Name]                    AS [Name],
               [Extent2].[EmployeeId]              AS [EmployeeId],
               [Extent2].[FirstName]               AS [FirstName],
               [Extent2].[LastName]                AS [LastName],
               [Extent2].[Department_DepartmentId] AS [Department_DepartmentId],
               CASE
                 WHEN ([Extent2].[EmployeeId] IS NULL) THEN CAST(NULL AS int)
                 ELSE 1
               END                                 AS [C1]
        FROM   [dbo].[Departments] AS [Extent1]
               LEFT OUTER JOIN [dbo].[Employees] AS [Extent2]
                 ON [Extent1].[DepartmentId] = [Extent2].[Department_DepartmentId]) AS [Project1]
ORDER  BY [Project1].[DepartmentId] ASC,
          [Project1].[C1] ASC


متد Include در نگارش‌هاي اخير EF پيشرفت كرده است و همانند مثال فوق، امكان كار با lambda expressions را جهت تعريف خواص مورد نظر به صورت strongly typed ارائه مي‌دهد. در نگارش‌هاي قبلي اين متد، تنها امكان استفاده از رشته‌ها براي معرفي خواص وجود داشت.
همچنين توسط متد Include امكان eager loading چندين سطح با هم نيز وجود دارد؛ مثلا x.Employees.Kids و همانند آن.


چند نكته در مورد نحوه خاموش كردن Lazy loading

امكان خاموش كردن Lazy loading در تمام كلاس‌هاي برنامه با تنظيم خاصيت Configuration.LazyLoadingEnabled كلاس Context برنامه به نحو زير ميسر است:

public class Sample06Context : DbContext
{
        public Sample06Context()
        {
            this.Configuration.LazyLoadingEnabled = false;
        }

يا اگر تنها در مورد يك كلاس نياز است اين خاموش سازي صورت گيرد، كلمه كليدي virtual را حذف كنيد. براي مثال با نوشتن public ICollection<Employee> Employees بجاي public virtual ICollection<Employee> Employees در اولين بار وهله سازي كلاس دپارتمان، ليست كارمندان آن به نال تنظيم مي‌شود. البته در اين حالت null object pattern را نيز فراموش نكنيد (وهله سازي پيش فرض Employees در سازنده كلاس):

public class Department
{
     public int DepartmentId { get; set; }
     public string Name { get; set; }

     public  ICollection<Employee> Employees { get; set; }
     public Department()
     {
         Employees = new HashSet<Employee>();
     }
}

به اين ترتيب به خطاي null reference object بر نخواهيم خورد. همچنين وهله سازي، با مقدار دهي ليست دريافتي از بانك اطلاعاتي متفاوت است. در اينجا نيز بايد از متد Include استفاده كرد.

بنابراين در صورت خاموش كردن lazy loading، حتما نياز است از متد Include استفاده شود. اگرlazy loading فعال است، جهت تبديل آن به eager loading از متد Include استفاده كنيد (اما اجباري نيست).

۱۳۹۱/۰۲/۲۲

EF Code First #9


تنظيمات ارث بري كلاس‌ها در EF Code first


بانك‌هاي اطلاعاتي مبتني بر SQL، تنها روابطي از نوع «has a» يا «داراي» را پشتيباني مي‌كنند؛ اما در دنياي شيءگرا روابطي مانند «is a» يا «هست» نيز قابل تعريف هستند. براي توضيحات بيشتر به مدل‌هاي زير دقت نمائيد:


using System;

namespace EF_Sample05.DomainClasses.Models
{
    public abstract class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime DateOfBirth { get; set; }
    }
}

namespace EF_Sample05.DomainClasses.Models
{
    public class Coach : Person
    {
        public string TeamName { set; get; }        
    }
}

namespace EF_Sample05.DomainClasses.Models
{
    public class Player : Person
    {
        public int Number { get; set; }        
        public string Description { get; set; }
    }
}

در اين مدل‌ها كه بر اساس ارث بري از كلاس شخص، تهيه شده‌اند؛ بازيكن، يك شخص است. مربي نيز يك شخص است؛ و به اين ترتيب خوانده مي‌شوند:

Coach "is a" Person
Player "is a" Person

در EF Code first سه روش جهت كار با اين نوع كلاس‌ها و كلا ارث بري وجود دارد كه در ادامه به آن‌ها خواهيم پرداخت:

الف) Table per Hierarchy يا TPH

همانطور كه از نام آن نيز پيدا است، كل سلسله مراتبي را كه توسط ارث بري تعريف شده است، تبديل به يك جدول در بانك اطلاعاتي مي‌كند. اين حالت، شيوه برخورد پيش فرض EF Code first با ارث بري كلاس‌ها است و نياز به هيچگونه تنظيم خاصي ندارد.
براي آزمايش اين مساله، كلاس Context را به نحو زير تعريف نمائيد و سپس اجازه دهيد تا EF بانك اطلاعاتي معادل آن‌را توليد كند:

using System.Data.Entity;
using EF_Sample05.DomainClasses.Models;

namespace EF_Sample05.DataLayer.Context
{
    public class Sample05Context : DbContext
    {
        public DbSet<Person> People { set; get; }
    }
}

ساختار جدول توليد شده آن همانند تصوير زير است:


همانطور كه ملاحظه مي‌كنيد، تمام كلاس‌هاي مشتق شده از كلاس شخص را تبديل به يك جدول كرده است؛ به علاوه يك فيلد جديد را هم به نام Discriminator به اين جدول اضافه نموده است. براي درك بهتر عملكرد اين فيلد، چند ركورد را توسط برنامه به بانك اطلاعاتي اضافه مي‌كنيم. حاصل آن به شكل زير خواهد بود:


از فيلد Discriminator جهت ثبت نام كلاس‌هاي متناظر با هر ركورد، استفاده شده است. به اين ترتيب EF حين كار با اشياء دقيقا مي‌داند كه چگونه بايد خواص متناظر با كلاس‌هاي مختلف را مقدار دهي كند.
به علاوه اگر به ساختار جدول تهيه شده دقت كنيد، مشخص است كه در حالت TPH، نياز است فيلدهاي متناظر با كلاس‌هاي مشتق شده از كلاس پايه، همگي null پذير باشند. براي نمونه فيلد Number كه از نوع int تعريف شده، در سمت بانك اطلاعاتي نال پذير تعريف شده است.
و براي كوئري نوشتن در اين حالت مي‌توان از متد الحاقي OfType جهت فيلتر كردن اطلاعات بر اساس كلاسي خاص، كمك گرفت:

db.People.OfType<Coach>().FirstOrDefault(x => x.LastName == "Coach L1")


سفارشي سازي نحوه نگاشت TPH

همانطور كه عنوان شد، TPH‌ نياز به تنظيمات خاصي ندارد و حالت پيش فرض است؛ اما براي مثال مي‌توان بر روي مقادير و نوع ستون Discriminator توليدي، كنترل داشت. براي اين منظور بايد از Fluent API به نحو زير استفاده كرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;

namespace EF_Sample05.DataLayer.Mappings
{
    public class CoachConfig : EntityTypeConfiguration<Coach>
    {
        public CoachConfig()
        {
            // For TPH
            this.Map(m => m.Requires(discriminator: "PersonType").HasValue(1));
        }
    }
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;

namespace EF_Sample05.DataLayer.Mappings
{
    public class PlayerConfig : EntityTypeConfiguration<Player>
    {
        public PlayerConfig()
        {
            // For TPH
            this.Map(m => m.Requires(discriminator: "PersonType").HasValue(2));
        }
    }
}

در اينجا توسط متد Map، نام فيلد discriminator به PersonType تغيير كرده. همچنين چون مقدار پيش فرض تعيين شده توسط متد HasValue عددي است، نوع اين فيلد در سمت بانك اطلاعاتي به int null تغيير مي‌كند.


ب) Table per Type يا TPT

در حالت TPT، به ازاي هر كلاس موجود در سلسله مراتب تعيين شده، يك جدول در سمت بانك اطلاعاتي تشكيل مي‌گردد.
در جداول متناظر با Sub classes، تنها همان فيلدهايي وجود خواهند داشت كه در كلاس‌هاي هم نام وجود دارد و فيلدهاي كلاس پايه در آن‌ها ذكر نخواهد گرديد. همچنين اين جداول داراي يك Primary key نيز خواهند بود (كه دقيقا همان كليد اصلي جدول پايه است كه به آن Shared primary key هم گفته مي‌شود). اين كليد اصلي، به عنوان كليد خارجي اشاره كننده به كلاس يا جدول پايه نيز تنظيم مي‌گردد:


براي تنظيم اين نوع ارث بري، تنها كافي است ويژگي Table را بر روي Sub classes قرار داد:

using System.ComponentModel.DataAnnotations;

namespace EF_Sample05.DomainClasses.Models
{
    [Table("Coaches")]
    public class Coach : Person
    {
        public string TeamName { set; get; }        
    }
}

using System.ComponentModel.DataAnnotations;

namespace EF_Sample05.DomainClasses.Models
{
    [Table("Players")]
    public class Player : Person
    {
        public int Number { get; set; }        
        public string Description { get; set; }
    }
}

يا اگر حالت Fluent API را ترجيح مي‌دهيد، همانطور كه در قسمت‌هاي قبل نيز ذكر شد، معادل ويژگي Table در اينجا، متد ToTable است.

ج) Table per Concrete type يا TPC

در تعاريف ارث بري كه تاكنون بررسي كرديم، مرسوم است كلاس پايه را از نوع abstract تعريف كنند. به اين ترتيب هدف اصلي، Sub classes تعريف شده خواهند بود؛ چون نمي‌توان مستقيما وهله‌اي را از كلاس abstract تعريف شده ايجاد كرد.
در حالت TPC، به ازاي هر sub class غير abstract، يك جدول ايجاد مي‌شود. هر جدول نيز حاوي فيلدهاي كلاس پايه مي‌باشد (برخلاف حالت TPT كه جداول متناظر با كلاس‌هاي مشتق شده، تنها حاوي همان خواص و فيلدهاي كلاس‌هاي متناظر بودند و نه بيشتر). به اين ترتيب عملا جداول تشكيل شده در بانك اطلاعاتي، از وجود ارث بري در سمت كدهاي ما بي‌خبر خواهند بود.


براي پياده سازي TPC نياز است از Fluent API استفاده شود:

using System.ComponentModel.DataAnnotations;
using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;

namespace EF_Sample05.DataLayer.Mappings
{
    public class PersonConfig : EntityTypeConfiguration<Person>
    {
        public PersonConfig()
        {
            // for TPC
            this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
        }
    }
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;

namespace EF_Sample05.DataLayer.Mappings
{
    public class CoachConfig : EntityTypeConfiguration<Coach>
    {
        public CoachConfig()
        {
            // For TPH
            //this.Map(m => m.Requires(discriminator: "PersonType").HasValue(1));

            // for TPT
            //this.ToTable("Coaches");

            //for TPC
            this.Map(m =>
            {
                m.MapInheritedProperties();
                m.ToTable("Coaches");
            });
        }
    }
}

using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;

namespace EF_Sample05.DataLayer.Mappings
{
    public class PlayerConfig : EntityTypeConfiguration<Player>
    {
        public PlayerConfig()
        {
            // For TPH
            //this.Map(m => m.Requires(discriminator: "PersonType").HasValue(2));

            // for TPT
            //this.ToTable("Players");

            //for TPC
            this.Map(m =>
                     {
                         m.MapInheritedProperties();
                         m.ToTable("Players");
                     });
        }
    }
}

ابتدا نوع فيلد Id از حالت Identity خارج شده است. اين مورد جهت كار با TPC ضروري است در غيراينصورت EF هنگام ثبت، به مشكل بر مي‌خورد، از اين لحاظ كه براي دو شيء، به يك Id خواهد رسيد و امكان ثبت را نخواهد داد. بنابراين در يك چنين حالتي استفاده از نوع Guid براي تعريف primary key شايد بهتر باشد. بديهي است در اين حالت بايد Id را به صورت دستي مقدار دهي نمود.
در ادامه توسط متد MapInheritedProperties، به همان مقصود لحاظ كردن تمام فيلدهاي ارث بري شده در جدول حاصل، خواهيم رسيد. همچنين نام جداول متناظر نيز ذكر گرديده است.


سؤال : از اين بين، بهتر است از كداميك استفاده شود؟

- براي حالت‌هاي ساده از TPH استفاده كنيد. براي مثال يك بانك اطلاعاتي قديمي داريد كه هر جدول آن 200 تا يا شايد بيشتر فيلد دارد! امكان تغيير طراحي آن هم وجود ندارد. براي اينكه بتوان به حس بهتري حين كاركردن با اين نوع سيستم‌هاي قديمي رسيد، مي‌شود از تركيب TPH و ComplexTypes (كه در قسمت‌هاي قبل در مورد آن بحث شد) براي مديريت بهتر اين نوع جداول در سمت كدهاي برنامه استفاده كرد.
- اگر علاقمند به استفاده از روابط پلي‌مرفيك هستيد ( براي مثال در كلاسي ديگر، ارجاعي به كلاس پايه Person وجود دارد) و sub classes داراي تعداد فيلدهاي كمي هستند، از TPH استفاده كنيد.
- اگر تعداد فيلدهاي sub classes زياد است و بسيار بيشتر است از كلاس پايه، از روش TPT استفاده كنيد.
- اگر عمق ارث بري و تعداد سطوح تعريف شده بالا است، بهتر است از TPC استفاده كنيد. حالت TPT از join استفاده مي‌كند و حالت TPC از union براي تشكيل كوئري‌ها كمك خواهد گرفت