تامین مقادیر پارامترها در حین نگاشتهای AutoMapper
- جلوگیری از Lazy loading اشتباه
- کاهش تعداد فیلدهای بازگشت داده شدهی از دیتابیس و محدود ساختن آنها به خواصی که قرار است نگاشت شوند. در حالت معمولی استفادهی از متد Mapper.Map، تمام فیلدهای مدل بارگذاری شده و سپس در سمت کلاینت توسط AutoMapper نگاشت خواهند شد. اما در حالت استفادهی از متد ویژهی Project To، کوئری SQL ارسالی به بانک اطلاعاتی نیز مطابق نگاشت تعریف شده، تغییر کرده و خلاصه خواهد شد.
در این حالت یک چنین سناریویی را درنظر بگیرید. مدل متناظر با جدول بانک اطلاعاتی ما چنین ساختاری را دارد:
public class UserModel { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
public class UserViewModel { public string FirstName { get; set; } public string LastName { get; set; } public string UserIdentityName { get; set; } }
تعریف نگاشتهای پارامتری
برای حل این مساله، از روش زیر استفاده میشود:
string userIdentityName = null; this.CreateMap<UserModel, UserViewModel>() .ForMember(d => d.UserIdentityName, opt => opt.MapFrom(src => userIdentityName));
اکنون جهت استفادهی از این متغیر با قابلیت جایگزینی، میتوان به نحو ذیل عمل کرد:
var uiUsers = users.AsQueryable() .Project() .To<UserViewModel>(new { userIdentityName = "User.Identity.Name Value Here" }) .ToList();
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
تهیه یک بانک اطلاعاتی نمونه
برای نمایش امکانات کار با روش Database first، نیاز است یک بانک اطلاعاتی را به صورت مستقل و متداولی ایجاد کنیم. به همین جهت اسکریپت SQL ذیل را توسط Management studio اجرا کنید تا بانک اطلاعاتی BloggingCore2016، به همراه دو جدول به هم وابسته، در آن ایجاد شوند:
CREATE DATABASE [BloggingCore2016] GO USE [BloggingCore2016] GO CREATE TABLE [Blog] ( [BlogId] int NOT NULL IDENTITY, [Url] nvarchar(max) NOT NULL, CONSTRAINT [PK_Blog] PRIMARY KEY ([BlogId]) ); GO CREATE TABLE [Post] ( [PostId] int NOT NULL IDENTITY, [BlogId] int NOT NULL, [Content] nvarchar(max), [Title] nvarchar(max), CONSTRAINT [PK_Post] PRIMARY KEY ([PostId]), CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([BlogId]) ON DELETE CASCADE ); GO INSERT INTO [Blog] (Url) VALUES ('https://www.dntips.ir/'), ('http://blogs.msdn.com/dotnet'), ('http://blogs.msdn.com/webdev'), ('http://blogs.msdn.com/visualstudio') GO
پیشنیازهای مهندسی معکوس ساختار بانک اطلاعاتی در EF Core
در قسمت اول در حین بررسی «برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است. همچنین وجود مدخل Microsoft.EntityFrameworkCore.SqlServer.Design برای تدارک امکانات مهندسی معکوس ساختار یک بانک اطلاعاتی SQL Server ضروری است.
تبدیل ساختار دیتابیس BloggingCore2016 به کدهای معادل EF Core آن
پس از فعال سازی ابزارهای خط فرمان EF Core، به پوشهی اصلی پروژه مراجعه کرده، کلید shift را نگه دارید. سپس کلیک راست کرده و گزینهی Open command window here را انتخاب کنید تا خط فرمان از این پوشه آغاز شود. در ادامه دستور ذیل را صادر کنید:
dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose
اجرا این دستور سبب اتصال به رشتهی اتصالی ذکر شده که به بانک اطلاعاتی BloggingCore2016 اشاره میکند، میشود. سپس پروایدر مدنظر ذکر شدهاست. سوئیچ o محل درج فایلهای نهایی را مشخص میکند. برای مثال در اینجا فایلهای نهایی مهندسی معکوس شده در پوشهی Entities درج میشوند (تصویر فوق). همچنین در اینجا امکان ذکر فایل context تولیدی نیز وجود دارد. اگر علاقمند باشید تا تمام ریز جزئیات این عملیات را نیز مشاهده کنید، میتوانید پارامتر اختیاری verbose را نیز به انتهای دستور اضافه نمائید.
بقیه مراحل کار با این فایلهای تولید شده، با نکاتی که تاکنون عنوان شدهاند یکی است. برای مثال اگر میخواهید رشتهی اتصالی پیش فرض را از این Context تولید شده خارج کنید:
public partial class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true"); }
بررسی پارامترهای دیگر ابزار مهندسی معکوس به Code First
اگر دستور dotnet ef dbcontext scaffold --help را صادر کنیم، خروجی راهنمای ذیل را میتوان مشاهده کرد:
Usage: dotnet ef dbcontext scaffold [arguments] [options] Arguments: [connection] The connection string of the database [provider] The provider to use. For example, Microsoft.EntityFrameworkCore.SqlServer Options: -a|--data-annotations Use DataAnnotation attributes to configure the model where possible. If omitted, the output code will use only the fluent API. -c|--context <name> Name of the generated DbContext class. -f|--force Force scaffolding to overwrite existing files. Otherwise, the code will only proceed if no output files would be overwritten. -o|--output-dir <path> Directory of the project where the classes should be output. If omitted, the top-level project directory is used. --schema <schema> Selects a schema for which to generate classes. -t|--table <schema.table> Selects a table for which to generate classes. -e|--environment <environment> The environment to use. If omitted, "Development" is used. -h|--help Show help information -v|--verbose Enable verbose output
- حالت پیش فرض تنظیمات روابط مدلها در این روش، حالت استفاده از Fluent API است. اگر میخواهید آنرا به حالت استفادهی از Data Annotations تغییر دهید، پارامتر a- و یا data-annotations-- را در دستور نهایی ذکر کنید.
- حالت پیش فرض تولید فایلهای نهایی این روش، عدم بازنویسی فایلهای موجود است. اگر میخواهید پس از تغییر بانک اطلاعاتی، مجددا این فایلها را از صفر تولید کنید، پارامتر f- و یا force- را در دستور نهایی ذکر کنید.
بنابراین اگر میخواهید هربار فایلهای نهایی را بازنویسی کنید و همچنین روش کار با Data Annotations را ترجیح میدهید، دستور نهایی، شکل زیر را پیدا خواهد کرد:
dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose --force --data-annotations
کار با یک بانک اطلاعاتی موجود، با روش مهاجرتهای Code First
فرض کنید میخواهید از یک بانک اطلاعاتی از پیش موجود EF 6.x (یا هر بانک اطلاعاتی از پیش موجود دیگری)، به روش پیش فرض EF Core استفاده کنید. برای این منظور:
- ابتدا جدول migration history قدیمی آنرا حذف کنید؛ چون ساختار آن با EF Core یکی نیست.
- سپس با استفاده از دستور dotnet ef dbcontext scaffold فوق، معادل کلاسها، روابط و Context سازگار با EF Core آنرا تولید کنید.
- در ادامه رشتهی اتصالی پیش فرض آنرا از کلاس Context تولیدی خارج کرده و از یکی از روشهای مطرح شدهی در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» استفاده کنید.
- سپس نیاز است این Context جدید را توسط متد services.AddDbContext به لیست سرویسهای برنامه اضافه کنید. این مورد نیز در قسمت اول بررسی شدهاست.
- مرحلهی بعد، افزودن جدول __EFMigrationsHistory جدید EF Core، به این بانک اطلاعاتی است. برای این منظور به روش متداول فعال کردن مهاجرتها، دستور ذیل را صادر کنید:
dotnet ef migrations add InitialDatabase
using Microsoft.EntityFrameworkCore.Migrations; namespace Core1RtmEmptyTest.DataLayer.Migrations { public partial class InitialDatabase : Migration { protected override void Up(MigrationBuilder migrationBuilder) { } protected override void Down(MigrationBuilder migrationBuilder) { } } }
سپس دستور به روز رسانی بانک اطلاعاتی را صادر کنید:
dotnet ef database update
پس از این مرحله، روش کار، Code first خواهد بود. برای مثال خاصیتی را به کلاسی اضافه میکنید و سپس دو دستور ذیل را صادر خواهید کرد که در آن v2 یک نام دلخواه است:
dotnet ef migrations add v2 dotnet ef database update
In addition to bug fixes and foundation work for larger features, this preview includes updates to ensure that converters and comparers are handled by type mapping and to support using converters with value generators.
Roslyn #7
Workspace، در حقیقت نمایش اجزای یک Solution در ویژوال استودیو است و یک Solution متشکل است از تعدادی پروژه به همراه وابستگیهای بین آنها. هدف از وجود Workspace API در Roslyn، دسترسی به اطلاعات لازم جهت انجام امور Refactoring در سطح یک Solution است. برای مثال اگر قرار است نام خاصیتی تغییر کند و این خاصیت در چندین پروژهی دیگر در حال استفاده است، این نام باید در سراسر Solution جاری یافت شده و تغییر یابد. همچنین برفراز Workspace API تعدادی سرویس زبان مانند فرمت کنندههای کدها، تغییرنام دهندههای سیمبلها و توصیه کنندهها نیز تهیه شدهاند.
همچنین این سرویسها و API تهیه شده، منحصر به ویژوال استودیو نیستند و VS 2015 تنها از آنها استفاده میکند. برای مثال نگارشهای جدیدتر mono-develop لینوکسی نیز شروع به استفادهی از Roslyn کردهاند.
نمایش اجزای یک Solution
در ادامه مثالی را مشاهده میکنید که توسط آن نام Solution و سپس تمام پروژههای موجود در آنها به همراه نام فایلهای مرتبط و همچنین ارجاعات آنها در صفحه نمایش داده میشوند:
var ws = MSBuildWorkspace.Create(); var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result; // Print the root of the solution. Console.WriteLine(Path.GetFileName(sln.FilePath)); // Get dependency graph to perform a sort. var g = sln.GetProjectDependencyGraph(); var ps = g.GetTopologicallySortedProjects(); // Print all projects, their documents, and references. foreach (var p in ps) { var proj = sln.GetProject(p); Console.WriteLine("> " + proj.Name); Console.WriteLine(" > References"); foreach (var r in proj.ProjectReferences) { Console.WriteLine(" - " + sln.GetProject(r.ProjectId).Name); } foreach (var d in proj.Documents) { Console.WriteLine(" - " + d.Name); } }
Roslyn.sln > Roslyn01 > References - Program.cs - AssemblyInfo.cs - .NETFramework,Version=v4.6.AssemblyAttributes.cs
ایجاد یک Syntax highlighter با استفاده از Classification service
هدف از Classification service، رندر کردن فایلها در ادیتور جاری است. برای این منظور نیاز است بتوان واژههای کلیدی، کامنتها، نامهای نوعها و امثال آنها را به صورت کلاسه شده در اختیار داشت و سپس برای مثال هرکدام را با رنگی مجزا نمایش داد و رندر کرد.
در ادامه مثالی از آنرا ملاحظه میکنید:
var ws = MSBuildWorkspace.Create(); var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result; // Get the Tests\Bar.cs document. var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests"); var test = proj.Documents.Single(d => d.Name == "Bar.cs"); var tree = test.GetSyntaxTreeAsync().Result; var root = tree.GetRootAsync().Result; // Get all the spans in the document that are classified as language elements. var spans = Classifier.GetClassifiedSpansAsync(test, root.FullSpan).Result.ToDictionary(c => c.TextSpan.Start, c => c); // Print the source text with appropriate colorization. var txt = tree.GetText().ToString(); var i = 0; foreach (var c in txt) { var span = default(ClassifiedSpan); if (spans.TryGetValue(i, out span)) { var color = ConsoleColor.Gray; switch (span.ClassificationType) { case ClassificationTypeNames.Keyword: color = ConsoleColor.Cyan; break; case ClassificationTypeNames.StringLiteral: case ClassificationTypeNames.VerbatimStringLiteral: color = ConsoleColor.Red; break; case ClassificationTypeNames.Comment: color = ConsoleColor.Green; break; case ClassificationTypeNames.ClassName: case ClassificationTypeNames.InterfaceName: case ClassificationTypeNames.StructName: case ClassificationTypeNames.EnumName: case ClassificationTypeNames.TypeParameterName: case ClassificationTypeNames.DelegateName: color = ConsoleColor.Yellow; break; case ClassificationTypeNames.Identifier: color = ConsoleColor.DarkGray; break; } Console.ForegroundColor = color; } Console.Write(c); i++; }
توضیحات:
در اینجا نیز کار با ایجاد یک Workspace و سپس گشودن Solution ایی مشخص در آن آغاز میشود. سپس در آن به دنبال پروژهای به نام Roslyn04.Tests میگردیم. این پروژه حاوی تعدادی کلاس، جهت بررسی و آزمایش هستند. برای مثال در اینجا فایل Bar.cs آن قرار است آنالیز شود. پس از یافتن آن، ابتدا syntax tree آن دریافت میگردد و سپس به سرویس Classifier.GetClassifiedSpansAsync ارسال خواهد شد. خروجی آن شامل لیستی از Classified Spans است؛ مانند کلمات کلیدی، رشتهها، کامنتها و غیره. در ادامه این لیست تبدیل به یک دیکشنری میشود که کلید آن محل آغاز این span و مقدار آن، مقدار span است. سپس متن syntax tree دریافت شده و حرف به حرف آن در طی یک حلقه بررسی میشود. در این حلقه، مقدار i به محل حروف جاری مورد آنالیز اشاره میکند. اگر این محل در دیکشنری Classified Spans وجود داشت، یعنی یک span جدید شروع شدهاست و بر این اساس، نوع آن span را میتوان استخراج کرد و سپس بر اساس این نوع، رنگ متفاوتی را در صفحه نمایش داد.
سرویس فرمت کردن کدها
این سرویس کار فرمت خودکار کدهای بهم ریخته را انجام میدهد؛ مانند تنظیم فاصلههای خالی و یا ایجاد indentation و امثال آن. در حقیقت Ctlr K+D در ویژوال استودیو، دقیقا از همین سرویس زبان استفاده میکند.
کار کردن با این سرویس از طریق برنامه نویسی به نحو ذیل است:
var ws = MSBuildWorkspace.Create(); var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result; // Get the Tests\Qux.cs document. var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests"); var qux = proj.Documents.Single(d => d.Name == "Qux.cs"); Console.WriteLine("Before:"); Console.WriteLine(); Console.WriteLine(qux.GetSyntaxTreeAsync().Result.GetText()); Console.WriteLine(); Console.WriteLine(); // Apply formatting and print the result. var res = Formatter.FormatAsync(qux).Result; Console.WriteLine("After:"); Console.WriteLine(); Console.WriteLine(res.GetSyntaxTreeAsync().Result.GetText()); Console.WriteLine();
Before: using System; namespace Roslyn04.Tests { class Qux { public void Baz() { Console.WriteLine(42); return; } } } After: using System; namespace Roslyn04.Tests { class Qux { public void Baz() { Console.WriteLine(42); return; } } }
سرویس یافتن سیمبلها
یکی دیگر از قابلیتهایی که در ویژوال استودیو وجود دارد، امکان یافتن سیمبلها است. برای مثال این نوع یا کلاس خاص، در کجاها استفاده شدهاست و به آن ارجاعاتی وجود دارد. مواردی مانند Find all references، Go to definition و نمایش Call hierarchy از این سرویس استفاده میکنند.
var ws = MSBuildWorkspace.Create(); var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result; // Get the Tests project. var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests"); // Locate the symbol for the Bar.Foo method and the Bar.Qux property. var comp = proj.GetCompilationAsync().Result; var barType = comp.GetTypeByMetadataName("Roslyn04.Tests.Bar"); var fooMethod = barType.GetMembers().Single(m => m.Name == "Foo"); var quxProp = barType.GetMembers().Single(m => m.Name == "Qux"); // Find callers across the solution. Console.WriteLine("Find callers of Foo"); Console.WriteLine(); var callers = SymbolFinder.FindCallersAsync(fooMethod, sln).Result; foreach (var caller in callers) { Console.WriteLine(caller.CallingSymbol); foreach (var location in caller.Locations) { Console.WriteLine(" " + location); } } Console.WriteLine(); Console.WriteLine(); // Find all references across the solution. Console.WriteLine("Find all references to Qux"); Console.WriteLine(); var references = SymbolFinder.FindReferencesAsync(quxProp, sln).Result; foreach (var reference in references) { Console.WriteLine(reference.Definition); foreach (var location in reference.Locations) { Console.WriteLine(" " + location.Location); } }
اکنون با استفاده از سرویس SymbolFinder.FindCallersAsync تمام فراخوانهای متد Foo را در سراسر Solution جاری مییابیم.
سپس با استفاده از سرویس SymbolFinder.FindReferencesAsync تمام ارجاعات به خاصیت Qux را در Solution جاری نمایش میدهیم.
سرویس توصیه کننده
Intellisense در ویژوال استودیو از سرویس توصیه کنندهی Roslyn استفاده میکند.
var ws = MSBuildWorkspace.Create(); var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result; // Get the Tests\Foo.cs document. var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests"); var foo = proj.Documents.Single(d => d.Name == "Foo.cs"); // Find the 'dot' token in the first Console.WriteLine member access expression. var tree = foo.GetSyntaxTreeAsync().Result; var model = proj.GetCompilationAsync().Result.GetSemanticModel(tree); var consoleDot = tree.GetRoot().DescendantNodes().OfType<MemberAccessExpressionSyntax>().First().OperatorToken; // Get recommendations at the indicated cursor position. // // Console.WriteLine // ^ var res = Recommender.GetRecommendedSymbolsAtPosition( model, consoleDot.GetLocation().SourceSpan.Start + 1, ws).ToList(); foreach (var rec in res) { Console.WriteLine(rec); }
تعدادی از خروجیهای مثال فوق به صورت زیر هستند:
System.Console.Beep() System.Console.Beep(int, int) System.Console.Clear()
سرویس تغییر نام دادن
هدف از سرویس Renamer.RenameSymbolAsync، تغییر نام یک identifier در کل Solution است. نمونهای از نحوهی کاربرد آنرا در مثال ذیل مشاهده میکنید:
var ws = MSBuildWorkspace.Create(); var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result; // Get Tests\Bar.cs before making changes. var oldProj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests"); var oldDoc = oldProj.Documents.Single(d => d.Name == "Bar.cs"); Console.WriteLine("Before:"); Console.WriteLine(); var oldTxt = oldDoc.GetTextAsync().Result; Console.WriteLine(oldTxt); Console.WriteLine(); Console.WriteLine(); // Get the symbol for the Bar.Foo method. var comp = oldProj.GetCompilationAsync().Result; var barType = comp.GetTypeByMetadataName("Roslyn04.Tests.Bar"); var fooMethod = barType.GetMembers().Single(m => m.Name == "Foo"); // Perform the rename. var newSln = Renamer.RenameSymbolAsync(sln, fooMethod, "Foo2", ws.Options).Result; // Get Tests\Bar.cs after making changes. var newProj = newSln.Projects.Single(p => p.Name == "Roslyn04.Tests"); var newDoc = newProj.Documents.Single(d => d.Name == "Bar.cs"); Console.WriteLine("After:"); Console.WriteLine(); var newTxt = newDoc.GetTextAsync().Result; Console.WriteLine(newTxt);
سرویس ساده کننده
هدف از سرویس ساده کننده، سادهکردن و کاهش کدهای ارائه شده، از دید Semantics است. برای مثال اگر فضای نامی در قسمت using ذکر شدهاست، دیگر نیازی نیست تا این فضای نام به ابتدای فراخوانی یک متد آن اضافه شود و میتوان این قطعه از کد را سادهتر کرد و کاهش داد.
var ws = MSBuildWorkspace.Create(); var sln = ws.OpenSolutionAsync(@"..\..\..\Roslyn.sln").Result; // Get the Tests\Baz.cs document. var proj = sln.Projects.Single(p => p.Name == "Roslyn04.Tests"); var baz = proj.Documents.Single(d => d.Name == "Baz.cs"); Console.WriteLine("Before:"); Console.WriteLine(); Console.WriteLine(baz.GetSyntaxTreeAsync().Result.GetText()); Console.WriteLine(); Console.WriteLine(); var oldRoot = baz.GetSyntaxRootAsync().Result; var memberAccesses = oldRoot.DescendantNodes().OfType<CastExpressionSyntax>(); var newRoot = oldRoot.ReplaceNodes(memberAccesses, (_, m) => m.WithAdditionalAnnotations(Simplifier.Annotation)); var newDoc = baz.WithSyntaxRoot(newRoot); // Invoke the simplifier and print the result. var res = Simplifier.ReduceAsync(newDoc).Result; Console.WriteLine("After:"); Console.WriteLine(); Console.WriteLine(res.GetSyntaxTreeAsync().Result.GetText()); Console.WriteLine();
کدهای کامل این سری را از اینجا میتوانید دریافت کنید:
Roslyn-Samples.zip
سؤالات مصاحبه SQL
CREATE TABLE Users ( id INT NOT NULL AUTO_INCREMENT, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, gender ENUM('Male', 'Female') NOT NULL, PRIMARY KEY (id) ); CREATE TABLE BlogPosts ( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, user_id INT NOT NULL, KEY user_id (user_id), CONSTRAINT `blogposts_ibfk_1` FOREIGN KEY (user_id) REFERENCES Users (id), PRIMARY KEY (id) )
SELECT U.id, CONCAT_WS(' ', U.first_name, U.last_name) AS FullName, BP.title FROM Users AS U JOIN BlogPosts AS BP ON U.id = BP.user_id;
تبدیل یک ساختار Relational به JSON
میتوانیم خروجی موردنظر را در قالب JSON نیز کوئری بگیریم:
SELECT JSON_OBJECT('id', U.id, 'user', CONCAT_WS(' ', U.first_name, U.last_name), 'title', BP.title) FROM Users AS U JOIN BlogPosts AS BP ON U.id = BP.user_id;
به عنوان مثال میتوانیم لیست کاربران را به همراه بلاگ پستهایشان، اینگونه کوئری بگیریم:
SELECT JSON_OBJECT('user', CONCAT_WS(' ', U.first_name, U.last_name), 'blog_posts', JSON_ARRAYAGG(JSON_OBJECT('id', BP.id, 'title', BP.title))) AS JSON FROM Users AS U JOIN BlogPosts AS BP ON U.id = BP.user_id GROUP BY U.id;
خروجی کوئری فوق اینچنین خواهد بود:
تبدیل یک ساختار JSON به Relational
همچنین میتوانیم یک ساختار JSON را به صورت Relational تبدیل کنیم. اینکار توسط تابع JSON_TABLE قابل انجام است. کاری که این تابع انجام میدهد، ایجاد یک جدول موقت و کپی کردن دیتای موردنظر درون آن است. فرض کنید ساختار JSON زیر را به اینصورت درون دیتابیس ذخیره کردهایم:
{ "id": "1", "new": false, "sku": "asdf123", "tag": ["fashion", "men", "jacket", "full sleeve"], "name": "Lorem ipsum jacket", "image": [ "/assets/img/product/fashion/1.jpg", "/assets/img/product/fashion/3.jpg", "/assets/img/product/fashion/6.jpg", "/assets/img/product/fashion/8.jpg", "/assets/img/product/fashion/9.jpg" ], "price": 12.45, "rating": 4, "category": ["fashion", "men"], "discount": 10, "offerEnd": "October 5, 2020 12:11:00", "saleCount": 54, "description": { "fullDescription": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?", "shortDescription": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur." } }
اکنون میخواهیم چنین خروجیای داشته باشیم:
کوئری موردنیاز برای تهیه خروجی فوق اینچنین خواهد بود:
SELECT newTable.* FROM experiments.productMetadata, JSON_TABLE( data, "$" COLUMNS ( id INT PATH "$.id", new CHAR(5) PATH "$.new", sku CHAR(20) PATH "$.sku", price FLOAT PATH "$.price", rating FLOAT PATH "$.rating", name CHAR(255) PATH "$.name", discount FLOAT PATH "$.discount", offerEnd TEXT PATH "$.offerEnd", saleCount INT PATH "$.saleCount", description TEXT PATH "$.description.shortDescription" ) ) AS newTable;
تابع JSON_TABLE دو ورودی نیاز خواهد داشت؛ ورودی اول ستون JSONی است که میخوایم از آن کوئری بگیریم. ورودی دوم با تعیین path شروع خواهد شد. از آنجائیکه محتوای داخل ستون data به صورت آبجکت ذخیره شدهاست، از $ استفاده کردهایم که به معنای داکیومنت جاری است. سپس توسط کلمه کلیدی COLUMNS ساختار جدول موقتمان را تعریف خواهیم کرد. این ساختار به صورت یک آرگومان به COLUMNS ارسال خواهد شد و شبیه به ساختار CREATE TABLE است؛ با این تفاوت که بعد از تعریف نوع دادهای هر ستون باید مسیر رسیدن به مقدار موردنظر را نیز تعیین کنیم که در واقع همان سینتکس pathی است که در مثالهای قبل نیز بررسی کردیم. به عنوان مثال برای رسیدن به مقدار پراپرتی name، مسیر را به صورت name.$ نوشتهایم. این path از آرایه نیز پشتیبانی میکند؛ مثلاً برای دسترسی به عنصر اول آرایه tag کافی است اینگونه عمل کنیم:
tag CHAR(20) PATH "$.tag[0]"
همچنین تابع JSON_TABLE، از ساختارهای تودرتو نیز پشتیبانی میکند. به عنوان مثال برای داشتن مقادیر tag, category, image در خروجی میتوانیم از کلمه کلیدی NESTED استفاده کنیم:
SELECT newTable.* FROM experiments.productMetadata, JSON_TABLE( data, "$" COLUMNS ( id INT PATH "$.id", new CHAR(5) PATH "$.new", sku CHAR(20) PATH "$.sku", price FLOAT PATH "$.price", rating FLOAT PATH "$.rating", NESTED PATH '$.tag[*]' COLUMNS (tag TEXT PATH '$'), name CHAR(255) PATH "$.name", discount FLOAT PATH "$.discount", offerEnd TEXT PATH "$.offerEnd", saleCount INT PATH "$.saleCount", description TEXT PATH "$.description.shortDescription", NESTED PATH '$.image[*]' COLUMNS (image TEXT PATH '$'), NESTED PATH '$.category[*]' COLUMNS (category TEXT PATH '$') ) ) AS newTable;
درون COLUMNS میتوانیم یک name FOR ORDINALITY نیز تعیین کنیم. این فیلد دقیقاً مشابه AUTO INCREMENT در ساختار CREATE TABLE میباشد. به این معنا که به ازای هر آیتم فیلد nested، یک واحد اضافه خواهد شد. از آن میتوانیم به عنوان rowId برای آیتم آرایه استفاده کنیم:
NESTED PATH '$.category[*]' COLUMNS (categoryRowId FOR ORDINALITY, category TEXT PATH '$')
همچنین از ON EMPTY برای پراپرتیهایی که در ساختار JSON وجود ندارند نیز میتوانیم استفاده کنیم. به عنوان مثال در کوئری زیر گفتهایم در صورت عدم وجود price، یک مقدار پیشفرض باید نمایش داده شود و همچنین در صورت عدم وجود name، یک خطا در خروجی نمایش داده شود:
name CHAR(255) PATH "$.name" ERROR ON EMPTY, price FLOAT PATH "$.price" DEFAULT "0" ON EMPTY,
همچنین میتوانیم مقدار NULL را در صورت عدم وجود name ست کنیم:
name CHAR(255) PATH "$.name" NULL ON EMPTY,
In summary, the most important issues here are:
- The Aggregate’s main task is to protect invariants (business rules, the boundary of immediate consistency)
- In a multi-threaded environment, when multiple threads are running simultaneously on the same Aggregate, a business rule may be broken
- A way to solve concurrency conflicts is to use Pessimistic or Optimistic concurrency techniques
- Pessimistic Concurrency involves the use of a database transaction and a locking mechanism. In this way, requests are processed one after the other, so basically concurrency is lost and it can lead to deadlocks.
- Optimistic Concurrency technique is based on versioning database records and checking whether the previously loaded version has not been changed by another thread.
- Entity Framework Core supports Optimistic Concurrency. Pessimistic Concurrency is not supported
- The Aggregate must always be treated and versioned as a single unit
- Domain events are an indicator, that state was changed so Aggregate version should be changed as well
public class AggregateRootBase : Entity, IAggregateRoot { private int _versionId; public void IncreaseVersion() { _versionId++; } }
internal sealed class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order> { public void Configure(EntityTypeBuilder<Order> builder) { builder.Property("_versionId").HasColumnName("VersionId").IsConcurrencyToken(); //... } }
var order = await _ordersContext.Orders.FindAsync(orderId); order.AddOrderLine(request.ProductCode); var domainEvents = DomainEventsHelper.GetAllDomainEvents(order); if (domainEvents.Any()) { order.IncreaseVersion(); } await _ordersContext.SaveChangesAsync();
از SQL Server 2008 به بعد، نوع داده جدیدی به نام geography به نوعهای قابل تعریف ستونها اضافه شدهاست. در این نوع ستونها میتوان طول و عرض جغرافیایی یک نقطه را ذخیره کرد و سپس به کمک توابع توکاری از آنها کوئری گرفت.
در اینجا نمونهای از نحوهی تعریف و همچنین مقدار دهی این نوع ستونها را مشاهده میکنید:
CREATE TABLE [Geo]( [id] [int] IDENTITY(1,1) NOT NULL, [Location] [geography] NULL ) insert into Geo( Location , long, lat ) values ( geography::STGeomFromText ('POINT(-121.527200 45.712113)', 4326))
در اینجا متدهای توکار دیگری مانند geography::STDistance برای یافتن فاصله مستقیم بین نقاط نیز ارائه شدهاند. خروجی آن بر حسب متر است.
پشتیبانی از Spatial Data در Entity framework
پشتیبانی از نوع مخصوص geography، در EF 5 توسط نوع دادهای DbGeography ارائه شد. این نوع دادهای immutable است. به این معنا که پس از نمونه سازی، دیگر مقدار آن قابل تغییر نیست.
در اینجا برای نمونه مدلی را مشاهده میکنید که از نوع دادهای DbGeography استفاده میکند:
using System.Data.Entity.Spatial; namespace EFGeoTests.Models { public class GeoLocation { public int Id { get; set; } public DbGeography Location { get; set; } public string Name { get; set; } public string Type { get; set; } public override string ToString() { return string.Format("Name:{0}, Location:{1}", Name, Location); } } }
using System; using System.Data.Entity; using EFGeoTests.Models; namespace EFGeoTests.Config { public class MyContext : DbContext { public DbSet<GeoLocation> GeoLocations { get; set; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } } }
private static DbGeography createPoint(double longitude, double latitude, int coordinateSystemId = 4326) { var text = string.Format(CultureInfo.InvariantCulture.NumberFormat,"POINT({0} {1})", longitude, latitude); return DbGeography.PointFromText(text, coordinateSystemId); }
تهیه منبع دادهی جغرافیایی
برای تدارک یک مثال واقعی جغرافیایی، نیاز به اطلاعاتی دقیق داریم. این نوع اطلاعات عموما توسط یک سری فایل مخصوص به نام Shapefiles که حاوی اطلاعات برداری جغرافیایی هستند ارائه میشوند. برای نمونه اطلاعات جغرافیایی به روز ایران را از آدرس ذیل میتوانید دریافت کنید:
http://download.geofabrik.de/asia/iran.html
http://download.geofabrik.de/asia/iran-latest.shp.zip
پس از دریافت این فایل، به تعدادی فایل با پسوندهای shp، shx و dbf خواهیم رسید.
فایلهای shp بیانگر فرمت اشکال ذخیره شده هستند. فایلهای shx یک سری ایندکس بوده و فایلهای dbf از نوع بانک اطلاعاتی dBase IV میباشند.
همچنین اگر فایلهای prj را باز کنید، یک چنین اطلاعاتی در آن موجودند:
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]
خواندن فایلهای shp در دات نت
پس از دریافت فایلهای shp و بانکهای اطلاعاتی مرتبط با اطلاعات جغرافیایی ایران، اکنون نوبت به پردازش این فایلهای مخصوص با فرمت بانک اطلاعاتی فاکس پرو مانند، رسیدهاست. برای این منظور میتوان از پروژهی سورس باز ذیل استفاده کرد:
این پروژه در خواندن فایلهای shp بدون نقص عمل میکند اما توانایی خواندن نامهای فارسی وارد شده در این نوع بانکهای اطلاعاتی را ندارد. برای رفع این مشکل، سورس آن را از Codeplex دریافت کنید. سپس فایل Shapefile.cs را گشوده و ابتدای خاصیت Current آنرا به نحو ذیل تغییر دهید:
/// <summary> /// Gets the current shape in the collection /// </summary> public Shape Current { get { if (_disposed) throw new ObjectDisposedException("Shapefile"); if (!_opened) throw new InvalidOperationException("Shapefile not open."); // get the metadata StringDictionary metadata = null; if (!RawMetadataOnly) { metadata = new StringDictionary(); for (int i = 0; i < _dbReader.FieldCount; i++) { string value = _dbReader.GetValue(i).ToString(); if (_dbReader.GetDataTypeName(i) == "DBTYPE_WVARCHAR") { // برای نمایش متون فارسی نیاز است value = Encoding.UTF8.GetString(Encoding.GetEncoding(720).GetBytes(value)); } metadata.Add(_dbReader.GetName(i), value); } }
using System.Collections.Generic; using System.Linq; using Catfood.Shapefile; namespace EFGeoTests { public class MapPoint { public Dictionary<string, string> Metadata { set; get; } public double X { set; get; } public double Y { set; get; } } public static class ShapeReader { public static IList<MapPoint> ReadShapeFile(string path) { var results = new List<MapPoint>(); using (var shapefile = new Shapefile(path)) { foreach (var shape in shapefile) { if (shape.Type != ShapeType.Point) continue; var shapePoint = shape as ShapePoint; if (shapePoint == null) continue; var metadataNames = shape.GetMetadataNames(); if(!metadataNames.Any()) continue; var metadata = new Dictionary<string, string>(); foreach (var metadataName in metadataNames) { metadata.Add(metadataName,shape.GetMetadata(metadataName)); } results.Add(new MapPoint { Metadata = metadata, X = shapePoint.Point.X, Y = shapePoint.Point.Y }); } } return results; } } }
افزودن اطلاعات جغرافیایی به بانک اطلاعاتی SQL Server به کمک Entity framework
فایل places.shp را در مجموعه فایلهایی که در ابتدای بحث عنوان شدند، میتوانید مشاهده کنید. قصد داریم اطلاعات نقاط آنرا به مدل GeoLocation انتساب داده و سپس ذخیره کنیم:
var points = ShapeReader.ReadShapeFile("IranShapeFiles\\places.shp"); using (var context = new MyContext()) { context.Configuration.AutoDetectChangesEnabled = false; context.Configuration.ProxyCreationEnabled = false; context.Configuration.ValidateOnSaveEnabled = false; if (context.GeoLocations.Any()) return; foreach (var point in points) { context.GeoLocations.Add(new GeoLocation { Name = point.Metadata["name"], Type = point.Metadata["type"], Location = createPoint(point.X, point.Y) }); } context.SaveChanges(); }
در فایلهای مرتبط با places.shp، متادیتا name، معادل نام شهرهای ایران است و type آن بیانگر شهر، روستا و امثال آن میباشد.
پس از اینکه اطلاعات مکانهای ایران، در SQL Server ذخیره شدند، نمایش بصری آنها را در management studio نیز میتوان مشاهده کرد:
کوئری گرفتن از اطلاعات جغرافیایی
فرض کنید میخواهیم مکانهایی را با فاصله کمتر از 5 کیلومتر از تهران پیدا کنیم:
var tehran = createPoint(51.4179604, 35.6884243); using (var context = new MyContext()) { // find any locations within 5 kilometers ordered by distance var locations = context.GeoLocations .Where(loc => loc.Location.Distance(tehran) < 5000) .OrderBy(loc => loc.Location.Distance(tehran)) .ToList(); foreach (var location in locations) { Console.WriteLine(location.Name); } }
و یا اگر بخواهیم دقیقا بر اساس مختصات یک نقطه، مکانی را بیابیم، میتوان از متد SpatialEquals استفاده کرد:
var tehran = createPoint(51.4179604, 35.6884243); using (var context = new MyContext()) { // find any locations within 5 kilometers ordered by distance var tehranLocation = context.GeoLocations.FirstOrDefault(loc => loc.Location.SpatialEquals(tehran)); if (tehranLocation != null) { Console.WriteLine(tehranLocation.Type); } }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
EFGeoTests.zip