Страницы

воскресенье, 29 апреля 2012 г.

Не используйте Fluent Migrator для интеграционных тестов.

  Библиотеки  миграций баз данных вроде Fluent Migrator и Migrator.Net прочно заняли место среди must have инструментов .net-разработчика. Простота использования подкупает настолько, что появляется соблазн использовать миграции и для создания полноценной тестовой базы данных. Но я не рекомендую так делать по нескольким причинам.

Проблемы.

  Как вы знаете, чтобы быть успешными тесты должны быть быстры. Молниеносно быстры. А прогон миграций на базе данных требует время. Трансформировать миграции в скрипты БД, выполнить их за N-обращений к базе данных – всё это включает в себя затраты самой базы данных, и временные пени за обращение к удалённому сервису. Понятно, что с ростом числа миграций ситуация будет становится всё хуже и хуже.

  А что если в процессе миграций мы заполняем базу данных некоторыми значениями по-умолчанию? Скажем, вносим справочник валют, данные организаций и т.д.? Получаем еще одну проблему: заполненная база данных может создавать неподходящий тестам контекст. В этом случае в коде тестов приходится править данные. Это как минимум непродуктивно. Но хуже того, создаёт ненужную связность между тестами и миграциями, что в свою очередь негативно сказывается на детерминированности и простоте поддержки тестов. Использование разных профилей для развёртывания и тестирования облегчит страдания, но полностью проблему не снимет. Создание единого контекста для всех тестов весьма трудоёмко, и требует особого способа написания тестов. Проще говоря единый контекст для всех тестов – это что-то из области вечного двигателя.  И конечно же, наличие данных лишь усугубляет ситуацию с временными потерями.

Решение.

  Нет необходимости прогонять миграции в каждом тесте. Мы тестируем не миграции, а некий другой функционал, связанный с базой данных. Всё, что необходимо для тестов – это наличии актуальной схемы базы данных. Если бы мы были счастливчиками, и работали с ruby on rails – мы могли использовать функционал schema dumping встроенный в библиотеку миграций. Увы, для fluent-migrator’а проблему придётся решать собственноручно: написать некий schema dumper, или кодогенератор на основе модели предметной области. Впрочем, если вы работаете с NHibernate – то можно воспользоваться встроенным классом SchemaExport для создания пустой базы для тестов. Аналогичные возможности есть и у Entity Framework’а и некоторых других средств доступа к данным.

  Последний момент – сами миграции тестировать так же необходимо. Я бы писал тест на каждый спринт (если у вас scrum) или любой другой отрезок времени. В таком тесте прогоняются  миграции от начала до конца с тестовым профилем.Тестовый профиль добавляет в тесты некоторое количество показательных данных в разные моменты миграций, проверяя их состояние после миграции.

  На мой взгляд, такой подход наиболее оптимален.

NHibernate transactions – обязательны при всех операциях с базой данных.

  Соблазн не использовать транзакции всегда велик. Вроде бы и ресурсов они расходуют, и риск взаимоблокировок всегда велик. По этим причинам многие отказываются от транзакций. Но не стоит следовать этой практике. И сейчас я расскажу как минимум о трёх причинах, по которым транзакции следует использовать при любом обращении к базе данных через NHibernate.

Почему надо использовать транзакции.

  1. РСУБД транзакционны по своей природе. Даже если вы не создаёте транзакцию явно, СУБД сделает это сама. При этом  транзакцию создаётся на каждый оператор (statement), и риск взаимоблокировок сохраняется. Создание транзакции не бесплатный процесс, и требует ресурсов. Отсюда вытекает простая логика: меньше транзакций – меньше расходуется ресурсов сервера.

  2. Согласованность данных. Думаю, никому не надо объяснять – это основа основ работы с БД. Без использования транзакций в БД может попасть всякий мусор, влияние которого на поведение системы порой бывает трудно предсказать. Чтобы особенно болезненно если ссылочная целостность проверяется только на уровне приложения (например связи many-to-any в NHibernate). Всегда проще откатить транзакцию, нежели разбираться с битыми данными.

  3. Кеш второго уровня. NHibernate за кадром делает много работы, чтобы поддерживать кэш в актуальном состоянии. Отсутствие транзакций в коде заставляет NHibernate игнорировать кэш второго уровня, и всегда делать запрос в базу. Причина в неизвестности результата DML-операций: при обновлении базы без транзакции в случае ошибки база оказывается в недетерминированном состоянии. Поэтому NHibernate вынужден инвалидировать данные в кэше, и направлять запрос в базу. В этом легко убедиться. Посмотрим код:

[Test]
public void Query_cache_invalidated()
{
    using (var session = factory.OpenSession())
    //using (var tx = session.BeginTransaction())
    {
        session.SaveOrUpdate(new Cat {Name = "Bazil"});
        session.Flush();
        //  tx.Commit();
    }
    using (var session = factory.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var reloadedPeter = session.Get<Cat>(50L);
        tx.Commit();
    }
    Console.WriteLine("HitCount: {0}", factory.Statistics.SecondLevelCacheHitCount);
}   

И вот что мы увидим в логах:


image


  Но стоит только снять комментарии с кода транзакции, и картина сразу меняется в лучшую сторону:


image


  Как видно из логов, в момент фиксации транзакции NHibernate кладёт в кэш изменённые данные, и в следующий раз даже не понадобится делать запрос в БД для загрузки. Поиск по ключу в кэше намного быстрее, чем запрос к БД.


  Полагаю, мне удалось убедить вас в необходимости использования транзакций. А теперь посмотрим на еще одну вещь, о которой стоит знать при использовании NHibernate с транзакциями.


Некоторые последствия.


  При использовании транзакций NHibernate старается обеспечивать согласованность данных. Давай рассмотри такой код:

[Test]
public void Sudden_insert_happened_on_read_operation2()
{
    using(var session = _sessionManager.OpenSession())
    using(var tx = session.BeginTransaction())
    {
        session.SaveOrUpdate(new Cat {Name = "Bazil"});
        tx.Commit();
    }
    using (var session = _sessionManager.OpenSession())
    using (var tx = session.BeginTransaction())
    {
        var peter = new Cat { Name = "Peter" };
        session.Persist(peter);
        var reloadedPeter = session.Get<Cat>(50L);
        reloadedPeter.Name = "Johnny";
        var totalCats = session
            .QueryOver<Cat>()
            .Where(x => x.Name != string.Empty)
            .RowCount();
    }
}

  В одной сессии создали сущность, и сохранили ее в базу, в другой добавили в сессию одну сущность, и изменили одно из свойств другой. Затем выполнили запрос. А теперь посмотрим, что же было в логах NHibernate?


image


    Я не делал ни Session.Flush(), ни tx.Commit(), но Nhibernate выполнил Insert и Update. Причина в том, что NHibernate понимает, что сделанные в сессии изменения могут повлиять на результат запроса. Поэтому предварительно приводит БД в согласованное с сессией состояние. Если убрать транзакцию, то предварительного обновления происходить не будет (всё равно БД будет в недетерминированном состоянии).


  Если еще не используете транзакции для работы с NHibernate – то самое время начать. Подмигивающая рожица

суббота, 21 апреля 2012 г.

Решаем проблему с SQLite x86 / x64.

  Уже давно использую SQLite совместно с NHibernate для быстрого тестирования приложений, и так же давно мучает проблема совместимости x86/x64. Открывая проект на старой машине приходилось нудно переустанавливать подходящую сборку SQLite. В простом варианте это можно принудительно выставить во всех проектах platform target = x86, но мне такой вариант принципиально не нравился. В общем, сегодня решил эту проблему в относительно приличном виде, используя nuget и немного магии msbuild. Итак, решение по шагам.

1. Используя nuget добавляем в проект ссылку на System.Data.SQLite.MSIL. Эта сборка понадобиться только на этапе компиляции. Лично я предпочитаю делать это через Package Manager Console:

install-package System.Data.SQLite.MSIL

2. Открыть в проекте файл packages.config, и дописать в него пакеты System.Data.SQLite.x64 и System.Data.SQLite.x64. Добавлять через nuget не стоит, т.к. при этом nuget заменит ссылку на msil-сборку ссылкой на платформо-специфичную сборку:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="System.Data.SQLite.MSIL" version="1.0.80.0" />
  <!--Добавить вручную-->
  <package id="System.Data.SQLite.x64" version="1.0.80.0" />
  <package id="System.Data.SQLite.x86" version="1.0.80.0" />
