NHibernate – Mapeando valores default como null: IUserType, IParameterizedType

Fala pessoal!

Faz um tempo que não escrevo (sempre o mesmo papinho), mas baterei o incrível recorde de 2 posts técnicos num mesmo mês com esta postagem, então minha motivação está lá em cima! Na verdade, este post servirá como guia para algumas coisas do trabalho, e isso acaba sendo um “motivador” e tanto..

Apesar de ser a primeira vez que o tema será abordado aqui, acredito que todos que lêem o blog (ou mesmo os que aterrisam aqui esporadicamente) já tenham utilizado ou pelo menos ouvido falar do NHibernate. O NHibernate é um framework de mapeamento objeto relacional para .NET, portado do excelente Hibernate (Java), que facilita sua vida em gravar e recuperar objetos do banco de dados. Acaba ajudando também a manter uma OO mínima, já que sem isso o trabalho fica muito mais doloroso, o que também é muito positivo.. O NH permite mapear diversos tipos de colunas, trabalhar com chaves geradas automaticamente e mais uma infinidade de coisas que nem uma centena de posts conseguiriam cobrir.

O fato é que, nesta semana, caímos num problema com campos que possuem valores default. Contextualizando: possuímos uma grande aplicação que foi desenvolvida originalmente em .NET 1.1 e hoje encontra-se e .NET 2.0. Na época do 1.1, tipos “nullables” não passavam de um sonho e as pessoas arrumavam soluções criativas para contornar a questão: criando structs próprios, utilizando valores default, etc. As soluções não eram exatamente o sonho de nenhum desenvolvedor, mas, dada as restrições do framework, funcionavam bem desde que fosse mantida certa disciplina. Neste projeto a solução utilizada foi a de valores default. Alguns campos são inicializados com extremos de seu tipo (ex.: Int32.MinValue) enquanto outros assumem valores “mágicos” (ex.: -1, 03/03/0003). No momento da persistência, estes valores são tratados pela camada apropriada (aquela mesma citada por problemas de performance em outro post) e transformados em DbNull (ou vice-versa).

Tudo funcionava bem e ninguém pensava em refatorar este comportamento nesse exato momento, até que tivemos que construir uma segunda implementação da camada DAO para armazenamento de um banco de dados local. Como o banco local não poderia ser SQL Server (ao qual nossa DAL está bastante amarrada), a opção de persistência foi o NHibernate. Mapeamos as entidades e, depois de alguns refactors no código para deixá-lo mais “OO”, as coisas pareciam funcionar bem. Isto, é claro, até o momento que foi percebido que as colunas que antes eram salvas como null e recuperadas como valores default não estavam mais comportando-se dessa maneira, o que acabava deixando a lógica de negócio inconsistente em alguns pontos. Neste momento, tínhamos duas opções:

  1. Partir pra tipos Nullable. Isto consistiria em: a) alterar todas as entidades e a camada de persistência para trabalharem com tipos nullable, b) alterar as classes de negócio para não esperarem os valores, c) torcer pra não ter esquecido nada e não ter quebrado nenhuma lógica do sistema com isso (não temos testes unitários cobrindo o código);
  2. Encontrar uma maneira de fazer o NHibernate entender que alguns valores deveriam ser mapeados como nulo sem ter que escrever muito código pra isso.

Pela minha “imparcial” exposição das opções vocês devem concluir que optamos pelo segundo caminho. Pode parecer gambiarra à primeira vista, mas garanto que foi uma decisão baseada numa boa dose de pragmatismo: garantia de testes, falta de recursos e prazos apertados não são coisas muito fáceis de serem conciliadas num projeto realmente grande e complexo.

Feita a decisão, partiu-se para a pesquisa. Alguém da equipe citou a possibilidade de UserTypes. Já havia ouvido falar de UserTypes no NHibernate mas nunca havia olhado para uma implementação, muito menos criado um na prática. Depois de uma procura rápida, fiquei mais tranqüilo com os exemplos que encontrei: bastaria implementar uma interface (IUserType) cujos vários métodos já estavam implementados com um dos sites encontrados e modificar somente 2 métodos. Moleza! O problema é que os benditos UserTypes, que já tinham até sido prototipados para o sistema, exigiriam a criação de uma classe para cada tipo e valor default. Assim, teríamos que ter uma classe para inteiros com valor default -1, shorts com valor default -32768, outra para shorts com valor default -1, e assim por diante. Nada prático! Os desenvolvedores teriam que criar uma nova classe toda vez que descobrissem um novo tipo com valor default.

