Dapper – micro-ORM — I
Недавно я рассказывал про легковесную ORM BLToolkit, и при поиске и изучении материала неизбежно наталкивался на сравнение BLT с другими разработками в области мапирования данных на бизнес-объекты. Одним из самых привлекательных вариантов по скорости, а так же по вниманию общественности, оказался Dapper.
Dapper – это даже не легковесная, а микро-ORM система для чтения (в основном) информации из реляционных баз данных. Данная микро-ORM система является разработкой Сэма Сафрона (Sam Saffron) для Stack Overflow, где она работает в связке с Linq2Sql. Для такого большого и посещаемого ресурса как Stack Overflow очень важно быстро получать информацию из базы данных, так как большинство пользователей просматривает ответы, использует их в своей работе, и сравнительно редко пишет. Для записи информации, что требуется значительно реже, до сих пор самым удобным и быстрым остается Linq2Sql.
О системе
Dapper – это по сути один файл с исходным кодом, который надо включить в свой проект. Dapper работает в некотором роде классом помощником, расширяя стандартный интерфейс IDbConnection с помощью extended методов. Т.е. данному фреймворку абсолютно без разницы, с какой базой вы работаете, если соединение с ней построено на указанном интерфейсе.
Запросы Dapper принимает только в текстовом виде, нет поддержки LINQ. Модификация бизнес-классов в основном не требуется, в редких случая потребуется использовать свойства вместо полей класса.
В Dapper нет поддержки маппинга сложных связанных объектов, так же как и в BLToolkit.
Ключевой особенностью Dapper является скорость и на страничке проекта приведены данные измерений времени, которое необходимо для выполнения 500 запросов выборки.
Производительность для операции SELECT для 500 итераций с мапингом - POCO объекты
| Реализация | Длительность | Заметки |
| Hand coded (using a SqlDataReader) | 47ms | |
| Dapper ExecuteMapperQuery<Post> | 49ms | |
| PetaPoco | 52ms | Can be faster |
| BLToolkit | 80ms | |
| SubSonic CodingHorror | 107ms | |
| NHibernate SQL | 104ms | |
| Linq 2 SQL ExecuteQuery | 181ms | |
| Entity framework ExecuteStoreQuery | 631ms |
Производительность для операции SELECT для 500 итераций с мапингом - динамические объекты
| Реализация | Длительность | Заметки |
| Dapper ExecuteMapperQuery (dynamic) | 48ms | |
| Massive | 52ms | |
| Simple.Data | 95ms |
Производительность для операции SELECT для 500 итераций с мапингом – типичное использование (сложный маппинг объектов)
| Реализация | Длительность | Заметки |
| Linq 2 SQL CompiledQuery | 81ms | Не совсем типичное использование, привлекается много сложносоставных объектов |
| NHibernate HQL | 118ms | |
| Linq 2 SQL | 559ms | |
| Entity framework | 859ms | |
| SubSonic ActiveRecord.SingleOrDefault | 3619ms |
Все тесты можно взять со страницы проекта в виде отдельного файла с C# классом.
Ограничения и оговорки
Каждый запрос к базе данных кэшируется, что позволяет быстро материализовывать объекты и распознавать параметры запросов. Текущая реализация кэша основана на ConcurrentDictionary. Объекты попадающие в это хранилище не очищаются во время работы приложения, так что если вы генерируете SQL запросы на ходу без использования параметров, возможны проблемы с памятью, так количество уникальных запросов сильно вырастет.
Простота фреймворка означает, что многие возможности которые идут с полновесными ORM опущены. Например нет identity map, нет помощников для обновления/выборки данных и так далее.
Так же Dapper не управляет временем жизни соединения и он предполагает что соединение уже открыто И используется монопольно, т.е. не существует других процессов читающих данные в текущем соединении. Если только не задействована опция MARS - Multiple Active Result Sets.
По умолчанию при запросе включено полное кэширование, т.е. произойдет загрузка всех данных в память. Так что будьте осторожны с большими объемами данных.
Установка
Установить dapper легче всего с помощью NuGet. На данный момент на сайте указано, что рекомендуемая версия 1.7 и выпущена она 5 ноября 2011 года.
install-package dapper
Результат:
PM> install-package dapper Successfully installed 'Dapper 1.7'. Successfully added 'Dapper 1.7' to DapperConsumer.
И новая папка с файлом в проекте. Что достаточно необычно для распространения через NuGet. Зато все желающие могут почитать код и осознать, насколько много вещей еще можно изучать.
Или как уже было сказано выше, можете просто включить файл с реализацией Dapper в проект.
Подготовка
Проверять удобство работы будем на тех же классах и данных, что были в статье про BLToolkit. Однако все равно наверно лучше указать здесь все необходимые скрипты и классы.
В качестве эксперимента у нас будут выступать некоторые сводные данные по людям, и их адреса. Итак, скрипт для создания данных в базе:
set xact_abort on begin tran create table Address( AddressId uniqueidentifier primary key default newid() ,Country nvarchar(100) ,Region nvarchar(100) ,City nvarchar(100) ,Street nvarchar(100) ,Residence nvarchar(100) ) go create table Person( PersonId uniqueidentifier primary key default newid() ,Name nvarchar(100) not null ,Birth datetime ,Resident bit default 0 ,Gender char default 'm' ,Weight int ,Height decimal (3,2) ,AddressId uniqueidentifier constraint fk_address2person foreign key (AddressId) references Address(AddressId) ) go insert into Person values(newid(), 'Adam', '01-01-1980', 0, 'm', 80, 1.80, null) insert into Person values(newid(), 'Amy', '04-07-1986', 1, 'f', 52, 1.65, null) insert into Address values(newid(), 'Russia', 'N.Novgorod', 'N.Novgorod','Minina', '1A') insert into Address values(newid(), 'Russia', 'Msk', 'Msk', 'Lenina', '40 - 123') update Person set Person.AddressId = Address.AddressId from Address where city = 'Msk' and Name = 'Adam' update Person set Person.AddressId = Address.AddressId from Address where city <> 'Msk' and Name = 'Amy' commit
Чтобы два раза не вставать создали сразу все таблицы и заполнили их данными.
Сразу создадим классы, которые будем заполнять информацией из базы данных:
public class Person {
public Guid PersonId;
public string Name;
public DateTime Birth;
public bool Resident;
public string Gender;
public int Weight;
public decimal Height;
public Guid AddressId;
public Address Address;
}
public class Address {
public Guid AddressId;
public string Country;
public string Region;
public string City;
public string Street;
public string Residence;
}
Все, структуры данных готовы, можно приступать к экспериментам. Ах да, еще надо упомянуть про отдельное создание соединения с базой данных, так как Dapper сам не контролирует время жизни соединения. Создадим отдельный класс, в котором будем писать методы с различными вариантами работы фреймворка. Пусть этот класс называется Dapper, и конструктор будет создавать соединение с базой данных.
public class Dapper {
private readonly SqlConnection con;
public Dapper() {
var cs = new SqlConnectionStringBuilder {
InitialCatalog = "Test",
IntegratedSecurity = true,
DataSource = @"LETHIATHAN\LCF11CTP3"
};
con = new SqlConnection(cs.ConnectionString);
}
}
Думаю, что тут пояснять ничего не требуется уже. Далее шаблоном для каждого метода будет выступать следующая заготовка:
public void MethodName() {
con.Open();
// код будет здесь
con.Close();
}
Базовые возможности
У нас все готово к тому, чтобы начать работу с Dapper:
- Таблицы и данные готовы
- Классы в C# коде есть
- Dapper в проект включен
Простой запрос
Начнем с самого простого, что можно только себе представить. Сделаем выборку полной таблицы Person и заполним соответствующий класс. Для этого воспользуемся методом расширения со строгой типизацией Query, который принимает текст запроса в качестве обязательного параметра.
public void SimpleStrongTypedSelect() {
con.Open();
var sql = "Select * from Person";
var persons = con.Query(sql);
foreach (var p in persons)
Console.WriteLine("{0} {1} {2}", p.Name, p.Birth, p.Resident);
con.Close();
}
Открываем соединение с базой, пишем текст запроса, вызываем метод расширения для соединения, получаем готовые классы. Профит! Легко и быстро до безобразия. Однако метод Query не так прост, у него много необязательных параметров для тонкой настройки. Полная сигнатура выглядит так:
public static IEnumerable Query(
this IDbConnection cnn,
string sql,
dynamic param = null,
IDbTransaction transaction = null,
bool buffered = true,
int? commandTimeout = null,
CommandType? commandType = null
)
- Sql – текст запроса,
- Param – значения параметров, если они включены в запрос
- Transaction – включение в транзакцию
- Buffered – полное чтение результатов или по мере необходимости
- CommandTimeout – ограничение времени выполнения запроса
- CommandType – тип запроса: запрос, команда
Запрос с параметрами
Далее, немаловажно уметь передавать параметры в запрос. В запросе параметры следуют нотации MSSQL, т.е. начинаются со знака @. Параметры передаются в виде анонимного класса, и только в виде него. Имена полей должны соответствовать именам параметрам, причем регистр имеет значение. Это правило действует для всех параметров.
public void SimpleStrongTypedSelectWithParam() {
con.Open();
var sql = "Select * from Person where weight < @Weight";
var persons = con.Query(sql, new {Weight = 70});
foreach (var p in persons)
Console.WriteLine("{0} {1} {2}", p.Name, p.Birth, p.Resident);
con.Close();
}
Оказалось не сложно, верно?
Простой не типизированный запрос
Можно опустить строгую типизацию и получить результат в виде dynamic типа.
public void SimpleDynamicSelectWithParam() {
con.Open();
var sql = "Select * from Person where weight < @Weight";
var persons = con.Query(sql, new {Weight = 70});
foreach (var p in persons)
Console.WriteLine("{0} {1} {2}", p.Name, p.Birth, p.Resident);
con.Close();
}
Команда, возвращающая набор данных
Работа с хранимыми процедурами не сильно отличается от работы с простыми запросами. Необходимо указать имя процедуры и что это все же процедура, а не запрос.
public void ExecuteSimpleSelectCommand() {
con.Open();
var persons = con.Query("Person_GetOlderThan",
new {someOtherParam = 70, age = 30, weight = 60},
commandType: CommandType.StoredProcedure);
foreach (var p in persons)
Console.WriteLine("{0} {1} {2}", p.Name, p.Birth, p.Resident);
con.Close();
}
В данном случае использован строго типизированный запрос, и чтобы не писать множество null, я использовал именованный параметр для указания типа запроса. CommandType взят из пространства имен System.Data.
Как видите и здесь нет ничего сложного, подводных камней нет тоже никаких. Вообще пользоваться Dapper’ом оказалось весьма просто и легко.
Команды без результирующего набора данных
Естественно, что не все команды возвращают набор данных в результате своей работы. Для таких вызовов предназначен другой метод расширяющий интерфейс IDbConnetction – Execute.
Метод Execute в качестве обязательного параметра принимает текст запроса, опциональным является тип запроса. Вообще данная команда предназначена для любого типа операций, которые не возвращают данных.
public void ExecuteNonSelectCommand() {
con.Open();
var affectedRows = con.Execute("DumbProc",
commandType: CommandType.StoredProcedure);
Console.WriteLine(affectedRows);
con.Close();
}
Результатом операции будет целое число, говорящее о количестве измененных строк.
Полная сигнатура метода:
public static int Execute(
this IDbConnection cnn,
string sql,
dynamic param = null,
IDbTransaction transaction = null,
int? commandTimeout = null,
CommandType? commandType = null
)
На этом можно завершить рассказ о базовых возможностях фреймворка.
Продолжение следует
Нет обратных ссылок на эту запись.
