دوره آموزشی Entity FrameWork Core - قسمت هفدهم
خوب EF Core به شما امکان مدلسازی entity typeهایی را میدهد که فقط میتوانند در ویژگیهای ناوبری(navigation properties) دیگر entity typeها ظاهر شوند. به اینها Owned Entity Types می گویند. موجودیت حاوی یک owned entity type، مالک(owner) آن است.
موجودیت های تحت مالکیت اساساً بخشی از مالک هستند و بدون آن نمی توانند وجود داشته باشند، آنها از نظر مفهومی شبیه به aggregateها هستند. این بدان معناست که موجودیت تحت مالکیت بنا به تعریف در سمت وابسته رابطه با مالک است.
بیشتر بخوانید: Entities, Value Objects, Aggregates and Roots
قبل از ادامه بیایید کمی بیشتر توضیح بدهم تا درک بهتری از یک owned types داشته باشیم:
یک موجودیت معمولاً از چندین ویژگی(attributes) تشکیل شده است. برای مثال، موجودیت Order را میتوان بهعنوان موجودیتی با هویت(دارای شناسه یا identity) مدلسازی کرد که از مجموعهای از ویژگیها مانند OrderId، OrderDate، OrderItems و غیره تشکیل شده است. اما موجودیت Address که حاوی street، city و غیره فاقد هویت(identity) است که باید به عنوان یک Owned EntityType مدلسازی و با آن رفتار شود.
public class Order { public int OrderId { get; private set; } public Address Address { get; set; } } public class Address { public string Street { get; set; } public string City { get; set; } public string State { get; set; } public string Country { get; set; } public string ZipCode { get; set; } }
توجه به این نکته مهم است که owned types هیچوقت به صورت قراردادی در EF Core کشف نمیشوند، بنابراین باید آنها را به صراحت اعلام کنید.
modelBuilder.Entity<Order>().OwnsOne(p => p.Address);
اگر ویژگی Address در Order به صورت private است، می توانید از متد زیر استفاده کنید:
modelBuilder.Entity<Order>().OwnsOne(typeof(Address), "Address");
توجه داشته باشید Owned Entity Types پیکربندی شده با متد OwnsOne همیشه یک رابطه یک به یک با مالک دارند، بنابراین به مقادیر کلیدی خود نیاز ندارند زیرا مقادیر کلید خارجی منحصر به فرد هستند. در مثال قبلی، نوع Address نیازی به تعریف پراپرتی کلید ندارد.
برای پیکربندی مجموعهای از owned typeها از متد OwnsMany در OnModelCreating استفاده کنید.
مثال زیر را در نظر بگیرید :
public class Distributor { public int Id { get; set; } public ICollection<Address> ShippingCenters { get; set; } }
بهطور پیشفرض، کلید اصلی مورد استفاده برای owned type بصورت («DistributorId»، «Id») خواهد بود که در آن «DistributorId» کلید خارجی و «Id» یک مقدار int منحصر به فرد است.
برای پیکربندی یک کلید اصلی متفاوت، HasKey را صدا کنید.
modelBuilder.Entity<Distributor>().OwnsMany( p => p.ShippingCenters, a => { a.WithOwner().HasForeignKey("OwnerId"); a.Property<int>("Id"); a.HasKey("Id"); });
مدل بالا به database schema زیر نگاشت شده است:
هنگام استفاده از پایگاههای داده رابطهای، بهطور پیشفرض، owned typeها به همان جدول مالک نگاشت میشوند. این مستلزم تقسیم جدول به دو بخش است: برخی از ستونها برای ذخیره دادههای مالک و برخی از ستونها برای ذخیره دادههای موجودیت owned type استفاده میشوند.
به متد "OwnsOne" توجه کنید. این همان چیزی است که به EF می گوید که موجودیت مالک ما می خواهد از یک owned type استفاده کند.
در واقع OwnsOne نشان می دهد کهowned type بخشی از موجودیت است. این همان چیزی است که به Entity Framework اجازه می دهد تا mapping را انجام دهد. طبق قرارداد Entity Framework هنگام اجرای migrations نام جدول را owned type_PropertyName میگذارد و هنگام mapping به دنبال آن میگردد. بنابراین در مورد آدرس، ستونهایی با نامهای Address_City، Address_State و غیره به پایان میرسد.
میتوانید برای تغییر نام آن ستونها، متد ()Property().HasColumnName را اضافه کنید. در موردی که Address یک public property است، نگاشت ها(mappings) به صورت زیر خواهد بود:
orderConfiguration.OwnsOne(p => p.Address) .Property(p=>p.Street).HasColumnName("ShippingStreet"); orderConfiguration.OwnsOne(p => p.Address) .Property(p=>p.City).HasColumnName("ShippingCity");
اگر در configهای مربوطه از ToTable استفاده کنیم و یک نام جدول به آن بدهیم می توانیم owned typeها را در جدول جداگانه نگهداری کنیم
builder.OwnsOne(x => x.Address).ToTable("Address");
با این کار یک رابطه یک به یک در دیتابیس ساخته می شود اما در عمکرد owned type تغییری ایجاد نخواهد شد.
این امکان وجود دارد که متد OwnsOne را در بصورت زنجیره ای نگاشت(Nested owned types) کنید. در مثال فرضی زیر، OrderDetails مالک BillingAddress و ShippingAddress است که هر دو نوع آدرس هستند. سپس OrderDetails متعلق به نوع Order است.
public class DetailedOrder { public int Id { get; set; } public OrderDetails OrderDetails { get; set; } public OrderStatus Status { get; set; } }
در ادامه :
public enum OrderStatus { Pending, Shipped }
هر navigation به یک owned type یک entity type جداگانه با پیکربندی کاملاً مستقل تعریف می کند.
public class OrderDetails { public DetailedOrder Order { get; set; } public StreetAddress BillingAddress { get; set; } public StreetAddress ShippingAddress { get; set; } }
در نهایت برای پیکربندی این مدل خواهیم داشت :
modelBuilder.Entity<DetailedOrder>().OwnsOne( p => p.OrderDetails, od => { od.WithOwner(d => d.Order); od.Navigation(d => d.Order).UsePropertyAccessMode(PropertyAccessMode.Property); od.OwnsOne(c => c.BillingAddress); od.OwnsOne(c => c.ShippingAddress); });
به متد WithOwner توجه کنید که برای تعریف navigation property که به سمت مالک اشاره می کند، استفاده می شود.
برای تعریف یک navigation به owner entity type که بخشی از رابطه مالکیت ()WithOwner نیست باید بدون هیچ آرگومان فراخوانی شود.
مدل بالا به database schema زیر نگاشت شده است:
هنگام پرس و جو از مالک، owned typeها به طور پیش فرض گنجانده می شود. نیازی به استفاده از متد Include نیست، حتی اگر owned typeها در یک جدول جداگانه ذخیره شوند. بر اساس مدلی که قبلا توضیح داده شد، کوئری زیر Order، OrderDetails و دو StreetAddresses متعلق به پایگاه داده را دریافت می کند:
var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending); Console.WriteLine($"First pending order will ship to: {order.OrderDetails.ShippingAddress.City}");
برخی از این محدودیتها برای نحوه عملکرد owned entity typeها اساسی هستند، اما برخی دیگر محدودیتهایی هستند که ممکن است در نسخههای بعدی ef core برطرف شوند:
محدودیت های طراحی
کاستی فعلی
بیشتر بخوانید : دوره آموزشی Entity FrameWork Core - قسمت نوزدهم
بیشتر بخوانید : دوره آموزشی Entity FrameWork Core
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core