همواره در تکنولوژی EF CodeFirst، چه در ASP.NET MVC و چه در ASP.NET Core، استفاده از امکانات بومی پایگاههای داده با محدودیتهایی مواجه بودهاست. یکی از این اشکالات، عدم توانایی این تکنولوژی در گرفتن لیستی از اطلاعات که منطبق بر بیشتر از یک مدل میباشد، هست. در این مقاله تمرکز بر روی رفع این اشکال، بدون نیاز به اضافه کردن مدخل جدیدی به پروژه میباشد. بنابراین پیشنیاز ضروری این مبحث، مطالعه «
شروع به کار با EF Core 1.0» ، مخصوصا «
استفاده از امکانات بومی بانکهای اطلاعاتی» است.
Stored Procedure چیست ؟
Stored Procedure یا SP یا به زبان فارسی «رویههای ذخیره شده» اشیایی اجرا پذیر در بانک اطلاعاتی SQL Server هستند که شامل یک یا چندین دستور SQL میشوند. این رویهها میتوانند پارامترهای ورودی و خروجی داشته باشند؛ همچنین میتوانند لیستی از موجودیتها را نیز برگردانند و یا میتوان داخل این رویهها به زبان T-SQL برنامه نویسی کرد.
مهمترین کاربر این رویهها، ذخیره کردن دستورات Select , Insert , Update , Delete هست و یا ترکیبی از اینها .
اشکال راه حلهای پیش فرض مبتنی بر Context
برای استفاده از راه حلهای پیش فرض مبتنی بر Context، همانطور که در مقاله «
استفاده از امکانات بومی بانکهای اطلاعاتی» به آن پرداخته شده، سه روش کلی برای استفاده از Stored Procedure پیشنهاد شدهاست:
- روش اول استفاده از متد fromsql است. اشکال این متد، محدودیت استفاده برای یک موجودیت برنامه است و به زبان ساده نمیتوان در کوئری پایگاه داده از join استفاده کرد.
- روش دوم استفاده از متد ExecuteSqlCommand موجود در context برنامه است . اشکال این متد void بودن آن است که باعث میشود بازگشتی از پایگاه داده حاصل نشود.
- روش سوم استفاده از متد ExecuteScalar موجود در Context برنامه است. اشکالی که به این متد گرفته میشود، Scalar بودن مقدار بازگشتی از آن است که باعث میشود نتوانیم لیستی از موجودیتها را به ViewModel مورد نظر نگاشت کنیم.
راه حل این مشکل
برای حل این مشکلات که بسیار هم مهم هستند، اول باید قطعه کد زیر را به Context برنامه اضافه نمود:
public void OpenConnection()
{
Database.OpenConnection();
}
public DbCommand Command()
{
DbCommand cmd = Database.GetDbConnection().CreateCommand();
return cmd;
}
سپس در اینترفیس IUnitOfWork که در مطلب «
لایه بندی و تزریق وابستگیها» در مورد آن بحث شده، متد OpenConnection و Command را اضافه میکنیم:
void OpenConnection();
DbCommand Command();
حال کلاس و اینترفیس جدیدی را برای پیاده سازی سرویس اتصال به Stored Procedure ایجاد کرده و در کلاس آغازین برنامه، بهصورت AddScopped این سرویس را برای استفاده از تزریق وابستگی توکار ASP.NET Core معرفی میکنیم:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<ISpReader, SpReader>();
}
سپس در سازنده کلاس این سرویس، اینترفیس IUnitOfWork را تزریق کرده تا بتوانیم از متدهای نوشته شده در Context استفاده کنیم. حال اقدام به پیاده سازی متد GetFromSp بصورت زیر میکنیم :
public List<ViewModel> GetFromSp <ViewModel>(string[,] Parametr, string NameSp) where ViewModel : new()
{
_uow.OpenConnection();
DbCommand cmd = _uow.Command();
cmd.CommandText = NameSp;
cmd.CommandType = CommandType.StoredProcedure;
var countParametr = Parametr.GetLength(0);
for (int i = 0; i < countParame tr; i++)
{
cmd.Parameters.Add(new SqlParameter { ParameterName = Parametr[i, 0], Value = Parametr[i, 1] });
}
List<ViewModel> list = new List<ViewModel >();
using (var reader = cmd.ExecuteReader())
{
if (reader != null && reader.HasRows)
{
var entity = typeof(ViewModel);
var propDict = new Dictionary<string, PropertyInfo>();
var props = entity.GetProperties
(BindingFlags.Instance | BindingFlags.Public);
propDict = props.ToDictionary(p => p.Name.ToUpper(), p => p);
while (reader.Read())
{
ViewModel newobject = new ViewModel ();
for (int index = 0; index < reader.FieldCount; index++)
{
if (propDict.ContainsKey(reader.GetName(index).ToUpper()))
{
var info = propDict[reader.GetName(index).ToUpper()];
if ((info != null) && info.CanWrite)
{
var val = reader.GetValue(index);
info.SetValue(newobject, (val == DBNull.Value) ? null : val, null);
}
}
}
list.Add(newobject);
}
}
return list;
}
در این متد، اول با استفاده از OpenConnection، اتصالی را به پایگاه داده، باز کرده سپس شیئ از DbCommand را میسازیم و نام Stored Procedure و نوع کوئری ارسالی را معین میکنیم. حال با استفاده از حلقه for، نام و مقدار پارامترهای ارسال شده به متد را به شیئ cmd اضافه میکنیم. در مرحله بعد، لیستی را از کلاس مدلی که باید مقادیر بازگشتی به آن نگاشت شوند و بعنوان کلاس جنریک به متد ارسال شده است، میسازیم. با متد ExecuteReader که در شیئ ساخته شده از Command موجود میباشد، اقدام به خواندن اطلاعات از Stored Procedure کرده و در شیئ Reader نگه داری میکنیم و سپس اطلاعات خوانده شده را با استفاده از Dictionary و متد Add به لیست ساخته شده اضافه میکنیم. در آخر لیست ساخته شده در حلقه While را بعنوان نتیجه متد باز میگردانیم.
همچنین میتوان برای استفاده این متد برای رویههای بدون پارامتر ورودی، از OverLoad این متد، با حذف قطعات کد زیر:
var countParametr = Parametr.GetLength(0);
for (int i = 0; i < countParametr; i++)
{
cmd.Parameters.Add(new SqlParameter { ParameterName = Parametr[i, 0], Value = Parametr[i, 1] });
}
و حذف آرایه string[,] Parameter از ورودی متد، استفاده نمود .
روش استفاده از این متد
برای استفاده از این متد، لازم است چند نکته رعایت شوند:
1- خروجی Stored Procedure دقیقا منطبق بر ViewModel ارسالی به متد جهت تشکیل لیست باشد.
2- لیست پارامترها باید بصورت آرایه دوبعدی باشد که اندازه بعد اول، تعداد پارامترها و اندازه بعد دوم 2 باشد.
3- در ماتریسی که از این پارامترها ساخته میشود، ستون اول نام پارامتر و ستون دوم مقدار پارامتر ست میشود.
بطور مثال Stored Procedure زیر حاوی سه پارامتر است :
CREATE PROCEDURE [dbo].[isRelation](
@TableName as varchar(50),
@FieldOfRelation as varchar(70),
@ValueOfField as int)
برای دسترسی به این رویه ابتدا در سرویس استفاده کننده، ISpReader را تزریق میکنیم و سپس بصورت زیر مقدمات استفاده از این سرویس را فراهم میکنیم:
public class EntityServices : IEntityService
{
private ISpreader _Reader;
public EntityServices( ISpreader reader)
{
_Reader = reader;
}
public List<StoreProcedureResultViewModel> IsRelation(string tableName , int keyValue, string keyFieldName)
{
List<StoreProcedureResultViewModel> IsContact;
try
{
string[,] Parametr = new string[3, 2];
Parametr[0, 0] = "@TableName";
Parametr[0, 1] = tableName ;
Parametr[1, 0] = "@ValueOfField";
Parametr[1, 1] = keyValue.ToString().Trim();
Parametr[2, 0] = "@FieldOfRelation";
Parametr[2, 1] = keyFieldName.Trim();
IsContact = _Reader.GetSp<StoreProcedureResultViewModel>(Parametr, "IsRelation");
return IsContact;
}
catch (Exception ex)
{
}
}
}
بدین ترتیب با استفاده از این متد توانستیم لیستی از ViewModel منطبق بر خروجی Stored Procedure را بدست آوریم.