بررسی ساختار بانک اطلاعاتی تمرینهای سایت PostgreSQL Exercises
بانک اطلاعاتی مثالهای سایت PostgreSQL Exercises از سه جدول با مشخصات زیر تشکیل میشود:
جدول کاربران
CREATE TABLE cd.members ( memid integer NOT NULL, surname character varying(200) NOT NULL, firstname character varying(200) NOT NULL, address character varying(300) NOT NULL, zipcode integer NOT NULL, telephone character varying(20) NOT NULL, recommendedby integer, joindate timestamp not null, CONSTRAINT members_pk PRIMARY KEY (memid), CONSTRAINT fk_members_recommendedby FOREIGN KEY (recommendedby) REFERENCES cd.members(memid) ON DELETE SET NULL );
جدول امکانات قابل ارائهی به کاربران
CREATE TABLE cd.facilities ( facid integer NOT NULL, name character varying(100) NOT NULL, membercost numeric NOT NULL, guestcost numeric NOT NULL, initialoutlay numeric NOT NULL, monthlymaintenance numeric NOT NULL, CONSTRAINT facilities_pk PRIMARY KEY (facid) );
جدول سوابق استفادهی کاربران از امکانات مجموعه
CREATE TABLE cd.bookings ( bookid integer NOT NULL, facid integer NOT NULL, memid integer NOT NULL, starttime timestamp NOT NULL, slots integer NOT NULL, CONSTRAINT bookings_pk PRIMARY KEY (bookid), CONSTRAINT fk_bookings_facid FOREIGN KEY (facid) REFERENCES cd.facilities(facid), CONSTRAINT fk_bookings_memid FOREIGN KEY (memid) REFERENCES cd.members(memid) );
هر رزرو کردن مکان و امکاناتی در این مجموعه، «نیم ساعته» است. بنابراین Slots در اینجا به معنای تعداد نیم ساعتهای رزرو کردن یک مکان خاص است؛ که به آن «half hour slots» نیز گفته میشود و زمان شروع این رزرو نیز ثبت میشود.
تبدیل ساختار بانک اطلاعاتی سایت PostgreSQL Exercises به EF Core Code First
در این دیاگرام، دیتابیس متشکل از سه جدول یاد شده را ملاحظه میکنید. برای تبدیل آنها به موجودیتهای EF Core، میتوان به صورت زیر عمل کرد:
موجودیت کاربران
namespace EFCorePgExercises.Entities { public class Member { public int MemId { set; get; } public string Surname { set; get; } public string FirstName { set; get; } public string Address { set; get; } public int ZipCode { set; get; } public string Telephone { set; get; } public virtual ICollection<Member> Children { get; set; } public virtual Member Recommender { set; get; } public int? RecommendedBy { set; get; } public DateTime JoinDate { set; get; } public virtual ICollection<Booking> Bookings { set; get; } } }
- خاصیتهای Children و Recommender برای تعریف رابطهی «خود ارجاعی» اضافه شدهاند. در اینجا هر کاربر میتواند توسط کاربر دیگری توصیه شده باشد.
- خاصیت Bookings برای بیان رابطهی یک به چند با موجودیت Booking، تعریف شدهاست؛ هر یک کاربر میتواند به هر تعدادی رزرو امکانات داشته باشد.
موجودیت Facility
namespace EFCorePgExercises.Entities { public class Facility { public int FacId { set; get; } public string Name { set; get; } public decimal MemberCost { set; get; } public decimal GuestCost { set; get; } public decimal InitialOutlay { set; get; } public decimal MonthlyMaintenance { set; get; } public virtual ICollection<Booking> Bookings { set; get; } } }
- خاصیت راهبری Bookings، بیانگر رابطهی یک به چند هرکدام از امکانات مجموعه با تعداد بار و سوابق رزرو شدن آنها است.
موجودیت Booking
namespace EFCorePgExercises.Entities { public class Booking { public int BookId { set; get; } public int FacId { set; get; } public virtual Facility Facility { set; get; } public int MemId { set; get; } public virtual Member Member { set; get; } public DateTime StartTime { set; get; } public int Slots { set; get; } } }
تنظیمات هر کدام از موجودیتها و روابط بین آنها در EF Core Code First
پس از مشخص شدن طراحی موجودیتها، اکنون نیاز است ارتباطات بین آنها را به EF Core، به نحو دقیقتری معرفی کرد و همچنین طول و یا دقت هر کدام از خواص را نیز مشخص نمود.
تنظیمات موجودیت کاربران
namespace EFCorePgExercises.Entities { public class MemberConfiguration : IEntityTypeConfiguration<Member> { public void Configure(EntityTypeBuilder<Member> builder) { builder.HasKey(member => member.MemId); builder.Property(member => member.MemId).IsRequired().UseIdentityColumn(seed: 0, increment: 1); builder.Property(member => member.Surname).HasMaxLength(200).IsRequired(); builder.Property(member => member.FirstName).HasMaxLength(200).IsRequired(); builder.Property(member => member.Address).HasMaxLength(300).IsRequired(); builder.Property(member => member.ZipCode).IsRequired(); builder.Property(member => member.Telephone).HasMaxLength(20).IsRequired(); builder.HasIndex(member => member.RecommendedBy); builder.HasOne(member => member.Recommender) .WithMany(member => member.Children) .HasForeignKey(member => member.RecommendedBy); builder.Property(member => member.JoinDate).IsRequired(); builder.HasIndex(member => member.JoinDate).HasName("IX_JoinDate"); builder.HasIndex(member => member.RecommendedBy).HasName("IX_RecommendedBy"); } } }
- سپس نحوهی تعریف رابطهی خود راجاعی این موجودیت را مشاهده میکنید.
- دو ایندکس هم در اینجا تعریف شدهاند که جزو اطلاعات موجود در فایل SQL این سری از مثالها هستند.
نکتهی مهم: در اینجا یک UseIdentityColumn(seed: 0, increment: 1) را نیز مشاهده میکنید که شاید برای شما تازگی داشته باشد. فیلد ID تمام جداول این مجموعه برخلاف معمول که از 1 شروع میشود، از صفر شروع میشود و ID مساوی صفر را برای کاربران مهمان درنظر گرفتهاست. روش تعریف چنین تنظیم خاصی را توسط متد UseIdentityColumn و دو پارامتر آن در اینجا مشاهده میکنید. این ID مساوی صفر، نکات خاصی را هم در حین ثبت اطلاعات اولیهی هر جدول، به همراه دارد که در ادامه بررسی خواهد شد.
تنظیمات موجودیت امکانات مجموعه
namespace EFCorePgExercises.Entities { public class FacilityConfiguration : IEntityTypeConfiguration<Facility> { public void Configure(EntityTypeBuilder<Facility> builder) { builder.HasKey(facility => facility.FacId); builder.Property(facility => facility.FacId).IsRequired().UseIdentityColumn(seed: 0, increment: 1); builder.Property(facility => facility.Name).HasMaxLength(100).IsRequired(); builder.Property(facility => facility.MemberCost).IsRequired().HasColumnType("decimal(18, 6)"); builder.Property(facility => facility.GuestCost).IsRequired().HasColumnType("decimal(18, 6)"); builder.Property(facility => facility.InitialOutlay).IsRequired().HasColumnType("decimal(18, 6)"); builder.Property(facility => facility.MonthlyMaintenance).IsRequired().HasColumnType("decimal(18, 6)"); } } }
تنظیمات موجودیت سوابق رزروهای امکانات مجموعه
namespace EFCorePgExercises.Entities { public class BookingConfiguration : IEntityTypeConfiguration<Booking> { public void Configure(EntityTypeBuilder<Booking> builder) { builder.HasKey(booking => booking.BookId); builder.Property(booking => booking.BookId).IsRequired().UseIdentityColumn(seed: 0, increment: 1); builder.Property(booking => booking.FacId).IsRequired(); builder.HasOne(booking => booking.Facility) .WithMany(facility => facility.Bookings) .HasForeignKey(booking => booking.FacId); builder.Property(booking => booking.MemId).IsRequired(); builder.HasOne(booking => booking.Member) .WithMany(member => member.Bookings) .HasForeignKey(booking => booking.MemId); builder.Property(booking => booking.StartTime).IsRequired(); builder.Property(booking => booking.Slots).IsRequired(); builder.HasIndex(booking => new { booking.MemId, booking.FacId }).HasName("IX_memid_facid"); builder.HasIndex(booking => new { booking.FacId, booking.StartTime }).HasName("IX_facid_starttime"); builder.HasIndex(booking => new { booking.MemId, booking.StartTime }).HasName("IX_memid_starttime"); builder.HasIndex(booking => booking.StartTime).HasName("IX_starttime"); } } }
ایجاد Context و معرفی موجودیتها و تنظیمات آنها
در ادامه توسط ApplicationDbContext که از DbContext ارثبری میکند، سه موجودیت تعریف شده را در معرض دید EF Core قرار میدهیم:
namespace EFCorePgExercises.DataLayer { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { } public DbSet<Member> Members { get; set; } public DbSet<Booking> Bookings { get; set; } public DbSet<Facility> Facilities { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(typeof(MemberConfiguration).Assembly); } } }
اجرای Migrations جهت تشکیل ساختار بانک اطلاعاتی
اکنون که موجودیتها، روابط بین آنها و Context برنامه مشخص شدند، میتوان با اجرای دستوارت زیر، سبب تولید کدهای Migration شد که با اجرای آنها، بانک اطلاعاتی متناظری به صورت خودکار تولید میشود:
dotnet tool install --global dotnet-ef --version 3.1.6 dotnet tool update --global dotnet-ef --version 3.1.6 dotnet build dotnet ef migrations add Init --context ApplicationDbContext
مقدار دهی اولیهی بانک اطلاعاتی
سایت PostgreSQL Exercises به همراه فایل SQL ایجاد جداول و مقدار دهی اولیهی آنها نیز هست. شاید عنوان کنید که چرا این اطلاعات به صورت متدهای HasData، به تنظیمات موجودیتها اضافه نشدند؟ علت آن به همان ID مساوی صفر بر میگردد! در حین استفادهی از متد HasData نمیتوانید ID ای داشته باشید که مقدار آن با مقدار پیشفرض آن نوع، یکی باشد. برای مثال مقدار پیش فرض int، مساوی صفر است. به همین جهت حتی با تنظیم UseIdentityColumn(seed: 0, increment: 1)، اجازهی ثبت Id مساوی صفر را نمیدهد؛ چون نمیتواند تشخیص دهد که این مقدار، یک مقدار صریح است یا خیر (^). بنابراین مجبور هستیم تا آنها را به صورت معمولی ثبت کنیم:
context.Facilities.Add(new Facility { Name = "Tennis Court 1", MemberCost = 5, GuestCost = 25, InitialOutlay = 10000, MonthlyMaintenance = 200 }); // مابقی موارد context.SaveChanges();
این روش برای ثبت اطلاعات Facilities و Booking کار میکند؛ اما ... چون Idهای کاربران پشت سر هم نیست و بین آنها فاصله وجود دارد، دیگر نمیتوان از روش فوق استفاده کرد و نیاز است بتوان مقدار Id را به صورت صریحی تعیین کرد که این مورد نکات جالبی را به همراه دارد:
- در حین کار با SQL Server نیاز است دستور SET IDENTITY_INSERT Members ON را در ابتدای کار، فراخوانی کرد تا بتوان مقدار فیلد ID خود افزایش دهنده را به صورت دستی مقدار دهی کرد.
- در هر زمان، فقط یک جدول و فقط یک سشن (یک اتصال) را میتوان توسط IDENTITY_INSERT در حالت ثبت و مقدار دهی ID آن قرار داد.
- EF Core، به ازای هر batch اطلاعاتی که ثبت میکند، یکبار اتصال را باز و بسته میکند. این مورد سبب میشود که فراخوانی ExecuteSqlCommand با دستور یاد شده، تاثیری نداشته باشد. برای رفع این مشکل باید یک تراکنش را باز کرد، تا اتصال به بانک اطلاعاتی، در طول آن باز باقی بماند.
در اینجا برای ثبت کاربر با ID مساوی صفر، باز هم میتوان به صورت معمولی عمل کرد:
context.Members.Add(new Member { ... }); context.SaveChanges(); // For id = 0 = Int's CLR Default Value!
using (var transaction = context.Database.BeginTransaction()) { try { context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Members ON"); context.Members.Add(new Member { ... }); // مابقی موارد context.SaveChanges(); transaction.Commit(); } catch { transaction.Rollback(); throw; } finally { context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT Members OFF"); } }
کدهای کامل موجودیتهای این قسمت به همراه تنظیمات آنها
کدهای کامل تنظیم Context و همچنین مقدار دهی اولیهی بانک اطلاعاتی