عنوان:

‫پشتیبانی از نوع داده JSON در EF Core 10: تحولی در مدیریت داده‌های ساخت‌یافته


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۸/۱۰ ۱۱:۰۰
آدرس: www.dntips.ir
اگر با Entity Framework Core (EF Core) کار کرده باشید، احتمالاً بارها با چالش ذخیره و مدیریت داده‌های JSON روبرو شده‌اید. در نسخه ۱۰ EF Core، مایکروسافت گام بزرگی برداشته و پشتیبانی کامل از نوع داده JSON را اضافه کرده‌است. این ویژگی، که با SQL Server ۲۰۲۵ (نسخه ۱۷.x) و Azure SQL Database همخوانی دارد، به شما اجازه می‌دهد داده‌های پیچیده مانند آبجکت‌ها و آرایه‌ها را مستقیماً به عنوان JSON ذخیره کنید، بدون نیاز به تبدیل‌های دستی. در این مقاله، به بررسی این قابلیت می‌پردازیم.

توضیح مسئله
ذخیره داده‌های JSON در دیتابیس همیشه دردسرساز بوده. تصور کنید می‌خواهید اطلاعاتی مانند آدرس یا لیست شماره تلفن‌های یک مشتری را ذخیره کنید. این داده‌ها ساخت‌یافته هستند، اما اگر آن‌ها را به صورت رشته (string) ذخیره کنید، مشکلات زیادی پیش می‌آید:
  • کوئری سخت و ناکارآمد: برای جستجو داخل JSON، باید از توابع SQL مانند JSON_VALUE یا JSON_QUERY استفاده کنید، اما این کار دستی است و خطاپذیر. علاوه بر این، بدون ایندکس مناسب، عملکرد ضعیف می‌شود.
  • به‌روزرسانی پیچیده: تغییر بخشی از JSON (مثل تغییر کد پستی در آدرس) اغلب نیاز به خواندن کل داده، تغییر آن در کد، و ذخیره مجدد دارد. این کار نه تنها کند است، بلکه ریسک خطا را افزایش می‌دهد.
  • عدم تضمین اعتبار داده: دیتابیس نمی‌تواند بررسی کند که داده واقعاً JSON معتبر باشد، پس ممکن است داده‌های نامعتبر وارد شوند و بعداً مشکلات ایجاد کنند.
  • عملکرد پایین: ذخیره به عنوان nvarchar(max) یا varchar(max) باعث می‌شود عملیات خواندن و نوشتن کندتر شود، چون دیتابیس نمی‌تواند بهینه‌سازی‌های خاصی برای JSON اعمال کند.
این مسائل نه تنها کد شما را پیچیده می‌کنند، بلکه مقیاس‌پذیری اپلیکیشن را هم تحت تأثیر قرار می‌دهند. خوشبختانه، EF Core ۱۰ این مشکلات را با معرفی نوع داده native JSON حل کرده است.

راه‌حل قدیمی
در نسخه‌های قبلی EF Core، رایج‌ترین روش ذخیره JSON استفاده از ستون‌های رشته‌ای مانند nvarchar(max) بود. برای مثال، می‌توانستید یک پراپرتی را به عنوان string تعریف کنید و آن را دستی serialize/deserialize کنید. اما این رویکرد مشکلات زیادی داشت:
  • نیاز به کد اضافی برای تبدیل JSON به string و بالعکس.
  • کوئری‌ها باید با توابع SQL دستی نوشته می‌شدند، مثل JSON_VALUE([Column], '$.Path').
  • به‌روزرسانی‌ها اغلب کامل بودند، یعنی کل ستون را جایگزین می‌کردید، نه فقط بخشی از آن.
  • عدم پشتیبانی مستقیم از ایندکس روی فیلدهای داخل JSON، که جستجو را کند می‌کرد.
علاوه بر این، اگر از Owned Entities (موجودیت‌های مالکیت‌دار) استفاده می‌کردید، EF Core داده‌ها را به ستون‌های جداگانه مپ می‌کرد، نه به JSON واحد. این کار برای داده‌های پیچیده مناسب نبود و مدل دیتابیس را شلوغ می‌کرد. در نهایت، این روش‌ها کارآمد نبودند و توسعه‌دهندگان را مجبور به نوشتن کدهای boilerplate می‌کردند.

