Desenvolvimento - C#

Persistência de dados via Reflection - parte 2

Esta é a segunda parte do artigo sobre persistência de dados via Reflection, nesta parte iremos ver Reflection, alguns conceitos, suas classes e utilização.

por Luciano Lima



Bem, dando continuidade a nosso artigo sobre Persistência de dados com Reflection (veja a parte 1), iremos hoje implementar nossa classe de Persistência, para isso iremos utilizar Reflection.

Antes de começarmos a ver um pouco de reflection, vamos criar nossa tabela, na qual iremos trabalhar nosso projeto.

Crie uma tabela chamada CLIENTE, você poderá utilizar qualquer banco de dados que queira, pois este artigo irá abordar utilizar uma forma "genérica" de acesso a dados, porém não é foco de nosso artigo, ficando para um próximo artigo.

Para os que utilizarem o SQL Server, segue abaixo o código para gerar a tabela:

CREATE TABLE [CLIENTE] 
(
	[ID] [int] IDENTITY (1, 1) NOT NULL ,
	[NOME] [varchar] (80) COLLATE Latin1_General_CI_AS NOT NULL ,
	[EMAIL] [varchar] (50) COLLATE Latin1_General_CI_AS NULL ,
	[TELEFONE] [varchar] (13) NULL 
) 
GO

Feito isso, vamos ver quem é esse tal de Reflection!

Para aqueles que já programaram em Java, Reflection não será uma novidade, para nós do .Net é um assunto que merece uma atenção maior, por isso, volto a frisar, este artigo irá mostrar o básico, para mais detalhes procure na Net tudo sobre Reflection, pois vale a pena conferir.

Definindo: reflection ou reflexão, é o processo de obter informações sobre assemblies e os tipos definidos dentro dele, e ainda a criação, invocação e acesso às instâncias de tipos em tempo de execução. Esta é uma definição segundo o SDK do Net Framwork. Reflection, nos permite inspecionar programaticamente uma montagem e obter informações sobre ela, assim como seus tipos e atributos. Nós também podemos criar as nossas próprias montagens e tipos utilizando os serviços disponíveis na class System.Reflection.Emit. Tudo que você precisa para trabalhar com Reflexão está na class System.Reflection.

Parece meio confuso, mas vocês verão que não é tanto assim!

Para entendermos melhor, vamos mostrar alguns exemplos bem simples de Reflection antes de partirmos para o que realmente nos interessa assim vocês entenderão o que é Reflection.

Começaremos com um exemplo que irá exibir os Construtores de uma montagem qualquer. Montagem ou assembly é um programa feito em .Net, exibido como uma unidade única, ou seja, tudo que você faz em .Net e empacota é uma montagem. Exemplo, uma class, uma dll, um executável, tudo isso é uma montagem.

  • Crie um projeto do tipo Console Application, defina seu nome como Reflection;
  • Faça uma chamada ao namespace System.Reflection: using System.Reflection;
  • Agora digite todo o código abaixo.

Observem que criamos uma class a parte, chamada Teste, para que possamos testar nosso método.

Não é necessário criarmos classes para testar, podemos, ao invés de verificarmos o tipo Teste, verificarmos System.Object, System.Int32 ou até mesmo uma class que vocês tenham criado.

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection; //Biblioteca responsável pela reflexão

namespace Reflection
{
   class Program
   {
      static void Main( string[] args )
      {
         //Tipo a ser pesquisado
         Type tipo = typeof( Teste ); 	//Podemos substituir a class Teste por qualquer
         //outra class que exista no Framework
 
         //Chama o método exibeConstrutores(Type tipo), passando o tipo para ele
         exibeConstrutores( tipo );

         //Aguardando o precionar de uma tecla
         Console.Read( );
      }

