Обзор ServiceStack.OrmLite — micro-ORM для .NET05.05.2015 20:03
OrmLite — это дружелюбная micro-ORM с открытым исходным кодом и коммерческой лицензией (бесплатна для небольших проектов с ограничением в 10 таблиц). Входит в состав известного фреймворка ServiceStack (и обладает высокой производительностью — взгляните на benchmark от разработчиков Dapper). В данной статье мы рассмотрим основы работы с OrmLite в связке с SQL Server. Если сравнивать OrmLite и Entity Framework, то сразу бросается в глаза отсутствие контекста и отслеживания изменений (change tracking). И это далеко не единственные отличия.План статьи:
Заинтересовавшихся приглашаю под кат.Подготовка к работе. Code-first и database-first подходы
Устанавливаем OrmLite в наш проект:
Install-Package ServiceStack.OrmLite.SqlServer
OrmLite — в первую очередь code-first ORM. Тем не менее, есть возможность генерации POCO классов на основе существующей БД. С такой генерации мы и начнем, дополнительно установив T4 шаблоны:
Install-Package ServiceStack.OrmLite.T4
Если все прошло успешно, в проект будет добавлено 3 файла:
OrmLite.Core.ttincludeOrmLite.Poco.ttOrmLite.SP.tt
Добавим в в app/web.config строку подключения, заполним ConnectionStringName в файле OrmLite.Poco.tt (для единственной строки в app.config необязательно), жмем на файле Run Custom Tool и получаем сгенерированные POCO классы, например:
[Alias («Order»)]
[Schema («dbo»)]
public partial class Order: IHasId
{
[AutoIncrement]
public int Id { get; set; }
[Required]
public int Number { get; set; }
public string Text { get; set; }
public int? CustomerId { get; set; }
}
ОК, модель готова. Сделаем тестовый запрос к БД. Обращение к функционалу OrmLite происходит через экземпляр класса OrmLiteConnection, реализующего IDbConnection:
var dbFactory = new OrmLiteConnectionFactory (ConnectionString, SqlServerDialect.Provider);
using (IDbConnection db = dbFactory.Open ())
{
//db.AnyMethod…
}
Давайте запомним данный паттерн, далее он подразумевается при обращении к объекту db.Выберем все записи из таблицы Order с значением Number, большим 50:
List orders = db.Select(order => order.Number > 50);
Просто! А что внутри OrmLiteConnection?
Обычный SqlConnection:
public override IDbConnection CreateConnection (string connectionString, Dictionary options)
{
var isFullConnectionString = connectionString.Contains (»;»);
if (! isFullConnectionString)
{
var filePath = connectionString;
var filePathWithExt = filePath.ToLower ().EndsWith (».mdf»)
? filePath
: filePath + ».mdf»;
var fileName = Path.GetFileName (filePathWithExt);
var dbName = fileName.Substring (0, fileName.Length — ».mdf».Length);
connectionString = string.Format (
@«Data Source=.\SQLEXPRESS; AttachDbFilename={0}; Initial Catalog={1}; Integrated Security=True; User Instance=True;»,
filePathWithExt, dbName);
}
if (options!= null)
{
foreach (var option in options)
{
if (option.Key.ToLower () == «read only»)
{
if (option.Value.ToLower () == «true»)
{
connectionString += «Mode = Read Only;»;
}
continue;
}
connectionString += option.Key + »=» + option.Value + »;»;
}
}
return new SqlConnection (connectionString);
}
Перейдем к подходу code-first. Последовательно выполнить DROP и CREATE для нашей таблицы можно так:
db.DropAndCreateTable();
Необходимо отметить, что ранее сгенерированные с помощью Т4 POCO классы утратили часть информации о таблицах БД (длины строковых данных, внешние ключи и др.). OrmLite предоставляет всё необходимое для добавления такой информации в наши POCO (code-first oriented же!). В следующем примере создается некластеризованный индекс, указывается тип nvarchar (20) и создается внешний ключ — для полей Number, Text, и CustomerId соответственно:
[Schema («dbo»)]
public partial class Order: IHasId
{
[AutoIncrement]
public int Id { get; set; }
[Index (NonClustered = true)]
public int Number { get; set; }
[CustomField («NVARCHAR (20)»)]
public string Text { get; set; }
[ForeignKey (typeof (Customer))]
public int? CustomerId { get; set; }
}
В результате, при вызове db.CreateTable будет выполнен следующий SQL-запрос:
CREATE TABLE «dbo».«Order»
(
«Id» INTEGER PRIMARY KEY IDENTITY (1,1),
«Number» INTEGER NOT NULL,
«Text» NVARCHAR (20) NULL,
«CustomerId» INTEGER NULL,
CONSTRAINT «FK_dbo_Order_dbo_Customer_CustomerId» FOREIGN KEY («CustomerId») REFERENCES «dbo».«Customer» («Id»)
);
CREATE NONCLUSTERED INDEX idx_order_number ON «dbo».«Order» («Number» ASC);
Запросы к БД
В OrmLite доступны 2 основных способа для построения запросов к БД: лямбда-выражения и параметризованный SQL. Нижеприведенный код демонстрирует получение всех записей из таблицы Order c указанным CustomerId различными способами:1) лямбда-выражения и SqlExpression:
List orders = db.Select(order => order.CustomerId == customerId);
List orders = db.Select (db.From().Where (order => order.CustomerId == customerId));
2) параметризованный SQL:
List orders = db.SelectFmt(«CustomerId = {0}», customerId);
List orders = db.SelectFmt(«SELECT * FROM [Order] WHERE CustomerId = {0}», customerId);
Построение простых insert/update/delete запросов также не должно вызвать сложностей. Под спойлером несколько примеров из официальной документации.Простой CRUD
db.Update (new Person { Id = 1, FirstName = «Jimi», LastName = «Hendrix», Age = 27});
SQL:
UPDATE «Person» SET «FirstName» = 'Jimi', «LastName» = 'Hendrix', «Age» = 27 WHERE «Id» = 1
db.Insert (new Person { Id = 1, FirstName = «Jimi», LastName = «Hendrix», Age = 27 });
SQL:
INSERT INTO «Person» («Id», «FirstName», «LastName», «Age») VALUES (1,'Jimi','Hendrix',27)
db.Delete(p => p.Age == 27);
SQL:
DELETE FROM «Person» WHERE («Age» = 27)
Подробнее мы рассмотрим более интересные случаи.JOIN и navigation properties
Добавим к уже известной нам таблице Order связанную таблицу Customer:
class Customer
{
[AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
}
Для их внутреннего соединения (INNER JOIN) достаточно выполнить код:
List orders = db.Select(q => q.Join());
SQL:
SELECT «Order».«Id», «Order».«Details», «Order».«CustomerId»
FROM «Order» INNER JOIN «Customer» ON («Customer».«Id» = «Order».«CustomerId»)
Соответственно, для LEFT JOIN используется метод q.LeftJoin и т.д. Для получения данных из нескольких таблиц одновременно, способ №1 — произвести маппинг результирующей выборки на следующий класс OrderInfo:
class OrderInfo
{
public int OrderId { get; set; }
public string OrderDetails { get; set; }
public int? CustomerId { get; set; }
public string CustomerName { get; set; }
}
List info = db.Select(db.From().Join());
SQL:
SELECT «Order».«Id» as «OrderId», «Order».«Details» as «OrderDetails», «Order».«CustomerId», «Customer».«Name» as «CustomerName»
FROM «Order» INNER JOIN «Customer» ON («Customer».«Id» = «Order».«CustomerId»)
Единственное необходимое условие для класса OrderInfo — его свойства должны быть именованы по шаблону {TableName}{FieldName}.Способ №2 в стиле EF — воспользоваться navigation properties (в терминологии OrmLite они именуются «references»).Для этого добавим в класс Order следующее свойство:
[Reference]
Customer Customer { get; set; }
Данное свойство будет проигнорировано при любых запросах вида db.Select, что весьма удобно. Для загрузки связанных сущностей необходимо воспользоваться методом db.LoadSelect:
List orders = db.LoadSelect();
Assert.True (orders.All (order => order.Customer!= null));
SQL:
SELECT «Id», «Details», «CustomerId» FROM «Order»
SELECT «Id», «Name» FROM «Customer» WHERE «Id» IN (SELECT «CustomerId» FROM «Order»)
Аналогичным способом мы можем проинициализировать набор customer.Orders.Примечание: в приведенных примерах названия внешних ключей в связанных таблицах следовали шаблону {Parent}Id, что позволило OrmLite автоматически выбрать колонки, по которым производится соединение, тем самым упростив код. Альтернативный вариант — помечать внешние ключи атрибутом:
[References (typeof (Parent))]
public int? CustomerId { get; set; }
и явно задать колонки таблиц для соединения:
SqlExpression expression = db
.From()
.Join((order, customer) => order.CustomerId == customer.Id);
List orders = db.Select (expression);
Lazy и Async
Отложенные SELECT запросы реализованы через IEnumerable. Для *Lazy-методов не поддерживаются лаконичные запросы с помощью лямбда-выражений. Так SelectLazy предполагается ТОЛЬКО использование параметризованного SQL:
IEnumerable lazyQuery = db.SelectLazy(«UnitPrice > @UnitPrice», new { UnitPrice = 10 });
что при обходе перечисления аналогично следующему вызову:
db.Select(q => q.UnitPrice > 10);
Для ColumnLazy (возвращает список значений в колонке таблицы) дополнительно поддерживается SqlExpression:
IEnumerable lazyQuery = db.ColumnLazy(db.From().Where (x => x.UnitPrice > 10));
Не в пример lazy queries, большая часть OrmLite API имеет async-версии:
List employees = await db.SelectAsync(employee => employee.City == «London»);
Транзакции
Поддерживаются:
db.DropAndCreateTable();
Assert.IsTrue (db.Count() == 0);
using (IDbTransaction transaction = db.OpenTransaction ())
{
db.Insert (new Employee {Name = «First»});
transaction.Commit ();
}
Assert.IsTrue (db.Count() == 1);
using (IDbTransaction transaction = db.OpenTransaction ())
{
db.Insert (new Employee { Name = «Second» });
Assert.IsTrue (db.Count() == 2);
transaction.Rollback ();
}
Assert.IsTrue (db.Count() == 1);
Под капотом у db.OpenTransaction — вызов SqlConnection.BeginTransaction, поэтому на теме транзакций подробно останавливаться не будем.Операции над группой строк. Сравнение производительности OrmLite и Entity Framework
В дополнение к различным вариациям выполнения SELECT-запросов, OrmLite API предлагает 3 метода для модификации группы строк в БД:
InsertAll (IEnumerable)UpdateAll (IEnumerable)DeleteAll (IEnumerable)
Поведение OrmLite в данном случае ничем не отличается от поведения «взрослых» ORM, в первую очередь Entity Framework — мы получаем одну INSERT/UPDATE инструкцию на строку в БД. Хотя было бы интересно посмотреть на решение для INSERT с использованием Row Constructor, но не судьба. Очевидно, что разница в скорости выполнения образуется в основном за счет архитектурных особенностей самих фреймворков. А так ли велика эта разница? Ниже — замеры времени выполнения выборки, вставки и модификации 103 строк из таблицы Order средствами Entity Framework и OrmLite. Итерация повторяется 103 раз, и в таблице представлено суммарное время выполнения (в секундах). На каждой итерации генерируется новый набор случайных данных и происходит очистка таблицы. Код доступен на GitHub.Тестовое окружение
.NET 4.5MS SQL Server 2012Entity Framework 6.1.3 (Code First)OrmLite 4.0.38
Код
Entity Framework:
//select
context.Orders.AsNoTracking ().ToList ();
//insert
context.Orders.AddRange (orders);
context.SaveChanges ();
//update
context.SaveChanges ();
OrmLite:
//select
db.Select();
//insert
db.InsertAll (orders);
//update
db.UpdateAll (orders);
Время выполнения в секундах:
Select
Insert
Update
EF
4,0
282
220
OrmLite
7,3
94
88
OrmLite, ты серьезно? Select выполняется медленнее, чем у EF? После таких результатов было решено написать дополнительный тест, измеряющий скорость чтения 1 строки по Id.Код
Entity Framework:
context.Orders.AsNoTracking ().FirstOrDefault (order => order.Id == id);
OrmLite:
db.SingleById(id);
Время выполнения в секундах:
Select single by id
EF
1,9
OrmLite
1,0
На этот раз у OrmLite почти двухкратный перевес, и это неплохо. О причинах падения производительности при выгрузке большого количества строк из БД рассуждать пока не берусь. В большинстве сценариев OrmLite все таки быстрее EF, как было показано — в 2–3 раза.О измерениях и систематической погрешности
Так как измерялось общее время выполнения кода на клиенте, то время выполнения SQL-инструкции на сервере вносит в измерения систематическую погрешность. При использовании высоконагруженного «боевого» экземпляра SQL Server мы бы получили «примерно одинаковые» времена выполнения для обоих ORM. Очевидно, что сервер должен быть как можно более производительным и не загруженным для получения более точных результатов.
Заключение
В завершение статьи я бы хотел рассказать о своих (безусловно, субъективных) впечатлениях от работы с OrmLite, подытожить достоинства и недостатки этой micro-ORM.Плюсы:
легковесность, простота в настройке и развертывании;
простые CRUD-запросы действительно просто написать;
Минусы: вариативность построения запросов (то лямбды, то параметризованный SQL, то SqlExpression) для различных методов. Нет единого универсального синтаксиса, поддерживаемого любым методом из API;
слабая документированность: отсутствуют XML-комментарии к методам, официальная документация плохо структурирована и располагается на единственной веб-странице;
неясный API. Попробуйте сходу догадаться, чем отличаются вызовы db.Select, db.LoadSelect, db.Where? Или db.Insert и db.Save? Приедут ли из базы navigation properties при вызове db.Join?
Перечисленные пункты повышают «порог вхождения» в технологию. Отчасти, это лишает OrmLite одного из главных преимуществ micro-ORM — простоты и легкости использования по сравнению с «взрослыми» ORM. В целом, у меня сложилось нейтральное впечатление. OrmLite безусловно пригодна к использованию, но от коммерческого продукта ожидалось большее.
© Habrahabr.ru