</packages>

3. Настроить восстановление сборок в проекте из контекстного меню файла проекта:


image


4.  Теперь пора немного пошаманить в файле проекта. Для начала из контекстного меню проекта выбираем меню Unload Project:


image


а затем открываем его на редактирование:


image


5. Идейно простая часть – определить тип операционной системы, и переписать поверх MSIL сборок платформо-специфичные. Пришлось немного повозиться с переменными окружения, т.к. они отличаются для разных операционных систем. Но теме не менее, проблему удалось успешно победить, добавив в конец файла проекта немного кода:

<PropertyGroup>
  <SQLitePath Condition="$(PROCESSOR_ARCHITEW6432) == 'AMD64'">..\packages\System.Data.SQLite.X64.1.0.80.0\lib\net40\</SQLitePath>
  <SQLitePath Condition="$(PROCESSOR_ARCHITEW6432) == 'X86'">..\packages\System.Data.SQLite.X86.1.0.80.0\lib\net40\</SQLitePath>
  <SQLitePath Condition="$(PROCESSOR_ARCHITEW6432) == '' AND $(PROCESSOR_ARCHITECTURE) == 'AMD64'">..\packages\System.Data.SQLite.X64.1.0.80.0\lib\net40\</SQLitePath>
  <SQLitePath Condition="$(PROCESSOR_ARCHITEW6432) == '' AND $(PROCESSOR_ARCHITECTURE) == 'X86'">..\packages\System.Data.SQLite.X86.1.0.80.0\lib\net40\</SQLitePath>
</PropertyGroup>
<Target Name="AfterBuild">
  <Exec Command="xcopy /s /y &quot;$(SQLitePath)*.*&quot; &quot;$(TargetDir)&quot;"/>
</Target>

6. Снова загрузить проект, и запустить build. После этого всё должно работать.


  В завершении хотелось бы сказать несколько замечаний. Все эти сложности нужны только если бы компилирует проект под AnyCPU, под конкретную платформу это всё не надо. Плохо, что путь к сборкам жёстко зашит. Его можно было бы выдернуть из ссылок, но на текущей момент мне не хотелось этим заниматься (программировать на xml всегда неудобно).


  Удачной разработки с SQLite. Улыбка

вторник, 17 апреля 2012 г.

Абстракция – предмет простой.

  В прошлый раз я упоминал про закон дырявых абстракций, и в процессе написания блога пришло осознание, что такое абстракция на самом деле. Давайте посмотрим на определение, которое предлагает википедия:

Абстра́кция  - отвлечение в процессе познания от несущественных сторон, свойств, связей объекта (предмета или явления) с целью выделения их существенных, закономерных признаков.

  Определение из области философии очень хорошо соотносится с моим пониманием абстракций в коде. И оно лучше отражает смысл абстракции, нежели определение из области программирования. Итак, если сложить закон дырявых абстракций, и определение можно сделать вывод:

Настоящая абстракция чрезвычайно проста.

Что это значит? Давайте посмотрим на примере. Допустим у нас есть такой класс:

public class Person 
{
    public Name Name [get;set;}
    
    public Sex Sex {get;set;}
    
    public Person Mother {get; set;}
    
    public Person Father {get; set;}
    
    public IEnumerable<Person> Children {get; set;}
    
    public DateTime DateOfBirth {get; set;}
    
    public Passport Passport {get;set;}
    
    public Passport ForeignPassport {get;set;}
    
    public DriverLicense DriverLicense {get;set}
}

  Вроде бы не слишком сложно, да? На первый взгляд Person можно было бы назвать абстракцией “человек”. Но на самом деле это не абстракция. Во-первых, мы пытались смоделировать объект реального мира, впихнув все свойства, которые к нему относятся: семейно положение, гражданство, водительские права, личная информация. Проблема в том, что реального мира не существует в мире (хороших) программ, они живут отношениями клиент-сервер (на уровне классов), При этом клиента мало интересуют потроха сервера.


  А что же такое настоящая абстракция? Извольте осмотреть пару примеров:

