Dapper - один из лучших современных ORM для разработчиков .NET, и большинство из нас уже используют его при разработке приложений. С введением Dapper в приложение мы определенно будем ожидать хорошего улучшения производительности всего приложения.

К сожалению, большинство из вас, читающих эту статью, могло оказаться здесь, потому что вы не получили ожидаемого результата, а вместо этого получили падение производительности. Возможно, вам интересно, что пошло не так с вашим приложением или средой, которая кричит на вас из-за всплеска использования ЦП и нагрузки на сервер. Эта статья может помочь вам найти причину (если вы еще не выяснили ее и, надеюсь, это причина, по которой вы столкнулись с проблемой).

Я уверен, что вы прочитали документацию по Dapper и учебник по dapper, но, возможно, упустили важность этой части в вашей среде. Он четко указывает на использование DbString при передаче строкового параметра в Dapper для построения запроса.

Для лучшего понимания давайте посмотрим, что Dapper делает со своими параметрами. Когда строка передается в качестве параметра, Dapper принимает ее тип как NVARCHAR, даже если столбец в вашей БД равен VARCHAR. Та-да !!! Теперь, если вы использовали эти параметры в своих предложениях WHERE, ваша БД должна сравнить столбец VARCHAR с данными NVARCHAR. Увидев это, БД просто решит, что не может использовать индекс в таблице. Это означает, что вместо использования поиска на столе он будет выполнять сканирование! Также он должен преобразовать каждую строку таблицы в NVARCHAR для сравнения.

(Совет профессионалов: NVARCHAR и NCHAR поддерживает символы UNICODE, а VARCHAR и CHAR поддерживает только Символы ANSII)

Только представьте, какую нагрузку он создает на сервере. Если вы не можете (или не хотите) вообразить, давайте попробуем количественно оценить вышеприведенное наблюдение, используя простой пример реализации Dapper.

Простой образец

Давайте создадим класс Employee следующим образом:

public class Employee
{    
    public string empID { get; set; }
    public string empFirstName { get; set; }
    public string empLastName { get; set; }
}

И мы также создадим таблицу для данных следующим образом:

CREATE TABLE  (
    empID VARCHAR(15) PRIMARY KEY,
    empFirstName VARCHAR(255) ,
    empLastName VARCHAR(255) 
);

И давайте воспользуемся Dapper для чтения и сопоставления активной записи с экземпляром Employee. (Для тех из вас, кто задается вопросом, почему id является строкой, а не числом, я просто хотел продемонстрировать, что происходит со строкой в ​​Dapper).

static void Main(string[] args)
{
    DbConnection db = new SqlConnection(
                         ConfigurationManager
                           .ConnectionStrings["DapperSQLConString"]
                           .ConnectionString
                      );
    db.Open();
    employee = db.QueryFirst<Employee>(
        "select * from employeeDetails 
         With (nolock) where empID = @empID ", 
        new { @empID = "1" });
}

Теперь, если мы запустим это консольное приложение с SQL Profiler в Microsoft SSMS, мы сможем найти запрос, который был выполнен для указанного выше попадания в БД. Это будет примерно так:

EXEC sp_executesql N'Select * from employeeDetails WITH(NOLOCK) where empID=@empID' ,N'@empID nvarchar(4000)' ,@empID = N'1'

Ты это видел? В запросе есть все, что есть в _12 _ !!!

Мы ожидали чего-то вроде

EXEC sp_executesql N'Select * from employeeDetails WITH(NOLOCK) where empID=@empID' ,N'@empID VARCHAR(15)' ,@empID = '1'

но мы получили все в NVARCHAR(4000) вместо VARCHAR(15).

Если вам все еще интересно, как это повлияет на производительность, я рекомендую вам выполнить указанные выше запросы в SSMS и посмотреть на сгенерированный план выполнения. Вы убедитесь в этом сами.

Сервер SQL должен преобразовать каждую строку в NVARCHAR , а затем сравнить, что совершенно нежелательно для больших таблиц. Вместо использования поиска по индексу теперь необходимо сканировать таблицу, чтобы получить результат. Вот пример сравнения затрат на поиск по индексу и сканирование в таблице (другая таблица, чем обсуждаемая здесь, но с той же проблемой в запросе Dapper) с большим количеством строк.

Если мы посмотрим на количество прочитанных строк, расчетную стоимость ЦП, стоимость оператора, стоимость ввода-вывода и cst поддерева, мы сможем понять, насколько тяжелым это будет для сервера.

Так как же решить эту проблему?

У нас есть два способа решить эту проблему. Один из способов - указать Dapper, что каждый аргумент в виде строки следует рассматривать как VARCHAR.

Dapper.SqlMapper.AddTypeMap(typeof(string),System.Data.DbType.AnsiString);

Но это хорошо только тогда, когда вы не используете NVARCHAR столбцов во всей базе данных. Лучший способ, который я бы порекомендовал вам, - это указать его на уровне потребления с помощью DbString.. Таким образом, приведенный выше код становится:

employee = db.QueryFirst<Employee>(
        "select * from employeeDetails 
         With (nolock) where empID = @empID ", 
        new { @empID = new DbString { Value = "1", IsFixedLength = false, IsAnsi = true, Length = 15 } });

Что ж, теперь мы получим точно такой же запрос, который, как мы ожидали, сгенерирует Dapper. Именно по этой причине существует DbString !!!

Я надеюсь, что здесь все ясно, и это может решить проблему, с которой мы столкнулись в какой-то момент при использовании Dapper. На момент написания этой статьи это было проверено с версией 1.60.6 и были получены указанные здесь сведения.

Для тех, кому нужен быстрый и краткий справочник о том, как DbString определяет тип, обратите внимание на это:

Надеюсь, это помогло. Удачного кодирования!