OrmLite — это дружелюбная micro-ORM с открытым исходным кодом и коммерческой лицензией (бесплатна для небольших проектов с ограничением в 10 таблиц). Входит в состав известного фреймворка ServiceStack (и обладает высокой производительностью — взгляните на benchmark от разработчиков Dapper). В данной статье мы рассмотрим основы работы с OrmLite в связке с SQL Server. Если сравнивать OrmLite и Entity Framework, то сразу бросается в глаза отсутствие контекста и отслеживания изменений (change tracking). И это далеко не единственные отличия.
План статьи:
Заинтересовавшихся приглашаю под кат.
Устанавливаем OrmLite в наш проект:
OrmLite — в первую очередь code-first ORM. Тем не менее, есть возможность генерации POCO классов на основе существующей БД. С такой генерации мы и начнем, дополнительно установив T4 шаблоны:
Если все прошло успешно, в проект будет добавлено 3 файла:
Добавим в в app/web.config строку подключения, заполним ConnectionStringName в файле OrmLite.Poco.tt (для единственной строки в app.config необязательно), жмем на файле Run Custom Tool и получаем сгенерированные POCO классы, например:
ОК, модель готова. Сделаем тестовый запрос к БД. Обращение к функционалу OrmLite происходит через экземпляр класса OrmLiteConnection, реализующего IDbConnection:
Давайте запомним данный паттерн, далее он подразумевается при обращении к объекту db.
Выберем все записи из таблицы Order с значением Number, большим 50:
Просто!
Перейдем к подходу code-first. Последовательно выполнить DROP и CREATE для нашей таблицы можно так:
Необходимо отметить, что ранее сгенерированные с помощью Т4 POCO классы утратили часть информации о таблицах БД (длины строковых данных, внешние ключи и др.). OrmLite предоставляет всё необходимое для добавления такой информации в наши POCO (code-first oriented же!). В следующем примере создается некластеризованный индекс, указывается тип nvarchar(20) и создается внешний ключ — для полей Number, Text, и CustomerId соответственно:
В результате, при вызове db.CreateTable будет выполнен следующий SQL-запрос:
В OrmLite доступны 2 основных способа для построения запросов к БД: лямбда-выражения и параметризованный SQL. Нижеприведенный код демонстрирует получение всех записей из таблицы Order c указанным CustomerId различными способами:
1) лямбда-выражения и SqlExpression:
2) параметризованный SQL:
Построение простых insert/update/delete запросов также не должно вызвать сложностей. Под спойлером несколько примеров из официальной документации.
Подробнее мы рассмотрим более интересные случаи.
Добавим к уже известной нам таблице Order связанную таблицу Customer:
Для их внутреннего соединения (INNER JOIN) достаточно выполнить код:
SQL:
Соответственно, для LEFT JOIN используется метод q.LeftJoin и т.д. Для получения данных из нескольких таблиц одновременно, способ №1 — произвести маппинг результирующей выборки на следующий класс OrderInfo:
SQL:
Единственное необходимое условие для класса OrderInfo — его свойства должны быть именованы по шаблону {TableName}{FieldName}.
Способ №2 в стиле EF — воспользоваться navigation properties (в терминологии OrmLite они именуются «references»).
Для этого добавим в класс Order следующее свойство:
Данное свойство будет проигнорировано при любых запросах вида db.Select, что весьма удобно. Для загрузки связанных сущностей необходимо воспользоваться методом db.LoadSelect:
SQL:
Аналогичным способом мы можем проинициализировать набор customer.Orders.
Примечание: в приведенных примерах названия внешних ключей в связанных таблицах следовали шаблону {Parent}Id, что позволило OrmLite автоматически выбрать колонки, по которым производится соединение, тем самым упростив код. Альтернативный вариант — помечать внешние ключи атрибутом:
и явно задать колонки таблиц для соединения:
Отложенные SELECT запросы реализованы через IEnumerable. Для *Lazy-методов не поддерживаются лаконичные запросы с помощью лямбда-выражений. Так SelectLazy предполагается ТОЛЬКО использование параметризованного SQL:
что при обходе перечисления аналогично следующему вызову:
Для ColumnLazy (возвращает список значений в колонке таблицы) дополнительно поддерживается SqlExpression:
Не в пример lazy queries, большая часть OrmLite API имеет async-версии:
Поддерживаются:
Под капотом у db.OpenTransaction — вызов SqlConnection.BeginTransaction, поэтому на теме транзакций подробно останавливаться не будем.
В дополнение к различным вариациям выполнения SELECT-запросов, OrmLite API предлагает 3 метода для модификации группы строк в БД:
Поведение OrmLite в данном случае ничем не отличается от поведения «взрослых» ORM, в первую очередь Entity Framework — мы получаем одну INSERT/UPDATE инструкцию на строку в БД. Хотя было бы интересно посмотреть на решение для INSERT с использованием Row Constructor, но не судьба. Очевидно, что разница в скорости выполнения образуется в основном за счет архитектурных особенностей самих фреймворков. А так ли велика эта разница?
Ниже — замеры времени выполнения выборки, вставки и модификации 103 строк из таблицы Order средствами Entity Framework и OrmLite. Итерация повторяется 103 раз, и в таблице представлено суммарное время выполнения (в секундах). На каждой итерации генерируется новый набор случайных данных и происходит очистка таблицы. Код доступен на GitHub.
Время выполнения в секундах:
OrmLite, ты серьезно? Select выполняется медленнее, чем у EF? После таких результатов было решено написать дополнительный тест, измеряющий скорость чтения 1 строки по Id.
Время выполнения в секундах:
На этот раз у OrmLite почти двухкратный перевес, и это неплохо. О причинах падения производительности при выгрузке большого количества строк из БД рассуждать пока не берусь. В большинстве сценариев OrmLite все таки быстрее EF, как было показано — в 2-3 раза.
В завершение статьи я бы хотел рассказать о своих (безусловно, субъективных) впечатлениях от работы с OrmLite, подытожить достоинства и недостатки этой micro-ORM.
Плюсы:
Минусы:
Перечисленные пункты повышают «порог вхождения» в технологию. Отчасти, это лишает OrmLite одного из главных преимуществ micro-ORM — простоты и легкости использования по сравнению с «взрослыми» ORM. В целом, у меня сложилось нейтральное впечатление. OrmLite безусловно пригодна к использованию, но от коммерческого продукта ожидалось большее.
План статьи:
- Подготовка к работе. Code-first и database-first подходы
- Запросы к БД
- JOIN и navigation properties
- Lazy и Async
- Транзакции
- Сравнение производительности OrmLite и Entity Framework
- Заключение
Заинтересовавшихся приглашаю под кат.
Подготовка к работе. Code-first и database-first подходы
Устанавливаем OrmLite в наш проект:
Install-Package ServiceStack.OrmLite.SqlServer
OrmLite — в первую очередь code-first ORM. Тем не менее, есть возможность генерации POCO классов на основе существующей БД. С такой генерации мы и начнем, дополнительно установив T4 шаблоны:
Install-Package ServiceStack.OrmLite.T4
Если все прошло успешно, в проект будет добавлено 3 файла:
OrmLite.Core.ttinclude
OrmLite.Poco.tt
OrmLite.SP.tt
Добавим в в app/web.config строку подключения, заполним ConnectionStringName в файле OrmLite.Poco.tt (для единственной строки в app.config необязательно), жмем на файле Run Custom Tool и получаем сгенерированные POCO классы, например:
[Alias("Order")]
[Schema("dbo")]
public partial class Order : IHasId<int>
{
[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<Order> orders = db.Select<Order>(order => order.Number > 50);
Просто!
А что внутри OrmLiteConnection?
Обычный SqlConnection:
public override IDbConnection CreateConnection(string connectionString, Dictionary<string, string> 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<Order>();
Необходимо отметить, что ранее сгенерированные с помощью Т4 POCO классы утратили часть информации о таблицах БД (длины строковых данных, внешние ключи и др.). OrmLite предоставляет всё необходимое для добавления такой информации в наши POCO (code-first oriented же!). В следующем примере создается некластеризованный индекс, указывается тип nvarchar(20) и создается внешний ключ — для полей Number, Text, и CustomerId соответственно:
[Schema("dbo")]
public partial class Order : IHasId<int>
{
[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<Order> orders = db.Select<Order>(order => order.CustomerId == customerId);
List<Order> orders = db.Select(db.From<Order>().Where(order => order.CustomerId == customerId));
2) параметризованный SQL:
List<Order> orders = db.SelectFmt<Order>("CustomerId = {0}", customerId);
List<Order> orders = db.SelectFmt<Order>("SELECT * FROM [Order] WHERE CustomerId = {0}", customerId);
Построение простых insert/update/delete запросов также не должно вызвать сложностей. Под спойлером несколько примеров из официальной документации.
Простой CRUD
SQL:
SQL:
SQL:
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<Person>(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<Order> orders = db.Select<Order>(q => q.Join<Customer>());
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<OrderInfo> info = db.Select<OrderInfo>(db.From<Order>().Join<Customer>());
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<Order> orders = db.LoadSelect<Order>();
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<Order> expression = db
.From<Order>()
.Join<Order, Customer>((order, customer) => order.CustomerId == customer.Id);
List<Order> orders = db.Select(expression);
Lazy и Async
Отложенные SELECT запросы реализованы через IEnumerable. Для *Lazy-методов не поддерживаются лаконичные запросы с помощью лямбда-выражений. Так SelectLazy предполагается ТОЛЬКО использование параметризованного SQL:
IEnumerable<Product> lazyQuery = db.SelectLazy<Product>("UnitPrice > @UnitPrice", new { UnitPrice = 10 });
что при обходе перечисления аналогично следующему вызову:
db.Select<Product>(q => q.UnitPrice > 10);
Для ColumnLazy (возвращает список значений в колонке таблицы) дополнительно поддерживается SqlExpression:
IEnumerable<string> lazyQuery = db.ColumnLazy<string>(db.From<Product>().Where(x => x.UnitPrice > 10));
Не в пример lazy queries, большая часть OrmLite API имеет async-версии:
List<Employee> employees = await db.SelectAsync<Employee>(employee => employee.City == "London");
Транзакции
Поддерживаются:
db.DropAndCreateTable<Employee>();
Assert.IsTrue(db.Count<Employee>() == 0);
using (IDbTransaction transaction = db.OpenTransaction())
{
db.Insert(new Employee {Name = "First"});
transaction.Commit();
}
Assert.IsTrue(db.Count<Employee>() == 1);
using (IDbTransaction transaction = db.OpenTransaction())
{
db.Insert(new Employee { Name = "Second" });
Assert.IsTrue(db.Count<Employee>() == 2);
transaction.Rollback();
}
Assert.IsTrue(db.Count<Employee>() == 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.5
MS SQL Server 2012
Entity Framework 6.1.3 (Code First)
OrmLite 4.0.38
MS SQL Server 2012
Entity Framework 6.1.3 (Code First)
OrmLite 4.0.38
Код
Entity Framework:
OrmLite:
//select
context.Orders.AsNoTracking().ToList();
//insert
context.Orders.AddRange(orders);
context.SaveChanges();
//update
context.SaveChanges();
OrmLite:
//select
db.Select<Order>();
//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:
OrmLite:
context.Orders.AsNoTracking().FirstOrDefault(order => order.Id == id);
OrmLite:
db.SingleById<Order>(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 безусловно пригодна к использованию, но от коммерческого продукта ожидалось большее.