Com o problema de precisar criar uma dezena de classes para um problema tão simples, voltei pro Google. Dessa vez a pesquisa rendeu uma segunda interface que, quando implementada, permite setar parâmetros definidos via mapeamento que são passados para a classe. Voila! Com um UserType parametrizável, basta passarmos o tipo .NET que queremos mapear, qual tipo do banco de dados que ele corresponde (um Enum do próprio NH) e dizer o valor default que deve ser comparado para termos uma solução genérica, elegante e simples para toda a equipe de desenvolvimento: eles só precisarão alterar os arquivos de mapeamento minimamente para informar o valor default de cada campo.

Para mostrar como implementar o conceito, montei um exemplo com duas classes Person: NullablePerson e RegularPerson. A idéia com as duas classes é poder comparar exatamente o comportamente dos tipos nullable com a solução que utilizaremos. Isto também facilita um pouco a criação dos testes unitários. O código das classes é ridiculamente simples e ficou assim:

7 public class RegularPerson

8 {

9 public int ID { get; set; }

10 public string Name { get; set; }

11 public short Age { get; set; }

12 public DateTime RegisterDate { get; set; }

13 }

14

15 public class NullablePerson

16 {

17 public int ID { get; set; }

18 public string Name { get; set; }

19 public short? Age { get; set; }

20 public DateTime? RegisterDate { get; set; }

21 }

 

Percebam que a única diferença entre as classes é a utilização do símbolo ? depois dos tipos, que equivale à utilização de Nullable<>. A tabela do banco de dados que armazenará estas entidades (mesma tabela para ambas entidades) é igualmente simples:

CREATE TABLE [dbo].[PERSON](
 [ID] [int] IDENTITY(1,1) PRIMARY KEY,
 [NAME] [varchar](100) NULL,
 [AGE] [smallint] NULL,
 [DT_REGISTER] [datetime] NULL
)

Feito isto, basta o mapeamento do NHibernate para que as entidades possam ser salvas no banco de dados:

2 <hibernate-mapping xmlns=urn:nhibernate-mapping-2.2 assembly=NHibernateEntity namespace=NHibernateEntity>

3 <class name=NullablePerson table=PERSON lazy=false>

4 <id name=ID column=ID type=Int32 unsaved-value=0>

5 <generator class=identity />

6 </id>

7 <property name=Name column= NAME type=String length=100/>

8 <property name=Age column= AGE type=Int16 />

9 <property name=RegisterDate column= DT_REGISTER type=DateTime />

10 </class>

11 </hibernate-mapping>

Notem que este é o arquivo de mapeamento da classe NullablePerson. Com ela possui propriedades nullable, só com isto já funciona maravilhosamente bem. Para nossa classe RegularPerson, entretanto, precisamos criar uma estrutura que permita que valores default sejam transformados em null e vice-versa. Para isto, vamos criar uma classe chamada NullDefaultValueType, que implementará duas interfaces: IUserType e IParameterizedType. A primeira é responsável por permitir que nossa classe possa ser mapeada no arquivo do NHibernate como um tipo de qualquer coluna. A segunda, por sua vez, permite que parâmetros sejam mapeados no arquivo de configuração para serem passados para esta classe. Apesar da descrição parecer complexa, a solução é bastante lógica e linear, acompanhe na sequência.

Abaixo segue a declaração da classe com as implementações relevantes da interface IUserType. Os demais métodos foram omitidos por não adiconarem comportamento relevante ao nosso caso.

14 public class NullDefaultValueType : IUserType, IParameterizedType

15 {

16 private Type Type { get; set; }

17 private DbType DbType { get; set; }

18 private string DefaultValue { get; set; }

19

20 public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner)

21 {

22 object value = rs.GetValue(rs.GetOrdinal(names[0]));

23 if (value == DBNull.Value)

24 {

25 return Convert.ChangeType(this.DefaultValue, this.Type);

26 }

27 else

28 {

29 return value;

30 }

31 }

32

33 public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index)

