Júnior Oliveira

C Sharp; Delphi; ASP .NET; PHP; Javascript;

NHibernate

25 Jun 2014 csharp, nhibernate, singleton

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.

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:

 1 <?xml version='1.0' encoding='utf-8'?>
 2 <hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
 3 
 4   <session-factory>
 5     <property name="connection.provider">
 6       NHibernate.Connection.DriverConnectionProvider, NHibernate
 7     </property>
 8     <property name="connection.connection_string_name">
 9       ConnectionString
10     </property>
11     <property name="dialect">
12       NHibernate.Dialect.MySQLDialect
13     </property>
14     <property name="current_session_context_class">
15       web
16     </property>
17   </session-factory>  
18 
19 </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.

 1 public class Account : Entity<int> {
 2 
 3     public virtual string Email { get; protected set; }
 4     public virtual string Password { get; protected set; }
 5     public virtual string Name { get; set; }
 6     public virtual string Surname { get; set; }
 7 
 8     protected Account() { }
 9 
10     public Account(string email, string password) {
11         Email = email;
12         Password = password;
13     }
14 
15     public virtual void NewPassword() {
16         Password = Guid.NewGuid().ToString().Substring(0, 5);
17     }
18 }

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.

 1 <?xml version="1.0" encoding="utf-8" ?>
 2 <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
 3                    assembly="Dolrath"
 4                    namespace="Dolrath.Domain.Entities">
 5 
 6   <class name="Account" table="Accounts">
 7     <id name="Id" type="Int32">
 8       <generator class="hilo"/>
 9     </id>
10 
11     <property name="Email" type="string" length="100" not-null="true" />
12     <property name="Password" type="string" length="100" not-null="true" />
13     <property name="Name" type="string" length="100" not-null="false" />
14     <property name="Surname" type="string" length="100" not-null="false" />
15 
16   </class>
17 </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.

 1 <class name="Order" table="tbOrder">
 2   <id name="Id" column="OrderId">
 3     <generator class="identity"/>
 4   </id>
 5 
 6   <bag name="Items" inverse="true" cascade="all" access="field.camelcase-underscore">
 7     <key column="OrderId" />
 8     <one-to-many class="OrderItem" />
 9   </bag>    
10 
11 </class>

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

 1 <class name="Character" table="Characters">
 2   <id name="Id" type="Int32">
 3     <generator class="hilo"/>
 4   </id>
 5 
 6   <set name="Inventory" lazy="true" access="field.camelcase-underscore">
 7     <key column="CharacterId"/>
 8     <many-to-many class="Item" column="ItemId" />
 9   </set>
10 
11 </class>

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

1 <class name="Character" table="Characters">
2   <id name="Id" type="Int32">
3     <generator class="hilo"/>
4   </id>
5 
6   <many-to-one name="Account" unique="true" column="AccountId" />
7 </class>

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

1 <class name="Account" table="Accounts">
2   <id name="Id" type="Int32">
3     <generator class="hilo"/>
4   </id>
5 
6   <one-to-one name="Character" class="Character" cascade="all" />
7 
8 </class>

Outras configurações

Discriminator

 1 <class name="Character" table="Characters">
 2   <id name="Id" type="Int32">
 3     <generator class="hilo"/>
 4   </id>
 5 
 6   <discriminator column="Discriminator" not-null="true" type="string" />
 7 
 8   <subclass name="Humano" discriminator-value="Humano"></subclass>
 9   <subclass name="Elf" discriminator-value="Elf"></subclass>
