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.

1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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.

1
2
3
4
5
6
7
8
9
10
11
<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.

1
2
3
4
5
6
7
8
9
10
11
<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).

1
2
3
4
5
6
7
<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).

1
2
3
4
5
6
7
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
<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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.