راه‌حل جدید
در EF Core ۱۰، با معرفی روش ToJson() برای Complex Properties (ویژگی‌های پیچیده)، همه چیز ساده‌تر شده. این روش اجازه می‌دهد آبجکت‌ها و آرایه‌ها را مستقیماً به ستون JSON مپ کنید. این ویژگی روی SQL Server ۲۰۲۵ (با سطح سازگاری ۱۷۰) و Azure SQL Database کار می‌کند، اما حتی روی نسخه‌های قدیمی‌تر SQL Server (مانند ۲۰۲۲ با سطح سازگاری ۱۶۰) هم با استفاده از nvarchar(max) و توابع JSON_MODIFY قابل استفاده است. همچنین، پشتیبانی مشابهی برای PostgreSQL و SQLite وجود دارد، که کوئری روی فیلدهای JSON را بدون deserialize امکان‌پذیر می‌کند.
بیایید با یک مثال عملی شروع کنیم. فرض کنید مدل زیر را داریم:
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public required Contact Contact { get; set; }
}

public class Contact
{
    public required Address Address { get; set; }
    public List<PhoneNumber> PhoneNumbers { get; set; } = new List<PhoneNumber>();
}

public class Address
{
    public required string Street { get; set; }
    public required string City { get; set; }
    public required string State { get; set; }
    public required string PostalCode { get; set; }
}

public class PhoneNumber
{
    public PhoneNumberType Type { get; set; }
    public string Number { get; set; }

    public PhoneNumber(PhoneNumberType type, string number)
    {
        Type = type;
        Number = number;
    }
}

public enum PhoneNumberType
{
    Mobile,
    Home
}
حالا، در DbContext، پراپرتی Contact را به JSON مپ می‌کنیم:
public class MyDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("<ConnectionString>", x => x.UseCompatibilityLevel(170)); // برای SQL Server 2025
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Customer>(x =>
        {
            x.ComplexProperty(c => c.Contact, b => b.ToJson());
        });
    }
}
این تنظیم، جدول Customers را با ستون Contact از نوع json ایجاد می‌کند:
CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [Contact] json NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);

کوئری و پروجکشن
حالا می‌توانید با LINQ مستقیماً روی فیلدهای داخل JSON کوئری بزنید، بدون نیاز به کد اضافی:
var customersInWA = await context.Customers
    .Where(c => c.Contact.Address.State == "WA")
    .ToListAsync();
SQL تولیدشده:
SELECT [c].[Id], [c].[Name], [c].[Contact]
FROM [Customers] AS [c]
WHERE JSON_VALUE([c].[Contact], '$.Address.State') = N'WA'
برای پروجکشن (انتخاب بخشی از داده):
var distinctStates = await context.Customers
    .Select(c => c.Contact.Address.State)
    .Distinct()
    .ToListAsync();
SQL:
SELECT DISTINCT JSON_VALUE([c].[Contact], '$.Address.State')
FROM [Customers] AS [c]
برای PostgreSQL یا SQLite، می‌توانید از توابع مانند EF.Functions.JsonContains استفاده کنید:
var premiumUsers = db.Users
    .Where(u => EF.Functions.JsonContains(u.Metadata, "{ \"Premium\": true }"))
    .ToList();
این کار کوئری را مستقیماً روی دیتابیس اجرا می‌کند و عملکرد را بهبود می‌بخشد.

به‌روزرسانی جزئی
یکی از بهترین ویژگی‌ها، پشتیبانی از به‌روزرسانی جزئی با ExecuteUpdateAsync است. این روش فقط بخش مورد نظر JSON را تغییر می‌دهد:
await context.Customers
    .Where(c => c.Name == "John Doe")
    .ExecuteUpdateAsync(setters => setters.SetProperty(c => c.Contact.Address.PostalCode, "98102"));
SQL برای SQL Server ۲۰۲۵:
UPDATE [c]
SET [Contact] = JSON_MODIFY([Contact], '$.Address.PostalCode', @p0)
FROM [Customers] AS [c]
WHERE [c].[Name] = N'John Doe'
این ویژگی حتی روی SQL Server قدیمی‌تر (با nvarchar(max)) کار می‌کند، اما فقط وقتی از ComplexProperty استفاده کنید، نه Owned Entities. اگر از آرایه‌ها (مثل Tags در مدل Blog) استفاده کنید، EF Core آن‌ها را به عنوان json array مپ می‌کند و کوئری روی آن‌ها (مثل فیلتر بر اساس تعداد عناصر) آسان می‌شود.

نکته تکمیلی: برای بهبود عملکرد، می‌توانید ایندکس روی فیلدهای داخل JSON ایجاد کنید، مثل CREATE INDEX idx_State ON Customers ((JSON_VALUE(Contact, '$.Address.State')))؛ این کار جستجوها را سریع‌تر می‌کند، چیزی که در روش‌های قدیمی سخت بود.