O NeoMatrix Tech está de casa nova!

Você deverá ser redirecionado em 6 segundos. Se não, visite:
http://www.leonelfraga.com/neomatrixtech
e atualize seus favoritos.

Aviso IMPORTANTÍSSIMO!

Aviso aos navegantes:

O NeoMatrix Tech mudou de casa!!!

A partir de agora, acessem pelo novo endereço:

http://www.leonelfraga.com/neomatrixtech

Ué... mas é só o domínio mudou de lugar?

R: Na verdade, não é bem assim hehe. Este domínio que você acessa agora aponta para um blog hospedado no Blogger, enquanto no novo, aponta para um blog na plataforma Wordpress, hospedagem própria, muito mais rápida e com um layout mais agradável de ler ;)

Não vou fechar este domínio igual ao que eu fiz com o NM Light (que já está 100% na nova plataforma). Talvez beeeeeeem depois eu faça isso.

Todos os posts daqui se encontram lá, e novos posts serão colocados somente no novo endereço.
A única coisa que não consegui importar foram os comentários. Mas em breve vai ter um post contando sobre a epopéia que foi migrar o NeoMatrix Tech!

Somente vou fechar a área de comentários daqui. Caso queiram comentar, favor ver o post correspondente no "Novo NeoMatrix Tech" e comentem por lá. É bem melhor! (pena que os permalinks "amigáveis para SEO" não funcionam lá, dá erro 404 e não consigo fazer a configuração funcionar. E olha que eu já vi vários artigos falando desse assunto :( ).

Quem assina o feed, já está lendo o conteúdo do novo NeoMatrix Tech!

domingo, 1 de março de 2009

Simple PIM – Exemplo Nova Classe de Conexão – Parte 2: Estrutura da App e Biblioteca de Classes

Tendo feito a modelagem do nosso Simple PIM e fazendo o Provider Factory do Firebird funcionar, vamos fazer a nossa brincadeira começar a ficar séria a partir de agora.

Vamos estruturar a aplicação em si: Visão geral da Solution, estrutura dos diretórios e após o break detalhar a construção da Biblioteca de Classes.

Visão da Solution no VS e Estrutura de Diretórios

Nossa Solução será composta de dois projetos, sendo um a interface Web, um site em ASP.NET habilitado para AJAX (portanto, não se esqueça de instalar as extensões AJAX no VS 2005!) e uma biblioteca de classes.

Veja no screenshot abaixo a estrutura da aplicação Web:

Visão da Alicação Web

Vamos detalhar a função de cada arquivo no próximo post, por que o que nos interessa no momento é a biblioteca de classes, onde toda a mágica acontece e é onde a Classe de Conexão (que é o foco desta série de artigos) é utilizada.

Biblioteca de Classes do Simple PIM (SPIMCore)

No screenshot abaixo temos a estrutura de nossa biblioteca de classes:

img_solition_classlibrary

Cada classe será constituída de três camadas, e elas estão nos diretórios a saber:

DAL – Data Abstraction Layer: É a camada onde ficam os comandos SQL para inserção, atualização, seleção, exclusão e pesquisa de cada tabela do banco de dados, sendo cada uma representada por uma classe.

VO – Value Objects: É a camada onde ficam as propriedades de cada classe. Cada campo da tabela é representado por uma variável privada e uma propriedade, com métodos get e set. Nela também há outras propriedades que não são relacionadas com o BD, por exemplo, os Lazy (ligações com outros objetos).

BLL – Bussiness Logic Layer: É a camada onde ficam as demais regras de negócio de cada classe, por exemplo, validação dos dados. Como não implementamos validação (ainda), este diretório não está criado em nossa aplicação.

Temos ainda a pasta generico, onde há o framework de Conexão (classe Conexao) e as classes auxiliares (classes Consts.*) de nosso programa.

Normalmente, quando separamos a nossa aplicação neste estilo, cada camada é uma classe distinta, ou seja, uma classe para o VO, para o DAL e para o BLL.

Como na plataforma .NET versão 2.0 em diante temos o conceito de classes parciais, ou seja, cada classe pode ser dividida em vários arquivos, as três camadas estão em uma única classe, cada classe sendo parcial e em cada arquivo contendo somente o que interessa para a camada em questão.

Em todas as classes, na parte DAL, os métodos de inserção, atualização, seleção e exclusão são absolutamente iguais. Sim, é isto mesmo que você leu: estas rotinas são exatamente iguais.

Exatamente por serem absolutamente iguais, na pasta generico temos uma classe chamada TGenericDAL, que faz a implementação destes quatro métodos comuns. As outras classes (TContato, TEndereco, TFormaContato, TTpFormaContato) são herdadas de TGenericDAL ao invés de serem herdadas de Conexao, como geralmente é feito.

TGenericDAL é herdada de Conexao, e portanto, faz as chamadas que vamos estudar agora. Vamos examinar seu código:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Data;
   4: using System.Text;
   5: using System.Reflection;
   6:  
   7: namespace SPIMCore
   8: {
   9:     /// <summary>
  10:     /// Classe genérica para Cadastros.
  11:     /// </summary>
  12:     public class TGenericDAL : Conexao
  13:     {
  14:         protected string _SearchSQL = "";
  15:         
  16:         /// <summary>
  17:         /// Carrega um registro na classe através de sua chave única
  18:         /// </summary>
  19:         /// <param name="pkValues">Array contendo os valores da chave única, na ordem em que foram definidos em _DataKeys</param>
  20:         public virtual void SetByID(object[] pkValues)
  21:         {
  22:             //Monta as cláusula Where do SQL, baseando-se na propriedade _DataKeys
  23:             int pkCount = 0;
  24:             string Where = "";
  25:             ClearSQLParams();
  26:             foreach (string s in _DataKeys)
  27:             {
  28:                 Where += String.Format(" and ({0} = {1}{2}) ", s, _ParameterDefinedBy, s);
  29:                 AddSQLParam(_ParameterDefinedBy + s, pkValues[pkCount], ParameterDirection.Input);
  30:             }
  31:             _SelectSQL = String.Format("select * from {0} where (1 = 1) {1}", _TableName, Where);
  32:             this.Select((CountSQLParams() > 0));
  33:         }
  34:  
  35:         /// <summary>
  36:         /// Validação de dados: a classe filha deve sobreescrever este método!
  37:         /// </summary>
  38:         /// <returns>True se os dados forem válidos, false caso contrário</returns>
  39:         public virtual bool DataIsValid()
  40:         {
  41:             return true;
  42:         }
  43:  
  44:         /// <summary>
  45:         /// Persiste a classe no banco de dados, inserindo.
  46:         /// </summary>
  47:         /// <returns>True em caso de sucesso, false em caso de erro</returns>
  48:         public virtual bool Insert()
  49:         {
  50:             if (DataIsValid())
  51:             {
  52:                 ClearSQLParams();
  53:                 _InsertSQL = AutoIsql(OperacaoBD.opInsert);
  54:                 BindObject2Parameters();
  55:                 if (base.Salvar(OperacaoBD.opInsert, (CountSQLParams() > 0)))
  56:                 {
  57:                     this.fMsgInfo = "Inserido com sucesso!";
  58:                     return true;
  59:                 }
  60:                 else
  61:                 {
  62:                     this.fMsgInfo = "Ops.. deu pau em alguma coisa aí! -> " + fMsgInfo;
  63:                     return false;
  64:                 }
  65:             }
  66:             else
  67:             {
  68:                 return false;
  69:             }
  70:         }
  71:  
  72:         /// <summary>
  73:         /// Persiste a classe no banco de dados, atualizando.
  74:         /// </summary>
  75:         /// <returns>True em caso de sucesso, false em caso de erro</returns>
  76:         public virtual bool Update()
  77:         {
  78:             if (DataIsValid())
  79:             {
  80:                 ClearSQLParams();
  81:                 _UpdateSQL = AutoIsql(OperacaoBD.opUpdate);
  82:                 BindObject2Parameters();
  83:                 if (base.Salvar(OperacaoBD.opUpdate, (CountSQLParams() > 0)))
  84:                 {
  85:                     this.fMsgInfo = "Atualizado com sucesso!";
  86:                     return true;
  87:                 }
  88:                 else
  89:                 {
  90:                     this.fMsgInfo = "Ops.. deu pau em alguma coisa aí! -> " + fMsgInfo;
  91:                     return false;
  92:                 }
  93:             }
  94:             else
  95:             {
  96:                 return false;
  97:             }
  98:         }
  99:  
 100:         /// <summary>
 101:         /// Exclui o registro no banco de dados, baseando-se na chave primária
 102:         /// </summary>
 103:         /// <returns>True em caso de sucesso, false em caso de erro</returns>
 104:         public virtual bool Delete()
 105:         {
 106:             ClearSQLParams();
 107:             _DeleteSQL = AutoIsql(OperacaoBD.opDelete);
 108:             
 109:             ///Atribui os parâmetros manualmente, pois o BindObject2Parameters cria para todos os campos
 110:             ///e alguns bancos de dados não admitem que sejam declarados mais parâmetros dos que estão na instrução SQL.
 111:             ///Como a função AutoIsql cria os nomes dos parâmetros padronizados, utilizo o mesmo padrão para essa adição manual :-)
 112:             foreach (string s in _DataKeys)
 113:             {
 114:                 ///Como também preciso atribuir o valor que está no campo correspondente às chaves, pego esse valor via Reflection :-)
 115:                 ///A mesma coisa se fossemos utilizar a função BindObject2Parameters, só que sem as rotinas de conversão de nulo, pois
 116:                 ///como os campos são chave primária, o mesmo não pode ser nulo.
 117:                 Type t = this.GetType();
 118:                 FieldInfo fi = t.GetField("_" + s, (BindingFlags.NonPublic | BindingFlags.Instance));
 119:                 AddSQLParam(_ParameterDefinedBy + s, fi.GetValue(this), ParameterDirection.Input);
 120:             }
 121:  
 122:             if(base.Salvar(OperacaoBD.opDelete, (CountSQLParams() > 0)))
 123:             {
 124:                 this.fMsgInfo = "Excluído com sucesso!";
 125:                 return true;
 126:             }
 127:             else
 128:             {
 129:                 this.fMsgInfo = "Ops.. deu pau em alguma coisa aí! -> " + fMsgInfo;
 130:                 return false;
 131:             }
 132:         }
 133:  
 134:     }
 135: }

Primeiramente, temos a variável privada _SearchSQL, que é utilizada para armazenarmos o SQL das consultas que realizaremos em cada classe, gerando um List<> no final e que poderá ser utilizado para alimentar um grid, um combo-box, etc.

Todos os métodos são virtuais, ou seja, de acordo com a necessidade podem ser sobreescritos (override) pelas subclasses.

Vamos analisar cada método:

Método SetByID: Este método atribui os campos do banco de dados em um objeto. Os dados do banco são selecionados através da(s) chave(s) primária(s), que é passado via array de Object (depois, em um outo post vemos como utilizar um array implícito, usando parâmetros indefinidos ;-) ).

