Обзор ServiceStack.OrmLite — micro-ORM для .NET

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