      /// <summary>
      /// Método responsável por exibir os contrutores da class
      /// </summary>
      static void exibeConstrutores( Type tipo )
      {
         //Responsável por armazenar os construtores encontrados
         ConstructorInfo[] cInfo = tipo.GetConstructors( );

         //Exibe a quantidade de construtores da class
         Console.WriteLine( "Construtor(es) encontrado(s): " + cInfo.Length );
         Console.WriteLine( "" );

         //Varre nosso Objeto, e exibe os construtores
         foreach( ConstructorInfo ci in cInfo )
         {
            Console.WriteLine( "Nome Construtor: " + ci.Name );

            //Recupera os parâmetros dos construtores encontrados
            ParameterInfo[] param = ci.GetParameters( );

            //Testa senão está vazio
            if( param.Length> 0 )
            {
               Console.WriteLine( "" );
               Console.WriteLine( "Parametro(s) encontrado(s): " + param.Length );

               //Varre a Collection de parametros
               foreach( ParameterInfo p in param )
               {
                  Console.Write( p.Name + " ");
                  Console.WriteLine( "" );
               }
            }
         }
     }
   }

   class Teste
   {
      public Teste( )
      {
        //Vazio
      }

      public Teste( string nome )
      {
         //Um parâmetro
      }

      public Teste( string nome, int valor )
      {
         //Dois parâmetros
      }
    }
}

Vamos a alguams definições, para que nosso artigo não fique tão básico assim, os dois primeiros itens(ConstructorInfo e ParameterInfo) nós utilizamos, o restante fica a cargo de vocês testarem.

ConstructorInfo[] - Recebe uma Collection contendo todos os construtores encontrado na class na qual realizamos a pesquisa;

ParameterInfo[] - Recebe uma Collection contendo os parâmetros encontrados em cada construtor que foi localizado na class que pesquisamos;

FieldInfo - Recebe informação sobre os campos contido na class;

PropertyInfo - Recebe informações sobre as propriedades na class;

MethodInfo - Recebe informações sobre os métodos contidos na class. Estas são algumas das funcionalidades que podemos encontrar na class System.Reflection, para mais informações consulte o SDK do .Net Framework, é uma fonte e tanto para pesquisa. Mas para aqueles que ainda não dominam o inglês, segue o link do mesmo material em português: http://msdnwiki.microsoft.com/pt-br/mtpswiki/default.aspx

Segue abaixo um outro exemplo de Reflection, desta vez vamos invocar um método de nossa class Teste, para isso efetue algumas modificações na class Teste, para que ela fique como abaixo:

class Teste
{
   public Teste( )
   {
      //Vazio		  
   }

   public void Escreve( string nome )
   {
      Console.WriteLine( "Unico parametro: " + nome );
   }

   public void Escreve( string nome, int valor )
   {
      Console.WriteLine( "Dois parametros, nome: " + nome + ", valor: " + valor.ToString( ) );
   }
}

Agora, vamos ver o código responsável por invocar nossos métodos da class Teste. Cole o código abaixo logo após as chamadas ao construtor, dentro do bloco Main de nossa aplicação.

//Faz referencia a class a ser carregada dinamicamente
Assembly myClass = Assembly.GetAssembly( typeof( Teste ) );

//Cria o objeto a ser invocado
//Observem que o objeto vem acompanhado de seu namespace, que no nosso caso é Reflection
//Ms poderá ser qualquer um, isso irá variar conforme o seu projeto.
object myObj = myClass.CreateInstance( "Reflection.Teste" );

