EF Core 7: ستون های Json

توجه داشته باشید
اکثر پایگاه های داده رابطه ای از ستون هایی پشتیبانی می کنند که حاوی اسناد JSON هستند. JSON در این ستونها را میتوان با پرسوجوها دریل کرد. به عنوان مثال، این امکان فیلتر کردن و مرتب سازی بر اساس عناصر اسناد و همچنین نمایش عناصر از اسناد به نتایج را فراهم می کند. ستونهای JSON به پایگاههای اطلاعاتی رابطهای اجازه میدهند تا برخی از ویژگیهای پایگاههای داده اسناد را به خود بگیرند و ترکیبی مفید بین این دو ایجاد کنند. -از مایکروسافت
مهم
به جای استفاده از انتقال، پایگاه داده در SSMS ایجاد شد (SQL-Server Management Studio) سپس با EF Power Tools مهندسی معکوس شد تا اینکه ویژگی هایی را تغییر دهد که برای json برای اشاره به کلاس ها به جای نوع nvarchar بود.
اگر با EF Core نسبتاً تازه کار هستید، برای دانلود کد نمونه زمان بگذارید، کد را مطالعه کنید، کد را اجرا کنید و کد را درک کنید.
وقتی از ستونهای json استفاده میکنید، مطمئن شوید که برای مدل دادههای شما مناسب است نه اینکه صرفاً از آن استفاده کنید زیرا جدید است.
هدف این مقاله
ارائه چندین نمونه کد واضح و مختصر برای کار با ستونهای Json، زیرا بسیاری از نمونههای کد در وب آزمایش کردن آسان نیست.
مثال 1
ما یک جدول Person داریم که یک تا چندین آدرس را برای مدل شخص ذخیره می کند.
public class Address
{
public string Company { get; set; }
public string Street { get; set; }
public string City { get; set; }
public override string ToString() => Company;
}
مدل شخص
public partial class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public List<Address> Addresses { get; set; }
public override string ToString() => $"{FirstName} {LastName}";
}
در پایگاه داده آدرس به صورت nvarchar تعریف شده است.
برای پیکربندی EF Core برای شناسایی ستونهای Json برای ویژگی Address مدل Person، از کد زیر استفاده میکنیم که OwnsMany کلید آن است و به ویژگی Address اشاره میکند.
بیایید یک رکورد جدید به پایگاه داده اضافه کنیم سپس ویژگی City یکی از آدرس ها را تغییر دهیم.
private static void AddOnePerson()
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
Person person = new Person()
{
Addresses = new List<Address>()
{
new()
{
Company = "Company1",
City = "Wyndmoor",
Street = "123 Apple St"
},
new()
{
Company = "Company2",
City = "Portland",
Street = "999 34th St"
},
},
FirstName = "Karen",
LastName = "Payne",
DateOfBirth = new DateTime(1956, 9, 24)
};
context.Add(person);
context.SaveChanges();
context.Person.FirstOrDefault()!
.Addresses
.FirstOrDefault()
!.City = "Ambler";
context.SaveChanges();
}
اگر مدتی است که با EF Core کار کرده اید، آخرین بلوک کد واقعاً با عدم استفاده از ستون های Json تفاوتی ندارد.
بیایید داده ها را دوباره بخوانیم.
private static void ReadOnePerson()
{
using var context = new Context();
var person = context.Person.FirstOrDefault();
if (person is Person)
{
AnsiConsole.MarkupLine($"[white]{person.Id,-4}{person.FirstName,-10}{person.LastName,-10}{person.DateOfBirth:d}[/]");
foreach (var address in person.Addresses)
{
AnsiConsole.MarkupLine($"\t[green]{address.Company,-10}{address.Street,-15}{address.City}[/]");
}
}
var firstPerson = context.Person.FirstOrDefault(x => x.Id == 1);
var portlandAddress = firstPerson!.Addresses.FirstOrDefault(x => x.City == "Portland");
AnsiConsole.MarkupLine($"[white]{firstPerson.LastName,-8}{portlandAddress!.Company}[/]");
}
فرض کنید برنامههایی وجود دارند که توسعهدهندگان به دلایلی از EF Core استفاده نمیکنند، آنها همچنان میتوانند با این دادهها کار کنند اما به کار بیشتری نیاز دارند. در اینجا یک مثال خوانده شده است.
internal class DataProviderOperations
{
public static void ReadPersonAddress(int index = 0)
{
AnsiConsole.MarkupLine($"[cyan]Read data for address {index +1}[/]");
var statement =
"SELECT Id, FirstName, LastName, DateOfBirth, " +
$"JSON_VALUE(Addresses, '$[{index}].Street') AS Street, JSON_VALUE(Addresses, '$[{index}].City') AS City, JSON_VALUE(Addresses, '$[{index}].Company') AS Company FROM dbo.Person;";
using SqlConnection cn = new(ConfigurationHelper.ConnectionString());
using SqlCommand cmd = new() { Connection = cn, CommandText = statement };
cn.Open();
DataTable dt = new DataTable();
dt.Load(cmd.ExecuteReader());
foreach (DataRow row in dt.Rows)
{
Console.WriteLine($"{string.Join(", ", row.ItemArray)}");
}
Console.WriteLine();
AnsiConsole.MarkupLine("[cyan]DataTable columns[/]");
foreach (DataColumn column in dt.Columns)
{
Console.WriteLine($"{column.ColumnName,-15}{column.DataType.Name}");
}
}
}
مثال 2
یک توسعهدهنده یک کپی از قوانین WCAG را در یک فایل json پیدا کرد و میخواهد دادهها را در جدول پایگاه داده SQL-Server با استفاده از json زیر ذخیره کند.
{
"Section": "1-2-1",
"id": "media-equiv-av-only-alt",
"title": "Audio-only and Video-only (Prerecorded)",
"description": "For prerecorded audio-only and prerecorded video-only media, the following are true, except when the audio or video is a media alternative for text and is clearly labeled as such:",
"uri": "http://www.w3.org/TR/WCAG20/#media-equiv-av-only-alt",
"conformance_level": "A",
"wuhcag_summary": "Provide an alternative to video-only and audio-only content",
"wuhcag_detail": "\u003Cp\u003E\u003Cstrong\u003EProvide an alternative to video-only and audio-only content\u003C/strong\u003E\u003C/p\u003E\n\u003Cp\u003ESome users will find it difficult to use or understand things like podcasts and silent videos or animations.\u003C/p\u003E\n\u003Ch2\u003EWhat to do\u003C/h2\u003E\n\u003Cul\u003E\n \u003Cli\u003EWrite text transcripts for any audio-only media;\u003C/li\u003E\n \u003Cli\u003EWrite text transcripts for any video-only media; or\u003C/li\u003E\n \u003Cli\u003ERecord an audio-track for any video-only media;\u003C/li\u003E\n \u003Cli\u003EPlace the text transcript, or link to it, close to the media.\u003C/li\u003E\n\u003C/ul\u003E\n",
"wuhcag_tips": "\u003Cp\u003EAudio-only and video-only content needs to be supported by text transcripts that convey the same information as the media. Sometimes this is quite simple, other times you have to make a judgement call on what that really means. The best bet is, as always,to be honest with your customers: what does the media convey and does your transcript do the same? Could you swap one for the other?\u003C/p\u003E\n\u003Cp\u003EOne of the most common uses for text transcripts is when a podcast is published online. Embedding a podcast in a page is a great way of sharing your content but no good for your customers with hearing impairments. A text transcript should contain everything mentioned in the recording.\u003C/p\u003E\n\u003Cp\u003ELess commonly, some videos do not have sound. Your customers with visual impairments need help with this kind of content. A text transcript for a video without sound should describe what is going on in the video as clearly as possible. Try to focus on\n what the video is trying to say rather than getting bogged down with detail.\u003C/p\u003E\n\u003Cdiv class=\u0027mailmunch-forms-in-post-middle\u0027 style=\u0027display: none !important;\u0027\u003E\u003C/div\u003E\n\u003Cp\u003EAs an alternative for video-only content, you could also choose to record an audio track that narrates the video.\u003C/p\u003E\n\u003Cp\u003EFor both audio-only and video-only, create your text transcript and place it either directly beneath the content or insert a link next to the content.\u003C/p\u003E\n",
"wuhcag_what_to_do": "",
"wuhcag_exceptions": "\u003Cp\u003EIf the content is itself an alternative (you don\u2019t have to provide a transcript of the audio track you provided to explain the silent video you used).\u003C/p\u003E\n",
"wuhcag_related": [
{
"Section": "1-2-2",
"conformance_level": "A"
},
{
"Section": "1-2-3",
"conformance_level": "A"
},
{
"Section": "1-2-5",
"conformance_level": "AA"
},
{
"Section": "1-2-7",
"conformance_level": "AAA"
},
{
"Section": "1-2-8",
"conformance_level": "AAA"
}
],
"RelatedList": [
{
"Section": "\u00221-2-2\u0022",
"ConformanceLevel": "\u0022A\u0022"
},
{
"Section": "\u00221-2-3\u0022",
"ConformanceLevel": "\u0022A\u0022"
},
{
"Section": "\u00221-2-5\u0022",
"ConformanceLevel": "\u0022AA\u0022"
},
{
"Section": "\u00221-2-7\u0022",
"ConformanceLevel": "\u0022AAA\u0022"
},
{
"Section": "\u00221-2-8\u0022",
"ConformanceLevel": "\u0022AAA\u0022"
}
]
}
توجه داشته باشید که داده های بالا در ابتدا چندان تمیز نبودند و رفع آن زمان بر بود.
RelatedList را میتوان در یک جدول جداگانه قرار داد، اما در نظر بگیرید که دادهها تغییر نمیکنند.
مدل برای RelatedList
public class Related
{
public string Section { get; set; }
public string ConformanceLevel { get; set; }
public override string ToString() => $"{Section, -10}{ConformanceLevel}";
}
در اینجا کلاس/مدل اصلی با ویژگیها وجود دارد، بنابراین نام ویژگیها به خوبی تعریف شده است.
public partial class WebStandards
{
public int Identifier { get; set; }
public string Section { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("uri")]
public string Uri { get; set; }
[JsonPropertyName("conformance_level")]
public string ConformanceLevel { get; set; }
[JsonPropertyName("wuhcag_summary")]
public string Summary { get; set; }
[JsonPropertyName("wuhcag_detail")]
public string Detail { get; set; }
[JsonPropertyName("wuhcag_tips")]
public string Tips { get; set; }
[JsonPropertyName("wuhcag_what_to_do")]
public string Remedy { get; set; }
[JsonPropertyName("wuhcag_exceptions")]
public string Exceptions { get; set; }
public List<Related> RelatedList { get; set; }
}
مدل جدول پایگاه داده
پیکربندی در DbContext
کد برای خواندن json از یک فایل
internal class JsonOperations
{
private static string FileName => "wcagNew.json";
public static List<WebStandards> Read()
{
var jsonString = File.ReadAllText(FileName);
return JsonSerializer.Deserialize<List<WebStandards>>(jsonString);
}
}
برای اضافه کردن محتویات فایل json به جدول و انجام چندین پرس و جو کد کنید.
internal class DataOperations
{
/// <summary>
/// Populate table from reading a json file
/// </summary>
/// <param name="list">Data from json</param>
public static void AddRange(List<WebStandards> list)
{
using var context = new Context();
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.AddRange(list);
Console.WriteLine(context.SaveChanges());
}
/// <summary>
/// * Read data from database
/// * Get all AA complaint items
/// </summary>
public static void Read()
{
using var context = new Context();
var standards = context.WebStandards.ToList();
foreach (var standard in standards)
{
Console.WriteLine($"{standard.Identifier,-5}{standard.Title}");
// not all items have related items so assert for null list
if (standard.RelatedList is not null)
{
foreach (var related in standard.RelatedList)
{
Console.WriteLine($"\t{related.Section,-10}{related.ConformanceLevel}");
}
}
}
var aaStandards = standards.Where(x => x.ConformanceLevel == "AA");
AnsiConsole.MarkupLine("[cyan]ConformanceLevel AA[/]");
Console.WriteLine(aaStandards.Count());
AnsiConsole.MarkupLine("[cyan]Keyboard traps[/]");
var keyboardTraps = standards.FirstOrDefault(x => x.Title == "No Keyboard Trap");
Console.WriteLine(keyboardTraps.Description);
Console.WriteLine(keyboardTraps.Uri);
foreach (var related in keyboardTraps.RelatedList)
{
Console.WriteLine($"\t{related.Section,-10}{related.ConformanceLevel}");
}
Console.WriteLine();
}
}
فراخوانی موارد فوق از یک پروژه کنسول.
internal partial class Program
{
static void Main(string[] args)
{
DataOperations.AddRange(JsonOperations.Read());
DataOperations.Read();
AnsiConsole.MarkupLine("[yellow]Done[/]");
Console.ReadLine();
}
}
مثال 3
در مثالهای قبلی که از OwnMany استفاده کردیم، در این مثال یک راهاندازی برنامههای کاربردی مدل اصلی با دو ستون json داریم، یکی برای اطلاعات ایمیل و دیگری برای تنظیمات عمومی.
public partial class Applications
{
public int ApplicationId { get; set; }
/// <summary>
/// Application identifier
/// </summary>
public string ApplicationName { get; set; }
/// <summary>
/// Contact name
/// </summary>
public string ContactName { get; set; }
/// <summary>
/// For sending email messages
/// </summary>
public MailSettings MailSettings { get; set; }
public GeneralSettings GeneralSettings { get; set; }
}
public partial class GeneralSettings
{
public required string ServicePath { get; set; }
public required string MainDatabaseConnection { get; set; }
}
public partial class MailSettings
{
public required string FromAddress { get; set; }
public required string Host { get; set; }
public required int? Port { get; set; }
public required int? TimeOut { get; set; }
public required string PickupFolder { get; set; }
}
سپس در DbContext
و در نهایت کد برای پر کردن و خواندن مجدد داده ها.
namespace HybridTestProject
{
/// <summary>
/// Fast and dirty, not true test
/// </summary>
[TestClass]
public partial class MainTest : TestBase
{
[TestMethod]
[Ignore]
[TestTraits(Trait.EntityFrameworkCore)]
public void AddRecordsTest()
{
using var context = new Context();
Applications application1 = new()
{
ApplicationName = "ACED",
ContactName = "Kim Jenkins",
MailSettings = new MailSettings()
{
FromAddress = "FromAddressAced",
Host = "AcedHost",
PickupFolder = "C:\\MailDrop",
Port = 15,
TimeOut = 2000
},
GeneralSettings = new GeneralSettings()
{
ServicePath = "http://localhost:11111/api/",
MainDatabaseConnection = "Data Source=.\\sqlexpress;Initial Catalog=WorkingWithDate;Integrated Security=True;Encrypt=False"
}
};
Applications application2 = new()
{
ApplicationName = "SIDES",
ContactName = "Mike Adams",
MailSettings = new MailSettings()
{
FromAddress = "FromAddressSides",
Host = "SidesHost",
PickupFolder = "C:\\MailDrop",
Port = 15,
TimeOut = 2000
},
GeneralSettings = new GeneralSettings()
{
ServicePath = "http://localhost:22222/api/",
MainDatabaseConnection = "Data Source=.\\sqlexpress;Initial Catalog=WorkingWithTime;Integrated Security=True;Encrypt=False"
}
};
context.Add(application1);
context.Add(application2);
context.SaveChanges();
}
[TestMethod]
[TestTraits(Trait.EntityFrameworkCore)]
public void SimpleReadTest()
{
using var context = new Context();
var apps = context.Applications.ToList();
foreach (var app in apps)
{
Console.WriteLine($"{app.ApplicationId,-4}{app.ApplicationName,-8}{app.MailSettings.Host}");
Console.WriteLine($" {app.GeneralSettings.MainDatabaseConnection}");
}
}
[TestMethod]
[TestTraits(Trait.EntityFrameworkCore)]
public void ReadOneTest()
{
using var context = new Context();
var app = context.Applications.FirstOrDefault(x =>
x.MailSettings.FromAddress == "FromAddressSides");
Assert.IsNotNull(app);
}
}
}
خلاصه
تیم Microsoft EF Core شروع کار با json را در پایگاه داده SQL-Server آسان کرده است. آنها آنچه را که با ستون های json در نسخه بعدی EF Core، EF Core 8 امکان پذیر است، اصلاح خواهند کرد.
کد منبع
مخزن GitHub زیر را کلون کنید.
پروژه ها
همچنین ببینید
اعلام Entity Framework Core 7 RC2: JSON Columns