مطالب
استفاده از دیتابیس Sqlite در الکترون (قسمت دوم)
در مقاله قبلی با یکی از کتابخانه‌های مدیریت دیتابیس sqlite آشنا شدیم و و یاد گرفتیم که چگونه یک دیتابیس جدید را بسازیم و اطلاعات را از آن دریافت کنیم. در این مقاله قصد داریم، بیشتر در مورد دستورات این کتابخانه بدانیم و بفهمیم که چگونه باید آن‌ها را به کار بست.

دستورات بدون خروجی:
یک سری از دستورات هستند که خروجی ندارند و رکوردی را باز نمی‌گردانند و برای اجرای دستوراتی چون افزودن، به روزرسانی و حذف بسیار مناسبند. اجرای  این دستورات را ما به متدی به نام run می‌سپاریم. در دفعه قبل که از این دستور استفاده کردیم، پارامتری برای تعیین کردن نداشت؛ ولی در این مقاله، دستور با پارامتر آن را اجرا می‌کنیم:
ابتدا کدهای زیر را به فایل html، برای درج رکورد جدید اضافه می‌کنیم:
First Name:<br/>
<input type="text" id="txtfname" /><br/>
Last Name:<br/>
<input type="text" id=txtlname /><br/>
Number:<br/>
<input type="tel" id="txttel" /><br/>
<button id="btnsubmit">Save</button><br/>
برای خواندن مقادیر هم از این تابع کمک می‌گیریم:
function GetValues()
{
  let fname=$("#txtfname").val();
  let lname=$("#txtlname").val()
  let tel=$("#txttel").val();
  let row=
  {
    fname:fname,
    lname:lname,
    number:tel
  };
  return row;
}
سپس تکه کد زیر را  با کمک  جی کوئری اضافه می‌کنیم:
$("#btnsubmit").click((e)=>{
  e.preventDefault();
  let row=GetValues();

  //save in db
  //get last id
  let statement==db.prepare("select id from numbers order by id desc limit 1");
  let lastRecord=statement.getAsObject({}).id;
  row.id=lastRecord++;
  let count=db.prepare("select count(*) as count from numbers order by id desc").getAsObject({}).count;
  statement.free();
  let insertCommand="insert into numbers values(?,?,?,?)";
  db.run(insertCommand,[row.id,row.fname,row.lname,String(row.number)])
  let newcount=db.prepare("select count(*) as count from numbers order by id desc").getAsObject({}).count;
  SaveChanges();

//show in the table
if(count<newcount)
{
  AddToTable(row);
}
});
});
متد GetValues  مقادیر را از input‌ها دریافت و در متغیر row نگهداری می‌کند. سپس برای درج رکورد، از آنجاکه ما فیلد id را افزایشی تعیین نکرده‌ایم، باید آخرین رکورد موجود در جدول را واکشی کنیم که برای این منظور از متد prepare کمک می‌گیریم. این متد در عوض کوئری داده شده، یک شیء statement را بر می‌گرداند که حاوی نتایج کوئری داده شده است؛ ولی هنوز به مرحله اجرا نرسیده است. برای اجرای دستور کوئری، دو متد وجود دارند که یکی step است و دیگری getAsObject می‌باشد. متد getAsObject برای زمانی خوب است که شما در حد یک رکورد خروجی دارید و می‌خواهید همان یک رکورد را به صورت شیء برگردانید؛ یعنی با "نام شی.نام خصوصیت" به آن دسترسی پیدا کنید. همانطور که می‌بینید ما درخواست فیلد id را کرده‌ایم و تنها یک رکورد را درخواست کرده‌ایم که باعث می‌شود به راحتی به فیلد id دسترسی داشته باشیم. وجود علامت {} داخل این متد به این معنی است که پارامتری برای ارسال وجود ندارد. ولی اگر قرار بود پارامتری را داشته باشیم، به این شکل می‌نوشتیم:
var statement= db.prepare("SELECT * FROM NUMBERS WHERE fname=@fname AND lname=@lname");

var result = statement .getAsObject({'@fname' :'ali', '@lname' : 'yeganeh'});
اگر دوست داشتید دوباره از این دستور کوئری بگیرید ولی با مقادیر متفاوت، می‌توانید از متد bind کمک بگیرید:
statement.bind(['hossein','yeganeh']);
متد دیگری هم برای پاکسازی پارامترها به نام reset، وجود دارد که فضای اختصاصی و گرفته شده توسط پارامترها را باز می‌گرداند.

متد step همانند متدهای next در cursor یا read در datareader عمل میکند و با هر بار صدا زدن، یک رکورد، به سمت جلو حرکت می‌کند. دریافت هر رکورد جاری توسط متد get و نوع خروجی آرایه انجام می‌شود:
while(statement.step())
{
     var rec=statement.get();
}
بعد از اینکه کارمان با آن تمام شد، برای پاکسازی حافظه از متد free استفاده می‌کنیم. در دستورات بعد، شیء statement را مستقیما مورد استفاده قرار داده‌ایم و توسط آن تعداد رکوردها را دریافت کرده‌ایم. سپس با استفاده از متد run دستور درج را داده‌ایم. اینبار این متد را به شکل متفاوتی استفاده کردیم و به آن پارامتری هم دادیم. نحوه ارائه پارامتر به این متد، باید به صورت آرایه و به ترتیب علامت‌های ؟ باشد. نهایتا با دریافت تعداد رکوردهای جاری و مقایسه با تعداد رکوردهای سابق متوجه می‌شویم که آیا رکوردی اضافه شده است یا خیر؟ در صورتی که اضافه شده است، باید رکورد جدید در جدول، توسط جی کوئری نمایش داده شود و تغییرات دیتابیس هم روی دیسک سخت ذخیره شوند. چون دیتابیس مورد استفاده به صورت in-memory یعنی مقیم در حافظه مورد استفاده قرار میگیرد، باید کل دیتابیس، بر روی دیسک سخت رونویسی شود. متد SaveChanges شامل کد زیر است که حاوی کد ارسال پیام به Main Thread یا Main Process می‌باشد تا در دیسک سخت بنویسد:
  const {ipcRenderer} = require('electron');