//Invoca o método Escreve da class teste
myObj.GetType( ).InvokeMember( "Escreve", BindingFlags.InvokeMethod, null, myObj, new object[] { "Escrevendo com 
Reflection", 5 } );

Caso queiram, basta mudar os parâmetros para que nosso código invoque o outro método, com um parametro apenas. Faça o teste, crie novos metodos e tente invocálos.

Pois bem, acho que já dá pra ter uma idéias das possibilidades que temos com Reflection, vimos como começar a utilizar reflection em nossas aplicações, tendo como exemplos os códigos acima, agora vamos voltar a nossa class de Persistência.

Vamos começar criando nossa classe que irá servir como base para funcionamento da class persistência.

Esta class deve conter os atributos personalizados que criamos no artigo anterior. Vejam o código abaixo:

using System;
using System.Collections.Generic;
using System.Text;
using Persistencia;

namespace Pessoa
{
    [Table( "CLIENTE" )]
    public class Cliente : PersistClass
    {
	   //Variáveis privadas
	   private int _id;
	   private string _nome;
	   private string _email;
	   private string _telefone;

	   /// <summary>
	   /// Construtor padrão
	   /// </summary>
	   public Cliente( ){ }


	   [Key( "ID" )]
	   public int ID
	   {
		  get { return _id; }
		  set {  _id = value; }
	   }

	   [Column( "NOME", ColumnType.String, 50 )]
	   public string Nome
	   {
		  get { return _nome; }
		  set { _nome = value; }
	   }

	   [Column( "EMAIL", ColumnType.String, 50 )]
	   public string Email
	   {
		  get { return _email; }
		  set { _email = value; }
	   }

	   [Column( "TELEFONE", ColumnType.String, 13 )]
	   public string Telefone
	   {
		  get { return _telefone; }
		  set { _telefone = value; }
	   }
    }
}

Como vocês podem ver, fizemos uma chamada ao namespace Persistencia, de inicio ele não está disponível pois não criamos a class Persist ainda, sendo assim não se preocupe, ele estará disponível assim que terminarmos o artigo, mas para irmos adiantando, basta criar a class como acima.

Vamos às explicações detalhadas sobre a class.

Essa parte de código é responsável por nos informar o nome da tabela na qual iremos utilizar no banco. Existe a possibilidade de não termos que criar atributos, mas eles irão nos dar maior flexibilidade futuramente, conforme formos melhorando nossa classe.

[Table( "CLIENTE" )]
public class Cliente : PersistClass

Logo após, temos a informação sobre a Chave Primária e sobre as Colunas no banco, observem que os nomes em nosso atributo personalizado corresponde exatamente com o nome das colunas no banco de dados, isso é muito importante. Nosso atributo Column, como vocês perceberam recebe três parâmetros, nome da coluna, tipo e tamanho.

[Key( "ID" )]
public int ID

[Column( "NOME", ColumnType.String, 50 )]
public string Nome

[Column( "EMAIL", ColumnType.String, 50 )]
public string Email

[Column( "TELEFONE", ColumnType.String, 13 )]
public string Telefone

Ok, feito isso, temos nossa classe que irá ser instanciada e salva no banco de dados.
Vamos agora partir para a parte legal da coisa!!

Antes de começarmos, vamos rever duas instruções SQL que serão de máxima importancia para nós, são elas:
"INSERT" e "UPDATE"

Para que possamos inserir um registro em um banco de dados, fazemos da seguinte forma:

"insert into tabela (campo1, campo2,...) values (value1, value2,...)".

Pois bem, desta forma conseguiremos inserir um registro, a partir deste contexto iremos "formatar" nossas instruções dinamicamente, a ponto de conseguir monta-las via string.Format() e utilizar Reflection para nos retornar os dados necessários. Como? Vejam o código abaixo:

private static string _insert = "INSERT INTO {0} ({1}) VALUES({2})";

Esta instrução nada mais é do que nossa instrução pré formatada, a paritr dela utilizaremos métodos que irão substituir os números entre chaves, pelos campos e valores necessários. Sendo assim, vamos por partes. Primeiro vamos criar um método que nos retorne o nome da tabela. Observem o código abaixo:

/// <summary>
/// Recupera o nome da tabela (class)
/// </summary>
/// <param name="type">Objeto</param>
/// <returns></returns>
string getTableName( System.Type type )
{
	object[] obj = type.GetCustomAttributes( typeof( TableAttribute ), true );
	return ( (TableAttribute)obj[0] ).TableName;
}

Este código é bem simples e já estamos utilizando Attribute e Reflection, reparem que nosso método recebe um tipo, que no caso será a nossa classes que contem os dados a serem gravados (class Cliente).

Logo depois como não sabemos o tipo do objeto, criamos um array de objects para receber os atributos que criamos, que no caso são do tipo TableAttribute , esse retorno nada mais é do que o nome da tabela que informamos quando criamos a classe:

[Table( "CLIENTE" )]
public class Cliente : PersistClass

Pronto, já temos um método que lê uma classe e nos retorna um atributo, caso ela possua. Viu, nada complicado!

Vamos agora buscar os "campos" que iremos trabalhar, nesse caso o método irá retornar algo assim: "NOME, TELEFONE, EMAIL", que nada mais é do que os atributos das propriedade da classe Cliente. Basta observarem os comentarios para entenderem o que cada pedacinho de código faz.

/// <summary>
/// Recupera os campos da tabela (class)
/// </summary>
/// <param name="type">Objeto</param>
/// <returns></returns>
static string getColumns( System.Type type )
{
//Cria uma string vazia
string columns = string.Empty;

//Faz uma varredura nas propriedades da class e as joga dentro
//de um objeto do tipo PropertyInfo 
foreach ( PropertyInfo prop in type.GetProperties( ) )
{
	//Criamos um array de objects selecionando apenas as pripriedades
	//que estão marcadas com o atributo do tipo ColumnAttribute
	object[] cols = prop.GetCustomAttributes( typeof( ColumnAttribute ), true );

	//Verificamos senão esta vazio nosso objeto
	if ( cols.Length == 1 )
	{
		//Para que possamos chamar nosso atributos pelo nome
		//devemos criar um objeto do seu tipo, ColumnAttribute
		//e dizer ao objeto cols[0] que ele será do tipo ColumnAttribute
		ColumnAttribute ca = cols[0] as ColumnAttribute;

		//Assim podemos chamar nosso objeto pelo nome
		//no caso ColumnName
		columns += ca.ColumnName + ", ";
	}
}

//Este retorno se dá com objeto - 2, porque nossa instrução
//concatenou uma virgula e um espaco no final, ficando assim:
// nome, telefone, email, - daí temos que remover essa vírgula e este espaço
return columns.Remove( columns.Length - 2 );	

Novamente, graças ao fabuloso Reflection, conseguimos recuperar os campos que iremos gravar no banco de dados. Mas atenção, são OS CAMPOS, e não os valores a serem adicionados.

Agora que já temos nosso método que retorna o nome da tabela, o método que retorna os campos da classe, nos falta implementar o método que retorna os dados a serem gravados. Este método é bem parecido com o método para recuperar os campos. Obervem o código abaixo:

/// <summary>
/// Recupera os valores dos campos da tabela (class)
/// </summary>
/// <param name="type">Objeto</param>
/// <returns></returns>
string getValues( )
{
//Criamos uma string vazia
string values = string.Empty;

//Varremos a classe diretamente instanciada, isso porque nossa classe cliente
//herdou de Persist, assim podemos chamá-la diretamente com this.GetType()
foreach ( PropertyInfo field in this.GetType( ).GetProperties( ) )
{
	//Coloquei esse if propositalmente, isso porque no inicio do artigo
	//havia comentado que como estamos utilizando atributos temos uma maior
	//flexibilidade, nesse caso, podemos testar qual o tipo da coluna
	//para que possamos formatá-la adequadamente na instrução colocando aspas
	//para strings, ## para datas no Acces, etc..etc..
	if ( field.GetValue( this, null ).GetType( ) == typeof( String ) )
	{
		//Fazemos a concatenação normalmente
		values += """ + field.GetValue( this, null ) + "", ";
	}
}

//Removemos o espaço e a virgula e retornamos a senteça de valores
return values.Remove( values.Length - 2 );

Ok, nossa classe de persistência está quase completa, devemos implementar agora nossa método que será responsável por receber um objeto e graválo em nosso banco.

Eu havia dito que explicaria sobre "conexão gnéricas", mas vamos deixar para um próximo artigo, vamos apenas nos concentrarmos em nossa classe de persistência.

Vejamos, agora que temos todos os metodos que varrem um objeto e nos retorna o tipo de dado que precisamos, basta apenas nós criamos uma conexão com o banco e formatar nossa string insert para finalizarmos.

Vamos lá?

/// <summary>
/// Atualiza os dados no banco
/// </summary>
/// <returns></returns>
public Boolean Salvar( )
{
factory = DbProviderFactories.GetFactory( getProviderName( "conString" ) );
con = factory.CreateConnection( );
con.ConnectionString = getConnectionStringsFromConfigFile( "conString" );
cmd = con.CreateCommand( );
cmd.CommandText = string.Format(_insert, getTableName(this.GetType( )), 
   getColumns( this.GetType( ) ), getValues( ));
cmd.CommandType = System.Data.CommandType.Text;
cmd.Connection = con;

try
{
	con.Open( );
	int total = cmd.ExecuteNonQuery( );
	if ( total >= 1 )
	{
		return true;
	}
	else
	{
		return false;
	}
}
catch ( DbException dbE )
{
	throw new ArgumentException( "Erro ao criar DbFactory: " + dbE.Message );
}
finally
{
	con.Close( );
}

Este método não tem nada demais, a não ser a class DbProviderFactories que é responsável por criar nossa conexão com o banco.

A parte de maior atenção é essa:

cmd.CommandText = string.Format(_insert, getTableName(this.GetType( )), getColumns( this.GetType( )), getValues( ));

Esta é a parte onde recebemos nossa instrução insert pré formatada e depois damos forma a ela. Como parâmetros para a formatação temos:

_insert - instrução insert pre formatada;

getTableName( this.GetType( ) ) - responsável por retornar o nome de nossa tabela;

getColumns( this.GetType( ) ) - responsável por retornar os campos da class, ou seja, os campos que criamos no banco de dados;

getValues( ) - retorna os dados que informamos ao nosso objeto do tipo Cliente.

Agora que temos nossa classe pronta para inserir um registro, vamos a parte mais complicada de nosso artigo, a instanciação do objeto.

  • Para isso, crie um novo projeto do tipo ConsoleApplication;
  • Faça uam chamada ao namespace Persistencia;
  • Insira o código abaixo no bloco Main() de nosso novo projeto
  • Rode a aplicação.
using Persistencia;

Cliente c = new Cliente( );
c.Nome = "José da Silva";
c.Email = "jose@da.silva.com.br";
c.Telefone = "(99)9999-9999";
bool ok = c.Salvar( );
if ( ok )
{
	Console.WriteLine( "Cadastrado com sucesso!" );
}
else
{
	Console.WriteLine( "Erro ao cadastrar!" );
}

Pronto, seus dados serão gravados sem o menor problema no banco!! Não vou dizer que foi tão simples assim, mas a partir desta idéia vocês podem criar outras, implementar mais a classe de persistência. O que vale é a criatividade.

Caso queiram testar mais campos, basta incluir o campo no banco de dados e depois na classe cliente, não precisando se preocupar com StoredProcedures, instruções SQL nem nada, criou, inseriu!

Fazendo o download do projeto completo, vocês irão encontrar uma implementação para update, onde passamos o ID e atualizamos o registro no banco, da mesma forma, sem nos preocuparmos com instruções.

Existe também um arquivo App.config, em nosso projeto, mas como falei, a parte de acesso a dados "genericamente" vamos deixar para um proximo artigo, assim poderemos analizar bem as classes base do framwork (Dbxxxxxx) de onde se originam SqlConnection, OracleConnection, OdbcConnection, etc. Essa parte merece um artigo bem detalhado, pois assim como Reflection, podemos criar muita coisa a nosso favor.

Espero que tenham gostado!

Aguardo sugestões, e para aqueles que quiserem, façam modificações, implementações, crie, use a imaginação e depois entre em contato, podemos criar muitas coisas legais e ao mesmo tempo ajudar a outros programadores. Mas nunca se esqueçam de dar os méritos a quem de direito.

Para montar e aprender sobre, estudei vários códigos e sites, vou colocar aqui alguns onde encontrei informação muito boa.

www.msdn.microsoft.com/library
www.codeproject.com
livro: Beginning C# programado

Faça o download do código.

Abraço a todos, e até o próximo artigo.

Luciano Lima.

Luciano Lima

Luciano Lima - Microsoft Certified Professional C#, Diretor de Projetos da iArts Soluções e Tecnologia, já atuou em empresas como CEF (Caixa Econômica Federal) e FGV (Fundação Getúlio Vargas), na área de Desenvolvimento de sistema. Atualmente presta serviços de consultoria em Desenvolvimento e Coordenação de Projetos para empresas no Brasil, Estado Unidos e Inglaterra, utilizando a plataforma .Net.