Para montar o SQL de Select, varro a propriedade _DataKeys da tabela e monto a cláusula Where através de concatenação de strings. Esse SQL é parametrizado, sendo que o indicador de parâmetro é pego de _ParameterDefinedBy e o nome do parâmetro é o nome do campo. O valor do parâmetro é o seu correspondente no array pkValues. Um cuidado é: passar cada valor no array no índice correspondente à chave em _DataKeys. Este valor é colocado na coleção SQLParams da classe Conexao.

O Select final é feito pegando-se o nome da tabela na propriedade _TableName, e o ( 1 = 1) na cláusula Where serve para “forçar” pelo menos um AND e simplificar a construção do filtro (veja este artigo para maiores detalhes dessa gambiarra :-) ).

Método DataIsValid: Este método faz a validação dos dados. Porém, ele DEVE ser sobreescrito na subclasse, caso utilizemos validação.

Métodos Insert e Update: Estes métodos são praticamente idênticos. Primeiramente, fazemos a validação dos dados utilizando o método DataIsValid, e caso não passe pela validação, retornamos false (as mensagens de validação são implementadas na subclasse também). Caso os dados sejam válidos, geramos a instrução SQL correspondente à operação utlizando o método AutoIsql() e colocando na variável desta operação (_InsertSQL ou _UpdateSQL).