function SaveChanges()
{
    ipcRenderer.send("SaveToDb");
}
و برای ترد اصلی:
const {ipcMain} = require('electron');
ipcMain.on("SaveToDb", (event, arg) => {
  SaveToDb();
});

function SaveToDb()
{
  var data=db.export();
  var buffer=new Buffer(data);
  fs.writeFileSync(dbPath,buffer);
}

پی نوشت :یکی از بهترین روش‌ها این است که از همین تعداد سطرها برای id رکورد استفاده کنیم. چرا که فیلد id به عنوان کلید اصلی است و چینش فیزیکی دارد و از لحاظ کارایی بهتر است ولی به علت آموزشی بودن مطلب و آشنایی با دیگر دستورات، این شیوه را انتخاب کردیم.

ویرایش رکورد
ابتدا template string سطر جدول را به شکل زیر تغییر می‌دهیم:
function AddToTable(row)
{
  let tableBody=$("#people");
  let rowTemplate=`<tr><td>${row.fname}</td><td>${row.lname}</td><td>${row.number}</td><td><button class=
  "btn btn-success btnupdate" data-id="${row.id}" >Edit</button></td></tr>`;
  tableBody.append(rowTemplate);
}
تغییری که رخ داده است این است که یک دکمه ویرایش، به صفحه اضافه شده‌است که خصوصیت data-id آن با id رکورد پر خواهد شد.
سپس در تگ اسکریپت، در رویداد ready جی کوئری، این دستورات را اضافه می‌کنیم:
$("#people").on('click','.btnupdate',function(e)
{
  e.preventDefault();

  row.id=$(this).data("id");
  let row=GetValues();
  db.run("UPDATE NUMBERS SET FNAME=?,LNAME=?,NUMBER=? WHERE ID=?",[row.fname,row.lname,row.number,row.id]);
  SaveChanges();
  
  tr=$(this).closest("tr");
  let column=0;
  tr.find("td").each(function(index)
  {
    oldRow=$(this);
    switch(column)
    {
      case 0: //fname
        oldRow.text(row.fname);
        break;
      case 1: //lname
        oldRow.text(row.lname);
        break;
      case 2: //number
        oldRow.text(row.number);
        break;
    }
    column++;
  });
});
ابتدای id ذخیره شده در المان و مقادیر جدید را دریافت می‌کنیم. با استفاده از متد run کوئری به روزرسانی را به همراه پارامترها ارسال میکنیم و نتیجه را بر روی دیسک سخت ذخیره می‌کنیم. از اینجا به بعد نقش جی کوئری پر رنگ‌تر می‌شود و به خوبی می‌توانیم اهمیت آن را درک کنیم. سطر دکمه جاری را پیدا می‌کنیم و مقادیر جدید را ستون به ستون تغییر می‌دهیم.

خواندن و بازگردانی رکوردها

در مقاله قبلی با دستور each آشنا شدیم که یک متد غیرهمزمان بود و نتیجه هر رکورد را با یک callback به ما بازگشت میداد. در اصل این متد شامل 4 پارامتر است: پارامتر اول آن، کوئری ارسالی است. پارامتر دوم آن، پارامتر کوئری‌ها ، پارامتر سوم، تابع callback که به ازای هر رکورد اجرا می‌شود و پارامتر چهارم، تابع done می باشد. یعنی زمانی که کلیه رکوردها بازگشت داده شدند. شکل کامل آن به این صورت است:
db.each("SELECT name,age FROM users WHERE age >= $majority",
  {$majority:18},
  function(row){console.log(row.name)},
                                function(){console.log("done");}
  );


در این مقاله با متد دیگری به نام exec نیز آشنا می‌شویم که بازگردانی مقادیر در آن به صورت همزمان صورت می‌گیرد و توانایی آن را دارد که چندین دستور select را بازگردانی کند. به عنوان مثال دستور زیر را در نظر بگیرید:
SELECT ID FROM NUMBERS;SELECT FNAME,LNAME FROM NUMBERS
شکل خروجی آن به این صورت خواهد بود:
 [
         {columns: ['id'], values:[[1],[2],[3]]},
         {columns: ['fname','lname'], values:[['ali','yeganeh'],['hossein','yeganeh'],['mohammad','yeganeh']]}
    ]
پس اگر بخواهیم کد بازیابی رکورهای جدول numbers را در این پروژه با این متد بازنویسی کنیم، از کد زیر استفاده می‌کنیم:
var records=db.exec("select * from numbers");
let values=records[0].values;
let length=values.length;

