Por que eu devo ler este artigo:

Serão vistas algumas boas práticas de programação orientadas a objetos durante o desenvolvimento do projeto, como a utilização do padrão DAO para acesso ao banco de dados.

Será demonstrado como fazer o relacionamento correto entre as classes mestre e detalhe e como desenvolver o modelo relacional persisti-las no banco de dados através de uma única transação.

No desenvolvimento de um sistema temos Classes de Negócio, Classes DAO, Classes de Formulário, Tabelas de Banco de Dados pra persistir. Neste artigo serão abordadas todas as etapas deste processo de desenvolvimento em um exemplo de relacionamento Mestre-Detalhe.

No desenvolvimento de sistemas temos diversas vezes o tipo de relacionamento um-para-muitos. Na maioria dos artigos, blogs, tutoriais, temos exemplos deste tipo de relacionamento, porém quase sempre temos exemplos abstratos e que não abordam todo o problema, algumas vezes só exibem a modelagem das classes, outras vezes somente o modelo relacional, deixando os desenvolvedores em dúvidas em outras partes do desenvolvimento que não foram tratadas.

O desenvolvimento de um módulo, mesmo que simples de um sistema, consiste em vários passos e vários momentos de desenvolvimento e que todos tem sua importância e deve ter sua devida atenção, a fim de termos um sistema seguro e consistente em um todo.

De nada adianta termos um bom modelo relacional, onde são adotados os princípios da normalização de dados (BOX 1), uso de índices e relacionamentos corretos das tabelas, se não tivermos um bom diagrama de classes, o contrário também se aplica, se tivermos um bom diagrama de classes e um banco de dados mal concebido, teremos problemas também.

Devemos também ter uma boa modularização do sistema, o separando logicamente em camadas, onde cada uma dessas camadas terá uma função definida que possam ser modificas sem interferência em outras partes do sistema.

BOX 1: Normalização de Dados

A Normalização em bancos de dados são regras que devem ser aplicadas a todas as tabelas, a fim de evitar falhas no projeto, como redundância de dados, má distribuição das informações nas tabelas, etc. Essas regras são chamadas de formas normais e as mais conhecidas são a primeira, segunda e terceira formas normais.

Dessa maneira, respeitando e aplicando cada uma destas regras as tabelas do banco, temos a garantia de um banco de dados íntegro e seguro, com uma grande possibilidade de sucesso no projeto como um todo.

Divisão em Camadas

A divisão do sistema em camadas nos possibilita uma melhor organização e interação de nossas classes, dividindo em módulos as responsabilidades do sistema.

  • Camada de Negócio: é a camada responsável pela lógica do sistema, trata de como os dados serão tratados pelo sistema e fica acessível as demais camadas lógicas;
  • Camada de Apresentação: trata da parte gráfica do sistema, através de botões, menus e textos, permitindo que o usuário interaja com o sistema. Em .NET temos Windows Foms, WPF, Silverlight;
  • Camada de Persistência: responsável por gravar e recuperar dados de algum lugar, geralmente bancos de dados. Utiliza as classes de acesso a dados (DAO) para este fim. Somente nesta classe existirá comandos SQL (select, insert, update, delete).

ODBC

O ODBC (Open Database Connectivity) é uma especificação criada pela Microsoft, que se assemelha ao JDBC do Java (BOX 2), e que padroniza as interfaces dos drivers de comunicação. Os drivers de conexão que seguem esta especificação são chamados de drivers de conexão ODBC.

Para que os drivers ODBC possam ser instalados em uma máquina e ser utilizados pelas aplicações, é necessário fazer uso do ODBC Manager, que já vem instalado com o Windows por padrão, e pode ser acessado através do item “Ferramentas Administrativas” do Painel de Controle ou através do Executar odbcad32.exe.

Na Figura 1 temos a interface do ODBC Manager, na guia Drivers podemos verificar todos os drivers disponíveis no sistema operacional.

ODBC Manager
Figura 1. ODBC Manager

O funcionamento das aplicações via ODBC seguem o seguinte fluxo:

  • Cliente cria um comando para execução;
  • Driver ODBC interpreta o comando e direciona para o banco de dados;
  • Banco de dados elabora uma resposta e devolve para o driver;
  • Driver retorna o resultado para o cliente de forma padronizada.

Existem várias vantagens na utilização do ODBC para a conexão com o banco de dados como, por exemplo a possibilidade de acessar vários bancos de dados sem quase nenhuma alteração na camada de dados. Também temos a padronização de mensagens de erro para os diversos bancos de dados.

A única desvantagem vista nesta abordagem é que o ODBC é uma solução proprietária da Microsoft, portanto funcionando somente em ambiente Windows.

BOX 2: JDBC (Java Database Connectivity)

É uma API ou conjunto de classes e interfaces, escritas em Java para envio de SQL para qualquer banco de dados relacional, desde que exista um driver do banco de dados a ser utilizado.

Namespace System.Data.Odbc

O Namespace System.Data.Odbc é o mecanismo oferecido pelo .NET Framework para o trabalho com os bancos de dados via ODBC.

Esse namespace oferece um vasto conjunto de classes para esse trabalho, sendo os principais os seguintes:

  • OdbcConnection: Representa uma conexão com o banco de dados;
  • OdbcCommand: Representa uma instrução SQL para executar no banco de dados;
  • OdbcParameter: Representa um parâmetro do OdbcCommand;
  • OdbcDataReader: Oferece uma maneira de ler os dados retornados em um comando de consulta no banco de dados;
  • OdbcTransaction: Representa uma transação para ser feita no banco de dados.

