Alex Belozerov Blog

IT, бизнес, саморазвитие

NHibernate и Fluent NHibernate

Сейчас работаю на проекте, где активно юзается NHibernate и библиотека Fluent NHibernate для реализации data access layer. До этого никогда с Hibernate не работал, незначительное количество времени применял ADO.NET, и начиная с 2008 все проекты шли на LINQ-to-SQL и немного Entity Framework. Было очень интересно пощупать альтернативу вездесущим ORM от Microsoft, узнать ее плюсы, минусы, наиболее подходящие области применения.

По порядку.

Что такое NHibernate? Это ORM-система, позволяющая создавать абстрагированный от конкретной базы данных объектно-ориентированный data access layer. Связи между data objects и таблицами базы данных определяются через mappings, прописываемые программистом в XML-файле.

Fluent NHibernate - надстройка над NHibernate, позволяющая прописывать mappings вместо XML через .NET-классы посредством лямбда-выражений, т.е. более удобным и защищенным от ошибок способом.

Далее везде буду описывать именно NHibernate в сочетании с Fluent NHibernate.

Как описать data object?

Первое что бросается в глаза после LINQ-to-SQL и EF - отсутствие “из коробки” автоматической генерации классов, каждую сущность программисту необходимо создавать вручную. С другой стороны, объем кода, приходящийся на каждый из классов, где-то на порядок меньше кода, сгенерированного майкрософтовскими генераторами. Если к примеру в data object поля называются так же, как столбцы в таблице в базе данных и нет внешних ключей, то mappings вообще не нужны, достаточно описать класс вот так:

public class Address
{
public virtual int Id { get; set; }

[NotNull, NotEmpty]
public virtual string AddressType { get; set; }

[NotNull, NotEmpty]
public virtual string Address1 { get; set; }
public virtual string Address2 { get; set; }

[NotNull, NotEmpty]
public virtual string City { get; set; }

[NotNull, NotEmpty]
public virtual string State { get; set; }

[NotNull, NotEmpty, Pattern(@"^((\d{5}-\d{4})|(\d{5})|([A-Z]\d[A-Z]\s\d[A-Z]\d))$")]
public virtual string PostalCode { get; set; }

[NotNull, NotEmpty, Pattern(@"USA|Canada")]
public virtual string Country { get; set; }
public virtual float Latitude { get; set; }
public virtual float Longitude { get; set; }

public virtual double Distance { get; set; }
}

Тут же можно настроить и валидацию, например для поля PostalCode.

Что если есть связи с другими объектами? Как выглядят mappings?

Пример:
public class RetailerMaps : IAutoMappingOverride<Retailer>
{
public void Override(AutoMapping<Retailer> mapping)
{
mapping.Table("Retailers");
mapping.References(x => x.Store, "StoreId");
mapping.References(x => x.Type, "RetailerTypeId");
mapping.HasMany(x => x.Alerts).Table("RetailerAlerts").KeyColumn("RetailerId").Cascade.All();
}
}

Класс Retailer имеет поля Store и Type, являющиеся ссылками на объекты из другой таблицы (связь один-ко-многим), а также коллекцию Alerts (много-к-одному)

Как вытащить объекты из базы данных?

Я использовал библиотеку SharpArch, позволяющая организовать доступ к данным посредством паттерна Repository: например, для того чтобы поработать с объектами типа Address, нужно использовать AddressRepository, для других объектов соответственно будут другие репозитории: StoreRepository, EmployeeRepository… Repository предоставляет базовые операции для получения и сохранения объектов: GetAll(), GetById(), Update() и так далее.

Ок, с единичными объектами всё понятно - работаем через Repository, но как быть если нам необходимо сделать выборку по каким-нибудь критериям?