for(let i=0;i<length;i++)
{
  let object=values[i];
  let row={
    id:object[0],
    fname:object[1],
    lname:object[2],
    number:object[3]
  };
  AddToTable(row);
}
در کد بالا از آنجا که تنها یک data result یا query result داریم، فقط اندیس 0 آن را می‌خوانیم و سپس تعداد آرایه‌ها را که برابر تعداد رکوردهاست، دریافت می‌کنیم و در یک حلقه، رکورد به رکورد، در جدول اضافه می‌کنیم.
مطالب
اتریبیوت اختصاصی برای قفل کردن یک اکشن جهت جلوگیری از تداخلات درخواست‌های همزمان

در کتابخانه‌ی Microsoft AspNetCore Identity میتوان با این کد، فیلد Email را منحصر به‌فرد کرد:

//Program.cs file
builder.Services.AddIdentity<User, Role>(options =>
{
    options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<DatabaseContext>();

برنامه را اجرا و درخواست‌ها را یکی یکی به سمت سرور ارسال میکنیم و اگر ایمیل تکراری باشد به ما خطا میده و میگه: "ایمیل تکراری است".

ولی مشکل اینجاست که کد بالا فیلد Email رو داخل دیتابیس منحصر به‌فرد نمیکنه و فقط از سمت نرم افزار بررسی تکراری بودن ایمیل رو انجام میده. حالا اگه ما با استفاده از نرم افزارهای "تست برنامه‌های وب" مثل Apache JMeter تعداد زیادی درخواست را به سمت برنامه‌مان ارسال کنیم و بعد رکوردهای داخل جدول کاربران را نگاه کنیم، با وجود اینکه داخل نرم افزارمان پراپرتی Email را منحصر به‌فرد کرده‌ایم، ولی چندین رکورد، با یک ایمیل مشابه در داخل جدول User وجود خواهد داشت.

برای تست این سناریو، برنامه Apache JMeter را از این لینک دانلود می‌کنیم (در بخش Binaries فایل zip رو دانلود می کنیم).

نکته: داشتن jdk ورژن 8 به بالا پیش نیاز است. برای اینکه بدونید ورژن جاوای سیستمتون چنده، داخل cmd دستور java -version رو صادر کنید.

اگه تمایل به نصب، یا به روز رسانی jdk را داشتید، میتونید از این لینک استفاده کنید و بسته به سیستم عاملتون، یکی از تب‌های Windows, macOS یا Linux رو انتخاب کنید و فایل مورد نظر رو دانلود کنید (برای Windows فایل x64 Compressed Archive رو دانلود و نصب میکنیم).

حالا فایل دانلود شده JMeter رو استخراج میکنیم، وارد پوشه‌ی bin میشیم و فایل jmeter.bat رو اجرا میکنیم تا برنامه‌ی JMeter اجرا بشه.

قبل از اینکه وارد برنامه JMeter بشیم، کدهای برنامه رو بررسی می‌کنیم.

موجودیت کاربر:

public class User : IdentityUser<int>;

ویوو مدل ساخت کاربر:

public class UserViewModel
{
    public string UserName { get; set; } = null!;

    public string Email { get; set; } = null!;

    public string Password { get; set; } = null!;
}

کنترلر ساخت کاربر:

[ApiController]
[Route("/api/[controller]")]
public class UserController(UserManager<User> userManager) : Controller
{
    [HttpPost]
    public async Task<IActionResult> Add(UserViewModel model)
    {
        var user = new User
        {
            UserName = model.UserName,
            Email = model.Email
        };
        var result = await userManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            return Ok();
        }
        return BadRequest(result.Errors);
    }
}

حالا وارد برنامه JMeter میشیم و اولین کاری که باید انجام بدیم این است که مشخص کنیم چند درخواست را در چند ثانیه قرار است ارسال کنیم. برای اینکار در برنامه JMeter روی TestPlan کلیک راست میکنیم و بعد:

Add -> Threads (Users) -> Thread Group

حالا باید بر روی Thread Group کلیک کنیم و بعد در بخش Number of threads (users) تعداد درخواست‌هایی را که قرار است به سمت سرور ارسال کنیم، مشخص کنیم؛ برای مثال عدد 100.

گزینه Ramp-up period (seconds) برای اینه که مشخص کنیم این 100 درخواست قرار است در چند ثانیه ارسال شوند که آن را روی 0.1 ثانیه قرار می‌دهیم تا درخواست‌ها را با سرعت بسیار زیاد ارسال کند.

الان باید مشخص کنیم چه دیتایی قرار است به سمت سرور ارسال شود:

برای اینکار باید یک Http Request اضافه کنیم. برای این منظور روی Thread Group که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Sampler -> Http Request

حالا روی Http Request کلیک میکنیم و متد ارسال درخواست رو که روی Get هست، به Post تغییر میدیم و بعد Path رو هم به آدرسی که قراره دیتا رو بهش ارسال کنیم، تغییر میدهیم:

https://localhost:7091/api/User

حالا پایینتر Body Data رو انتخاب میکنیم و دیتایی رو که قراره به سمت سرور ارسال کنیم، در قالب Json وارد میکنیم:

{
  "UserName": "payam${__Random(1000, 9999999)}",
  "Email": "payam@gmail.com",
  "Password": "123456aA@"
}

چون بخش UserName در پایگاه داده منحصر به‌فرد است، با این دستور:

${__Random(1000, 9999999)}

یک عدد Random رو به UserName اضافه میکنیم که دچار خطا نشیم.

حالا فقط باید یک Header رو هم به درخواستمون اضافه کنیم، برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Config Element -> Http Header Manager

حالا روی دکمه‌ی Add در پایین صفحه کلیک میکنیم و این Header رو اضافه میکنیم:

Name: Content-Type
Value: application/json

همچنین میتونیم یک View result رو هم اضافه کنیم تا وضعیت تمامی درخواست‌های ارسال شده رو مشاهده کنیم. برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Listener -> View Results Tree

فایل Backup، برای اینکه مراحل بالا رو سریعتر انجام بدید:

File -> Open

حالا بر روی دکمه‌ی سبز رنگ Play در Toolbar بالا کلیک میکنیم تا تمامی درخواست ها را به سمت سرور ارسال کنه و همچنین میتونیم از طریق View result tree ببینیم که چند درخواست موفقیت آمیز و چند درخواست ناموفق انجام شده‌است.

حالا اگر وارد پایگاه داده بشیم، میبینیم که چندین رکورد، با Email یکسان، در جدول User وجود داره:

در حالیکه ایمیل رو در تنظیمات کتابخانه Microsoft AspNetCore Identity به صورت Unique تعریف کرده‌ایم:

//Program.cs file
builder.Services.AddIdentity<User, Role>(options =>
{
    options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<DatabaseContext>();

دلیل این مشکل این است که درخواست‌ها در قالب یک صف، یک به یک اجرا نمیشوند؛ بلکه به صورت همزمان فریم ورک ASP.NET Core برای بالا بردن سرعت اجرای درخواست‌ها از تمامی Thread هایی که در اختیارش هست استفاده می‌کند و در چندین Thread جداگانه، درخواست‌هایی رو به کنترلر User میفرسته و در نتیجه، در یک زمان مشابه، چندین درخواست ارسال میشه که آیا یک ایمیل برای مثال با مقدار payam@yahoo.com وجود داره یا خیر و در تمامی درخواست‌ها چون همزمان انجام شده، جواب خیر است. یعنی ایمیل تکراری با آن مقدار، در پایگاه داده وجود ندارد و تمامی درخواست‌هایی که همزمان به سرور رسیده‌اند، کاربر جدید را با ایمیل مشابهی ایجاد می‌کنند.

این مشکل را میتوان حتی در سایت‌های فروش بلیط نیز پیدا کرد؛ یعنی چند نفر یک صندلی را رزرو کرده‌اند و همزمان وارد درگاه پرداخت شده و هزینه‌ایی را برای آن پرداخت میکنند. اگر آن درخواست‌ها را وارد صف نکنیم، امکان دارد که یک صندلی را به چند نفر بفروشیم. این سناریو برای زمانی است که در پایگاه داده، فیلد‌ها را Unique تعریف نکرده باشیم. هر چند که اگر فیلدها را نیز Unique تعریف کرده باشیم تا یک صندلی را به چند نفر نفروشیم، در آن صورت هم برنامه دچار خطای 500 خواهد شد. پس بهتر است که حتی در زمان‌هایی هم که فیلدها را Unique تعریف میکنیم، باز هم از ورود چند درخواست همزمان به اکشن رزرو صندلی جلوگیری کنیم.

راه حل

برای حل این مشکل میتوان از Lock statement استفاده کرد که این راه حل نیز یک مشکل دارد که در ادامه به آن اشاره خواهم کرد.

Lock statement به ما این امکان رو میده تا اگر بخشی از کد ما در یک Thread در حال اجرا شدن است، Thread دیگری به آن بخش از کد، دسترسی نداشته باشد و منتظر بماند تا آن Thread کارش با کد ما تموم شود و بعد Thread جدید بتونه کد مارو اجرا کنه.

نحوه استفاده از Lock statement هم بسیار ساده‌است:

public class TestClass
{
    private static readonly object _lock1 = new();

    public void Method1()
    {
        lock (_lock1)
        {
            // Body
        }
    }
}

حالا باید کدهای خودمون رو در بخش Body اضافه کنیم تا دیگر چندین Thread به صورت همزمان، کدهای ما رو اجرا نکنند.

اما یک مشکل وجود داره و آن این است که ما نمیتوانیم در Lock statement، از کلمه کلیدی await استفاده کنیم؛ در حالیکه برای ساخت User جدید باید از await استفاده کنیم:

var result = await userManager.CreateAsync(user, model.Password);

برای حل این مشکل میتوان از کلاس SemaphoreSlim بجای کلمه‌ی کلیدی lock استفاده کرد:

[ApiController]
[Route("/api/[controller]")]
public class UserController(UserManager<User> userManager) : Controller
{
    private static readonly SemaphoreSlim Semaphore = new (initialCount: 1, maxCount: 1);

    [HttpPost]
    public async Task<IActionResult> Add(UserViewModel model)
    {
        var user = new User
        {
            UserName = model.UserName,
            Email = model.Email
        };

        // Acquire the semaphore
        await Semaphore.WaitAsync();
        try
        {
            // Perform user creation
            var result = await userManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                return Ok();
            }
            return BadRequest(result.Errors);
        }
        finally
        {
            // Release the semaphore
            Semaphore.Release();
        }
    }
}

این کلاس نیز مانند lock عمل میکند، ولی توانایی‌های بیشتری را در اختیار ما قرار میدهد؛ برای مثال میتوان تعیین کرد که همزمان چند ترد میتوانند به این کد دسترسی داشته باشند؛ در حالیکه در lock statement فقط یک Thread میتوانست به کد دسترسی داشته باشد. مزیت دیگر کلاس SemaphoreSlim این است که میتوان برای اجرای کدمان Timeout در نظر گرفت تا از بلاک شدن نامحدود Thread جلوگیری کنیم.

با فراخوانی await semaphore.WaitAsync، دسترسی کد ما توسط سایر Thread ها محدود و با فراخوانی Release، کد ما توسط سایر Thread ها قابل دسترسی می‌شود.

مشکل قفل کردن Thread ها

هنگام قفل کردن Thread ها، مشکلی وجود دارد و آن این است که اگر برنامه‌ی ما روی چندین سرور مختلف اجرا شود، این روش جوابگو نخواهد بود؛ چون قفل کردن Thread روی یک سرور تاثیری در سایر سرورها جهت محدود کردن دسترسی به کد ما ندارد. اما به صورت کلی میتوان از این روش برای بخش‌هایی خاص از برنامه‌هایمان استفاده کنیم.

پیاده سازی با کمک الگوی AOP

برای اینکه کارمون راحت تر بشه، میتونیم کدهای بالا رو به یک Attribute انتقال بدیم و از اون Attribute در بالای اکشن‌هامون استفاده کنیم تا کل عملیات اکشن‌هامونو رو در یک Thread قفل کنیم:

[AttributeUsage(AttributeTargets.Method)]
public class SemaphoreLockAttribute : Attribute, IAsyncActionFilter
{
    private static readonly SemaphoreSlim Semaphore = new (1, 1);

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // Acquire the semaphore
        await Semaphore.WaitAsync();
        try
        {
            // Proceed with the action
            await next();
        }
        finally
        {
            // Release the semaphore
            Semaphore.Release();
        }
    }
}

حالا میتونیم این Attribute را برای هر اکشنی استفاده کنیم:

[HttpPost]
[SemaphoreLock]
public async Task<IActionResult> Add(UserViewModel model)
{
    var user = new User
    {
        UserName = model.UserName,
        Email = model.Email
    };

    var result = await userManager.CreateAsync(user, model.Password);
    if (result.Succeeded)
    {
        return Ok();
    }
    return BadRequest(result.Errors);
}
مطالب
LocalDB FAQ
SQL Server Express LocalDB یا به صورت خلاصه LocalDB، یک بانک اطلاعاتی‌است که به صورت متصل به پروسه‌ی برنامه‌ی جاری اجرا می‌شود؛ برخلاف رویه‌ی متداول بانک‌های اطلاعاتی که به صورت یک سرویس مستقل اجرا می‌شوند. هدف آن، جایگزین کردن نگارش Express نیست و بیشتر حجم کم و سهولت توزیع آن مدنظر بوده‌است. برای مثال نگارش Express به صورت یک سرویس مجزا و مستقل بر روی سیستم نصب می‌شود؛ اما LocalDB به همراه و متصل به برنامه‌ی نوشته شده، اجرا می‌شود:


اگر به تصویر فوق دقت کنید، یک child process جدید به نام sqlservr.exe نیز به همراه برنامه‌ی آزمایشی ما به صورت خودکار اجرا شده‌است. این child process به همراه پارامترهای ذیل است (که توسط NET Framework. مقدار دهی می‌شوند و مدیریت نهایی آن خودکار است):
 "C:\Program Files\Microsoft SQL Server\120\LocalDB\Binn\\sqlservr.exe"   
-c -SMSSQL12E.LOCALDB
-sLOCALDB#5657074F
-d"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\master.mdf"
-l"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\mastlog.ldf"
-e"C:\Users\Vahid\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances\MSSQLLocalDB\error.log"
بنابراین LocalDB برخلاف SQL Server CE، یک بانک اطلاعاتی in-process نیست و به صورت یک پروسه‌ی مجزا اجرا می‌شود. زمانیکه از SQL Server CE استفاده می‌شود، موتور این بانک اطلاعاتی چند فایل DLL بیشتر نیستند و نهایتا اجرای آن داخل پروسه‌ی برنامه‌ی ما و همانند اجرای سایر DLLهای متصل و مورد استفاده‌ی به آن است.
اما LocalDB یک بانک اطلاعاتی user-mode است و در پروفایل کاربر جاری سیستم اجرا می‌شود. این بانک اطلاعاتی یک بار بر روی سیستم نصب می‌شود و در هر برنامه‌ای که از آن استفاده می‌کنید، یک child process مجزای خاص خودش را (sqlservr.exe) اجرا خواهد کرد. اجرا و خاتمه‌ی این child processها نیز خودکار هستند و نیازی به دخالت مستقیم برنامه ندارند.
البته به نظر توسعه‌ی SQL Server CE متوقف شده‌است و دیگر پشتیبانی نمی‌شود. بنابراین گزینه‌ی ترجیح داده شده‌ی برای کارهایی با حجم‌های بانک اطلاعاتی زیر 10 گیگابایت ، می‌تواند LocalDB باشد. به علاوه اینکه قابلیت‌های T-SQL بیشتری را نیز پشتیبانی می‌کند و همچنین پشتیبانی منظمی نیز از آن وجود دارد. برای مثال پیش نمایش نگارش 2016 آن نیز موجود است.

در ادامه، یک سری پرسش و پاسخ متداول جهت کار با LocalDB را مرور خواهیم کرد.


محل دریافت آخرین نگارش مستقل آن کجاست؟

همانطور که عنوان شد، یکی از مهم‌ترین اهداف LocalDB، سهولت توزیع آن است و عدم نیاز به یک Admin سیستم، برای نصب و نگهداری آن. نگارش 2014 SP1 آن‌را از آدرس ذیل می‌توانید دریافت کنید:
https://www.microsoft.com/en-us/download/details.aspx?id=46697

در اینجا نسخه‌های متعددی وجود دارند. برای مثال اگر سیستم شما 64 بیتی است، تنها نیاز است ENU\x64\SqlLocalDB.msi را دریافت و نصب کنید:



پارامترهای نصب خاموش آن برای توزیع ساده‌ی برنامه کدامند؟

اگر می‌خواهید نصاب LocalDB را به همراه setup برنامه‌ی خود توزیع کنید، می‌توانید روش توزیع خاموش را با ذکر پارامترهای ذیل، مورد استفاده قرار دهید:
 msiexec /i SqlLocalDB.msi /qn IACCEPTSQLLOCALDBLICENSETERMS=YES


رشته‌ی اتصالی مخصوص آن کدام است؟

  <connectionStrings>
    <add name="Sample35Context"
        connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\test.mdf;Integrated Security=True;"
        providerName="System.Data.SqlClient" />
  </connectionStrings>
اگر نگارش 2014 SP1 آن‌را نصب کرده باشید، رشته‌ی اتصالی فوق، تمام آن‌چیزی است که برای شروع به کار با آن، نیاز دارید و دارای دو قسمت مهم است:
الف) ذکر وهله‌ی مدنظر
در اینجا وهله‌ی MSSQLLocalDB ذکر شده‌است؛ اما چه وهله‌هایی بر روی سیستم نصب هستند و چطور می‌توان وهله‌ی دیگری را ایجاد کرد؟ برای این منظور، به پارامترهای sqlservr.exe ابتدای بحث دقت کنید. اکثر آن‌ها به پوشه‌ی ذیل اشاره می‌کنند:
 C:\Users\your_user_name_here\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances
با یک چنین محتوایی

در این پوشه، وهله‌‌های موجود و نصب شده‌ی بر روی سیستم شما نمایش داده می‌شوند که یکی از آن‌ها را می‌توانید در رشته‌ی اتصالی فوق ذکر کنید.
به علاوه، این لیست را توسط برنامه‌ی کمکی SqlLocalDB.exe، به همراه پارامتر info یا i نیز می‌توانید دریافت و بررسی کنید:


برنامه‌ی کمکی SqlLocalDB.exe به همراه نصاب LocalDB، نصب می‌شود و توسط آن می‌توان نگارش‌های مختلف نصب شده‌را با پارامتر v و وهله‌ی مختلف موجود را با پارامتر i مشاهده کرد.
همچنین اگر می‌خواهید وهله‌ی جدیدی را بجز وهله‌ی پیش فرض MSSQLLocalDB ایجاد کنید، می‌توانید از پارامتر create آن به نحو ذیل استفاده نمائید:
For LocalDB SQL EXPRESS 2014
 "C:\Program Files\Microsoft SQL Server\120\Tools\Binn\SqlLocalDB.exe" create "v12.0" 12.0 -s

For LocalDB SQL Express 2012
 "C:\Program Files\Microsoft SQL Server\110\Tools\Binn\SqlLocalDB.exe" create "v11.0" 11.0 -s