Padrão DAO

DAO (Data Access Object) ou Objeto de Acesso a Dados é um padrão de projeto que visa separar as regras de negócio das regras de acesso a dados.

Surgiu em aplicações J2EE do Java, mas que pode ser aplicada a qualquer linguagem de programação orientada a objetos.

Este padrão permite que possamos mudar a forma de persistência sem que isso influencie em nada na lógica de negócio.

Classes DAO são responsáveis por trocar informações com o SGBD e fornecer operações CRUD e de pesquisas, ela deve ser capaz de buscar dados no banco e transformar em objetos, também deverá receber os objetos, converter em instruções SQL e mandar para o bando de dados.

Se aplicarmos este padrão corretamente ele vai abstrair completamente o modo de busca e gravação dos dados, tornando isso transparente para aplicação e facilitando muito na hora de fazermos manutenção na aplicação ou migrarmos de banco de dados.

Também conseguimos centralizar a troca de dados com o SGBD, teremos um ponto único de acesso a dados, tendo assim nossa aplicação um ótimo design orientado a objeto.

Composição

Em cadastros mestre-detalhe, quanto a orientação a objetos temos relacionamento de composição na notação UML (Unified Modeling Language). Quando temos este tipo de relacionamento, estes objetos devem obedecer alguns princípios:

  • Um objeto contém uma lista de outros objetos;
  • Os objetos contidos não fazem sentido fora do contexto do objeto que os contém;
  • Exemplo: Venda > Itens. Se excluirmos a venda, os itens devem ser excluídos também, pois não fazem sentido fora da venda.

Transações

Transações são um conjunto de operações atômicas que devem ser executadas pelo banco de dados. Existe para garantirmos a integridade dos dados após séries atualizações, nos garante que todas ocorreram com sucesso, no caso de uma falha todo o processo é cancelado o que traz uma grande segurança ao sistema.

Controle de transações é algo obrigatório em sistemas de informações, quando criamos uma transação em nossos aplicativos também podemos definir o nível de isolamento desta transação em relação às outras que estão ativas.

Criação das Classe de Modelo

Neste exemplo trabalharemos em duas classes bastante comuns no dia a dia de desenvolvedores que trabalham em empresas no módulo de recursos humanos. Vamos abstrair para nosso sistema os Funcionários da empresa e os seus Dependentes.

Esta é uma ligação forte, pois um dependente só pode existir na existência do funcionário, do contrário não. Para o desenvolvimento do exemplo criamos um novo projeto no Visual Studio do tipo Windows Forms Application para iniciar o desenvolvimento das classes de negócio.

Nossa primeira classe será a Funcionario, e criaremos algumas propriedades básicas que desejamos que todos os funcionários tenham como Nome, Departamento e Salario.

Seguindo o pressuposto que Funcionários possuam Dependentes, fica bastante claro que em nossa classe Funcionario teremos uma coleção de Dependente.

Para termos esta coleção faremos uso da classe List, que manterá uma lista de Dependentes vinculadas a classe Funcionario, conforme a Listagem 1.

Além disso, criamos um construtor padrão para a classe Funcionario, onde instanciamos a lista de dependentes. Utilizamos o recurso de Auto Implemented Properties, para a declaração de propriedades (BOX 3).


  01 using System;
  02 using System.Collections.Generic;
  03 
  04 namespace MestreDetalhe
  05 {
  06     public class Funcionario
  07     {
  08         public int ID { get; set; }        
  09         public string Nome { get; set; }
  10         public string Departamento { get; set; }
  11         public double Salario { get; set; }
  12         public List<Dependente> Dependentes { get; set; }
  13 
  14         public Funcionario()
  15         {
  16             Dependentes = new List<Dependente>();
  17         }
  18     }
  19 } 
Listagem 1. Classe Funcionário

BOX 3: Auto Implemented Properties

Auto Implemented Properties é um recurso existente desde a versão 3.0 do C#, que visa facilitar a declaração de propriedades, que são implementadas automaticamente pelo compilador:

public string Nome { get; set; }

Este código faz com que seja declarado um atributo do tipo string de conhecimento somente do compilador e gera um get e set para ler e escrever nesta propriedade. Com este recurso, somente é possível acessar o valor do atributo através da propriedade.

Para controlar a visibilidade tanto do get quanto do set precisamos adicionar o private antes de cada um deles como ,por exemplo, para deixar uma propriedade somente leitura, declaramos:

public double Salario { get; private set; }

A segunda classe é a de dependente onde teremos o seu Nome e o Parentesco que possui com o Funcionário, como por exemplo, Cônjuge, Filho, etc. Observe o seu código na Listagem 2.

Observe que até este momento não foi falado em Windows Forms, interface gráfica, Banco de Dados, persistência, padrão DAO, ADO e nenhum outro conceito relacionado. Tudo o que temos são regras de negócio (Nota).


  01 using System;
  02 
  03 namespace MestreDetalhe
  04 {
  05     public class Dependente
  06     {
  07         public int ID { get; set; }
  08         public string Nome { get; set; }
  09         public string Parentesco { get; set; }
  10     }
  11 }
Listagem 2. Classe Dependente

Para finalizar, adicionamos mais uma coluna em cada classe que se chamará ID e servirá para identificar unicamente cada objeto no sistema.