Em seguida, criamos os parâmetros para as instruções SQL geradas e atribuimos os respectivos valores utilizando o método BindObject2Parameters(). Em seguida, chamamos o método Salvar() da classe Conexao e de acordo com o resultado retornamos uma mensagem de sucesso ou não, além do resultado true ou false.

Método Delete: Começamos o método criando a instrução SQL de exclusão utilizando AutoIsql() e colocando o resultado em _DeleteSQL.

Ao invés de atribuir os parâmetros automaticamente via BindObject2Parameters(), criaremos os parâmetros (cláusula Where e atribuição de valores) manualmente através das chaves localizadas em _DataKeys.

Isto é necessário pois, dependendo do banco de dados, a contagem de parâmetros que está na coleção Params de um objeto Command (que é utilizado na classe Conexao) deverá ser igual ao número de variáveis declaradas na instrução SQL a ser executada.

Para fazer a atribuição das chaves nos parâmetros, fazemos um laço foreach, e dentro dele, utilizaremos Reflection!

Pegamos as informações de tipo da classe utilizando a classe Type do .NET Framework. A partir dela podemos obter informações sobre a classe tais como variáveis privadas, métodos, propriedades, fazer atribuição de valores, entre outras coisas, tudo em tempo de execução do nosso programa.

Dentro da variável “t”, armazenaremos as informações sobre a própria classe, chamando o método GetType() de objeto desta classe. No nosso caso, como queremos a classe da própria instância, é utilizado o ponteiro this.