ب) ذکر DataDirectory
در رشته‌ی اتصالی فوق، پارامتر DataDirectory نیز ذکر شده‌است تا بتوان مسیر بانک اطلاعاتی را به صورت نسبی و بدون ذکر عبارت دقیق آن که ممکن است در سیستم‌های دیگر متفاوت باشد، پردازش کرد. این پارامتر در برنامه‌های وب به پوشه‌ی استاندارد app_data اشاره می‌کند و نیازی به تنظیم اضافه‌تری ندارد. اما در برنامه‌های دسکتاپ باید به نحو ذیل به صورت دستی، در آغاز برنامه مقدار دهی شود:
 AppDomain.CurrentDomain.SetData("DataDirectory", AppDomain.CurrentDomain.BaseDirectory);
به این ترتیب DataDirectory به محل قرارگیری فایل exe برنامه اشاره می‌کند. بدیهی است در اینجا هر پوشه‌ی دیگری را نیز می‌توانید ذکر کنید:
 AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "db"));
برای نمونه تنظیم فوق به زیر پوشه‌ی db، در کنار فایل exe برنامه اشاره می‌کند.


محل نصب بانک‌های اطلاعاتی پیش فرض آن کدام است؟

ذکر AttachDbFilename در رشته‌ی اتصالی فوق، اختیاری است. در صورت عدم ذکر آن، بانک اطلاعاتی ایجاد شده را در یکی از مسیرهای ذیل می‌توانید جستجو کنید:
 C:\Users\USERNAME\AppData\Local\Microsoft\Microsoft SQL Server Local DB\Instances
