فصل ۸: Interpreted Queries، IQueryable، EF Core و Expression Treeها
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
Interpreted Queryها
LINQ دو معماری موازی فراهم میکند: Local Query برای Collectionهای محلی Object و Interpreted Query برای منابع دادهٔ Remote. تا اینجا معماری Local Queryها را دیدیم که روی Collectionهای پیادهکنندهٔ IEnumerable<T> کار میکنند. Local Queryها بهطور پیشفرض به Query Operatorهای Class با نام Enumerable Resolve میشوند و آن Operatorها نیز به Chainهایی از Decorator Sequenceها تبدیل میشوند. Delegateهایی که میپذیرند — چه با Query Syntax، چه Fluent Syntax یا Delegate سنتی بیان شده باشند — کاملاً محلی و بخشی از کد Intermediate Language یا IL هستند، درست مانند هر Method دیگر C#.
در مقابل، Interpreted Queryها توصیفیاند. آنها روی Sequenceهایی کار میکنند که IQueryable<T> را پیادهسازی میکنند و به Query Operatorهای Class با نام Queryable Resolve میشوند؛ این Operatorها Expression Treeهایی تولید میکنند که در Runtime تفسیر میشوند. برای مثال این Expression Treeها میتوانند به SQL Query ترجمه شوند و اجازه دهند با LINQ یک Database را Query کنید.
برای نوشتن Interpreted Query باید از APIای شروع کنید که Sequenceهایی از Type با نام IQueryable<T> ارائه دهد. نمونهٔ آن Entity Framework Core یا EF Core مایکروسافت است که Query کردن انواع Databaseها از جمله SQL Server، Oracle، MySQL، PostgreSQL و SQLite را ممکن میکند.
همچنین با Method با نام AsQueryable میتوان یک Wrapper از نوع IQueryable<T> دور یک Enumerable Collection عادی ساخت. AsQueryable در «Building Query Expressions» صفحهٔ 466 شرح داده میشود.
برای مثال، یک Table سادهٔ Customer در SQL Server میسازیم و با Script زیر چند Name در آن درج میکنیم:
create table Customer
(
ID int not null primary key,
Name varchar(30)
)
insert Customer values (1, 'Tom')
insert Customer values (2, 'Dick')
insert Customer values (3, 'Harry')
insert Customer values (4, 'Mary')
insert Customer values (5, 'Jay')
با وجود این Table، میتوانیم در C# یک LINQ Interpreted Query بنویسیم که با EF Core مشتریانی را بگیرد که Name آنها حرف a دارد:
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using var dbContext = new NutshellContext();
IQueryable<string> query = from c in dbContext.Customers
where c.Name.Contains ("a")
orderby c.Name.Length
select c.Name.ToUpper();
foreach (string name in query) Console.WriteLine (name);
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
}
// We’ll explain the following class in more detail in the next section.
public class NutshellContext : DbContext
{
public virtual DbSet<Customer> Customers { get; set; }
protected override void OnConfiguring (DbContextOptionsBuilder builder)
=> builder.UseSqlServer ("...connection string...");
protected override void OnModelCreating (ModelBuilder modelBuilder)
=> modelBuilder.Entity<Customer>().ToTable ("Customer")
.HasKey (c => c.ID);
}
EF Core این Query را به SQL زیر ترجمه میکند:
SELECT UPPER([c].[Name])
FROM [Customers] AS [c]
WHERE CHARINDEX(N'a', [c].[Name]) > 0
ORDER BY CAST(LEN([c].[Name]) AS int)
Result نهایی:
Interpreted Queryها چگونه کار میکنند
بیایید پردازش Query قبلی را بررسی کنیم. نخست Compiler، Query Syntax را دقیقاً مانند Local Query به Fluent Syntax تبدیل میکند:
IQueryable<string> query = dbContext.Customers
.Where (n => n.Name.Contains ("a"))
.OrderBy (n => n.Name.Length)
.Select (n => n.Name.ToUpper());
سپس Compiler Query Operator Methodها را Resolve میکند. همینجا Local و Interpreted Query از هم جدا میشوند: Interpreted Query به Operatorهای Queryable میرود نه Enumerable.
منبع اصلی Query، یعنی dbContext.Customers، از Type با نام DbSet<T> است که IQueryable<T> — و در نتیجه IEnumerable<T> — را پیادهسازی میکند. بنابراین Compiler برای Resolve کردن Where دو انتخاب دارد؛ Extension Method در Enumerable یا این Extension Method در Queryable:
public static IQueryable<TSource> Where<TSource> (this
IQueryable<TSource> source, Expression <Func<TSource,bool>> predicate)
Compiler، Queryable.Where را انتخاب میکند چون Signature آن Match اختصاصیتری است. Queryable.Where Predicate را داخل Expression<TDelegate> میپذیرد. این به Compiler دستور میدهد Lambda دادهشده — یعنی n => n.Name.Contains("a") — را به Expression Tree تبدیل کند، نه Delegate کامپایلشده. Expression Tree یک Object Model مبتنی بر Typeهای System.Linq.Expressions است که در Runtime قابل Inspection است تا EF Core بعداً آن را به SQL Statement تبدیل کند.
چون Queryable.Where نیز IQueryable<T> برمیگرداند، همین فرایند برای OrderBy و Select ادامه پیدا میکند. شکل 8-9 Result را نشان میدهد؛ در بخش سایهخورده Expression Treeی وجود دارد که کل Query را توصیف میکند و در Runtime قابل Traversal است.
شکل 8-9 — Queryهای InterpretedExpression Tree به Provider داده میشود تا به زبان منبع راهدور مانند SQL ترجمه شود.
" alt="شکل 8-9: Composition یک Interpreted Query"/>شکل 8-9 — Interpreted Query Composition؛ Queryable Operatorها بهجای Delegate، Expression Tree تولید میکنند.اجرا
Interpreted Queryها مانند Local Query از مدل Deferred Execution پیروی میکنند. SQL Statement تا زمانی که Enumerate کردن Query را شروع نکنید تولید نمیشود؛ و اگر همان Query را دو بار Enumerate کنید، Database نیز دو بار Query میشود.
اما در پشت صحنه نحوهٔ اجرا متفاوت است. هنگام Enumerate کردن Interpreted Query، Sequence بیرونی برنامهای اجرا میکند که کل Expression Tree را Traversal کرده و بهصورت یک Unit پردازش میکند. در مثال ما EF Core Expression Tree را به SQL Statement ترجمه، آن را اجرا و Resultها را به شکل Sequence برمیگرداند.
پیشتر LINQ Query را به خط تولید تشبیه کردیم. اما با Enumerate کردن Conveyor Belt از نوع IQueryable، برخلاف Local Query کل خط تولید به حرکت درنمیآید. فقط Belt مربوط به IQueryable با Enumerator ویژهای فعال میشود که یک «مدیر تولید» را فراخوانی میکند. مدیر کل خط تولید را بررسی میکند؛ این خط بهجای کد کامپایلشده از پوستههای Method Call Expression تشکیل شده که دستورها، یعنی Expression Treeها، روی آنها چسبانده شده است. مدیر همهٔ Expressionها را Traverse میکند و در این مثال آنها را به یک SQL Statement واحد تبدیل میکند.
SQL Statement اجرا میشود و Resultها به Consumer بازگردانده میشوند. فقط یک Belt واقعاً میچرخد؛ بقیهٔ خط تولید شبکهای از Shellهای خالی است که صرفاً توضیح میدهد چه کاری باید انجام شود.
این مدل پیامدهای عملی دارد. در Local Query میتوانید نسبتاً آسان با Iteratorها Query Method خودتان را بنویسید و مجموعهٔ Operatorهای ازپیشتعریفشده را گسترش دهید. در Remote Query این کار دشوار و حتی نامطلوب است. اگر Extension Method با نام MyWhere برای IQueryable<T> بنویسید، مثل این است که Dummy خودتان را وارد خط تولید کردهاید؛ Provider نمیداند با آن چه کند. حتی اگر در همان مرحله دخالت کنید، Solution به Provider مشخصی مثل EF Core Hardwire میشود و با پیادهسازیهای دیگر IQueryable کار نمیکند. مزیت مجموعهٔ Standard Methodهای Queryable این است که Vocabulary استانداردی برای Query هر Remote Collection میسازند؛ با گسترش Vocabulary، Interoperability را از دست میدهید.
پیامد دیگر این است که Provider از نوع IQueryable شاید نتواند بعضی Queryها را حتی با Standard Methodها پردازش کند. EF Core به قابلیتهای Database Server محدود است و بعضی LINQ Queryها ترجمهٔ SQL ندارند. اگر SQL بدانید معمولاً شهود خوبی دربارهٔ این محدودیتها خواهید داشت، هرچند گاهی باید آزمایش کنید تا ببینید چه چیزی Runtime Error میدهد؛ و گاهی تعجب میکنید که چه چیزهایی واقعاً کار میکنند.
ترکیب Interpreted و Local Query
یک Query میتواند هم Interpreted Operator و هم Local Operator داشته باشد. Pattern رایج این است که Local Operatorها بیرون و Componentهای Interpreted داخل باشند؛ یعنی Interpreted Query به Local Query داده بدهد. این Pattern برای Query کردن Database مناسب است.
فرض کنید Extension Method سفارشی زیر Stringهای Collection را دوتادوتا Pair میکند:
public static IEnumerable<string> Pair (this IEnumerable<string> source)
{
string firstHalf = null;
foreach (string element in source)
if (firstHalf == null)
firstHalf = element;
else
{
yield return firstHalf + ", " + element;
firstHalf = null;
}
}
میتوانیم آن را در Queryی استفاده کنیم که EF Core و Local Operator را مخلوط میکند:
using var dbContext = new NutshellContext ();
IEnumerable<string> q = dbContext.Customers
.Select (c => c.Name.ToUpper())
.OrderBy (n => n)
.Pair() // Local from this point on.
.Select ((n, i) => "Pair " + i.ToString() + " = " + n);
foreach (string element in q) Console.WriteLine (element);
// Pair 0 = DICK, HARRY
// Pair 1 = JAY, MARY
چون dbContext.Customers Typeی است که IQueryable<T> را پیادهسازی میکند، Select به Queryable.Select Resolve میشود و Output آن نیز IQueryable<T> است؛ پس OrderBy نیز به Queryable.OrderBy میرود. اما Operator بعدی، Pair، Overloadی برای IQueryable<T> ندارد و فقط IEnumerable<T> را میپذیرد، بنابراین به Method محلی ما Resolve میشود و Interpreted Query را داخل Local Query Wrap میکند. Pair هم IEnumerable برمیگرداند، پس Select بعد از آن نیز Local است.
در سمت EF Core، SQL حاصل معادل این است:
SELECT UPPER([c].[Name]) FROM [Customers] AS [c] ORDER BY UPPER([c].[Name])
باقی کار Local انجام میشود؛ یعنی در بیرون یک Local Query داریم که Source آن یک Interpreted Query در داخل است.
AsEnumerable
Enumerable.AsEnumerable سادهترین Query Operator است. تعریف کامل آن:
public static IEnumerable<TSource> AsEnumerable<TSource>
(this IEnumerable<TSource> source)
{
return source;
}
هدفش Cast کردن Sequence از نوع IQueryable<T> به IEnumerable<T> است تا Query Operatorهای بعدی به Operatorهای Enumerable Bind شوند، نه Queryable. در نتیجه ادامهٔ Query بهصورت Local اجرا میشود.
فرض کنید Table با نام MedicalArticles داریم و میخواهیم با EF Core همهٔ Articleهای مربوط به Influenza را بگیریم که Abstract آنها کمتر از 100 Word دارد. برای شرط دوم به Regular Expression نیاز داریم:
Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");
using var dbContext = new NutshellContext ();
var query = dbContext.MedicalArticles
.Where (article => article.Topic == "influenza" &&
wordCounter.Matches (article.Abstract).Count < 100);
SQL Server از Regular Expression پشتیبانی نمیکند، بنابراین EF Core با خطای «قابل ترجمه به SQL نیست» متوقف میشود.
راهحل این است که Query را در دو مرحله اجرا کنیم: ابتدا همهٔ Articleهای Influenza را با EF Core بگیریم، سپس Abstractهای کمتر از 100 Word را روی Client Filter کنیم:
Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");
using var dbContext = new NutshellContext ();
IEnumerable<MedicalArticle> efQuery = dbContext.MedicalArticles
.Where (article => article.Topic == "influenza");
IEnumerable<MedicalArticle> localQuery = efQuery
.Where (article => wordCounter.Matches (article.Abstract).Count < 100);
چون efQuery از Type با نام IEnumerable<MedicalArticle> است، Query دوم به Local Query Operatorها Bind میشود و آن بخش Filter روی Client اجرا میشود. با AsEnumerable همین را در یک Query مینویسیم:
Regex wordCounter = new Regex (@"\b(\w|[-'])+\b");
using var dbContext = new NutshellContext ();
var query = dbContext.MedicalArticles
.Where (article => article.Topic == "influenza")
.AsEnumerable()
.Where (article => wordCounter.Matches (article.Abstract).Count < 100);
Alternative، فراخوانی ToArray یا ToList است. مزیت AsEnumerable این است که نه اجرای Query را فوراً Force میکند و نه Storage Structure جدیدی میسازد.
نمونههای بیشتر ترکیب Interpreted و Local Query در فصل 10 میآیند.
EF Core
در این فصل و فصل 9 از EF Core برای نشاندادن Interpreted Query استفاده میکنیم. اکنون قابلیتهای اصلی آن را بررسی میکنیم.
Entity Classهای EF Core
EF Core اجازه میدهد هر Classی را نمایندهٔ داده قرار دهید، به شرط اینکه برای هر Columnی که میخواهید Query کنید Public Property داشته باشد. برای Query و Update Table با نام Customers میتوانیم Entity Class زیر را تعریف کنیم:
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
}
DbContext
پس از Entity Classها، گام بعدی Subclass کردن DbContext است. یک Instance از آن Session کاری شما با Database را نمایندگی میکند. معمولاً Subclass شما برای هر Entity در Model یک Property از نوع DbSet<T> دارد:
public class NutshellContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
... properties for other tables ...
}
Object از نوع DbContext سه کار اصلی انجام میدهد:
- بهعنوان Factory برای ساخت Objectهای
DbSet<> قابل Query عمل میکند. - Changeهای Entityها را Track میکند تا بتوانید آنها را به Database برگردانید؛ «Change Tracking» صفحهٔ 461.
- Virtual Methodهایی برای Override کردن Configuration Connection و Model فراهم میکند.
پیکربندی Connection
با Override کردن OnConfiguring میتوانید Database Provider و Connection String را مشخص کنید:
public class NutshellContext : DbContext
{
...
protected override void OnConfiguring (DbContextOptionsBuilder
optionsBuilder) =>
optionsBuilder.UseSqlServer
(@"Server=(local);Database=Nutshell;Trusted_Connection=True");
}
در این مثال Connection String بهصورت String Literal است؛ Application تولیدی معمولاً آن را از Configuration File مانند appsettings.json میخواند. UseSqlServer Extension Methodی در Assembly مربوط به Package با نام Microsoft.EntityFramework.SqlServer است. Packageهای Providerهای دیگر مانند Oracle، MySQL، PostgreSQL و SQLite نیز موجودند.
در OnConfiguring Optionهای دیگری مانند Lazy Loading را نیز میتوان فعال کرد.
پیکربندی Model
EF Core بهطور پیشفرض Convention-based است؛ یعنی Schema را از نام Class و Property استنباط میکند. با Override کردن OnModelCreating و فراخوانی Extension Methodها روی ModelBuilder میتوانید Defaultها را با Fluent API تغییر دهید. برای مثال نام Table مربوط به Entity با نام Customer را صریحاً مشخص میکنیم:
protected override void OnModelCreating (ModelBuilder modelBuilder) =>
modelBuilder.Entity<Customer>()
.ToTable ("Customer"); // Table is called 'Customer'
بدون این کد EF Core Entity را به Table با نام Customers Map میکند، چون Property با نام Customers در DbContext داریم:
public DbSet<Customer> Customers { get; set; }
کد زیر همهٔ Entityها را به Table Nameهایی Map میکند که با نام Entity Class — معمولاً Singular — برابرند نه نام Property از نوع DbSet<T> — معمولاً Plural:
protected override void OnModelCreating (ModelBuilder modelBuilder)
{
foreach (IMutableEntityType entityType in
modelBuilder.Model.GetEntityTypes())
{
modelBuilder.Entity (entityType.Name)
.ToTable (entityType.ClrType.Name);
}
}
Fluent API Syntax گستردهای برای Configuration Columnها دارد. دو Method رایج: HasColumnName که Property را به Column با نام متفاوت Map میکند، و IsRequired که نشان میدهد Column Nullable نیست.
protected override void OnModelCreating (ModelBuilder modelBuilder) =>
modelBuilder.Entity<Customer> (entity =>
{
entity.ToTable ("Customer");
entity.Property (e => e.Name)
.HasColumnName ("Full Name") // Column name is 'Full Name'
.IsRequired(); // Column is not nullable
});
جدول 8-1 — Methodهای مهم Fluent API برای Configuration Model| Method | هدف | نمونه |
|---|
ToTable | تعیین نام Table برای یک Entity | builder.Entity<Customer>().ToTable("Customer") |
HasColumnName | تعیین نام Column متفاوت برای یک Property | builder.Entity<Customer>().Property(c => c.Name).HasColumnName("Full Name") |
HasKey | تعیین Key، معمولاً وقتی از Convention منحرف است | builder.Entity<Customer>().HasKey(c => c.CustomerNr) |
ادامهٔ جدول 8-1| Method | هدف | نمونه |
|---|
IsRequired | Property باید مقدار داشته باشد و Nullable نیست | builder.Entity<Customer>().Property(c => c.Name).IsRequired() |
HasMaxLength | حداکثر Length یک Type با Length متغیر مانند String | builder.Entity<Customer>().Property(c => c.Name).HasMaxLength(60) |
HasColumnType | تعیین Database Data Type برای Column | builder.Entity<Purchase>().Property(p => p.Description).HasColumnType("varchar(80)") |
Ignore | نادیدهگرفتن یک Type | builder.Ignore<Products>() |
Ignore | نادیدهگرفتن Property یک Type | builder.Entity<Customer>().Ignore(c => c.ChatName) |
HasIndex | تعریف Property یا ترکیب Propertyها بهعنوان Index | builder.Entity<Purchase>().HasIndex(p => new { p.Date, p.Price })؛ و برای Index یکتا: builder.Entity<MedicalArticle>().HasIndex(a => a.Topic).IsUnique() |
HasOne | تعریف Relation یکبهچند از سمت Child؛ بخش Navigation Properties | builder.Entity<Purchase>().HasOne(p => p.Customer).WithMany(c => c.Purchases) |
HasMany | تعریف Relation یکبهچند از سمت Parent | builder.Entity<Customer>().HasMany(c => c.Purchases).WithOne(p => p.Customer) |
ساخت Database
EF Core از Code-first Approach پشتیبانی میکند: میتوانید اول Entity Classها را تعریف و سپس از EF Core بخواهید Database را ایجاد کند. سادهترین روش:
dbContext.Database.EnsureCreated();
روش بهتر استفاده از Migrationهای EF Core است که هم Database را میسازد و هم آن را طوری Configure میکند که در آینده با تغییر Entity Classها Schema خودکار Update شود. در Package Manager Console ویژوال استودیو:
Install-Package Microsoft.EntityFrameworkCore.Tools
Add-Migration InitialCreate
Update-Database
Command اول Toolهای مدیریت EF Core را داخل Visual Studio نصب میکند؛ دوم C# Class ویژهای به نام Code Migration میسازد که Instructionهای ساخت Database را دارد؛ سوم آن Instructionها را روی Connection String مشخصشده در Application Configuration اجرا میکند.
استفاده از DbContext
بعد از تعریف Entity Classها و Subclass کردن DbContext، میتوانید Context را Instantiate و Database را Query کنید:
using var dbContext = new NutshellContext();
Console.WriteLine (dbContext.Customers.Count());
// Executes "SELECT COUNT(*) FROM [Customer] AS [c]"
همچنین با Context میتوانید در Database بنویسید. درج Row جدید:
using var dbContext = new NutshellContext();
Customer cust = new Customer()
{
Name = "Sara Wells"
};
dbContext.Customers.Add (cust);
dbContext.SaveChanges(); // Writes changes back to database
Query کردن Customer تازهدرجشده:
using var dbContext = new NutshellContext();
Customer cust = dbContext.Customers
.Single (c => c.Name == "Sara Wells")
Update Name و نوشتن تغییر:
cust.Name = "Dr. Sara Wells";
dbContext.SaveChanges();
Object Tracking
یک DbContext همهٔ Entityهایی را که Instantiate میکند Track میکند تا هر بار همان Row را درخواست کردید همان Object قبلی را برگرداند. در Lifetime یک Context هرگز دو Entity جدا که به یک Row با Primary Key یکسان اشاره دارند منتشر نمیشوند. این قابلیت Object Tracking نام دارد.
اگر Customer با Name الفبایی اول همان Lowest ID را نیز داشته باشد، در مثال زیر a و b به همان Object Reference میدهند:
using var dbContext = new NutshellContext ();
Customer a = dbContext.Customers.OrderBy (c => c.Name).First();
Customer b = dbContext.Customers.OrderBy (c => c.ID).First();
Dispose کردن DbContext
هرچند DbContext، IDisposable را پیادهسازی میکند، عموماً میتوانید بدون Dispose صریح هم کار کنید. Dispose، Connection Context را Dispose میکند، اما معمولاً لازم نیست چون EF Core بعد از پایان Retrieve Resultهای Query، Connection را خودکار میبندد.
Dispose زودهنگام Context بهدلیل Lazy Evaluation مشکلساز است:
IQueryable<Customer> GetCustomers (string prefix)
{
using (var dbContext = new NutshellContext ())
return dbContext.Customers
.Where (c => c.Name.StartsWith (prefix));
}
...
foreach (Customer c in GetCustomers ("a"))
Console.WriteLine (c.Name);
این کد Fail میشود چون Query هنگام Enumeration ارزیابی میشود، یعنی بعد از Dispose شدن Context.
چند Caveat برای Dispose نکردن Context وجود دارد: متکی است که Connection Object همهٔ Unmanaged Resourceها را در Close آزاد کند؛ اگر بهصورت دستی GetEnumerator را فراخوانی کنید و نه Enumerator را Dispose کنید و نه Sequence را کامل مصرف کنید، Connection باز میماند؛ و برخی از نظر Style ترجیح میدهند هر Object پیادهکنندهٔ IDisposable را Dispose کنند.
اگر میخواهید Context را صریح Dispose کنید، باید Instance آن را به Methodهایی مانند GetCustomers Pass کنید. در ASP.NET Core MVC، Context معمولاً از DI میآید و Infrastructure عمر آن را مدیریت میکند: با آغاز Unit of Work مثل HTTP Request ساخته و با پایان آن Dispose میشود.
وقتی EF Core Query دوم را میبیند، Database را Query و یک Row میگیرد، Primary Key آن را میخواند و در Entity Cache Context Lookup میکند. اگر Match پیدا شود همان Object موجود را بدون Update کردن Valueها برمیگرداند. بنابراین اگر User دیگری Name آن Customer را در Database تازه تغییر داده باشد، مقدار جدید نادیده گرفته میشود. این رفتار برای جلوگیری از Side Effect غیرمنتظره و مدیریت Concurrency ضروری است؛ اگر Propertyهای Customer Object را تغییر داده ولی هنوز SaveChanges نزده باشید، نمیخواهید آن Propertyها خودکار Overwrite شوند.
برای گرفتن اطلاعات تازه از Database، Context جدید بسازید یا Reload را فراخوانی کنید:
dbContext.Entry (myCustomer).Reload();
Best Practice این است که برای هر Unit of Work یک DbContext تازه داشته باشید تا نیاز به Reload دستی نادر شود.
Change Tracking
وقتی Property یک Entity Loadشده با DbContext را تغییر دهید، EF Core Change را تشخیص میدهد و با SaveChanges Database را Update میکند. برای این کار Snapshotی از State Entityهای Loadشده میسازد و هنگام SaveChanges — یا Query دستی Change Tracker — State فعلی را با Original مقایسه میکند.
میتوانید Changeهای Trackشده را Enumerate کنید:
foreach (var e in dbContext.ChangeTracker.Entries())
{
Console.WriteLine ($"{e.Entity.GetType().FullName} is {e.State}");
foreach (var m in e.Members)
Console.WriteLine (
$" {m.Metadata.Name}: '{m.CurrentValue}' modified: {m.IsModified}");
}
هنگام SaveChanges، EF Core از ChangeTracker برای ساخت SQL Statementهایی استفاده میکند که Database را با Changeهای Objectها هماهنگ کنند: insert برای Rowهای جدید، update برای تغییر داده و delete برای Rowهای حذفشده از Object Graph. هر TransactionScope موجود رعایت میشود؛ و اگر وجود نداشته باشد همهٔ Statementها داخل Transaction جدید قرار میگیرند.
با پیادهسازی INotifyPropertyChanged و بهصورت اختیاری INotifyPropertyChanging در Entityها میتوان Change Tracking را Optimize کرد.
INotifyPropertyChanged اجازه میدهد EF Core هزینهٔ مقایسهٔ Entity فعلی با Original را حذف کند؛ INotifyPropertyChanging اجازه میدهد Original Valueها اصلاً Store نشوند. بعد از پیادهسازی، هنگام Configure Model، Method با نام HasChangeTrackingStrategy را روی ModelBuilder فراخوانی کنید.
Navigation Propertyها
Navigation Propertyها دو کار اصلی را آسان میکنند: Query کردن Tableهای مرتبط بدون Join دستی؛ و Insert، Remove و Update Rowهای مرتبط بدون Update صریح Foreign Key.
اگر Customer چند Purchase داشته باشد، One-to-many Relationship را میتوان با Entityهای زیر نمایش داد:
public class Customer
{
public int ID { get; set; }
public string Name { get; set; }
// Child navigation property, which must be of type ICollection<T>:
public virtual List<Purchase> Purchases {get;set;} = new List<Purchase>();
}
public class Purchase
{
public int ID { get; set; }
public DateTime Date { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int CustomerID? { get; set; } // Foreign key field
public Customer Customer { get; set; } // Parent navigation property
}
EF Core از روی Naming Convention میفهمد CustomerID Foreign Key به Table با نام Customer است. اگر Database را از این Entityها بسازد، Constraint بین Purchase.CustomerID و Customer.ID ایجاد میکند.
با Navigation Property میتوان Queryهایی مانند زیر نوشت:
var customersWithPurchases = Customers.Where (c => c.Purchases.Any());
جزئیات Queryهای Navigation در فصل 9 میآیند.
افزودن و حذف Entity از Navigation Collection
وقتی Entity جدیدی به Collection Navigation Property اضافه کنید، EF Core هنگام SaveChanges Foreign Keyها را خودکار پر میکند:
Customer cust = dbContext.Customers.Single (c => c.ID == 1);
Purchase p1 = new Purchase { Description="Bike", Price=500 };
Purchase p2 = new Purchase { Description="Tools", Price=100 };
cust.Purchases.Add (p1);
cust.Purchases.Add (p2);
dbContext.SaveChanges();
در این مثال EF Core مقدار 1 را در Column با نام CustomerID هر Purchase جدید مینویسد و ID تولیدشده توسط Database را در Purchase.ID قرار میدهد.
وقتی Entity را از Collection Navigation Property حذف و SaveChanges کنید، EF Core بسته به Configuration/Inference Relation یا Foreign Key را Clear میکند یا Row متناظر را Delete میکند. اینجا Purchase.CustomerID Nullable است تا Purchase بدون Customer یا Cash Transaction قابل نمایش باشد، بنابراین Remove کردن Purchase از Customer فقط Foreign Key را Clear میکند، نه Row را.
Load کردن Navigation Propertyها
EF Core هنگام Populate کردن Entity، بهطور پیشفرض Navigation Property را Populate نمیکند:
using var dbContext = new NutshellContext();
var cust = dbContext.Customers.First();
Console.WriteLine (cust.Purchases.Count); // Always 0
یک راه، Extension Method با نام Include برای Eager Loading است:
var cust = dbContext.Customers
.Include (c => c.Purchases)
.Where (c => c.ID == 2).First();
راه دیگر Projection است، بهویژه وقتی فقط بعضی Propertyها لازماند و میخواهید Data Transfer کمتر شود:
var custInfo = dbContext.Customers
.Where (c => c.ID == 2)
.Select (c => new
{
Name = c.Name,
Purchases = c.Purchases.Select (p => new { p.Description, p.Price })
})
.First();
هر دو تکنیک به EF Core میگویند چه Dataای لازم دارید تا در یک Database Query Fetch شود. همچنین میتوانید Explicitly Navigation Property را Load کنید:
dbContext.Entry (cust).Collection (b => b.Purchases).Load();
// cust.Purchases is now populated.
این Explicit Loading است و برخلاف روشهای قبلی یک Round Trip اضافه به Database ایجاد میکند.
Lazy Loading
روش دیگر Load کردن Navigation Property، Lazy Loading است. اگر فعال باشد، EF Core Navigation Propertyها را On Demand Populate میکند؛ برای هر Entity Class یک Proxy Class میسازد که Access به Navigation Property Loadنشده را Intercept میکند. هر Navigation Property باید virtual و Class قابل Inherit باشد، و هنگام Lazy Load شدن Context نباید Dispose شده باشد تا Request اضافی Database ممکن باشد.
فعالسازی در OnConfiguring:
protected override void OnConfiguring (DbContextOptionsBuilder
optionsBuilder)
{
optionsBuilder
.UseLazyLoadingProxies()
...
}
همچنین باید Package با نام Microsoft.EntityFrameworkCore.Proxies را Reference کنید.
هزینهٔ Lazy Loading این است که هر بار Navigation Property Loadنشده را Access کنید EF Core Request اضافه به Database میفرستد. تعداد زیاد این Requestها بهخاطر Round-tripping زیاد Performance را کم میکند.
Deferred Execution در EF Core
EF Core Queryها مانند Local Queryها Deferred هستند و بنابراین Query را میتوان Progressive ساخت. اما در یک مورد Semantics ویژه دارند: وقتی Subquery داخل Expression با نام Select باشد.
در Local Query، Double-deferred Execution دارید، چون از دید Functional یک Sequence از Queryها Select میکنید؛ اگر Outer Result Sequence را Enumerate کنید ولی Inner Sequenceها را هرگز Enumerate نکنید، Subquery اصلاً اجرا نمیشود.
در EF Core، Subquery همزمان با Main Outer Query اجرا میشود تا Round-trip اضافی جلوگیری شود. مثال زیر با رسیدن به نخستین foreach در یک Round Trip اجرا میشود:
using var dbContext = new NutshellContext ();
var query = from c in dbContext.Customers
select
from p in c.Purchases
select new { c.Name, p.Price };
foreach (var customerPurchaseResults in query)
foreach (var namePrice in customerPurchaseResults)
Console.WriteLine ($"{ namePrice.Name} spent { namePrice.Price}");
هر Navigation Property که صریحاً Project کنید در یک Round Trip کاملاً Populate میشود:
var query = from c in dbContext.Customers
select new { c.Name, c.Purchases };
foreach (var row in query)
foreach (Purchase p in row.Purchases) // No extra round-tripping
Console.WriteLine (row.Name + " spent " + p.Price);
اما اگر Navigation Property را بدون Eager Load یا Projection Enumerate کنیم، قواعد Deferred Execution اعمال میشود. با فرض فعالبودن Lazy Loading، در هر Iteration یک Query جدید Purchases اجرا میشود:
foreach (Customer c in dbContext.Customers.ToArray())
foreach (Purchase p in c.Purchases) // Another SQL round-trip
Console.WriteLine (c.Name + " spent " + p.Price);
این مدل زمانی مفید است که بخواهید Inner Loop را براساس Testی که فقط روی Client قابل انجام است Selectively اجرا کنید:
foreach (Customer c in dbContext.Customers.ToArray())
if (myWebService.HasBadCreditHistory (c.ID))
foreach (Purchase p in c.Purchases) // Another SQL round trip
Console.WriteLine (c.Name + " spent " + p.Price);
Select Subqueryها در فصل 9، «Projecting» صفحهٔ 473، بیشتر بررسی میشوند.
ساخت Query Expressionها
تا اینجا برای Composition پویا، Query Operatorها را شرطی Chain کردیم. این در بسیاری Scenarioها کافی است، اما گاهی باید Granularتر کار کنید و Lambda Expressionهایی که Operatorها را تغذیه میکنند بهطور Dynamic Compose کنید.
در این بخش Class زیر را فرض میکنیم:
public class Product
{
public int ID { get; set; }
public string Description { get; set; }
public bool Discontinued { get; set; }
public DateTime LastSale { get; set; }
}
Delegate در برابر Expression Tree
یادآوری: Local Queryها که Operatorهای Enumerable دارند Delegate میگیرند؛ Interpreted Queryها که Operatorهای Queryable دارند Expression Tree میگیرند. Signature Where در این دو Class:
public static IEnumerable<TSource> Where<TSource> (this
IEnumerable<TSource> source, Func<TSource,bool> predicate)
public static IQueryable<TSource> Where<TSource> (this
IQueryable<TSource> source, Expression<Func<TSource,bool>> predicate)
وقتی Lambda داخل Query است، چه به Enumerable Bind شود و چه Queryable، ظاهری یکسان دارد:
IEnumerable<Product> q1 = localProducts.Where (p => !p.Discontinued);
IQueryable<Product> q2 = sqlProducts.Where (p => !p.Discontinued);
اما اگر Lambda را به Variable میانی Assign کنید، باید صریح باشید که Delegate یعنی Func<> میخواهید یا Expression Tree یعنی Expression<Func<>>. predicate1 و predicate2 قابل جایگزینی نیستند:
Func <Product, bool> predicate1 = p => !p.Discontinued;
IEnumerable<Product> q1 = localProducts.Where (predicate1);
Expression <Func <Product, bool>> predicate2 = p => !p.Discontinued;
IQueryable<Product> q2 = sqlProducts.Where (predicate2);
Compile کردن Expression Tree
با Compile میتوانید Expression Tree را به Delegate تبدیل کنید. این بهخصوص برای Methodهایی که Reusable Expression برمیگردانند مفید است. مثلاً Static Methodی در Product که Predicateی برمیگرداند که اگر Product Discontinued نباشد و در 30 روز گذشته Sale داشته باشد true است:
public class Product
{
public static Expression<Func<Product, bool>> IsSelling()
{
return p => !p.Discontinued && p.LastSale > DateTime.Now.AddDays (-30);
}
}
این Method هم در Interpreted و هم Local Query قابل استفاده است:
void Test()
{
var dbContext = new NutshellContext();
Product[] localProducts = dbContext.Products.ToArray();
IQueryable<Product> sqlQuery =
dbContext.Products.Where (Product.IsSelling());
IEnumerable<Product> localQuery =
localProducts.Where (Product.IsSelling().Compile());
}
AsQueryable
Operator با نام AsQueryable اجازه میدهد Query کامل را طوری بنویسید که هم روی Local و هم Remote Sequence اجرا شود:
IQueryable<Product> FilterSortProducts (IQueryable<Product> input)
{
return from p in input
where ...
orderby ...
select p;
}
void Test()
{
var dbContext = new NutshellContext();
Product[] localProducts = dbContext.Products.ToArray();
var sqlQuery = FilterSortProducts (dbContext.Products);
var localQuery = FilterSortProducts (localProducts.AsQueryable());
...
}
AsQueryable لباس IQueryable<T> را دور Local Sequence میپیچد تا Query Operatorهای بعدی به Expression Tree Resolve شوند. هنگام Enumeration Result، Expression Treeها بهطور ضمنی Compile میشوند — با Performance Cost کوچک — و Local Sequence مانند حالت عادی Enumerate میشود.
Expression Treeها
گفتیم Conversion ضمنی از Lambda Expression به Expression<TDelegate> باعث میشود C# Compiler کدی Emit کند که Expression Tree میسازد. با کمی Program کردن میتوانید همین کار را دستی و در Runtime انجام دهید: Expression Tree را از صفر Dynamic بسازید. Result را میتوان به Expression<TDelegate> Cast و در EF Core Query استفاده کرد یا با Compile به Delegate عادی تبدیل کرد.
Expression DOM
Expression Tree یک Code DOM مینیاتوری است. هر Node در Tree با Typeای در Namespace با نام System.Linq.Expressions نمایش داده میشود. شکل 8-10 این Typeها را نشان میدهد.
شکل 8-10 — ساخت Expression Treeبرچسبها ماهیت Nodeهای Expression Tree را نشان میدهند.
" alt="شکل 8-10: Typeهای Expression"/>شکل 8-10 — Typeهای Expression در System.Linq.Expressions.
Base Class همهٔ Nodeها Class غیرGeneric با نام Expression است. Class Generic با نام Expression<TDelegate> در واقع به معنی «Typed Lambda Expression» است و اگر نام طولانی و دستوپاگیر نبود میتوانست LambdaExpression<TDelegate> نامیده شود:
LambdaExpression<Func<Customer,bool>> f = ...
Base Type مربوط به Expression<T> Class غیرGeneric با نام LambdaExpression است. LambdaExpression Type Unification برای Lambda Expression Treeها فراهم میکند؛ هر Expression<T> Typed را میتوان به LambdaExpression Cast کرد.
چیزی که LambdaExpression را از Expression عادی جدا میکند این است که Lambda Expression Parameter دارد.
برای ساخت Expression Tree، Node Typeها را مستقیم Instantiate نکنید؛ Static Methodهای Class با نام Expression مانند Add، And، Call، Constant، LessThan و غیره را فراخوانی کنید.
شکل 8-11 Expression Tree مربوط به Assignment زیر را نشان میدهد:
Expression<Func<string, bool>> f = s => s.Length < 5;
شکل 8-11 — ترکیب ExpressionهاExpressionهای کوچکتر بهصورت Tree برای تولید منطق نهایی Query با هم ترکیب میشوند.
" alt="شکل 8-11: Expression Tree"/>شکل 8-11 — Expression Tree برای Lambda با شرط s.Length < 5.میتوانیم آن را Inspect کنیم:
Console.WriteLine (f.Body.NodeType); // LessThan
Console.WriteLine (((BinaryExpression) f.Body).Right); // 5
اکنون همان Expression را از صفر میسازیم. اصل این است که از پایین Tree شروع کرده و به بالا بروید. پایینترین Node یک ParameterExpression برای Parameter با نام s و Type با نام string است:
ParameterExpression p = Expression.Parameter (typeof (string), "s");
گام بعد ساخت MemberExpression و ConstantExpression است. برای اولی باید Property با نام Length از Parameter با نام s را Access کنیم:
MemberExpression stringLength = Expression.Property (p, "Length");
ConstantExpression five = Expression.Constant (5);
سپس Comparison با نام LessThan:
BinaryExpression comparison = Expression.LessThan (stringLength, five);
در پایان Lambda Expression را میسازیم که Body Expression را به Collection Parameterها وصل میکند:
Expression<Func<string, bool>> lambda
= Expression.Lambda<Func<string, bool>> (comparison, p);
راه مناسب برای Test، Compile کردن Lambda به Delegate است:
Func<string, bool> runnable = lambda.Compile();
Console.WriteLine (runnable ("kangaroo")); // False
Console.WriteLine (runnable ("dog")); // True
بحث بیشتر دربارهٔ Expression Tree در مکمل آنلاین کتاب آمده است؛ URL خارجی متن اصلی در این ترجمه بهصورت لینک فعال درج نشده است.