NHibernate

Olá pessoal, depois de um tempo parado, vamos falar um pouco de NHibernate. Este post estava pronto a bastante tempo pode ser que alguma coisa não seja mais aplicado nas novas versão do NHibernate.

Instalando

A forma mais simples de instalar o NHibernate é usando o NuGet, abaixo o comando para instalar o NHibernate. No exemplo estou usando a versão 3.3.3.4000 para o .NET Framework 4.0.

NuGet
Install-Package NHibernate

Configurando

Adicione um arquivo XML na solução chamado hibernate.cfg.xml. Este arquivo irá conter as especificações de conexão a base de dados e outras configurações da conexão. Abaixo segue uma configuração padrão para o banco MySQL e uma aplicação Web:

hibernate.cfg.xml
<?xml version='1.0' encoding='utf-8'?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">

  <session-factory>
    <property name="connection.provider">
      NHibernate.Connection.DriverConnectionProvider, NHibernate
    </property>
    <property name="connection.connection_string_name">
      ConnectionString
    </property>
    <property name="dialect">
      NHibernate.Dialect.MySQLDialect
    </property>
    <property name="current_session_context_class">
      web
    </property>
  </session-factory>  

</hibernate-configuration>

Link com a lista de propriedades de configuração do NHibernate.

Domínio

O domínio abaixo é uma representação de uma conta de jogador, retirada do projeto Dolrath e adaptada para ficar mais didática.

Account.cs
public class Account : Entity<int> {

    public virtual string Email { get; protected set; }
    public virtual string Password { get; protected set; }
    public virtual string Name { get; set; }
    public virtual string Surname { get; set; }

    protected Account() { }

    public Account(string email, string password) {
        Email = email;
        Password = password;
    }

    public virtual void NewPassword() {
        Password = Guid.NewGuid().ToString().Substring(0, 5);
    }
}

Alguns pontos importantes ao notar o domínio é a presença do modificador virtual nos métodos e propriedades, este modificador possibilita o NHibernate criar um proxy sobre a nossa classe e que não existe nenhuma presença de infraestrutura de acesso a dados.

Mapeamento

O mapeamento abaixo esta escrito em XML, existem outras formas de mapeamento como por exemplo atributos ou por código usando NHibernate Fluent. Uma vantagens que eu vejo ao usar XML é que você não precisa recompilar a aplicação para alterar alguma regra no mapeamento e a forma de utilização ao meu ver é mais simples e didática.

Abaixo esta o mapeamento da nossa classe de domínio acima.

XML
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
                   assembly="Dolrath"
                   namespace="Dolrath.Domain.Entities">

  <class name="Account" table="Accounts">
    <id name="Id" type="Int32">
      <generator class="hilo"/>
    </id>

    <property name="Email" type="string" length="100" not-null="true" />
    <property name="Password" type="string" length="100" not-null="true" />
    <property name="Name" type="string" length="100" not-null="false" />
    <property name="Surname" type="string" length="100" not-null="false" />

  </class>
</hibernate-mapping>

Uma das configurações mais importantes deste mapeamento é o gerador da chave primária, neste exemplo estou usando hilo, ele é usado para gerar a chave sem precisar ir até a base de dados não quebramos a Unit Of Work.

Click no elemento abaixo para maiores detalhes de seus atributos.

hibernate-mapping class id property

Relações

Abaixo as duas formas de fazer uma relação de N para N (many-to-many).

bag: É usado quando temos uma situação onde a coleção pode haver valores duplicados.

XML
<class name="Order" table="tbOrder">
  <id name="Id" column="OrderId">
    <generator class="identity"/>
  </id>

  <bag name="Items" inverse="true" cascade="all" access="field.camelcase-underscore">
    <key column="OrderId" />
    <one-to-many class="OrderItem" />
  </bag>    

</class>

set: É usado quando temos uma situação onde a coleção não pode haver valores duplicados.

XML
<class name="Character" table="Characters">
  <id name="Id" type="Int32">
    <generator class="hilo"/>
  </id>

  <set name="Inventory" lazy="true" access="field.camelcase-underscore">
    <key column="CharacterId"/>
    <many-to-many class="Item" column="ItemId" />
  </set>

</class>

Relação de N para 1 (many-to-one).

XML
<class name="Character" table="Characters">
  <id name="Id" type="Int32">
    <generator class="hilo"/>
  </id>

  <many-to-one name="Account" unique="true" column="AccountId" />
</class>

Relação de 1 para 1 (one-to-one).

XML
<class name="Account" table="Accounts">
  <id name="Id" type="Int32">
    <generator class="hilo"/>
  </id>

  <one-to-one name="Character" class="Character" cascade="all" />
</class>

Outras configurações

Discriminator

XML
<class name="Character" table="Characters">
  <id name="Id" type="Int32">
    <generator class="hilo"/>
  </id>

  <discriminator column="Discriminator" not-null="true"	type="string" />

  <subclass name="Humano" discriminator-value="Humano"></subclass>
  <subclass name="Elf" discriminator-value="Elf"></subclass>
  <subclass name="Draconiano" discriminator-value="Draconiano"></subclass>
  <subclass name="Metamorfo" discriminator-value="Metamorfo"></subclass>

</class>

Session Factory

A Session Factory é responsável por construir as sessões. Quando uma aplicação é iniciada deve-se carregar todas as configurações do NHibernate e mapeamentos para criar a Session Factory, a criação da Session Factory é um processo demorado e deve ser feito apenas uma vez na aplicação. O código abaixo é uma implementação do padrão Singleton e garante que a Session Factory será criada apenas uma vez na aplicação.