C:\Users\USERNAME\AppData\Local\Microsoft\VisualStudio\SSDT
همچنین در این محل‌ها فایل‌های log متنی خطاهای این بانک اطلاعاتی را نیز می‌توان مشاهده کرد. بنابراین اگر به خطای خاصی برخوردید، بهترین کار، بررسی این فایل‌‌ها است.


آیا می‌توان فایل‌های mdf و ldf آن‌را به نگارش کامل SQL Server متصل (attach) کرد؟

بله. اما باید دقت داشته باشید که SQL Server به محض اتصال یک بانک اطلاعاتی با نگارش پایین‌تر به آن، ابتدا شماره نگارش آن‌‌را به روز می‌کند. یعنی دیگر نخواهید توانست این بانک اطلاعاتی را با نگارش پایین‌تر LocalDB باز کنید و یک چنین پیام خطایی را دریافت خواهید کرد:
 The database xyz cannot be opened because it is version 706. This server supports version 663 and earlier. A downgrade path is not supported.


چگونه محتوای بانک‌های اطلاعاتی LocalDB را با VS.NET مشاهده کنیم؟

از منوی view گزینه‌ی server explorer را انتخاب کنید. بر روی data connections کلیک راست کرده و گزینه‌ی Add connection را انتخاب کنید.