Nota: Um dos maiores erros dos desenvolvedores é iniciar o desenvolvimento de sistemas pela criação do banco de dados. Quando trabalhamos orientado a objetos, a primeira tarefa é o desenvolvimento das classes de negócio do sistema.

O modelo relacional posteriormente, simplesmente refletirá este modelo relacional, e não interferir nele como ocorre quando iniciamos projetos pelo banco de dados. Algumas ferramentas possibilitam até a geração de todo o modelo relacional a partir do diagrama de classes desenvolvido.

Poderíamos ter adicionado também um atributo do tipo Funcionario na classe Dependente, a fim de mantermos uma referência deste. Neste caso iria se ter um relacionamento bidirecional entre estas classes. Este tipo de relacionamento tem algumas desvantagens como o problema de referência circular, onde ambas as classes se referenciam, podendo criar uma espécie de loop indesejado entre elas.

O recomendado é sempre termos uma única referência em um único sentido entre as classes, até para manter um baixo acoplamento entre elas.

Criação do Banco de Dados e Tabelas

Utilizaremos o Sistema Gerenciador de Banco de Dados SQL Server para a construção do exemplo prático. O script de criação do banco de dados e tabelas pode ser visto na Listagem 3.


  01 CREATE DATABASE mestredetalhe;
  02 
  03 CREATE TABLE [dbo].[Funcionarios](
  04   [Id] [int] IDENTITY(1,1) NOT NULL,
  05   [Nome] [varchar](50) NOT NULL,
  06   [Departamento] [varchar](50) NOT NULL,
  07   [Salario] [float] NOT NULL,
  08  CONSTRAINT [PK_Funcionarios] PRIMARY KEY CLUSTERED 
  09 
  10 CREATE TABLE [dbo].[Dependentes](
  11   [Id] [int] IDENTITY(1,1) NOT NULL,
  12   [Nome] [varchar](50) NOT NULL,
  13   [Parentesco] [varchar](50) NOT NULL,
  14   [FuncionarioId] [int] NOT NULL,
  15  CONSTRAINT [PK_Dependentes] PRIMARY KEY CLUSTERED 
  16 
  17 ALTER TABLE [dbo].[Dependentes]  WITH CHECK ADD  CONSTRAINT 
     [FK_Dependentes_Funcionarios]
  18 FOREIGN KEY([FuncionarioId]) REFERENCES [dbo].[Funcionarios] ([Id])
Listagem 3. Criação do Banco de Dados e Tabelas

O banco de dados é relativamente simples, onde temos uma coluna para cada propriedade de nossas classes e uma chave estrangeira na tabela Dependentes referenciando a chave primária da tabela Funcionarios, a fim de mantermos a integridade dos dados e o relacionamento entre as tabelas.

Criamos também as colunas ID das duas tabelas como chaves primárias e colunas de identidade, de autoincremento.

Criando uma Fábrica de Conexões

Para estabelecermos uma conexão com o banco de dados é necessário:

  • Escolher o driver de conexão;
  • Definir a localização do banco de dados;
  • Informar o nome da base de dados;
  • Ter um usuário e senha cadastrados no banco de dados.

Todas estas informações unidas farão a nossa string de conexão. Para realizarmos a conexão com o banco de dados faremos uso da classe OdbcConnection presente no namespace System.Data.Odbc.

Para fazermos a conexão com o banco de dados, faremos uso do padrão Factory que tem o objetivo de encapsular a criação de objetos complexos, afim de facilitar o uso destes pelo restante da aplicação, pois a conexão é um processo trabalhoso e uma boa prática de programação é centralizarmos esta ação de nosso sistema. Através desse mecanismo podemos fazer mecanismos de pooling (BOX 4), logs de conexões, etc.

O melhor é que ao mudarmos a implementação de nossa fábrica, adicionando recursos como os citados anteriormente, isso sem que seja necessária nenhuma manutenção extra nas classes que fazem uso desta fábrica, pois a interface não muda, só a implementação dela. Veja na Listagem 4.


  01 using System;
  02 using System.Data.Odbc;
  03 using System.Text;
  04 
  05 namespace MestreDetalhe
  06 {
  07    static class ConnectionFactory
  08    {
  09       public static OdbcConnection CreateConnection()
  10       {
  11          string driver = @"SQL Server";
  12          string server = @" NOTEBOOK-FILIPE\SQLEXPRESS";
  13          string database = @"mestredetalhe";
  14 
  15          StringBuilder connectionString = new StringBuilder();
  16          connectionString.Append("driver=");
  17          connectionString.Append(driver);
  18          connectionString.Append(";server=");
  19          connectionString.Append(server);
  20          connectionString.Append(";database=");
  21          connectionString.Append(database);
  22 
  23          return new OdbcConnection(connectionString.ToString());
  24       }
  25    }
  26 }
Listagem 4. Classe ConnectionFactory

BOX 4: Pooling

Mecanismos de Pooling é algo muito recomendado em aplicações reais onde temos vários acessos simultâneos as classes de conexão ao banco de dados. Isso porque a abertura de uma conexão é um processo oneroso, e se termos várias aberturas em simultâneo pode ocorrer até travamentos e deixar o sistema inutilizável.

O Pool de conexões mantém um número limitado de conexões abertas com o banco de dados. Quando é solicitada uma nova conexão com o banco a classe responsável pelo Pool analisará se abrirá uma nova conexão ou irá atribuir ao novo cliente uma das conexões já ativas anteriormente.

Na string de conexão escolhemos o driver SQL Server e definimos o nome do servidor (nome da máquina mais o serviço), além do nome da base de dados (mestredetalhe).

Observe que na string de conexão não adicionamos os parâmetros de usuário e senha, isso porque foi utilizada a autenticação do Windows para a criação do banco de dados do exemplo.

A classe ConnectionFactory foi declarada como estática (static), desta forma não poderá ser instanciada. O único método desta classe é o de obtenção da conexão e também é estático, não sendo necessário criar qualquer instância desta classe para o uso.

Foi utilizada a classe StringBuilder, contida no namespace System.Text para a criação da string de conexão, isto porque o tipo string é imutável e toda vez que queremos adicionar informações nela, o sistema tem que recriá-la na memória.

Em situações onde temos que fazer muitas mudanças na mesma string devemos fazer uso da classe StringBuilder ao contrário do StringBuilder que é dinâmico e muito mais rápido, tendo uma melhor performance, principalmente em strings grandes.

Para adicionarmos informações em objetos da classe StringBuilder, usamos o método Append, passando a string desejada. Para lermos toda a string gerada no final, utilizamos o método ToString, conforme linha 23.

Desta forma sempre que precisarmos executar algum comando, tanto de consulta, quanto de atualização de dados, faremos uso da classe ConnectionFactory e a conexão estará disponível para este fim.

Classe DAO

Geralmente criamos uma classe DAO para cada classe de negócio, porém quando temos relacionamentos um-para-muitos onde temos uma classe Parte que não existirá no sistema sem a classe Todo, podemos modelar as operações de banco de dados na mesma classe Todo, como será neste exemplo.

Nesse exemplo teremos a classe FuncionarioDao que irá ter todas as funcionalidades referentes a persistência, tanto da tabela Funcionarios quanto da tabela Dependentes, conforme a Listagem 5.


01  public void Insert(Funcionario funcionario)
02  {
03      string sql = @"INSERT INTO Funcionarios (Nome, Departamento, Salario)
         VALUES (?,?,?)";
04      using (OdbcConnection conexao = ConnectionFactory.CreateConnection())
05      {
06          conexao.Open();
07          OdbcTransaction transacao = conexao.BeginTransaction();
08          try
09          {
10              OdbcCommand comando = new OdbcCommand(sql, conexao, transacao);
11              comando.Parameters.AddWithValue("@Nome", funcionario.Nome);
12              comando.Parameters.AddWithValue
                ("@Departamento", funcionario.Departamento);
13              comando.Parameters.AddWithValue("@Salario", funcionario.Salario);
14              comando.ExecuteNonQuery();
15              foreach (Dependente d in funcionario.Dependentes)
16              {
17                  this.AddDependente(funcionario.ID, d, conexao, transacao);
18              }
19              transacao.Commit();
20          }
21          catch (Exception ex)
22          {
23              transacao.Rollback();
24              throw new Exception(ex.Message);
25          }
26      }
27  }
28 
29  private void AddDependente(int funcionarioId, Dependente dependente,
    OdbcConnection conexao, OdbcTransaction transacao)
30  {
31      string sql = @"INSERT INTO Dependentes 
        (Nome, Parentesco, FuncionarioId) VALUES (?,?,?)";
32      OdbcCommand comando = new OdbcCommand(sql, conexao, transacao);
33      comando.Parameters.AddWithValue("@Nome", dependente.Nome);
34      comando.Parameters.AddWithValue("@Parentesco", dependente.Parentesco);
35      comando.Parameters.AddWithValue("@FuncionarioId", funcionarioId);
36      comando.ExecuteNonQuery();
37  }
Listagem 5. Implementação dos Insert’s em Funcionarios e Dependentes

O primeiro passo é a criação da query para fazer a inserção na tabela Funcionarios do banco de dados. Foi usado o recurso de sanitize para que recebamos os parâmetros através do ?, que será substituído posteriormente pelo dado real, na linha 3.

Para efetuarmos a conexão utilizamos nossa classe ConnectionFactory e o método CreateConnection, linha 4. Foi utilizado o bloco Using para estabelecermos a conexão, o que evita que a conexão com o banco de dados não seja fechada após sua utilização, ela é fechada automaticamente após o final deste bloco.

Após, abrimos a conexão através do método Open e iniciamos uma transação através do método BeginTransaction (linhas 6 e 7). A partir desse ponto é usado um try/catch para posteriormente confirmarmos as operações de banco através do Commit ou abortar todas as modificações com o Rollback.

Através de uns dos construtores da classe OdbcCommand, conseguimos passar a instrução SQL, a conexão e a transação para esse objeto (linha 10). Nas linhas 11, 12 e 13 atribuímos as propriedades do funcionário aos parâmetros da query através do método AddWithValue e em seguida fazemos a execução a partir do método ExecuteNonQuery na linha 14 que é do tipo void e apenas executa o comando não retornando nenhuma informação.

Se não houver nenhum erro com a inserção na tabela Funcionários, em seguida são percorridos todos os dependentes do funcionário utilizando o recurso de Foreach e para cada um deles é chamado o método AddDependente, passando o identificador do funcionário, a conexão e a transação para que ele reaproveite as mesmas já iniciadas, conforme a linha 17.

Observe neste momento que estamos passando a mesma conexão e a mesma transação para o método AddDependente, ou seja, todas essas operações serão feitas aproveitando uma única conexão aberta com o banco de dados, isso nos dá um bom ganho de performance e grande consistências nos dados.

O método AddDependente segue os mesmos passos para a execução dos inserts em dependentes, porém neste caso não foi necessário abrir a conexão e iniciar transação, apenas foi criado o comando, passado os parâmetros e executado, conforme as linhas 31 a 36.

Se tudo ocorrer bem com a execução desses inserts e não tivermos nenhum erro, seja ele de programação ou de dados inválidos, será chamado o Commit (linha 19), que confirmará esses dados no banco de uma vez só.

Se algo de errado acontecer, toda a operação será abortada, fazendo com que as tabelas não fiquem com dados inconsistentes, através do Rollback (linha 23), em seguida uma exceção é levantada passado a mensagem de erro que será retornada para a interface.

Hoje em dia há outras soluções de implementação da camada de acesso a dados que não seja o uso de SQL e a conversão manual de linhas para objetos e objetos para linhas, a chamada impedância entre os bancos de dados relacionais e os sistemas orientados a objetos.

Uma das ferramentas de mapeamento do .NET, é a ferramenta de ORM (Object Relational Mapper) Entity, que é um framework de mapeamento objeto-relacional padrão da Microsoft, porém não será vista neste artigo por não entrar no escopo do mesmo.

Nota: O caractere @utilizado antes de um valor literal do tipo string indica que os caracteres dentro desta string não devem ser processados como uma cadeia de caracteres especiais.

01  public void Delete(int id)
02  {
03      string sql = @"DELETE FROM Funcionarios WHERE Id = ?";
04      using (OdbcConnection conexao = ConnectionFactory.CreateConnection())
05      {
06          conexao.Open();
07          OdbcTransaction transacao = conexao.BeginTransaction();
08          try
09          {
10              this.RemoveDependentes(id, conexao, transacao);
11              OdbcCommand comando = new OdbcCommand(sql, conexao, transacao);
12              comando.Parameters.AddWithValue("@Id", id);
13              comando.ExecuteNonQuery();
14              transacao.Commit();
15          }
16          catch (Exception ex)
17          {
18              transacao.Rollback();
19              throw new Exception(ex.Message);
20          }                
21      }
22  }
23  
24  private void RemoveDependentes
    (int id, OdbcConnection conexao, OdbcTransaction transacao)
25  {
26      string sql = @"DELETE Dependentes WHERE Id = ?";
27      OdbcCommand comando = new OdbcCommand(sql, conexao, transacao);
28      comando.Parameters.AddWithValue("@Id", id);
29      comando.ExecuteNonQuery();
30  }
Listagem 6. Implementação dos Deletes para Funcionário e Dependentes

A exclusão se dá em dois momentos da mesma forma que a inserção. Porém desta vez, primeiramente precisamos excluir todos os dependentes para depois excluir o funcionário, conforme mostra a Listagem 6.

Cria-se a string do comando de exclusão do Funcionario (linha 3), estabelecemos a conexão (linha 4), abre-se a conexão e iniciamos a transação (linhas 6 e 7). Após isso é chamado o método que efetua exclusão de dependentes daquele funcionário.

Na remoção dos dependentes, aproveitamos a conexão e transação já aberta e apenas criamos a string de exclusão (linha 26), criamos o objeto de comando passando a conexão, transação e o comando desejado (linha 27), passamos o parâmetro e executamos o comando (linhas 28 e 29).

Executado o método de exclusão desses dependentes, segue-se o método principal, que exclui o funcionário e faz commit se tudo ocorrer bem (linha 14) ou rollback e nova exceção (linhas 18 e 19), se houver algum erro durante o processamento das exclusões.


01  public Funcionario FindById(int id)
02  {
03      string sql = @"SELECT * FROM Funcionarios WHERE Id = ?";
04  
05      using (OdbcConnection conexao = ConnectionFactory.CreateConnection())
06      {
07          OdbcCommand comando = new OdbcCommand(sql, conexao);
08          comando.Parameters.AddWithValue("@Id", id);
09          conexao.Open();
10         OdbcDataReader resultado = comando.ExecuteReader();
11          while (resultado.Read())
12          {
13              Funcionario funcionario = new Funcionario();
14              funcionario.ID = Convert.ToInt32(resultado["Id"] as string);
15              funcionario.Nome = resultado["Nome"] as string;
16              funcionario.Salario = resultado.GetDouble
                (resultado.GetOrdinal("Salario"));
17              funcionario.Dependentes = 
                this.FindByFuncionario(funcionario.ID, conexao);
18              return funcionario;
19          }
20          else return null;
21      }
22  }
23  
24  private List<Dependente> FindByFuncionario
    (int id, OdbcConnection conexao)
25  {
26      List<Dependente> dependentes = new List<Dependente>();
27      string sql = @"SELECT * FROM Dependentes WHERE FuncionarioId = ?";
28  
29      OdbcCommand comando = new OdbcCommand(sql, conexao);
30      comando.Parameters.AddWithValue("@FuncionarioId", id);
32      OdbcDataReader resultado = comando.ExecuteReader();
33      while (resultado.Read())
34      {
35          Dependente d = new Dependente();
36          d.ID = resultado.GetInt32(resultado.GetOrdinal("Id"));
37          d.Nome = resultado.GetString(resultado.GetOrdinal("Nome"));
38          d.Parentesco = 
            resultado.GetString(resultado.GetOrdinal("Parentesco"));
39          dependentes.Add(d);
40      }
41      return dependentes;
42  }
Listagem 7. Métodos de busca de funcionários e dependentes

O método FindById e FindByFuncionario (Listagem 7) será utilizado para carregarmos um funcionário com todos os seus dependentes para serem exibidos para a interface.

No método FindById da classe FuncionarioDao, primeiramente é criada a string do select (linha 3) que será efetuado, onde vamos carregar um registro da tabela Funcionarios passando o seu identificador. Em seguida mais uma vez fazemos uso do bloco using para a criação e utilização da conexão com o banco de dados (linha 5).

Agora observe que para a criação do OdbcCommand não faremos mais uso da transação (linha 07), porque em casos de consultas não se faz necessário o uso de transações, simplesmente é passada a conexão e o comando sql desejado.

Existem desenvolvedores que não entendem ao certo o conceito de transações e iniciam transações sem necessidade, mesmo quando só existem comandos de consulta para fazer, onde essa prática não se faz necessária.

O que muda na execução destes comandos de busca em relação aos comandos de atualização no banco, é que desta vez não será usado o método ExecuteNonQuery e sim o ExecuteReader (linha 10).

Porém este método não é void e possui um retorno, que é da classe OdbcDataReader, uma classe onde é possível acessar os dados que foram retornados do banco de dados, por isso criamos um objeto resultado do tipo OdbcDataReader.

Para fazer leitura desses dados da consulta, se faz necessário o uso de um while chamando o método Read da classe OdbcDataReader a cada iteração (linha 11). Este método retornará false quando não houver mais registros a consultar, saindo fora do laço de repetição.

Para cada registro encontrado, criamos uma instância da classe Funcionario e preenchemos seus atributos com cada campo correspondente da base de dados, através de métodos específicos para cada tipo de dados (GetString, GetDouble, etc) e passando o nome do campo desejado através do método GetOrdinal da classe OdbcReader (linhas 13 a 16).

Para a propriedade Dependentes da classe Funcionario fazemos a chamada do método seguinte, o método privado FindByFuncionario (linha 17), onde passamos a conexão que estamos utilizando e o identificador do funcionário que estamos querendo saber seus dependentes.

O método FindByFuncionario retorna uma lista de dependentes, para isso usamos a classe List, com generics. Primeiro é criada uma lista da classe Dependente e o comando sql que fará a consulta (linhas 26 e 27), setando o parâmetro e faz-se a consulta através do ExecuteReader (linha 32).

Depois percorre-se o resultado da consulta criando para cada registro um objeto da classe Dependente (linhas 35 a 39) que posteriormente é adicionado na lista final que será retornada (linha 41).

Atente novamente que não foi necessária nenhuma operação com relação ao estabelecimento de conexões e abertura de conexões, ela já veio pronta ao ser passada por parâmetro pelo método principal.


01 public List<Funcionario> FindAll()
02 {
03     List<Funcionario> funcionarios = new List<Funcionario>();
04     string sql = @"SELECT * FROM Funcionarios";
05     using (OdbcConnection conexao = ConnectionFactory.CreateConnection())
06     {
07         OdbcCommand comando = new OdbcCommand(sql, conexao);
08         conexao.Open();
09         OdbcDataReader resultado = comando.ExecuteReader();
10         while (resultado.Read())
11         {
12             Funcionario f = new Funcionario();
13             f.ID = resultado.GetInt32(resultado.GetOrdinal("Id"));
14             f.Nome = resultado.GetString(resultado.GetOrdinal("Nome"));
15             f.Departamento = 
               resultado.GetString(resultado.GetOrdinal("Departamento"));
16             f.Salario = resultado.GetDouble(resultado.GetOrdinal("Salario"));
17             f.Dependentes = this.FindByFuncionario(f.ID, conexao);
18             funcionarios.Add(f);
19         }
20     }
21     return funcionarios;
22 }
Listagem 8. Método que busca todos os funcionários

Para finalizar a classe DAO temos o método que busca todos os funcionários do sistema da Listagem 8. Este primeiramente cria uma lista de funcionários em memória e em seguida uma string com o comando que busca todos os funcionários da tabela (linhas 3 e 4).

Logo após é estabelecida a conexão com o banco de dados de forma protegida pelo bloco using, cria-se o comando, passando a conexão e o sql desejado e se executa o comando (linhas 5 a 9).

É feita a iteração em todos os registros retornados e criado um novo objeto funcionário para cada linha retornada. Para cada um dos funcionários também é feita a busca de seus dependentes através do método FindByFuncionario, passando a conexão e o identificador do funcionário desejado (linhas 12 a 18).

No fim esta lista de funcionário com todos os seus dependentes já carregados em memória é devolvido pelo método (linha 21).

Formulário Principal de Funcionários

Quanto ao desenvolvimento da interface gráfica do sistema, a primeira lição é que nunca escreveremos comandos SQL nessa fase de desenvolvimento.

Todas as operações de busca e atualizações de dados no banco serão feitas pela camada DAO e os formulários terão o único trabalho de usar a classe DAO e seus métodos.

Existem classes que facilitam a vida do desenvolvedor, como o SQLDataSource, mas eles não estão de acordo com os princípios da orientação a objetos (SOLID) e os padrões de projeto de software (Design Patterns).

Isso pelo fato deles ao fazer o uso deste tipo de componente estamos acessando diretamente a base de dados, quando apenas classes DAO deveriam ter este acesso. Esses componentes são recomendados em sistemas menores ou para iniciantes em programação que desejam ter um resultado rápido do aprendizado.

Para sistemas de médio e grande porte o uso desta abordagem pode ser muito prejudicial a longo prazo para os sistemas.

O desenvolvimento da tela principal de funcionários (Figura 2), temos na parte superior uma barra de ferramentas com os botões para trabalharmos com os dados. Logo abaixo um ComboBox onde escolhemos o funcionário que desejamos exibir seus dados e dependentes.

Declaramos também alguns atributos privados na classe do formulário para trabalharmos com os objetos e listas do restante do sistema, conforme Listagem 9.

Automaticamente já instanciamos a classe FuncionarioDao e ela estará disponível para a aplicação durante todo o tempo que estiver em execução, facilitando o trabalho (linhas 1 a 3).


01 private FuncionarioDao funcionarioDao = new FuncionarioDao();
02 private List<Funcionario> funcionarios;
03 private Funcionario funcionario;
04 
05 public FormFuncionario()
06 {
07     InitializeComponent();
08 }
09 
10 private void FormFuncionario_Load(object sender, EventArgs e)
11 {
12     this.funcionarios = funcionarioDao.FindAll();
13     comboFuncionarios.DataSource = funcionarios;
14     comboFuncionarios.DisplayMember = "Nome";
15 }
Listagem 9. Atributos e inicialização do formulário
Interface principal do cadastro de funcionários
Figura 2. Interface principal do cadastro de funcionários

No evento Load do formulário principal estamos carregando todos os funcionários para nossa lista e configurando ComboBox para receber esta lista e através da propriedade DataSource e através da propriedade DisplayName, informamos qual campo desejamos que seja exibido para o usuário no combobox (linhas 12 a 14).

No evento SelectedIndexChanged do ComboBox chamamos o método CarregaDados da nossa classe DAO que preenche os controles de tela do formulário, incluindo o grid de dependentes, conforme a Listagem 10 (linhas 9 a 12).


01 private void comboFuncionarios_SelectedIndexChanged(object sender, EventArgs e)
02 {
03     this.funcionario = comboFuncionarios.SelectedItem as Funcionario;
04     this.CarregaDados();
05 }
06         
07 private void CarregaDados()
08 {
09     textoNome.Text = this.funcionario.Nome;
10     textoDepartamento.Text = this.funcionario.Departamento;
11     textoSalario.Text = Convert.ToString(this.funcionario.Salario);
12     gridDependentes.DataSource = this.funcionario.Dependentes;
13 }
Listagem 10. Evento do ComboBox e método que preenche controles de tela

Ao trocarmos de item no ComboBox carregamos o objeto vinculado ao ComboBox através da propriedade SelectedItem, fazendo um Casting (BOX 5) para o tipo de dados das nossas classes (linha 3) e chamamos o método CarregaDados.

O método CarregaDados popula os controles visuais do formulário com os atributos da classe Funcionario, fazendo as conversões de tipo quando necessário e carrega todos os dependentes para o gridDependentes através da propriedade DataSource.

BOX 5: Casting

O Casting é uma forma de forçarmos o compilador a fazer uma conversão para um tipo de dados que desejamos. Pode-se fazer casting de dados tanto de tipos primitivos, como int e double, quando de objetos como Cliente, Vendedor.

O Casting é uma técnica um tanto perigosa pois o compilador não nos avisa se estivermos fazendo uma conversão errada, o sistema executará normalmente e no caso de haver erros de conversão, esses ocorrerão em tempo de execução, algo bem desagradável.

Ao acionarmos o Novo apenas limpamos os controles de tela e instanciamos a classe Funcionarios do nosso formulário, conforme a Listagem 11 (linhas 3 e 4).

O método privado LimpaControles apenas limpa cada campo da tela principal e também o grid de dependentes, passando null para a propriedade DataSource (linhas 9 a 12).


01 private void botaoNovo_Click(object sender, EventArgs e)
02 {
03     LimpaControles();
04     this.funcionario = new Funcionario();
05 }
06         
07 private void LimpaControles()
08 {
09     textoNome.Clear();
10     textoDepartamento.Clear();
11     textoSalario.Clear();
12     gridDependentes.DataSource = null;
13 }
Listagem 11. Ação Novo e método LimpaControles

Os botões Adicionar e Remover estão codificados conforme a Listagem 12. O adicionar cria uma instância do formulário de dependentes passando por parâmetro ele próprio, através do this (linha 3).

Em seguida ele abre o formulário de dependentes que será responsável pelo preenchimento dos dados e posterior persistência através do método ShowDialog (linha 4). Depois de fechado o formulário de dependentes, os dados são atualizados novamente através do método CarregaDados (linha 5).

Já o remover, coletamos o item selecionado no DataGridView fazendo um Foreach nos itens selecionados (linha 10) e removemos o item da lista de dependentes do objeto funcionário (linha 12), por último os dados do grid são recarregados para serem atualizados (linha 13).


01 private void botaoAdicionar_Click(object sender, EventArgs e)
02 {
03     FormDependente formDependente = new FormDependente(this);
04     formDependente.ShowDialog();
05     this.CarregaDados();
06 }
07 
08 private void botaoRemover_Click(object sender, EventArgs e)
09 {
10     foreach (DataGridViewRow item in this.gridDependentes.SelectedRows)
11     {
12         this.funcionario.Dependentes.RemoveAt
             (Convert.ToInt32(item.Cells[0].Value.ToString()));
13         this.CarregaDados();
14     }
15 }
Listagem 12. Implementação do Adicionar e Remover

Teremos um método público no formulário principal que será utilizado pelo formulário de dependentes para adicionar um dependente na lista.

Este método pode ser visto na Listagem 13, sendo que recebemos a instância de dependentes e adicionamos na lista (linha 3), atualizando os dados do grid na sequência (linha 4).


01  public void AdicionaDependente(Dependente dependente)
02  {
03      this.funcionario.Dependentes.Add(dependente);
04      this.CarregaDados();
05  }
06  
07  private void botaoSalvar_Click(object sender, EventArgs e)
08  {
09      this.PopulaObjeto();
10      try
11      {
12          funcionarioDao.Insert(funcionario);
13          MessageBox.Show("Funcionário e Dependentes cadastrados com sucesso");
14      }
15      catch (Exception ex)
16      {
17          MessageBox.Show(ex.Message);
18      }
19  }
20  
21  private void botaoExcluir_Click(object sender, EventArgs e)
22  {
23      try
24      {
25          funcionarioDao.Delete(funcionario.ID);
26          MessageBox.Show("Funcionário e Dependentes excluídos com sucesso");
27      }
28      catch (Exception ex)
29      {
30          MessageBox.Show(ex.Message);
31      }
32  }
33  
34  private void PopulaObjeto() {
35      funcionario.Nome = textoNome.Text;
36      funcionario.Departamento = textoDepartamento.Text;
37      funcionario.Salario = Convert.ToDouble(textoSalario.Text);
38  }
Listagem 13. Métodos AdicionaDependente, PopulaObjeto e eventos Salvar, Excluir

O evento de salvar inicia com a chamada ao método PopulaObjeto (linha 9) que coleta os dados informados no formulário e os atribui as propriedades do objeto (linhas 35 a 37) para em seguida executar o método Insert da classe FuncionarioDao passando este funcionário (linha 12).

Já o evento de exclusão é bem mais simples e simplesmente passamos o identificador do funcionário para o método Delete da classe FuncionarioDao (linhas 25 e 26).

Ambos esses eventos são circulados por try/catch, no caso de haver um erro, este será informado ao usuário através do MessageBox.

Formulário de Dependentes

Já temos um método que adiciona dependentes no formulário principal da aplicação, por isso este deverá ser chamado quando quisermos adicionar um objeto dependente na lista. Para isso o formulário de dependentes deverá ter uma referência do objeto principal (linha 1).

Declaramos um atributo privado na classe do formulário de dependentes e modificamos o construtor para receber a instância do formulário principal, conforme a Listagem 14 (linhas 3 a 7).


01 private FormFuncionario formPrincipal;
02        
03 public FormDependente(FormFuncionario formFuncionario)
04 {
05     this.formPrincipal = formFuncionario;
06     InitializeComponent();
07 }
Listagem 14. Referência ao formulário principal e construtor da classe

O botão confirmar irá adicionar um objeto de dependente na lista de dependentes do objeto de funcionários. Utilizamos o recurso Initializer (BOX 6) do C# para fazer a criação do objeto Dependente, conforme Listagem 15 (linhas 3 a 7).

Em seguida é chamado o método AdicionaDependente do formulário principal passando o objeto recém-criado (linha 8). Por último, uma mensagem de confirmação é exibida e o formulário e fechado (linhas 8 e 9). A interface do formulário pode ser vista na Figura 3.


01 private void botaoConfirmar_Click(object sender, EventArgs e)
02 {
03   Dependente dependente = new Dependente()
04   {
05       Nome = textoNome.Text,
06       Parentesco = textoParentesco.Text
07   };
08   this.formPrincipal.AdicionaDependente(dependente);
09   MessageBox.Show("Dependente cadastrado com sucesso.");
10   this.Close();
11 }
Listagem 15. Incluindo um dependente para o funcionário
Formulário de cadastro de Dependentes
Figura 3. Formulário de cadastro de Dependentes

BOX 6: Initializer

O Inicializer é um bloco de código que seve para inicializar as propriedades públicas de um objeto. Com isso evitamos repetir a mesma variável várias vezes para inicialização de propriedades de objeto de uma classe.

Relacionamentos mestre-detalhe são uma rotina no dia a dia de todo o analista/desenvolvedor de sistemas e deve-se sempre buscar as melhores alternativas de implementação deste tipo de situação. Existem várias formas de fazer a implementação, uma delas foi apresentada neste artigo e, com certeza, existem várias outras.

O que sempre se deve buscar é a utilização de boas práticas de programação, adotando os princípios da orientação a objetos e tentando seguir os vários padrões de projeto de software, a fim de se conseguir tem um sistema confiável, com um código limpo e de fácil entendimento de quem o veja, além é claro de deixar esse sistema pronto para melhoria e extensões. E que essas mudanças posteriores não acarretem em muita manutenção nesse sistema.

A separação do sistema em camadas independentes possibilitam por exemplo, uma adição de mais uma camada gráfica, como para um dispositivo móvel, sem que sejam modificadas as classes de negócio e as classes DAO de acesso a dados, ou seja, pode-se ter vários clientes utilizando a mesma base.

Confira também