ادامه بررسی Fluent API جهت تعریف نگاشت کلاسها به بانک اطلاعاتی
در قسمتهای قبل با استفاده از متادیتا و data annotations جهت بررسی نحوه نگاشت اطلاعات کلاسها به جداول بانک اطلاعاتی آشنا شدیم. اما این موارد تنها قسمتی از تواناییهای Fluent API مهیا در EF Code first را ارائه میدهند. یکی از دلایل آن هم به محدود بودن تواناییهای ذاتی Attributes بر میگردد. برای مثال حین کار با Attributes امکان استفاده از متغیرها یا lambda expressions و امثال آن وجود ندارد. به علاوه شاید عدهای علاقمند نباشند تا کلاسهای خود را با data annotations شلوغ کنند.
در قسمت دوم این سری، مروری مقدماتی داشتیم بر Fluent API. در آنجا ذکر شد که امکان تعریف نگاشتها به کمک تواناییهای Fluent API به دو روش زیر میسر است:
الف) میتوان از متد protected override void OnModelCreating در کلاس مشتق شده از DbContext کار را شروع کرد.
ب) و یا اگر بخواهیم کلاس Context برنامه را شلوغ نکنیم بهتر است به ازای هر کلاس مدل برنامه، یک کلاس mapping مشتق شده از EntityTypeConfiguration را تعریف نمائیم. سپس میتوان این کلاسها را در متد OnModelCreating یاد شده، توسط متد modelBuilder.Configurations.Add جهت استفاده و اعمال، معرفی کرد.
کلاسهای مدلی را که در این قسمت بررسی خواهیم کرد، همان کلاسهای User و Project قسمت سوم هستند و هدف این قسمت بیشتر تطابق Fluent API با اطلاعات ارائه شده در قسمت سوم است؛ برای مثال در اینجا چگونه باید از خاصیتی صرفنظر کرد، مسایل همزمانی را اعمال نمود و امثال آن.
بنابراین یک پروژه جدید کنسول را آغاز نمائید. سپس با کمک NuGet ارجاعات لازم را به اسمبلیهای EF اضافه نمائید.
در پوشه Models این پروژه، سه کلاس تکمیل شده زیر، از قسمت سوم وجود دارند:
using System;
using System.Collections.Generic;
namespace EF_Sample03.Models
{
public class User
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Name { set; get; }
public string LastName { set; get; }
public string FullName
{
get { return Name + " " + LastName; }
}
public string Email { set; get; }
public string Description { set; get; }
public byte[] Photo { set; get; }
public IList<Project> Projects { set; get; }
public byte[] RowVersion { set; get; }
public InterestComponent Interests { set; get; }
public User()
{
Interests = new InterestComponent();
}
}
}
using System;
namespace EF_Sample03.Models
{
public class Project
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
public string Description { set; get; }
public virtual User User { set; get; }
public byte[] RowVesrion { set; get; }
}
}
namespace EF_Sample03.Models
{
public class InterestComponent
{
public string Interest1 { get; set; }
public string Interest2 { get; set; }
}
}
سپس یک پوشه جدید به نام Mappings را به پروژه اضافه نمائید. به ازای هر کلاس فوق، یک کلاس جدید را جهت تعاریف اطلاعات نگاشتها به کمک Fluent API اضافه خواهیم کرد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
namespace EF_Sample03.Mappings
{
public class InterestComponentConfig : ComplexTypeConfiguration<InterestComponent>
{
public InterestComponentConfig()
{
this.Property(x => x.Interest1).HasMaxLength(450);
this.Property(x => x.Interest2).HasMaxLength(450);
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
namespace EF_Sample03.Mappings
{
public class ProjectConfig : EntityTypeConfiguration<Project>
{
public ProjectConfig()
{
this.Property(x => x.Description).IsMaxLength();
this.Property(x => x.RowVesrion).IsRowVersion();
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample03.Models;
using System.ComponentModel.DataAnnotations;
namespace EF_Sample03.Mappings
{
public class UserConfig : EntityTypeConfiguration<User>
{
public UserConfig()
{
this.HasKey(x => x.Id);
this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.ToTable("tblUser", schemaName: "guest");
this.Property(p => p.AddDate).HasColumnName("CreateDate").HasColumnType("date").IsRequired();
this.Property(x => x.Name).HasMaxLength(450);
this.Property(x => x.LastName).IsMaxLength().IsConcurrencyToken();
this.Property(x => x.Email).IsFixedLength().HasMaxLength(255); //nchar(128)
this.Property(x => x.Photo).IsOptional();
this.Property(x => x.RowVersion).IsRowVersion();
this.Ignore(x => x.FullName);
}
}
}
توضیحاتی در مورد کلاسهای تنظیمات نگاشتهای خواص به جداول و فیلدهای بانک اطلاعاتی
نظم بخشیدن به تعاریف نگاشتها
همانطور که ملاحظه میکنید، جهت نظم بیشتر پروژه و شلوغ نشدن متد OnModelCreating کلاس Context برنامه، که در ادامه کدهای آن معرفی خواهد شد، به ازای هر کلاس مدل، یک کلاس تنظیمات نگاشتها را اضافه کردهایم.
کلاسهای معمولی نگاشتها ازکلاس EntityTypeConfiguration مشتق خواهند شد و جهت تعریف کلاس InterestComponent به عنوان Complex Type، اینبار از کلاس ComplexTypeConfiguration ارث بری شده است.
تعیین طول فیلدها
در کلاس InterestComponentConfig، به کمک متد HasMaxLength، همان کار ویژگی MaxLength را میتوان شبیه سازی کرد که در نهایت، طول فیلد nvarchar تشکیل شده در بانک اطلاعاتی را مشخص میکند. اگر نیاز است این فیلد nvarchar از نوع max باشد، نیازی به تنظیم خاصی نداشته و حالت پیش فرض است یا اینکه میتوان صریحا از متد IsMaxLength نیز برای معرفی nvarchar max استفاده کرد.
تعیین مسایل همزمانی
در قسمت سوم با ویژگیهای ConcurrencyCheck و Timestamp آشنا شدیم. در اینجا اگر نوع خاصیت byte array بود و نیاز به تعریف آن به صورت timestamp وجود داشت، میتوان از متد IsRowVersion استفاده کرد. معادل ویژگی ConcurrencyCheck در اینجا، متد IsConcurrencyToken است.
تعیین کلید اصلی جدول
اگر پیش فرضهای EF Code first مانند وجود خاصیتی به نام Id یا ClassName+Id رعایت شود، نیازی به کار خاصی نخواهد بود. اما اگر این قراردادها رعایت نشوند، میتوان از متد HasKey (که نمونهای از آنرا در کلاس UserConfig فوق مشاهده میکنید)، استفاده کرد.
تعیین فیلدهای تولید شده توسط بانک اطلاعاتی
به کمک متد HasDatabaseGeneratedOption، میتوان مشخص کرد که آیا یک فیلد Identity است و یا یک فیلد محاسباتی ویژه و یا هیچکدام.
تعیین نام جدول و schema آن
اگر نیاز است از قراردادهای نامگذاری خاصی پیروی شود، میتوان از متد ToTable جهت تعریف نام جدول متناظر با کلاس جاری استفاده کرد. همچنین در اینجا امکان تعریف schema نیز وجود دارد.
تعیین نام و نوع سفارشی فیلدها
همچنین اگر نام فیلدها نیز باید از قراردادهای دیگری پیروی کنند، میتوان آنها را به صورت صریح توسط متد HasColumnName معرفی کرد. اگر نیاز است این خاصیت به نوع خاصی در بانک اطلاعاتی نگاشت شود، باید از متد HasColumnType کمک گرفت. برای مثال در اینجا بجای نوع datetime، از نوع ویژه date استفاده شده است.
معرفی فیلدها به صورت nchar بجای nvarchar
برای نمونه اگر قرار است هش کلمه عبور در بانک اطلاعاتی ذخیره شود، چون طول آن ثابت میباشد، توصیه شدهاست که بجای nvarchar از nchar برای تعریف آن استفاده شود. برای این منظور تنها کافی است از متد IsFixedLength استفاده شود. در این حالت طول پیش فرض 128 برای فیلد درنظر گرفته خواهد شد. بنابراین اگر نیاز است از طول دیگری استفاده شود، میتوان همانند سابق از متد HasMaxLength کمک گرفت.
ضمنا این فیلدها همگی یونیکد هستند و با n شروع شدهاند. اگر میخواهید از varchar یا char استفاده کنید، میتوان از متد IsUnicode با پارامتر false استفاده کرد.
معرفی یک فیلد به صورت null پذیر در سمت بانک اطلاعاتی
استفاده از متد IsOptional، فیلد را در سمت بانک اطلاعاتی به صورت فیلدی با امکان پذیرش مقادیر null معرفی میکند.
البته در اینجا به صورت پیش فرض byte arrayها به همین نحو معرفی میشوند و تنظیم فوق صرفا جهت ارائه توضیحات بیشتر در نظر گرفته شد.
صرفنظر کردن از خواص محاسباتی در تعاریف نگاشتها
با توجه به اینکه خاصیت FullName به صورت یک خاصیت محاسباتی فقط خواندنی، در کدهای برنامه تعریف شده است، با استفاده از متد Ignore، از نگاشت آن به بانک اطلاعاتی جلوگیری خواهیم کرد.
معرفی کلاسهای تعاریف نگاشتها به برنامه
استفاده از کلاسهای Config فوق خودکار نیست و نیاز است توسط متد modelBuilder.Configurations.Add معرفی شوند:
using System.Data.Entity;
using System.Data.Entity.Migrations;
using EF_Sample03.Mappings;
using EF_Sample03.Models;
namespace EF_Sample03.DataLayer
{
public class Sample03Context : DbContext
{
public DbSet<User> Users { set; get; }
public DbSet<Project> Projects { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new InterestComponentConfig());
modelBuilder.Configurations.Add(new ProjectConfig());
modelBuilder.Configurations.Add(new UserConfig());
//modelBuilder.ComplexType<InterestComponent>();
//modelBuilder.Ignore<InterestComponent>();
base.OnModelCreating(modelBuilder);
}
}
public class Configuration : DbMigrationsConfiguration<Sample03Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(Sample03Context context)
{
base.Seed(context);
}
}
}
در اینجا کلاس Context برنامه مثال جاری را ملاحظه میکنید؛ به همراه کلاس Configuration مهاجرت خودکار که در قسمتهای قبل بررسی شد.
در متد OnModelCreating نیز میتوان یک کلاس را از نوع Complex معرفی کرد تا برای آن در بانک اطلاعاتی جدول جداگانهای تعریف نشود. اما باید دقت داشت که اینکار را فقط یکبار میتوان انجام داد؛ یا توسط کلاس InterestComponentConfig و یا توسط متد modelBuilder.ComplexType. اگر هر دو با هم فراخوانی شوند، EF یک استثناء را صادر خواهد کرد.
و در نهایت، قسمت آغازین برنامه اینبار به شکل زیر خواهد بود که از آغاز کننده MigrateDatabaseToLatestVersion (قسمت چهارم این سری) نیز استفاده کرده است:
using System;
using System.Data.Entity;
using EF_Sample03.DataLayer;
namespace EF_Sample03
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample03Context, Configuration>());
using (var db = new Sample03Context())
{
var project1 = db.Projects.Find(1);
if (project1 != null)
{
Console.WriteLine(project1.Title);
}
}
}
}
}
ضمنا رشته اتصالی مورد استفاده تعریف شده در فایل کانفیک برنامه نیز به صورت زیر تعریف شده است:
<connectionStrings>
<clear/>
<add
name="Sample03Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
/connectionStrings>
در قسمتهای بعد مباحث پیشرفتهتری از تنظیمات نگاشتها را به کمک Fluent API، بررسی خواهیم کرد. برای مثال روابط ارث بری، many-to-many و ... چگونه تعریف میشوند.