Em seguida criamos um objeto do tipo FieldInfo. Esta classe contém propriedades e métodos para manipulação de variáveis privadas e propriedades de um objeto. Sim, ela permite que atribuamos e obtamos valores de objetos em tempo de execução.

Atribuimos na variável “fi”, as informações da propriedade correspondente às chaves da tabela, que estão em _DataKeys. Lembre-se: as variáveis privadas são nomeadas com o campo da tabela precedido de underline.

O método GetField de um objeto do tipo Type retorna as informações de uma propriedade ou uma variável privada, pedindo como parâmetros o nome da variável/propriedade, os BindingFlags, que são como filtros para a nossa pesquisa: NonPublic representa variáveis privadas, e Instance representa uma variável de instância; cada BindingFlag é concatenado com um operador OR ( | ) bit a bit (apenas um pipe ao invés de dois, como no operador lógico ao qual estamos mais acostumados).

Como seguimos a convenção ao nomear as variáveis privadas, concatenamos o valor atual da variável “s” na iteração do foreach, com um underline no começo e armazenamos a variável privada da classe na variável “fi”.

Feito isso, adicionamos um parâmetro na coleção SQLParams da classe Conexao. O nome do parâmetro é feito concatenando-se o valor de ParameterDefinedBy com o nome do campo da chave, e o seu valor é atribuido utilizando-se o valor da variável privada correspondente da classe.

Este valor é obtido utilizando-se do método GetValue de um objeto do tipo FieldInfo (nossa variável “fi”), que pede como parâmetro o objeto no qual este valor está contido (a variável “fi” já contem a informação sobre qual variável deste objeto queremos pegar o valor). No nosso caso, é do próprio objeto, utilizando mais uma vez o ponteiro this.

Construído o SQL e atribuido os valores das chaves, chamamos o método Salvar() passando como instrução o Delete e retornamos as mensagens e valores true ou false em caso de sucesso ou erro.

Terminamos aqui a classe de geração e execução das instruções SQL… mas onde estão as propriedades correspondentes aos campos da tabela em si?

Notamos que a classe TGenericDAL faz referências às propriedades dela própria (ponteiro this nos métodos que usam Reflection, inclusive nos herdados da classe Conexao), mas ela não tem NENHUMA propriedade!

