در قسمت بعد، ارتباطات self referencing را بررسی خواهیم کرد و چون EF Core هیچ راه حل بهینهای را برای کوئری گرفتن از این نوع روابط سلسله مراتبی ارائه نمیدهد (درEF 6.x نیز به همین ترتیب)، نیاز است مستقیما SQL نویسی کرد. به همین جهت در این قسمت نحوهی نوشتن کوئریهای مستقیم SQL و اجرای آنها را در EF Core بررسی میکنیم.
اجرای کوئریهای خام SQL بر روی بانک اطلاعاتی، توسط EF Core
گاهی از اوقات نیاز به استفادهی قابلیت خاصی از بانک اطلاعاتی مدنظر وجود دارد که توسط LINQ پشتیبانی نمیشود و یا کوئری SQL حاصل از LINQ to Entities آنچنان بهینه نیست. در یک چنین حالاتی راهی بجز نوشتن کوئریهای خام SQL وجود ندارد. امکان اجرای یک چنین کوئریهایی توسط EF Core پیش بینی شدهاست؛ اما با این محدودیتها:
- خروجی کوئری SQL، تنها باید معادل یکی از کلاسهای موجودیتهای شما باشد. قرار است این محدودیت در نگارش 1.1 برطرف شود.
- کوئری SQL نوشته شده باید تمام خواص موجودیتی را که قرار است به آن نگاشت شود، بازگشت دهد.
- نام ستونهای بازگشت داده شدهی توسط کوئری SQL باید با نام خواص موجودیت در حال کار، یکی باشند و برخلاف EF 6.x، از یک چنین عدم تطابقهایی صرفنظر نخواهد شد.
- کوئری SQL نوشته شده نباید به همراه اطلاعات ارتباطات موجودیتها باشد.
در اینجا برای نوشتن کوئریهای خام SQL میتوان از متد FromSql مرتبط با یکی از DbSetهای برنامه استفاده کرد:
var blogs = context.Blogs
.FromSql("SELECT * FROM dbo.Blogs")
.ToList();
و یا حتی میتوان از رویهی ذخیره شدهای استفاده کرد که خروجی ستونهای آن، معادل تمام خواص کلاس Blog باشد:
var blogs = context.Blogs
.FromSql("EXECUTE dbo.GetMostPopularBlogs")
.ToList();
بنابراین رفتار EF Core اندکی متفاوت است با EF 6.x. در اینجا اگر میخواهید از عبارت SQL خود خروجی بگیرید، باید از یکی از DbSetهای خود شروع کنید و متد FromSql را بر روی آن فراخوانی نمائید. همچنین کوئری نوشته شده باید اولا تمام ستونهای آن DbSet رابازگشت دهد و به علاوه این ستونها دقیقا با نامهای خواص آن کلاس، تطابق داشته باشند.
علت این مسایل نیز به این دلیل است که بتوان نتیجهی کوئری را به صورت خودکار وارد سیستم change tracking کرد و همچنین کوئریهای ترکیبی LINQ را نیز در اینجا فعال کرد.
ارسال پارامترها به کوئریهای خام SQL
تنها حالتی در EF Core که مستعد به حملات تزریق SQL است، دقیقا همین مورد دور شدن از LINQ و نوشتن عبارات مستقیم SQL است. در اینجا برای نوشتن کوئریهای پارامتری دو حالت پیش بینی شدهاست:
الف) روش parameter place holders
در اینجا متد FromSql، بسیار شبیه به متد String.Format است، اما در عمل اینطور نیست و تمام place holders آن به صورت خودکار تبدیل به پارامتر میشوند:
var user = "johndoe";
var blogs = context.Blogs
.FromSql("EXECUTE dbo.GetMostPopularBlogsForUser {0}", user)
.ToList();
ب) روش ساخت دستی DbParameterها
اگر میخواهید از پارامترهای نام دار استفاده کنید، با وهلهای از SqlParameter شروع کرده و سپس آنرا به متد FromSql ارسال کنید:
var user = new SqlParameter("user", "johndoe");
var blogs = context.Blogs
.FromSql("EXECUTE dbo.GetMostPopularBlogsForUser @user", user)
.ToList();
و یا این حالت را به شکل ساده شدهی ذیل نیز میتوان مورد استفاده قرار داد:
var results = _context.Contacts.FromSql(
@"SELECT Id, Name Address, City, State, Zip
FROM Contacts
WHERE Name IN (@p0, @p1)", name1, name2);
که در اینجا p0@ به name1 و p1@ به name2 نگاشت خواهد شد.
مزیت کار کردن با SqlParameter این است که میتوان برای مثال Direction و SqlDbType را نیز صریحا ذکر کرد (بسته به نوع پارامترهای رویهی ذخیره شده):
var nameParameter = new SqlParameter
{
ParameterName = "@name",
Value = "doc",
Direction = ParameterDirection.Input,
SqlDbType = SqlDbType.NVarChar
};
امکان ترکیب کوئریهای SQL و LINQ نیز پیش بینی شدهاست
در کوئری ذیل، قسمت select از جدولی به صورت SQL و قسمت where و order by آن توسط LINQ تهیه شدهاند که در نهایت به یک کوئری ترجمه شده و بر روی بانک اطلاعاتی اجرا میشوند.
یک مثال جالب آن، امکان کوئری گرفتن از
Table Value Functionها و سپس ترکیب آنها با LINQ است (این ترکیب، تنها یک کوئری SQL نهایی را تولید میکند):
var posts = context.Posts
.FromSql("SELECT * FROM dbo.GetMatchingPostByTitle({0})", searchTerm)
.Where(p => p.BlogId == 1)
.OrderByDescending(p => p.CreateDate)
.ToList();
واکشی ارتباطات یک موجودیت توسط SQL و LINQ
در ابتدای بحث در قسمت محدودیتهای کوئریهای SQL نوشته شده، ذکر شد «کوئری SQL نوشته شده نباید به همراه اطلاعات ارتباطات موجودیتها باشد». برای رفع این محدودیت میتوان از ترکیب SQL و LINQ به صورت ذیل استفاده کرد:
var searchTerm = ".NET";
var blogs = context.Blogs
.FromSql("SELECT * FROM dbo.SearchBlogs {0}", searchTerm)
.Include(b => b.Posts)
.ToList();
در اینجا برای واکشی ارتباطات یک موجودیت از متد Include استفاده شدهاست.
اجرای عبارات SQL، بدون بازگشت مقداری
تا اینجا در مورد عبارات SQL از نوع Select و یا اجرای رویههای ذخیره شده، بحث شد. برای اجرای عبارات SQL ایی مانند update و delete میتوان از متد ExecuteSqlCommand مربوط به context.Database استفاده کرد:
context.Database.ExecuteSqlCommand("UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 30");
و یا برای ارسال پارامترها به آن میتوان به این صورت عمل کرد (اجرای یک رویهی ذخیره شده با دو پارامتر ارسالی به آن):
context.Database.ExecuteSqlCommand("usp_CreateShipper @p0, @p1",
parameters: new[] { "hello", "world" });
اجرای عبارات SQL و دریافت خروجیهایی به غیر از موجودیتهای برنامه
در ابتدا بحث عنوان شد که محدودیت فعلی کوئریهای FromSQL که میتوانند خروجی را نیز ارائه دهند، مقید بودن آنها به DbSet در حال استفاده است و محدود بودن آنها به خواص کلاس متناظر تعریف شده. در این حالت اگر بخواهیم یک محاسبهی عددی را بازگشت دهیم چه باید کرد؟
متد ExecuteSqlCommand تنها وضعیت نهایی اجرای عملیات را بازگشت میدهد و FromSQL مقید است به DbSet متناظر. برای رفع این محدودیتها میتوان مستقیما به DbConnection دسترسی یافت و سپس کوئری گرفت؛ به نحو ذیل:
using (var connection = context.Database.GetDbConnection())
{
connection.Open();
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT COUNT(*) FROM Contacts";
var result = command.ExecuteScalar().ToString();
}
}
به عبارتی در اینجا امکان بازگشت به حالت ADO.NET خام نیز پیش بینی شدهاست.