Вот здесь NHibernate мне понравился множеством вариантов реализации данной задачи, разной степени простоты, гибкости и скорости :-) Отсортировано от более высокоуровневых вещей к более низкоуровневым:
  1. LINQ to NHibernate
  2. var result = from x in Session.Linq<Product>()
    where x.ShouldDisplay == visibility && x.Alignments.Any(z => alignmentIds.Contains(z.Id))
    orderby x.SortOrder
    select x
    Теоретически самый простой и удобный способ, но возникают проблемы: в версии NHibernate 2.x транслятор с LINQ реализован не полностью, многие операции тупо не работают, в версии 3.0, вышедшей буквально месяц назад, вроде доделали, но у меня не было возможности потестировать, на проекте используется версия 2.1.2; И та же бодяга, что и с LINQ-to-SQL - при сложных запросах генерится большой и медленно работающий SQL, для критичных к скорости частей приложения LINQ to NHibernate не подходит.

  3. HQL
  4. Hibernate Query Language - универсальный независимый от базы данных язык запросов

    var result = sess.CreateQuery("select cust
    from Product prod,
    Store store
    inner join store.Customers cust
    where prod.Name = 'widget'
    and store.Location.Name in ( 'Melbourne', 'Sydney' )
    and prod = all elements(cust.CurrentOrder.LineItems)");
    Очень похож на SQL, но удобнее, запросы получаются короче, плюс независимость от базы данных, плюс генерит неплохой SQL-код. Недостатки: ошибки в запросах обнаруживаются только во время выполнения

  5. ICriteria API
  6. IList cats = sess.CreateCriteria(typeof(Cat))
    .Add( Expression.In( "Name", new String[] { "Fritz", "Izi", "Pk" } ) )
    .Add( Expression.Disjunction()
    .Add( Expression.IsNull("Age") )
    .Add( Expression.Eq("Age", 0 ) )
    .Add( Expression.Eq("Age", 1 ) )
    .Add( Expression.Eq("Age", 2 ) )
    ) )
    .List();
    Более низкоуровневая вещь, позволяет поэтапное построение выражений, менее подвержен ошибкам, чем HQL, независимость от базы данных. Недостатки: Сложнее HQL и менее гибкий, есть непонятные ограничения. К примеру, мне не удалось сделать банальную проекцию так, как я хотел: выбрать определенные поля с одной таблицы и соединить с коллекцией элементов из другой.

  7. Native SQL
  8. Самый низкий уровень, но всё равно выше чем чистый SQL.

    var result = sess.CreateSQLQuery("SELECT c.ID, NAME, BIRTHDATE, DOG_ID, D_ID, D_NAME FROM CATS c, DOGS d WHERE c.DOG_ID = d.D_ID")
    .AddEntity("cat", typeof(Cat))
    .AddJoin("cat.Dog");
    Преимущества: максимальный уровень контроля над получающимся SQL кодом, максимально развязаны руки для оптимизации, возможность преобразования полученных строк из базы в data objects, в том числе сложные преобразования с коллекциями и связями (всё как у более высокоуровневых методов). Недостатки: сложно, отлов ошибок только во время выполнения, здесь уже имеем зависимость от конкретной базы данных. Я наиболее плотно работал именно с Native SQL, там много интересных возможностей, в ближайшее время планирую написать отдельную статью про него.


Если что-то хотите узнать подробнее, пишите в комментариях в блоге!

Archived comments

alex
to Corba
1) Репозиторий не пишется, он генерится
2) Он-то как раз к сожалению не все команды LINQ поддерживает, поэтому часто приходится переходить на более низкий уровень
3) Да, проблем нет, просто такая особенность. Собственно в LINQ-to-SQL довольно сложно менять/писать свой mapping, если возникла необходимость отойти от автоматически сгенерированного кода, а в NHibernate код классов аккурантый и понятный, это понравилось
Roman2311
У меня основной вопрос - чем же лучше NHibernate по сравнению с EntityFramework or LINQ ?
Corba
1) Паттерн репозиторий. Я обычно пишу такой класс:
Repository<\T> where T :new()
{
public IList<\T> GetAll()
public T GetById();
public Update(T entity);
etc.
}
что избавляет от написания репозитория на каждую сущность
2) Для организации запросов есть еще Linq2Nhibernate, зависший в первом релизе и кажется больше не развивающийся, но в принципе он со своими обязанностями справляется
3) Отсутствие генератора data objects конечно неприятно, особенно когда таблиц в БД дохрена, но в принципе можно сгенерить в студии классы для EF и выкинуть из них ненужные фиксапы и т.п.
Corba
Этот комментарий был удален автором.
Corba
Этот комментарий был удален автором.

Comments