Sim, pois as propriedades serão implementadas nas subclasses! Uma nota que não posso deixar passar em branco: A classe TGenericDAL poderia muito bem ser abstrata… Se quiserem, podem colocar (e até é recomendável) colocar o modificador abstract.

Vamos estudar a classe TContato, pois ela tem mais detalhes que as outras classes, e o mesmo estudo poderá ser feito para as outras classes nos diretórios DAL e VO.

Vamos olhar a parte VO de TContato:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4:  
   5: namespace SPIMCore
   6: {
   7:     public partial class TContato : TGenericDAL
   8:     {
   9:         #region Variáveis Privadas
  10:         private int _ID = 0;
  11:         private string _NOME = "";
  12:         private string _SOBRENOME = "";
  13:         private DateTime? _DT_NASCIMENTO = null;
  14:         private string _SEXO = "";
  15:         private string _RG = "";
  16:         private string _CPF = "";
  17:         private string _CNH = "";
  18:         private string _PROFISSAO = "";
  19:         #endregion
  20:  
  21:         #region Propriedades (com Get e Set)
  22:         public int Id { get { return _ID; } set { _ID = value; } }
  23:         public string Nome { get { return _NOME; } set { _NOME = value; } }
  24:         public string Sobrenome { get { return _SOBRENOME; } set { _SOBRENOME = value; } }
  25:         public DateTime? DtNascimento { get { return _DT_NASCIMENTO; } set { _DT_NASCIMENTO = value; } }
  26:         public string Sexo { get { return _SEXO; } set { _SEXO = value; } }
  27:         public string Rg { get { return _RG; } set { _RG = value; } }
  28:         public string Cpf { get { return _CPF; } set { _CPF = value; } }
  29:         public string Cnh { get { return _CNH; } set { _CNH = value; } }
  30:         public string Profissao { get { return _PROFISSAO; } set { _PROFISSAO = value; } }
  31:         public List<TEndereco> ListaEnderecos
  32:         {
  33:             get { return TEndereco.ListarPorContato(_ID); }
  34:         }
  35:         public List<TFormaContato> ListaFormaContato
  36:         {
  37:             get { return TFormaContato.ListarPorContato(_ID); }
  38:         }
  39:         #endregion
  40:     }
  41: }

Como vemos, TContato herda TGenericDAL, que é a responsável por gerar os comandos SQL para persistência do objeto na base de dados.

Cada variável privada é nomeada com o nome do campo na tabela precedido de underline. O campo _DT_NASCIMENTO é um DateTime nulável e é inicializado com null.

Para cada variável privada há uma propriedade com get e set, seguindo a convenção PascalCase, suprimindo os caracteres underline do nome do campo.

Temos também duas propriedades somente leitura que retornam a lista de Endereços e a lista de Formas de contato para aquele contato que está carregado na classe. O método ListarPorContato() faz um Select de pesquisa que retorna um list. Este list, por sua vez, é utilizado para alimentar um gridview na interface Web. Veremos como se faz esse select na classe TContato, pois da mesma maneira são construídos os métodos de TEndereco e TFormaContato.

Simples, não é? Agora vamos estudar a parte DAL:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4:  
   5: //DAL
   6: namespace SPIMCore
   7: {
   8:     public partial class TContato : TGenericDAL
   9:     {
  10:         public TContato()
  11:         {
  12:             _TableName = "CONTATOS";
  13:             _DataKeys = new string[1] { "ID" };
  14:             _FormatProperties.Add("DtNascimento;{0:dd/MM/yyyy}");
  15:         }
  16:  
  17:         private List<TContato> ListaGenerica()
  18:         {
  19:             List<TContato> retorno = new List<TContato>();
  20:             return (List<TContato>)this.CreateList(this.GetType(), retorno.GetType(), _SearchSQL, CountSQLParams() > 0);
  21:         }
  22:  
  23:         public static List<TContato> ListarTodos()
  24:         {
  25:             TContato c = new TContato();
  26:             c.ClearSQLParams();
  27:             c._SearchSQL = "select * from CONTATOS";
  28:             return c.ListaGenerica();
  29:         }
  30:     }
  31: }