SessionFactoryFactory.cs
public sealed class SessionFactoryFactory {

    private readonly ISessionFactory _sessionFactory;
    public ISessionFactory CurrentSessionFactory { get { return _sessionFactory; } }

    private static volatile SessionFactoryFactory _instance;
    private static readonly object SyncRoot = new Object();

    private SessionFactoryFactory() {
        var cfg = new Configuration();
        cfg.Configure();
        cfg.AddAssembly(typeof(SessionFactoryFactory).Assembly);

        _sessionFactory = cfg.BuildSessionFactory();
    }

    public static SessionFactoryFactory Instance {
        get {
            if (_instance == null) {
                lock (SyncRoot) {
                    if (_instance == null)
                        _instance = new SessionFactoryFactory();
                }
            }

            return _instance;
        }
    }
}

Action Filter - Transaction Per Request

Exitem várias formas de criar uma session em uma aplicação web a forma que irei adotar é bem comum, ela cria uma session por cada requisição feita.

Simplesmente vamos criar um action filter que irá interceptar a “entrada” e a “saída” da nossa action da controller.

Na entrada vamos pegar a session usando o contêiner de dependência e iniciar uma transação setando o nível de isolamento da mesma. Lista dos níveis de isolamentos.

Na saída vamos verificar se houve um erro na requisição. Caso não houve um erro a transação será feito um commit da transação, senão será feito um rollback na mesma.

No final a Session será destruída.

TransactionPerRequestAttribute.cs
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class TransactionPerRequestAttribute : ActionFilterAttribute {

    private readonly IsolationLevel _isolationLevel;
    private ISession _session;
    private ITransaction _transaction;

    public TransactionPerRequestAttribute(IsolationLevel isolationLevel = IsolationLevel.ReadUncommitted) {
        _isolationLevel = isolationLevel;
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        _session = IoCContainer.Get<ISession>();
        _transaction = _session.BeginTransaction(_isolationLevel);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        if (filterContext.Exception == null)
            _transaction.Commit();
        else
            _transaction.Rollback();
    }
}

Action Filter - NHibernate Session

Este actino filter será utilizado para poder acoplar e desacoplar a session do NHibernate no contexto atual, para ficar mais simples a utilização vamos registrar ele no global filter assim não precisamos decorar a nossa action com esta anotação.

FilterConfig.cs
public class FilterConfig {

    public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new NHibernateSessionAttribute());
    }
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class NHibernateSessionAttribute : ActionFilterAttribute {

    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        if (!CurrentSessionContext.HasBind(SessionFactoryFactory.Instance.CurrentSessionFactory)) {
            CurrentSessionContext.Bind(IoCContainer.Get<ISession>());
        }
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        var currentSession = CurrentSessionContext.Unbind(SessionFactoryFactory.Instance.CurrentSessionFactory);
        if (currentSession == null) return;
        currentSession.Close();
        currentSession.Dispose();
    }
}

Controller

Notem que estou recebendo a session no construtor da controller, quem irá fornecer esta session será o contêiner de dependência. A action create do método post esta decorada com a anotação TransactionPerRequest, isto indica que esta action terá um controle de transação.

AccountController.cs
public class AccountController : BaseController {

    private readonly ISession _session;

    public AccountController(ISession session) {
        _session = session;
    }

    [HttpGet]
    public ActionResult Create() {
        return View();
    }

    [HttpPost]
    [TransactionPerRequest]
    public ActionResult Create(CreateViewModel viewModel) {
        if (!ModelState.IsValid) return Error("Existem campos para preencher.", View(viewModel));

        var account = new Account(viewModel.Email, viewModel.Password);
        _session.Save(account);

        return Information("Sua conta foi criada com sucesso.", RedirectToAction("Index", "Home"));
    }
}

Existem várias formas de buscar dados usando NHibernate segue alguns abaixo:

Criteria Queries

AccountGetByIdQuery.cs
public class AccountGetByIdQuery : IQuery<Account> {

    private readonly ISession _session;

    public int Id { get; set; }

    public AccountGetByIdQuery(ISession session) {
        _session = session;
    }

    public Account GetResult() {
        return _session.CreateCriteria<Account>()
                        .Add(Restrictions.Eq("Id", Id))
                        .UniqueResult<Account>();
    }
}

QueryOver Queries

AccountGetByEmailQuery.cs
public class AccountGetByEmailQuery : IQuery<Account> {

    private readonly ISession _session;

    public string Email { get; set; }

    public AccountGetByEmailQuery(ISession session) {
        _session = session;
    }

    public virtual Account GetResult() {
        return _session.QueryOver<Account>()
                        .Where(c => c.Email == Email)
                        .SingleOrDefault<Account>();
    }
}

Native SQL

ItemResultGetAllQuery.cs
public class ItemResultGetAllQuery : IQuery<IEnumerable<ItemResult>> {

    private readonly ISession _session;

    public ItemResultGetAllQuery(ISession session) {
        _session = session;
    }

    public IEnumerable<ItemResult> GetResult() {
        return _session.CreateSQLQuery("SELECT Id, Name, Type FROM Items")
                        .SetResultTransformer(Transformers.AliasToBean<ItemResult>())
                        .List<ItemResult>();
    }
}

Obrigado pela visita e espero que tenha gostado, até a próxima.

Comentários