10   <subclass name="Draconiano" discriminator-value="Draconiano"></subclass>
11   <subclass name="Metamorfo" discriminator-value="Metamorfo"></subclass>
12 
13 </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.

 1 public sealed class SessionFactoryFactory {
 2 
 3     private readonly ISessionFactory _sessionFactory;
 4     public ISessionFactory CurrentSessionFactory { get { return _sessionFactory; } }
 5 
 6     private static volatile SessionFactoryFactory _instance;
 7     private static readonly object SyncRoot = new Object();
 8 
 9     private SessionFactoryFactory() {
10         var cfg = new Configuration();
11         cfg.Configure();
12         cfg.AddAssembly(typeof(SessionFactoryFactory).Assembly);
13 
14         _sessionFactory = cfg.BuildSessionFactory();
15     }
16 
17     public static SessionFactoryFactory Instance {
18         get {
19             if (_instance == null) {
20                 lock (SyncRoot) {
21                     if (_instance == null)
22                         _instance = new SessionFactoryFactory();
23                 }
24             }
25 
26             return _instance;
27         }
28     }
29 }

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.

 1 [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
 2 public class TransactionPerRequestAttribute : ActionFilterAttribute {
 3 
 4     private readonly IsolationLevel _isolationLevel;
 5     private ISession _session;
 6     private ITransaction _transaction;
 7 
 8     public TransactionPerRequestAttribute(IsolationLevel isolationLevel = IsolationLevel.ReadUncommitted) {
 9         _isolationLevel = isolationLevel;
10     }
11 
12     public override void OnActionExecuting(ActionExecutingContext filterContext) {
13         _session = IoCContainer.Get<ISession>();
14         _transaction = _session.BeginTransaction(_isolationLevel);
15     }
16 
17     public override void OnActionExecuted(ActionExecutedContext filterContext) {
18         if (filterContext.Exception == null)
19             _transaction.Commit();
20         else
21             _transaction.Rollback();
22     }
23 }

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.

 1 public class FilterConfig {
 2 
 3     public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
 4         filters.Add(new HandleErrorAttribute());
 5         filters.Add(new NHibernateSessionAttribute());
 6     }
 7 }
 8 
 9 [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
10 public class NHibernateSessionAttribute : ActionFilterAttribute {
11 
12     public override void OnActionExecuting(ActionExecutingContext filterContext) {
13         if (!CurrentSessionContext.HasBind(SessionFactoryFactory.Instance.CurrentSessionFactory)) {
14             CurrentSessionContext.Bind(IoCContainer.Get<ISession>());
15         }
16     }
17 
18     public override void OnActionExecuted(ActionExecutedContext filterContext) {
19         var currentSession = CurrentSessionContext.Unbind(SessionFactoryFactory.Instance.CurrentSessionFactory);
20         if (currentSession == null) return;
21         currentSession.Close();
22         currentSession.Dispose();
23     }
24 }

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.

 1 public class AccountController : BaseController {
 2 
 3     private readonly ISession _session;
 4 
 5     public AccountController(ISession session) {
 6         _session = session;
 7     }
 8 
 9     [HttpGet]
10     public ActionResult Create() {
11         return View();
12     }
13 
14     [HttpPost]
15     [TransactionPerRequest]
16     public ActionResult Create(CreateViewModel viewModel) {
17         if (!ModelState.IsValid) return Error("Existem campos para preencher.", View(viewModel));
18 
19         var account = new Account(viewModel.Email, viewModel.Password);
20         _session.Save(account);
21 
22         return Information("Sua conta foi criada com sucesso.", RedirectToAction("Index", "Home"));
23     }
24 }

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

Criteria Queries

 1 public class AccountGetByIdQuery : IQuery<Account> {
 2 
 3     private readonly ISession _session;
 4 
 5     public int Id { get; set; }
 6 
 7     public AccountGetByIdQuery(ISession session) {
 8         _session = session;
 9     }
10 
11     public Account GetResult() {
12         return _session.CreateCriteria<Account>()
13                         .Add(Restrictions.Eq("Id", Id))
14                         .UniqueResult<Account>();
15     }
16 }

QueryOver Queries

 1 public class AccountGetByEmailQuery : IQuery<Account> {
 2 
 3     private readonly ISession _session;
 4 
 5     public string Email { get; set; }
 6 
 7     public AccountGetByEmailQuery(ISession session) {
 8         _session = session;
 9     }
10 
11     public virtual Account GetResult() {
12         return _session.QueryOver<Account>()
13                         .Where(c => c.Email == Email)
14                         .SingleOrDefault<Account>();
15     }
16 }

Native SQL

 1 public class ItemResultGetAllQuery : IQuery<IEnumerable<ItemResult>> {
 2 
 3     private readonly ISession _session;
 4 
 5     public ItemResultGetAllQuery(ISession session) {
 6         _session = session;
 7     }
 8 
 9     public IEnumerable<ItemResult> GetResult() {
10         return _session.CreateSQLQuery("SELECT Id, Name, Type FROM Items")
11                         .SetResultTransformer(Transformers.AliasToBean<ItemResult>())
12                         .List<ItemResult>();
13     }
14 }

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