public interface IDriver 
{
    DriverLicencse {get;}
}
public interface ICitizen 
{
    public Passport Passport {get;}
    
    public Passport ForeignPassport {get;}
}

  И это действительно абстракции, с которыми могут оперировать другие классы. Главное отличие Person от IDriver – полное отсутствие нерелевантных деталей! Только самая суть, которая отличает гражданина от человека. Такой подход к пониманию абстракций есть суть ISP. Хорошая абстракция вряд ли может содержать более пяти членов (а идеальная -один). Но есть и хорошая новость – всё вышесказанное не означает, что надо отказаться от Person, и поделить его на более мелкие классы. Person - реализация не обязана быть простой, и может включать в себя реализацию нескольких абстракций. Просто не обманывайте себя, называя абстракцией всё, что включает в себя слова interface и abstract. Это лишь технические детали, элементы языка программирования, не более.


  Теперь, вооружившись новым (или не очень) взглядом на абстракции, внимательно посмотрите на свой код. Не увлекайтесь “абстракционизмом”, это не так красиво, как автору хочется верить. Улыбка

суббота, 7 апреля 2012 г.

Старый недобрый Repository.

  Уже долгие годы шаблон репозиторий будоражит умы опытных (и не очень) программистов, но актуален ли он сейчас? Позвольте объясню.

В чём проблема?

  Еще лет 10 назад хороших  технологий доступа к данным просто не существовало. Обращение к БД приосходило в виде хранимых процедур, или зашитых в код приложения запросов, или некой обёртки над ними. Весь этот ад SQL-кода был весьма некстати в коде логики доменных объектов, поэтому репозиторий и был рождён на свет. Но что сейчас даёт нам репозиторий? Когда я задаю этот вопрос, то чаще всего слышу эпический  ответ::реализацию репозитория легко заменить другой реализацией. Хотя один раз удалось услышать честный ответ: по привычке.

  Звучит красиво, и очень привлекательно для апологетов абстракционизма. Даже если не рассматривать реальное положение вещей, когда 99% проектов никода не меняют базу данных и метод доступа к ней, а обобщить немного теории становится понятно – утверждение о лёгкой замене реализации репозитория невозможно в принципе. Каждый раз мы строим репозиторий в оглядке на конкретный слой доступа к данным. Наш код может не знать о конкретной реализации, но всегда полагается на неё. Для примера. NHibernate LINQ не поддерживает left join через DefaultIfEmpty, а EF – поддерживает. Кто-нибудь может мне сказать, как легко (строчкой в конфигурации DI-контейнера) заменить EF на NHibernate в таком случае?

  Мы не можем положиться на абстракцию репозитория. Суровая правда жизни в том, что доступ к данным чрезвычайно сложен по своей природе. Мы по-прежнему должны решать когда, сколько и какие данные вытащить из базы, когда их можно положить в базу, управлять транзакциями и т.д.  Если мы считаем репозиторий коллекцией объектов в памяти, то как мы можем игнорировать обрыв соединения, взаимоблокировки и БД, задержки сети? Ответ шире самого вопроса, и проистекает из закона дырявых абстракций:

Все нетривильные абстракции в некоторой степени дырявы.

  В истинности я убедился на собственном горьком опыте. Несмотря на суровую правду жизни, попытки найти серебряную пулю продолжаются, Новую эру среди них открыл LINQ, и почтенный интерфейс IQueryable<T>, с аналогичным  результатом

Как жить дальше?

  Так, если мы не можем полагаться на абстракции, то стоит ли вообще от них отказаться? И да и нет. Во-первых, надо отказаться от рассмотрения слоя доступа к данным как абстракции. Смотрите на него как на API, и оценивайте с точки зрения удобства использования. Если доступ к данным осуществляется на голом ADO.NET – спрятать мелкие детали доступа к данным за неким самописным API. Но если у вас в руках уже хороший иструмент, вроде NHibernate или EF – репозиторий вам не нужен (и даже вреден). API хорошего фреймворка доступа к данным остаётся честным с вами, не пытаясь спрятать тех процессов, которые происходят под ним, но существенно ускоряет разработку.

  Напоследок могу посоветовать пару сообщений в тему от Ayende, и закон дырявых абстракций на языке Пушкина.