در صفحه‌ی باز شده، گزینه‌ی Microsoft SQL server  را انتخاب کنید. در صفحه‌ی بعد، ذکر server name مطابق data source رشته‌ی اتصالی بحث شده و سپس انتخاب گزینه‌ی attach a database file کفایت می‌کند:


پس از کلیک بر روی ok، امکان کار با اجزای این بانک اطلاعاتی را خواهید داشت:



چگونه از LocalDB با EF استفاده کنیم؟

EF 6.x به صورت پیش فرض از بانک اطلاعاتی LocalDB استفاده می‌کند و تنها داشتن یک چنین تنظیمی در فایل کانفیگ برنامه، برای کار با آن کافی است:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <connectionStrings>
    <add name="Sample35Context"
        connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\test.mdf;Integrated Security=True;"
        providerName="System.Data.SqlClient" />
  </connectionStrings>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <entityFramework>
    <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
      <parameters>
        <parameter value="mssqllocaldb" />
      </parameters>
    </defaultConnectionFactory>
    <providers>
      <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
    </providers>
  </entityFramework>
</configuration>
یک قسمت آن ذکر رشته‌ی اتصالی است که در مورد آن بحث شد و قسمت دوم آن، ذکر connection factory مخصوص localdb است که به صورت فوق می‌باشد. تنظیم دیگری برای کار با LocalDB و EF 6.x نیازی نیست.
البته باید دقت داشت که اسمبلی EntityFramework.SqlServer نیز به صورت خودکار به همراه بسته‌ی نیوگت EF 6.x به برنامه اضافه می‌شود که استفاده‌ی از connection factory ذکر شده را میسر می‌کند.


استفاده‌ی از LocalDB به همراه برنامه‌های وب چگونه است؟

سه نکته را باید در حین استفاده‌ی از LocalDB، در برنامه‌های وب اجرا شده‌ی بر روی IIS مدنظر داشت:
الف) LocalDB یک بانک اطلاعاتی user-mode است و child process آن تحت مجوز اکانت تنظیم شده‌ی برای آن کار می‌کند.
ب) همانطور که عنوان شد، در رشته‌ی اتصالی ذکر شده، پارامتر DataDirectory به پوشه‌ی استاندارد app_data اشاره می‌کند که فایل‌های قرار گرفته‌ی در آن توسط IIS محافظت می‌شوند و از طریق وب قابل دسترسی و دانلود نیستند.
ج) child process مربوط به LocalDB، نیاز به دسترسی write، برای کار با فایل‌های mdf و ldf خود دارد.

برای مورد الف نیاز است تا به تنظیمات application pool برنامه مراجعه کرده و سپس بر روی آن کلیک راست کرد و گزینه‌ی advanced settings را انتخاب نمود. در اینجا گزینه‌ی load user profile باید true باشد:


تنظیم load user profile ضروری است اما کافی نیست. پس از آن باید setProfileEnvironment را نیز به true تنظیم کرد. تنظیم این مورد در کنسول مدیریتی IIS به صورت زیر است.
ابتدا ریشه‌ی اصلی سرور را انتخاب کنید و سپس به configuration editor آن وارد شوید:


در ادامه از دارپ داون آن، گزینه‌ی system.applicationHost و زیر شاخه‌ی applicationPools آن‌را انتخاب کنید:


در اینجا application pool defaults و سپس در آن processModel را نیز باز کنید:


اکنون امکان ویرایش setProfileEnvironment را به true خواهید داشت:


پس از این تنظیم، ابتدا بر روی دکمه‌ی apply سمت راست صفحه کلیک کرده و سپس نیاز است یکبار IIS را نیز ریست کنید تا تنظیمات اعمال شوند.


در ادامه برای تنظیم دسترسی write (موارد ب و ج)، ابتدا بر روی پوشه‌ی app_data برنامه، کلیک راست کرده و برگه‌ی security آن‌را باز کنید. سپس بر روی دکمه‌ی edit کلیک کرده و در صفحه‌ی باز شده بر روی دکمه‌ی add کلیک کنید تا بتوان به کاربر application pool برنامه دسترسی write داد:


در اینجا iis apppool\TestLocalDB را وارد کرده و بر روی دکمه‌ی check name کلیک کنید.

iis apppool آن که مشخص است. عبارت TestLocalDB نام application pool ایی است که برای برنامه‌ی وب خود ایجاد کرده‌ایم (بهتر است به ازای هر برنامه‌ی وب، یک application pool مجزا تعریف شود).


در اینجا بر روی OK کلیک کرده و به این کاربر جدید اضافه شده، دسترسی full control را بدهید تا برنامه و یوزر آن بتواند فایل‌های mdf و ldf را ایجاد کرده و به روز رسانی کنند.

پس از تنظیم load user profile و همچنین set profile environment و دادن دسترسی write به کاربر application pool برنامه، اکنون child process مربوط به local db را می‌توان ذیل پروسه‌ی IIS مشاهده کرد و برنامه قادر به استفاده‌ی از LocalDB خواهد بود:

نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
استفاده‌ی همزمان از چندین Context متفاوت، نکات خاصی دارد و باید بررسی کنید که مناسب کار شما هست یا خیر. برای مطالعه‌ی بیشتر:
- «استفاده از چندین Context در EF 6 Code first»
- «استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First»
+ «تزریق وابستگی‌ها در حالتی‌که از یک اینترفیس چندین کلاس مشتق شده‌اند»  
مطالب
استفاده از Google Analytics در ASP.Net

قبل از استفاده از بلاگر، در سایت wordpress وبلاگ داشتم، که به‌دلایلی کنسل شد. تفاوت محسوسی را که اینجا مشاهده می‌کنم، نبود قسمت آمار سایت است. در سایت wordpress آمار مبسوطی را از بازدید کنندگان سایت می‌توانید در کنترل پنل مدیریتی وبلاگ مشاهده کنید، اما در اینجا خیر.
به همین جهت اولین کاری را که انجام دادم استفاده از سرویس رایگان persianstat بود که انصافا هم با کیفیت است و قابل مقایسه با آماری که wordpress ارائه می‌دهد، می‌باشد.
جالب اینجا است که هر چند هاست اینجا، گوگل است اما استفاده‌ی خودکار از ابزار Google analytics در آن مهیا نیست. احتمالا علت آن آماده نبودن API آن است که قرار است به زودی ارائه شود، بنابراین ارزش وقت گذاشتن را دارد.



برای استفاده از Google analytics ، پس از ثبت نام و ورود به آن، سایت مورد نظر را معرفی کرده (در قسمت Add Website Profile) و نهایتا یک کد جاوا اسکریپتی به شما خواهد داد که می‌توانید آنرا به صفحات مورد نظر خود در سایت اضافه نمائید تا تحت کنترل آماری قرار گیرد. محدودیتی هم در مورد تعداد سایت وجود ندارد و با یک اکانت می‌توانید چندین سایت را معرفی کرده و تحت کنترل قرار دهید.
اگر از ASP.Net استفاده می‌کنید، تنها کافی است به master page سایت مراجعه کنید و پیش از بسته شدن تگ body ، اسکریپت مربوط به Google analytics را اضافه کنید تا تمام سایت را تحت کنترل قرار دهید.
یا اگر علاقمند بودید که اینکار را به صورت "شیک‌تری" انجام دهید، می‌توان از این http module استفاده کرد. به این صورت ابتدا تگ بسته شدن body به صورت خودکار پیدا شده و سپس اسکریپت به پیش از آن اضافه می‌شود.
این روش بار بزرگ تهیه آمار سایت را حذف خواهد کرد. عموما دیتابیس جمع آوری آمار سایت خیلی زود (برای مثال پس از گذشت 6 ماه) حجیم می‌شود و تاثیر مشهودی را بر روی کارآیی سایت خواهد گذاشت. بنابراین، این سؤال مطرح می‌شود که چرا گوگل اینکار را برای ما انجام ندهد؟! هزینه بانک اس کیوال سرور بر روی هاست‌های اینترنتی بالا بوده و حجمی را هم که در اختیار قرار می‌دهند محدود است. در صورت نیاز به حجم‌های بالاتر باید هزینه بیشتری را پرداخت کرد. بنابراین هم از لحاظ قیمت و هچنین کارآیی سایت، استفاده از این سرویس واقعا مقرون به صرفه است. بعلاوه از تنوع آماری که ارائه می‌دهد نیز نمی‌توان چشم پوشی کرد. برای مثال کاربران چه واژه‌های کلیدی را در موتورهای جستجو وارد کرده‌اند تا به سایت شما رسیده‌اند؟ چند درصد کاربر وفادار دارید؟! (کاربرهای وفادار، منظور افرادی هستند که به صورت منظم به سایت سر می‌زنند) و امثال این. انصافا تهیه چنین ماژولی برای یک سایت از لحاظ برنامه نویسی شاید با برنامه نویسی کل یک سایت برابری کند.
اگر هم نیاز به یک برنامه سورس باز داشتید که هر روز به اکانت Google analytics شما سر بزند و اطلاعات آنرا استخراج کرده و در یک بانک SQL server ذخیره کند، می‌توانید به پروژه سی شارپ زیر مراجعه نمائید:
Google Analytics Data Extractor

البته باید دقت داشت که پس از ارائه API کامل Google analytics ، دیگر نیازی به این نوع روش‌های ابتکاری وجود نداشته و استخراج داده از آن بسیار ساده‌تر خواهد شد.

نظرات مطالب
EF Code First #1
چه پارامترهایی در انتخاب Code First، Model First, DataBase First برای یک پروژه وجود دارد؟

نظرات مطالب
استفاده از لوسین برای برجسته سازی عبارت جستجو شده در نتایج حاصل
سلام آقای نصیری،
دو تا سوال داشتم ازتون:
1- از این توابع مثلاً باید موقع درج مقاله(پست یا ..) در بانک اطلاعاتی برای ایندکس جدید استفاده کرد و موقع ویرایش و حذف مقاله(پست یا ..) هم از توابع معرفی شده متناسب استفاده کرد؟

2- جستجوی پیشرفته به چه صورت هستش؟مثلاً تاریخ درج مقاله از ... تا ... ،نام نویسنده ،کلمه کلیدی و ... (که هر کدام از این موارد میتونه به صورت اختیاری و یا اجباری باشه).