_logger.LogInformation("LogInformation");
یک CMS تجاری بزرگ با قابلیتهای زیر
using System.Collections.Generic; namespace AutoMapperComparison.Models { public class User { public int Id { get; set; } public string Name { get; set; } public ICollection<Address> Addresses { get; set; } } }
using System.ComponentModel.DataAnnotations.Schema; namespace AutoMapperComparison.Models { public class Address { public int Id { get; set; } public double? Code { get; set; } public string Title { get; set; } public int UserId { get; set; } [ForeignKey(nameof(UserId))] public virtual User User { get; set; } } }
using EntityFramework.BulkInsert.Extensions; using System.Collections.Generic; using System.Data.Entity; using System.Data.SqlClient; namespace AutoMapperComparison.Models { public class AppDbContextInitializer : DropCreateDatabaseAlways<AppDbContext> { protected override void Seed(AppDbContext context) { User user = context.Users.Add(new User { Name = "Test" }); context.SaveChanges(); List<Address> addresses = new List<Address>(); for (int i = 0; i < 500000; i++) { addresses.Add(new Address { Id = i, Code = 1, Title = "Test", UserId = user.Id }); } context.BulkInsert(addresses); base.Seed(context); } } public class AppDbContext : DbContext { static AppDbContext() { Database.SetInitializer(new AppDbContextInitializer()); //Database.SetInitializer<AppDbContext>(null); } public AppDbContext() : base(new SqlConnection(@"Data Source=.;Initial Catalog=AppDbContext;Integrated Security=True"), contextOwnsConnection: true) { Configuration.AutoDetectChangesEnabled = false; Configuration.EnsureTransactionsForFunctionsAndCommands = false; Configuration.LazyLoadingEnabled = false; Configuration.ProxyCreationEnabled = false; Configuration.ValidateOnSaveEnabled = false; Configuration.UseDatabaseNullSemantics = false; } public DbSet<User> Users { get; set; } public DbSet<Address> Addresses { get; set; } } }
namespace AutoMapperComparison.Models { public class AddressDto { public int Id { get; set; } public double? Code { get; set; } public string Title { get; set; } public int UserId { get; set; } public string UserName { get; set; } } }
using AutoMapper; using AutoMapper.QueryableExtensions; using AutoMapperComparison.Models; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace AutoMapperComparison { public class Program { public static void Main() { Mapper.Initialize(cfg => { cfg.CreateMap<Address, AddressDto>(); }); Console.WriteLine($"Create Db {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.Initialize(force: true); db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); //Removes all clean buffers from the buffer pool, and columnstore objects from the columnstore object pool Console.WriteLine(db.Addresses.ProjectTo<AddressDto>()); Console.WriteLine(db.Addresses.Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name })); } Console.WriteLine($"Normal Select {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); Stopwatch watch = Stopwatch.StartNew(); List<AddressDto> addresses = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList(); List<AddressDto> addresses2 = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList(); watch.Stop(); Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}"); } Console.WriteLine($"AutoMapper Exec {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); Stopwatch watch = Stopwatch.StartNew(); List<AddressDto> addresses = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList(); List<AddressDto> addresses2 = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList(); watch.Stop(); Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}"); } Console.ReadKey(); } } }
حال نتایج بدست آمده، در قسمت پایینتر آن نمایان میشود:
البته نتیجهی این آزمایش بسته به سخت افزار سیستم شما ممکن است کمی متفاوت باشد.
در سه آزمایش دیگر به صورت متوالی نتیجهی زیر بدست آمد:
Normal | AutoMapper |
2451 | 2378 |
2120 | 2111 |
2202 | 2124 |
اگر این مقدار جزئی از تفاوت بین دو نوع مختلف آزمایش را مورد نظر نگیریم، میتوان گفت که هر دو روش نتیجهی کاملا یکسانی خواهند داشت. فقط با استفاده از AutoMapper کدهای کمتری نوشته شدهاست!
اما دلیل چیست؟ از آنجایی که ProjectTo از Dto به Model انجام شده و Lambda Expressionی که به سمت Entity Framework فرستاده شدهاست با روش Normal کاملا برابر است و بقیهی عملیات توسط EF انجام میشود، با قاطعیت میتوان گفت که هر دو روش ذکر شده از نظر Performance کاملا یکسان خواهند بود.
نکته: البته به این موضوع باید توجه شود که اگر همین آزمایش را بطور مثال با استفاده از یک Listی از رکوردهای درون Memory ساخته شده توسط خودمان انجام دهیم، آن موقع نتیجهی یکسانی نخواهیم داشت، به دلیل اینکه EFی دیگر وجود نخواهد داشت که مسئولیت بازگشت دادهها را بر عهده بگیرد. از آنجائیکه اکثر کارهایی که توقع داریم AutoMapper برای ما انجام دهد، توسط ORM بازگشت داده میشود، پس میتوان گفت نکتهی فوق تقریبا در دنیای واقعی رخ نخواهد داد و باعث مشکل نخواهد شد.
الگوریتم HiLo در RavenDB چیست؟
HiLo چیست؟
HiLo روشی است برای ارائه idهای عددی افزایش یابنده، جهت استفاده در محیطهای چندکاربری. در این حالت، هنوز هم سرور تولید idها را کنترل میکند، اما هربار بازهای از Idها را در اختیار کلاینتها قرار میدهد. به این ترتیب کلاینت، بدون نیاز به رفت و برگشت اضافهای به سرور، میتواند Id مورد نیاز خود را از بازه ارائه شده تامین کرده و هر زمانیکه این بازه به پایان رسید، سری دیگری را درخواست کند.
بدیهی است در این حالت، ارائه کلیه Idهای یک بازه از طرف سرور ممکن است کاری سنگین باشد. به همین جهت سرور در درخواست ابتدایی کلاینت، یک تک id را به نام «Hi» جهت مشخص سازی ابتدای بازه تولید idها، در اختیار او قرار میدهد. قسمت «Lo» توسط خود کلاینت مدیریت میشود و در این بین به هر کلاینت یک capacity یا تعداد مجاز idهایی را که میتواند تولید کند، از پیش اختصاص داده شده است:
(currentHi - 1)*capacity + (++currentLo)
حالت پیش فرض کار کلاینتهای RavenDB نیز به همین نحو است و الگوریتم HiLo در DocumentConvention آن، از قبل تنظیم شده است و مزیت مهم روش HiLo این است که با هربار فراخوانی متد Store یک سشن، بدون رفت و برگشت اضافهتری به سرور، Idهای لازم در اختیار شما قرار خواهد گرفت و نیازی نیست تا زمان SaveChanges صبر کرد. به این ترتیب batch operations در RavenDB کارآیی بسیار بالایی خواهند یافت.
با استفاده از AutoComplete TextBoxes میتوان گوشهای از زندگی روزمرهی کاربران یک برنامه را سادهتر کرد. مشکل مهم dropDownList ها دریک برنامهی وب، عدم امکان تایپ قسمتی از متن مورد نظر و سپس نمایان شدن آیتمهای متناظر با آن در اسرع وقت میباشد. همچنین با تعداد بالای آیتمها هم حجم صفحه و زمان بارگذاری را افزایش میدهند. راه حلهای بسیار زیادی برای حل این مشکل وجود دارند و یکی از آنها ایجاد AutoComplete TextBoxes است. پلاگینهای متعددی هم جهت پیاده سازی این قابلیت نوشته شدهاند منجمله jQuery Autocomplete . این پلاگین دیگر توسط نویسندهی اصلی آن نگهداری نمیشود اما توسط برنامه نویسی دیگر در github ادامه یافته است. در ادامه نحوهی استفاده از این افزونه را در ASP.NET Webforms بررسی خواهیم کرد.
الف) دریافت افزونه
لطفا به آدرس GitHub ذکر شده مراجعه نمائید.
سپس برای مثال پوشهی js را به پروژه افزوده و فایلهای jquery-1.5.min.js ، jquery.autocomplete.js ، jquery.autocomplete.css و indicator.gif را در آن کپی کنید. فایل indicator.gif به همراه مجموعهی دریافتی ارائه نمیشود و یک آیکن loading معروف میتواند باشد.
علاوه بر آن یک فایل جدید custom.js را نیز جهت تعاریف سفارشی خودمان اضافه خواهیم کرد.
ب) افزودن تعاریف افزونه به صفحه
در ذیل نحوهی افزودن فایلهای فوق به یک master page نمایش داده شده است.
در اینجا از قابلیتهای جدید ScriptManager (موجود در سرویس پک یک دات نت سه و نیم و یا دات نت چهار) جهت یکی کردن اسکریپتها کمک گرفته شده است. به این صورت تعداد رفت و برگشتها به سرور بهجای سه مورد (تعداد فایلهای اسکریپت مورد استفاده)، یک مورد (نهایی یکی شده) خواهد بود و همچنین حاصل نهایی به صورت خودکار به شکلی فشرده شده به مرورگر تحویل داده شده، سرآیندهای کش شدن اطلاعات به آن اضافه میگردد (که در سایر حالات متداول اینگونه نیست)؛ به علاوه Url نهایی آن هم بر اساس hash فایلها تولید میشود. یعنی اگر محتوای یکی از این فایلها تغییر کرد، چون Url نهایی تغییر میکند، دیگر لازم نیست نگران کش شدن و به روز نشدن اسکریپتها در سمت کاربر باشیم.
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="AspNetjQueryAutocompleteTest.Site" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<asp:PlaceHolder Runat="server">
<link href="<%= ResolveClientUrl("~/js/jquery.autocomplete.css")%>" rel="stylesheet" type="text/css" />
</asp:PlaceHolder>
<asp:ContentPlaceHolder ID="head" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
<CompositeScript>
<Scripts>
<asp:ScriptReference Path="~/js/jquery-1.5.min.js" />
<asp:ScriptReference Path="~/js/jquery.autocomplete.js" />
<asp:ScriptReference Path="~/js/custom.js" />
</Scripts>
</CompositeScript>
</asp:ScriptManager>
<div>
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
ج) افزودن یک صفحهی ساده به برنامه
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="default.aspx.cs" Inherits="AspNetjQueryAutocompleteTest._default" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<asp:TextBox ID="txtShenas" runat="server" />
</asp:Content>
فرض کنید میخواهیم افزونهی ذکر شده را به TextBox استاندارد فوق اعمال کنیم. ID این TextBox در نهایت به شکل ContentPlaceHolder1_txtShenas رندر خواهد شد. البته در ASP.NET 4.0 با تنظیم ClientIDMode=Static میتوان ID انتخابی خود را به جای این ID خودکار درنظر گرفت و اعمال کرد. اهمیت این مساله در قسمت (ه) مشخص میگردد.
د) فراهم آوردن اطلاعات مورد استفاده توسط افزونهی AutoComplete به صورت پویا
مهمترین قسمت استفاده از این افزونه، تهیهی اطلاعاتی است که باید نمایش دهد. این اطلاعات باید به صورت فایلی که هر سطر آن حاوی یکی از آیتمهای مورد نظر است، تهیه گردد. برای این منظور میتوان از فایلهای ASHX یا همان Generic handlers استفاده کرد:
using System;
using System.Data.SqlClient;
using System.Text;
using System.Web;
namespace AspNetjQueryAutocompleteTest
{
public class AutoComplete : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string prefixText = context.Request.QueryString["q"];
var sb = new StringBuilder();
using (var conn = new SqlConnection())
{
//todo: این مورد باید از فایل کانفیگ خوانده شود
conn.ConnectionString = "Data Source=(local);Initial Catalog=MyDB;Integrated Security = true";
using (var cmd = new SqlCommand())
{
cmd.CommandText = @" select Field1 ,Field2 from tblData where Field1 like @SearchText + '%' ";
cmd.Parameters.AddWithValue("@SearchText", prefixText);
cmd.Connection = conn;
conn.Open();
using (var sdr = cmd.ExecuteReader())
{
if (sdr != null)
while (sdr.Read())
{
string field1 = sdr.GetValue(0) == DBNull.Value ? string.Empty : sdr.GetValue(0).ToString().Trim();
string field2 = sdr.GetValue(1) == DBNull.Value ? string.Empty : sdr.GetValue(1).ToString().Trim();
sb.AppendLine(field1 + "|" + field2);
}
}
}
}
context.Response.Write(sb.ToString());
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
در این مثال از ADO.NET کلاسیک استفاده شده است تا به عمد نحوهی تعریف پارامترها یکبار دیگر مرور گردند. اگر از LINQ to SQL یا Entity framework یا NHibernate و موارد مشابه استفاده میکنید، جای نگرانی نیست؛ زیرا کوئریهای SQL تولیدی توسط این ORMs به صورت پیش فرض از نوع پارامتری هستند (+).
در این مثال اطلاعات دو فیلد یک و دوی فرضی از جدولی با توجه به استفاده از like تعریف شده دریافت میگردد. به عبارتی همان متد StartsWith معروف LINQ بکارگرفته شده است.
به صورت خلاصه افزونه، کوئری استرینگ q را به این فایل ashx ارسال میکند. سپس کلیه آیتمهای شروع شده با مقدار دریافتی، از بانک اطلاعاتی دریافت شده و هر کدام قرارگرفته در یک سطر جدید بازگشت داده میشوند.
اگر دقت کرده باشید در قسمت sb.AppendLine ، با استفاده از "|" دو مقدار دریافتی از هم جدا شدهاند. عموما یک مقدار کفایت میکند (در 98 درصد موارد) ولی اگر نیاز بود تا توضیحاتی نیز نمایش داده شود از این روش نیز میتوان استفاده کرد. برای مثال یک مقدار خاص به همراه توضیحات آن به عنوان یک آیتم نمایش داده شده مد نظر است.
ه) اعمال نهایی افزونه به TextBox
در ادامه پیاده سازی فایل custom.js برای استفاده از امکانات فراهم شده در قسمتهای قبل ارائه گردیده است:
function formatItem(row) {
return row[0] + "<br/><span style='text-align:justify;' dir='rtl'>" + row[1] + "</span>";
}
$(document).ready(function () {
$("#ContentPlaceHolder1_txtShenas").autocomplete('AutoComplete.ashx', {
//Minimum number of characters a user has to type before the autocompleter activates
minChars: 0,
delay: 5,
//Only suggested values are valid
mustMatch: true,
//The number of items in the select box
max: 20,
//Fill the input while still selecting a value
autoFill: false,
//The comparison doesn't looks inside
matchContains: false,
formatItem: formatItem
});
});
پس از این مقدمات، اعمال افزونهی autocomplete به textBox ایی با id مساوی ContentPlaceHolder1_txtShenas ساده است. اطلاعات از فایل AutoComplete.ashx دریافت میگردد و تعدادی از خواص پیش فرض این افزونه در اینجا مقدار دهی شدهاند. لیست کامل آنها را در فایل jquery.autocomplete.js میتوان مشاهده کرد.
تنها نکتهی مهم آن استفاده از پارامتر اختیاری formatItem است. اگر در حین تهیهی AutoComplete.ashx خود تنها یک آیتم را در هر سطر نمایش میدهید و از "|" استفاده نکردهاید، نیازی به ذکر آن نیست. در این مثال ویژه، فیلد یک در یک سطر و فیلد دو در سطر دوم یک آیتم نمایش داده میشوند:
var connection = new SqliteConnection("Data Source=:memory:"); connection.Open(); var createCommand = connection.CreateCommand(); createCommand.CommandText = @" CREATE TABLE persian_letter ( value TEXT ); INSERT INTO persian_letter VALUES ('ا'),('ب'),('پ'),('ت'),('ث'),('ج'),('چ'),('ح'),('خ'),('د'),('ذ'),('ر'),('ز'),('ژ'),('س'),('ش'), ('ص'),('ض'),('ط'),('ظ'),('ع'),('غ'),('ف'),('ق'),('ک'),('گ'),('ل'),('م'),('ن'),('و'),('ه'),('ی'); "; createCommand.ExecuteNonQuery();
var queryCommand = connection.CreateCommand(); queryCommand.CommandText = @" SELECT value FROM persian_letter order by value "; var reader = queryCommand.ExecuteReader(); var sortedDbItems = new List<string>(); while (reader.Read()) { sortedDbItems.Add($"{reader["value"]}"); }
همانطور که ملاحظه میکنید، مرتب سازی حروف فارسی در اینجا به صورت پیشفرض کار نمیکند. علت اینجا است که روش پیشفرض مرتب سازی حروف در SQLite، بر اساس کد اسکی حروف است و فقط در مورد حروف ASCII از A تا Z درست کار میکند.
امکان تعریف Collation سفارشی در SQLite
در بانکهای اطلاعاتی، قابلیتی که مستقیما بر روی نحوهی جستجو و همچنین مرتب سازی حروف تاثیر میگذارد، Collation نام دارد و در SQLite برخلاف بسیاری از بانکهای اطلاعاتی دیگر، امکان تعریف Collation سفارشی نیز وجود دارد و برای این منظور باید یک function pointer را در اختیار آن قرار داد تا از آن در سمت بانک اطلاعاتی جهت مرتب سازی و جستجوی حروف استفاده کند.
خوشبختانه پروژهی Microsoft.Data.Sqlite امکان تبدیل یک managed delegate دات نتی را به یک function pointer مخصوص SQLite، میسر میکند. به عبارتی SQLite کدهای دات نتی را در حین انجام محاسبات خود اجرا خواهد کرد و این اجرا به صورتی نیست که ابتدا کل اطلاعات، به سمت برنامهی کلاینت منتقل شود و سپس در این سمت، در حافظه، عملیاتی بر روی آن صورت گیرد. کل عملیات در سمت بانک اطلاعاتی مدیریت میشود.
روش تعریف یک Collation جدید هم در اینجا بسیار سادهاست:
connection.CreateCollation("PersianCollationNoCase", (x, y) => string.Compare(x, y, ignoreCase: true));
SELECT value FROM persian_letter order by value COLLATE PersianCollationNoCase
تعریف Collation سفارشی غیرحساس به «ی و ک» !
این مورد شاید یکی از آرزوهای توسعه دهندگان SQL Server باشد! اما با SQLite به سادگی زیر قابل تعریف و مدیریت است:
connection.CreateCollation("PersianCollationNoCaseYekeInsensitive", (x, y) => string.Compare(x.ApplyCorrectYeKe(), y.ApplyCorrectYeKe(), ignoreCase: true));
در یک چنین حالتی اگر اطلاعاتی را به همراه «ی و ک» فارسی و یا عربی ثبت کنیم:
CREATE TABLE persian_letter ( value TEXT ); INSERT INTO persian_letter VALUES ('ی'),('ک');
SELECT count() FROM persian_letter WHERE value = 'ی' COLLATE PersianCollationNoCaseYekeInsensitive
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: SQLitePersianCollation.zip
تکرار هدر گروه بندی اطلاعات در هر صفحه
پس از انتشار جزوهی SVN در حدود دو سال قبل، ایمیل در این مورد زیاد داشتم. یکی از سؤالات هم این بود که: "چگونه از SVN جهت مدیریت نگارشهای مختلف یک بانک اطلاعاتی اس کیوال سرور در یک تیم استفاده کنیم؟ (منظور مدیریت schema است)" و من هم پاسخ مناسبی برای این مورد نداشتم چون کلاینتهای SVN حداقل با Management studio یکپارچه نمیشود (بر خلاف ابزارهای موجود برای VS.NET مانند VisualSVN ، AnkhSVN و غیره). صد البته میشود از آن همانند اعمال نگارش به یک فایل Text معمولی مانند فایلهای SQL استفاده کرد، اما خوب ...
و خبر خوب اینکه شرکت معظم RedGate چند روز قبل یک کتاب رایگان را در این مورد منتشر کرده است:
سرفصلهای این کتاب
Chapter 1: Writing Readable SQL
Chapter 2: Documenting your Database
Chapter 3: Change Management and Source Control
Chapter 4: Managing Deployments
Chapter 5: Testing Databases
Chapter 6: Reusing T-SQL Code
Chapter 7: Maintaining a Code Library
Chapter 8: Exploring your Database Schema
Chapter 9: Searching DDL and Build Scripts
Chapter 10: Automating CRUD
Chapter 11: SQL Refactoring
دریافت