34 {

35 if (Convert.ChangeType(this.DefaultValue, this.Type).Equals(value))

36 {

37 ((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value;

38 }

39 else

40 {

41 ((IDataParameter)cmd.Parameters[index]).Value = value;

42 }

43 }

44 }

O que precisa ser notado neste trecho de código são as propriedades, que posteriormente serão inicializadas pela implementação da outra interface, e os dois métodos: o que retorna o valor para o NHibernate (NullSafeGet) verifica se o valor do banco de dados é nulo e, se este for o caso, retorna o valor default configurado no lugar. O método de set (NullSafeSet), por sua vez, faz o contrário: verifica se o valor recebido foi o valor default e salva como nulo no banco de dados. Isto permitiria, por exemplo, que objetos NullablePerson e RegularPerson interagissem com o mesmo objeto sem problema de compatibilidade: o que para um é nulo, para o outro é valor default, de maneira transparente e independente do banco de dados.

A implementação da segunda interface (IParameterizedType) é mais simples: basta implementarmos um métodos que receberá os parâmetros e setar nossas propriedades. Ficou assim:

176 public void SetParameterValues(System.Collections.IDictionary parameters)

177 {

178 this.Type = Type.GetType(parameters[“Type”].ToString(), true);

179 this.DbType = (DbType)Enum.Parse(typeof(DbType), parameters[“DbType”].ToString());

180 this.DefaultValue = parameters[“DefaultValue”].ToString();

181 }

Vejam que a recuperação dos parâmetros é bastante simples. No método estamos pegando os 3 parâmetros necessários para nossa classe funcionar corretamente e setando em propriedades privadas de nosso UserType. Depois disto, basta alterar o mapeamento das propriedades de RegularPerson para algo assim:

1 <?xml version=1.0 encoding=utf-8 ?>

2 <hibernate-mapping xmlns=urn:nhibernate-mapping-2.2 assembly=NHibernateEntity namespace=NHibernateEntity>

3 <class name=RegularPerson table=PERSON lazy=false>

4 <id name=ID column=ID type=Int32 unsaved-value=0>

5 <generator class=identity />

6 </id>

7 <property name=Name column= NAME type=String length=100/>

8 <property name=Age column= AGE>

9 <type name=NHibernateEntity.NullDefaultValueType, NHibernateEntity>

10 <param name=Type>System.Int16</param>

11 <param name=DbType>Int16</param>

12 <param name=DefaultValue>-32768</param>

13 </type>

14 </property>

15 <property name=RegisterDate column= DT_REGISTER>

16 <type name=NHibernateEntity.NullDefaultValueType, NHibernateEntity>

17 <param name=Type>System.DateTime</param>

18 <param name=DbType>DateTime</param>

19 <param name=DefaultValue>03/03/0003</param>

20 </type>

21 </property>

22 </class>

23 </hibernate-mapping>

Apesar do mapeamento de NullablePerson permanecer exatamente o mesmo, vemos que as propriedades que requerem o tratamento de valores default ganharam alguns nodos a mais. Apesar deste pequeno acréscimo, o mapeamento para o restante da equipe foi facilitado ao máximo com apenas uma nova classe, totalmente parametrizável. Além disso, instâncias de NullablePerson e RegularPerson podem ser utilizadas em paralelo, sem problema. Isto é comprovado pelo teste unitário abaixo:

94 [Test]

95 public void PersistRegularPersonWithDefaultNullValue()

96 {

97 RegularPerson p = new RegularPerson();

98 p.Name = “Maria”;

99 p.Age = Int16.MinValue;

100 p.RegisterDate = new DateTime(3, 3, 3);

101

102 session.Save(p);

103

104 // recupera uma NullablePerson pra ter certeza que realmente ficou nulo

105 NullablePerson p2 = session.Get<NullablePerson>(p.ID);

106

107 Assert.IsNull(p2.Age);

108 Assert.IsNull(p2.RegisterDate);

109 }

E é isso! Com este post vimos como os UserTypes no NHibernate podem nos ajudar a contornar situações aparentemente não suportadas pela ferramenta, além de poderem ser parametrizados pelos próprios arquivos de mapeamento. Para quem quiser o código completo junto com o conjunto de testes unitários, coloquei os projetos no RapidShare, basta clicar aqui para baixar.

Um abraço, bons códigos e até a próxima!

Filipe

Anúncios

0 Responses to “NHibernate – Mapeando valores default como null: IUserType, IParameterizedType”



  1. Deixe um comentário

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s




Perfil

Olá! Meu nome é José Filipe e sou o autor deste blog. Trabalho como Gerente de Desenvolvimento da todo! BPO, onde sou responsável pela manutenção e evolução de pessoas, processos e sistemas desenvolvidos em diferentes tecnologias. Atualmente curso uma especialização em Engenharia de Software pela PUCPR, em Curitiba, mas moro em Florianópolis, onde me graduei em Sistemas de Informação pela UFSC. Possuo o título de MCP e com base nas experiências do dia-a-dia espero trazer ao blog assuntos interessantes sobre arquitetura e desenvolvimento .NET..

Arquivos

Páginas

maio 2009
S T Q Q S S D
« abr   jun »
 123
45678910
11121314151617
18192021222324
25262728293031

%d blogueiros gostam disto: