Violet Tape некоторые мысли о разработке на платформе .Net

14Ноя/110

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
    )

 

На этом можно завершить рассказ о базовых возможностях фреймворка.

 

Продолжение следует

Метки записи: , Оставить комментарий
Комментарии (0) Пинги (0)

Пока нет комментариев.


Оставить комментарий


Нет обратных ссылок на эту запись.