طراحی سیستماتیک خصیصههای تاریخچهدار (Historized Attributes) در EF Core
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۴/۰۷ ۱۰:۲۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
ValidFrom: زمان شروع اعتبار قیمت (UTC).ValidTo: زمان پایان اعتبار قیمت (UTC).NULL برای قیمتهای فعلی (که کوئریها را پیچیده میکند)، از یک مقدار نگهبان (Sentinel Value) مانند DateTime.MaxValue استفاده میکنیم. این کار نشان میدهد که قیمت مورد نظر تا آینده نامعلوم معتبر است.public class Fruit
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
// Navigation Property
public List<FruitPrice> Prices { get; set; } = new();
}
public class FruitPrice
{
public int Id { get; set; }
public int FruitId { get; set; }
public decimal Value { get; set; }
public DateTime ValidFrom { get; set; }
public DateTime ValidTo { get; set; } // استفاده از مقدار نگهبان به جای Null
// Navigation Property
public Fruit? Fruit { get; set; }
}FruitId, ValidFrom) حیاتی است. بدون این ایندکس، دیتابیس مجبور به اسکن کامل جدول (Full Table Scan) خواهد شد.using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class FruitPriceConfiguration : IEntityTypeConfiguration<FruitPrice>
{
public void Configure(EntityTypeBuilder<FruitPrice> builder)
{
builder.HasKey(fp => fp.Id);
builder.Property(fp => fp.Value)
.HasPrecision(18, 2);
// تعریف ایندکس ترکیبی برای بهبود کارایی کوئریهای زمانی
builder.HasIndex(fp => new { fp.FruitId, fp.ValidFrom });
// ارتباط یک به چند
builder.HasOne(fp => fp.Fruit)
.WithMany(f => f.Prices)
.HasForeignKey(fp => fp.FruitId);
}
}ValidFrom / ValidTo) همراه با مقدار نگهبان، سادگی و کارایی بالای کوئریهای LINQ است.3 را در تاریخ 2023-06-01 به دست آورید:var targetDate = new DateTime(2023, 6, 1, 0, 0, 0, DateTimeKind.Utc);
var priceAtTime = await context.FruitPrices
.Where(fp => fp.FruitId == 3
&& fp.ValidFrom <= targetDate
&& fp.ValidTo > targetDate)
.FirstOrDefaultAsync();نکته فنی مهم: به عملگرها دقت کنید. برای زمان شروع از>=و برای زمان پایان از<استفاده شده است. این فرمولاسیون تضمین میکند که بازههای زمانی با هم تداخل نداشته باشند.
DateTime.MaxValue) برای رکوردهای جاری، دریافت قیمت فعلی دقیقاً مشابه کوئری بالا با زمان DateTime.UtcNow است و نیازی به بررسی Is Null ندارد:var now = DateTime.UtcNow;
var currentPrice = await context.FruitPrices
.Where(fp => fp.FruitId == 3
&& fp.ValidFrom <= now
&& fp.ValidTo > now)
.FirstOrDefaultAsync();ValidFrom استفاده نکنیم؟ در نگاه اول، با داشتن یک ستون هم میتوان فهمید که قیمت از چه زمانی تغییر کرده است. اما بیایید پیادهسازی آن را در LINQ ببینیم:// رویکرد تک زمانه (توصیه نمیشود)
var price = await context.FruitPrices
.Where(fp => fp.FruitId == 3 && fp.ValidFrom <= targetDate)
.OrderByDescending(fp => fp.ValidFrom)
.FirstOrDefaultAsync();MAX یا سورتهای سنگین میکند، در حالی که ساختار دو زمانه با یک فیلتر بازهای ساده (Between منطقی) روی ایندکس ترکیبی، به سرعت پاسخ را مییابد.public class HistorizedAttribute<TValue>
{
public int Id { get; set; }
public int AnchorId { get; set; } // کلید خارجی به موجودیت اصلی
public TValue Value { get; set; } = default!;
public DateTime ValidFrom { get; set; }
public DateTime ValidTo { get; set; } = DateTime.MaxValue; // مقدار نگهبان
}