Nesta parte é que colocamos as instruções SQL que fazem pesquisa na base de dados e retornam vários registros.

Primeiramente, inicializamos, através de um construtor, as variáveis _TableName e _DataKeys com os parâmetros correspondentes à tabela CONTATOS do nosso banco de dados. Esta parte é fundamental para a classe TGenericDAL funcionar corretamente.

Também colocamos uma informação para formatarmos na interface de usuário a propriedade DtNascimento, colocando esse campo juntamente com a string de formatação, separados por ponto-e-vírgula, na coleção _FormatProperties.

Criamos o método privado ListaGenerica(), que retorna um List<> da classe TContato. Para gerar este List<>, utilizamos o novo método CreateList() da classe Conexao. Ele pede como parâmetros as informações sobre a classe de cada item do List<>, que no caso são as informações da própria classe TContato. Para tanto, precisamos utilizar o método GetType() de uma instância de TContato, que no caso é o ponteiro this.

O segundo parâmetro são as informações do tipo do List<>. Para isso, criamos uma instância de List<TContato>, que é a nossa variável “retorno” e chamamos o método GetType().

Por fim, nos dois últimos parâmetros temos as informações sobre a instrução SQL a ser executada: Passamos a instrução SQL em si, que guardamos na variável privada _SearchSQL, e se ela possui parâmetros. Para checar a existência de parâmetros no SQL, simplesmente verifico se a coleção _SQLParams contém itens. Isto é feito pelo método CountSQLParams().

Como CreateList() retorna um tipo object, precisamos fazer unboxing para o tipo de retorno, no caso List<TContato>.

Por fim, temos o método estático ListarTodos(), que armazena um “select *” em _SearchSQL, limpa a lista de parâmetros e retorna o método ListaGenerica().

Como esse método é estático, para acessarmos os métodos/variáveis privadas de instância, precisamos criar uma instância do próprio objeto dentro do método. Para isso serve a variável “c”.

Caso o SQL seja filtrado, basta montar uma cláusula where, adicionar os parâmetros e chamar ListaGenerica(). As informações sobre filtros poderão ser informadas via parâmetros no método e você poderá convencionar valores para que aquele filtro em questão não seja aplicado. Você pode ver um exemplo disso na classe TEndereco, no método ListarPorContato().

Na classe TFormaContato, há uma propriedade chamada DescricaoFormaContato, do tipo string. Em seu método get() (não tem método set() pois é somente leitura), é criada uma instância de TTpFormaContato, e esta é carregada com os dados da forma de contato em _TP_FORMA_CONTATO_ID (variável privada de TFormaContato) e o campo DESCRICAO da tabela TP_FORMA_CONTATO é retornado.

Pera ae… se fôssemos seguir à risca algumas das convenções da POO não iriamos fazer um Lazy (criar uma variável do tipo TTpFormaContato)?

Sim, faríamos um lazy sim. Mas como este campo vai alimentar um grid lá na frente, e a ligação é feita através de um campo que DEVE ser de tipo primitivo, isso não funcionaria, sendo necessário fazer isto através de um TemplateColumn e no evento RowDataBound, portanto, gerando mais trabalho.

Se não alimentasse um grid, ou necessitamos dele para outras coisas, o lazy com certeza é a abordagem mais adequada.

Quanto as outras classes (TFormaContato, TEndereco e TTpFormaContato), a forma de construção é idêntica.

Viu quanto código temos em cada classe? Pouquíssimo…

Um abraço e logo vamos construir a Interface de Usuário e com ela mais algumas surpresas ;-)

Download:

Projeto de Exemplo da Classe de Conexão – Simple PIM (263 kB)

0 comentários:


Postar um comentário

Para tornar este artigo ainda mais interessante, escreva suas críticas (desde que construtivas e sem ofenças), elogios, sugestões, complementos, dúvidas, etc, etc, etc!!!

  © Blogger templates ProBlogger Template by Ourblogtemplates.com 2008 - Editado e configurado por Leonel F.

Voltar ao TOPO