Skip to content

9. Acesso aos bancos de dados

9.1. Conector ADO.NET

Vamos retomar a arquitetura em camadas utilizada em diversas ocasiões

Nos exemplos analisados, a camada [dao] utilizou, até o momento, dois tipos de fontes de dados:

  • dados embutidos no código
  • dados provenientes de arquivos de texto

Neste capítulo, estudaremos o caso em que os dados provêm de um banco de dados. A arquitetura de três camadas evolui, então, para uma arquitetura multicamadas. Existem diversas variantes. Estudaremos os conceitos básicos com a seguinte:

No esquema acima, a camada [dao] [1] se comunica com a SGBD [3] por meio deuma biblioteca de classes específica do SGBD utilizado e fornecida junto com ele. Essa camada implementa funcionalidades padrão reunidas sob o termo ADO (Active X Data Objects). Chama-se a essa camada de provedor (neste caso, provedor de acesso a um banco de dados) ou conector. A maioria dos SGBD dispõe agora de um conector ADO.NET, o que não era o caso nos primórdios da plataforma .NET. Os conectores .NET não oferecem uma interface padrão para a camada [dao]; por isso, esta última contém em seu código os nomes das classes do conector. Se mudarmos de SGBD, mudamos de conector e de classes, e, portanto, é necessário alterar a camada [dao]. Trata-se de uma arquitetura eficiente, pois o conector .NET, tendo sido escrito para um SGBD específico, sabe como utilizá-lo da melhor forma possível, e também é rígida, pois mudar de SGBD implica mudar a camada [dao]. Esse segundo argumento deve ser relativizado: as empresas não trocam de SGBD com muita frequência. Além disso, veremos mais adiante que, desde a versão 2.0 do .NET, existe um conector genérico que proporciona flexibilidade sem sacrificar o desempenho.

9.2. Os dois modos de utilização de uma fonte de dados

A plataforma .NET permite a exploração de uma fonte de dados de duas maneiras diferentes:

  1. modo conectado
  2. modo desconectado

No modo conectado, o aplicativo

  1. abre uma conexão com a fonte de dados
  2. trabalha com a fonte de dados em modo de leitura/gravação
  3. fecha a conexão

No modo desconectado, o aplicativo

  1. abre uma conexão com a fonte de dados
  2. obtém uma cópia em memória de todos ou parte dos dados da fonte
  3. encerra a conexão
  4. trabalha com a cópia na memória dos dados em modo de leitura/gravação
  5. quando o trabalho estiver concluído, abre uma conexão, envia os dados modificados à fonte de dados para que ela os registre e fecha a conexão

Aqui, abordamos apenas o modo conectado.

9.3. Os conceitos básicos da utilização de um banco de dados

Apresentaremos os principais conceitos de uso de um banco de dados utilizando o SQL Server Compact 3.5. Esse SGBD é fornecido com o Visual Studio Express. Trata-se de um SGBD leve, capaz de gerenciar apenas um usuário por vez. No entanto, é suficiente para uma introdução à programação com bancos de dados. Posteriormente, apresentaremos outros SGBD.

A arquitetura utilizada será a seguinte:

Um aplicativo de console [1] utilizará um banco de dados do tipo SqlServer Compact [3,4] por meio do conector Ado.Net deste SGBD [2].

9.3.1. A é um exemplo de banco de dados

Vamos criar o banco de dados diretamente no Visual Studio Express. Para isso, criaremos um novo projeto do tipo console.

  • [1]: o projeto
  • [2]: abrimos a janela “Explorador de bancos de dados”
  • [3]: criamos uma nova conexão
  • [4]: seleciona-se o tipo do SGBD
  • [5,6]: escolhe-se o SGBD SQL Server Compact
  • [7]: cria-se o banco de dados
  • [8]: um banco de dados SQL Server Compact é encapsulado em um único arquivo com a extensão .sdf. Indica-se onde criá-lo, neste caso, na pasta do projeto C#.
  • [9]: atribuímos o nome [dbarticles.sdf] ao novo banco de dados
  • [10]: seleciona-se o idioma francês. Isso afeta as operações de classificação.
  • [11,12]: o banco de dados pode ser protegido por uma senha. Aqui, “dbarticles”.
  • [13]: confirme a página de informações. O banco de dados será criado fisicamente:
  • [14]: o nome do banco de dados que acabou de ser criado
  • [15]: marque a opção “Salvar minha senha” para não precisar digitá-la novamente todas as vezes
  • [16]: verifica-se a conexão
  • [17]: está tudo certo
  • [18]: confirme a página de informações
  • [19]: a conexão aparece no explorador de bancos de dados
  • [20]: por enquanto, o banco de dados não possui tabelas. Vamos criar uma. Um artigo terá os seguintes campos:
    • id: um identificador único — chave primária
    • nom: nome do artigo — único
    • prix: preço do artigo
    • stockactuel: seu estoque atual
    • stockminimum: o estoque mínimo abaixo do qual é necessário reabastecer o artigo
  • [21]: o campo [id] é do tipo inteiro e é a chave primária [22] da tabela.
  • [23]: essa chave primária é do tipo Identity. Esse conceito específico do servidor SGBD SQL indica que a chave primária será gerada pelo próprio SGBD. Aqui, a chave primária será um número inteiro que começa em 1 e é incrementado em 1 a cada nova chave.
  • [24]: os demais campos são criados. Observe-se que o campo [nom] possui uma restrição de exclusividade na [25].
  • [26]: atribui-se um nome à tabela
  • [27]: após validar a estrutura da tabela, ela aparece no banco de dados.
  • [28]: solicita-se a visualização do conteúdo da tabela
  • [29]: ela está vazia por enquanto
  • [30]: preenche-se com alguns dados. Uma linha é validada assim que se passa para a inserção da linha seguinte. O campo [id] não é preenchido: ele é gerado automaticamente quando a linha é validada.

Resta-nos configurar o projeto para que essa base de dados, que atualmente está na raiz do projeto, seja copiada automaticamente para a pasta de execução do projeto:

  • [1]: solicitamos a exibição de todos os arquivos
  • [2]: o arquivo [dbarticles.sdf] aparece
  • [3]: ela é incluída no projeto
  • [4]: a operação de adição de uma fonte de dados a um projeto inicia um assistente do qual não precisamos aqui [5].
  • [6]: o banco de dados agora faz parte do projeto. Voltamos ao modo normal [7].
  • [8]: o projeto e seu banco de dados
  • [9]: nas propriedades do banco de dados, podemos ver que [10] será automaticamente copiado para a pasta de execução do projeto. É lá que o programa que vamos escrever irá buscá-lo.

Agora que temos um banco de dados disponível, poderemos utilizá-lo. Antes disso, vamos fazer algumas revisões SQL.

9.3.2. Os quatro comandos básicos da linguagem SQL

SQL (Structured Language Query) é uma linguagem, parcialmente padronizada, para consulta e atualização de bancos de dados. Todas as SGBD respeitam a parte padronizada do SQL, mas acrescentam à linguagem extensões proprietárias que exploram certas particularidades do SGBD. Já encontramos dois exemplos disso: a geração automática de chaves primárias e os tipos permitidos para as colunas de uma tabela costumam depender do SGBD.

Os quatro comandos básicos da linguagem SQL que apresentamos são padronizados e aceitos por todos os SGBD:

select col1, col2,... 
from table1, table2,...
where condition
order by expression
...
A consulta que permite obter os dados contidos em um banco de dados. Apenas as palavras-chave da primeira linha são obrigatórias; as demais são opcionais. Existem outras palavras-chave não apresentadas aqui.
  1. É realizada uma junção com todas as tabelas que se encontram após a palavra-chave from
  2. Apenas as colunas que se seguem à palavra-chave `select` são mantidas
  3. Somente as linhas que atendem à condição da palavra-chave `where` são mantidas
  4. As linhas resultantes, ordenadas de acordo com a expressão da palavra-chave `order by`, formam o resultado da consulta. Esse resultado é uma tabela.
insert into table(col1,col2, ...)
values (val1,val2, ...)
Inserir uma linha na tabela. (col1, col2, ...) especifica as colunas da linha a serem preenchidas com os valores (val1, val2, ...).
update table
set col1=val1, col2=val2
where condition
Atualiza as linhas da tabela que atendem à condição (todas as linhas, se não houver where). Para essas linhas, a coluna coli recebe o valor vali
delete from table
where condition
Exclui todas as linhas da tabela que atendem à condição

Vamos escrever um aplicativo de console que permita emitir comandos SQL no banco de dados [dbarticles] que criamos anteriormente. Aqui está um exemplo de execução. O leitor é convidado a compreender os comandos SQL emitidos e seus resultados.

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;Persist Security Info=True]

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

Requête SQL (rien pour arrêter) : insert into articles(nom,prix,stockactuel,stockminimum) values('x',100,10,1)
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 100 10 1

Requête SQL (rien pour arrêter) : update articles set prix=prix*1.1 where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
9 x 110 10 1

Requête SQL (rien pour arrêter) : delete from articles where id=9
Il y a eu 1 ligne(s) modifiée(s)

Requête SQL (rien pour arrêter) : select id,nom,prix,stockactuel,stockminimum from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
...
6 bouteilles d'oxygène 120 10 5
  • linha 1: a chamada string de conexão: ela contém todos os parâmetros necessários para se conectar ao banco de dados.
  • linha 3: solicita-se o conteúdo da tabela [articles]
  • linha 16: insere-se uma nova linha. Observe-se que o campo id não é inicializado nesta operação, pois é o SGBD que irá gerar o valor desse campo.
  • linha 19: verificação. Linha 28: a linha foi adicionada corretamente.
  • linha 30: aumenta-se em 10% o preço do item que acabou de ser adicionado.
  • linha 33: verifica-se
  • linha 42: o aumento do preço ocorreu corretamente
  • linha 44: excluímos o item que havíamos adicionado anteriormente
  • linha 47: verificamos
  • linhas 53-55: o item não está mais lá.

9.3.3. As interfaces básicas do ADO.NET para o modo conectado

Voltemos ao esquema de um aplicativo que utiliza um banco de dados por meio de um conector ADO.NET:

No modo conectado, a aplicação:

  1. abre uma conexão com a fonte de dados
  2. trabalha com a fonte de dados em modo de leitura/gravação
  3. encerra a conexão

Três interfaces ADO.NET são as principais envolvidas nessas operações:

  • IDbConnection, que encapsula as propriedades e métodos da conexão.
  • IDbCommand, que encapsula as propriedades e métodos do comando SQL executado.
  • IDataReader, que encapsula as propriedades e métodos do resultado de um comando SQL Select.

A interface IDbConnection

Serve para gerenciar a conexão com o banco de dados. Os métodos M e propriedades P dessa interface que utilizaremos são os seguintes:

Nome
Tipo
Função
ConnectionString
P
cadeia de conexão com o banco de dados. Ela especifica todos os parâmetros necessários para estabelecer a conexão com um banco de dados específico.
Open
M
abre a conexão com o banco de dados definido por ConnectionString
Close
M
encerra a conexão
BeginTransaction
M
inicia uma transação.
State
P
estado da conexão: ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken

Se Connection for uma classe que implementa a interface IDbConnection, a conexão pode ser estabelecida da seguinte forma:

1
2
3
IDbConnection connexion=new Connection();
connexion.ConnectionString=...;
connexion.Open();

A interface IDbCommand

Serve para executar uma ordem SQL ou um procedimento armazenado. Os métodos M e propriedades P desta interface que utilizaremos serão os seguintes:

Nome
Tipo
Função
CommandType
P
indica o que deve ser executado — obtém seus valores de uma enumeração:
- CommandType.Text: executa a ordem SQL definida na propriedade CommandText. Esse é o valor padrão.
- CommandType.StoredProcedure: executa um procedimento armazenado no banco de dados
CommandText
P
- o texto da ordem SQL a ser executada se CommandType = CommandType.Text
- o nome da procedimento armazenado a ser executado se CommandType = CommandType.StoredProcedure
Connection
P
a conexão IDbConnection a ser utilizada para executar a ordem SQL
Transaction
P
a transação IDbTransaction na qual executar a ordem SQL
Parameters
P
a lista de parâmetros de uma ordem SQL configurada. A ordem update articles set price=price*1.1 where id=@id possui o parâmetro @id.
ExecuteReader
M
para executar uma ordem SQL Select. Obtém-se um objeto IDataReader que representa o resultado do Select.
ExecuteNonQuery
M
para executar uma ordem SQL: Atualizar, Inserir, Excluir. É exibido o número de linhas afetadas pela operação (atualizadas, inseridas, excluídas).
ExecuteScalar
M
para executar uma ordem SQL Select, que retorna apenas um único resultado, como em: select count(*) from articles.
CreateParameter
M
para criar os parâmetros IDbParameter de uma ordem SQL configurada.
Prepare
M
permite otimizar a execução de uma consulta parametrizada quando ela é executada várias vezes com parâmetros diferentes.

Se Command for uma classe que implementa a interface IDbCommand, a execução de uma ordem SQL sem transação terá a seguinte forma:

// abertura da conexão 
IDbConnection connexion=...
connexion.Open();
// preparação do pedido
IDbCommand commande=new Command();
commande.Connection=connexion;
// execução da ordem select
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// execução da ordem update, insert, delete
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// encerramento da conexão
connexion.Close();

A interface IDataReader

serve para encapsular os resultados de uma ordem SQL Select. Um objeto IDataReader representa uma tabela com linhas e colunas, que é processada sequencialmente: primeiro a primeira linha, depois a segunda, ... Os métodos M e propriedades P dessa interface que utilizaremos serão os seguintes:

Nome
Tipo
Função
FieldCount
P
o número de colunas da tabela IDataReader
GetName
M
GetName(i) retorna o nome da coluna nº i da tabela IDataReader.
Item
P
Item[i] representa a coluna nº i da linha atual da tabela IDataReader.
Read
M
avança para a próxima linha da tabela IDataReader. Retorna o valor booleano True se a leitura foi bem-sucedida; caso contrário, retorna False.
Close
M
fecha a tabela IDataReader.
GetBoolean
M
GetBoolean(i): retorna o valor booleano da coluna nº i da linha atual da tabela IDataReader. Os outros métodos análogos são os seguintes: GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString.
Getvalue
M
Getvalue(i): retorna o valor da coluna nº i da linha atual da tabela IDataReader como o tipo object.
IsDBNull
M
IsDBNull(i) retorna True se a coluna nº i da linha atual da tabela IDataReader não tiver valor, o que é simbolizado pelo valor SQL NULL.

A análise de um objeto IDataReader costuma ser semelhante ao seguinte:

// abertura da conexão 
IDbConnection connexion=...
connexion.Open();
// preparação do comando
IDbCommand commande=new Command();
commande.Connection=connexion;
// execução da ordem SELECT
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// análise dos resultados
while(reader.Read()){
     // análise da linha atual
        ...
}
// fechamento do leitor
reader.Close();
// encerramento da conexão
connexion.Close();

9.3.4. Gerenciamento de erros

Vamos revisar a arquitetura de um aplicativo com banco de dados:

A camada [dao] pode encontrar diversos erros durante a operação do banco de dados. Esses erros serão reportados como exceções lançadas pelo conector ADO.NET. O código da camada [dao] deve lidar com elas. Toda operação com o banco de dados deve ser realizada dentro de um bloco try / catch / finally para interceptar e tratar uma eventual exceção, além de liberar os recursos que precisam ser liberados. Assim, o código visto acima para processar o resultado de uma ordem Select passa a ser o seguinte:

// inicialização da conexão
IDbConnection connexion=...
// processamento da conexão
try{
     // abertura
    connexion.Open();
     // preparação do pedido
    IDbCommand commande=new Command();
    commande.Connection=connexion;
     // execução da ordem de seleção
    commande.CommandText="select ...";
    IDbDataReader reader=commande.ExecuteReader();
     // análise de resultados
    try{
        while(reader.Read()){
         // análise da linha atual
        ...
    }finally{
         // fechamento do leitor
        reader.Close();
    }
}catch(Exception ex){
     // gestão de exceção
    ...
}finally{
     // encerramento da conexão
    connexion.Close();
}
...

Seja qual for o caso, os objetos IDataReader e IDbConnection devem ser fechados. É por isso que esse fechamento é feito nas cláusulas finally.

O encerramento da conexão e do objeto IDataReader podem ser automatizados com uma cláusula using:

// análise da conexão
try{
    using(IDbConnection connexion=...){
         // abertura
        connexion.Open();
         // preparação do pedido
        IDbCommand commande=new Command();
        commande.Connection=connexion;
         // execução da ordem de seleção
        commande.CommandText="select ...";
        using(IDbDataReader reader=commande.ExecuteReader()){
             // análise de resultados
            while(reader.Read()){
             // análise da linha atual
            ...
        }// usando IData
    }//usando IDbConnection
}catch(Exception ex){
     // gestão de exceções
    ...
}
..
  • Na linha 3, a cláusula `using` garante que a conexão aberta no bloco `using(...){...}` será fechada fora dele, independentemente da forma como se sai do bloco: normalmente ou por ocorrência de uma exceção. Economizamos um finally, mas o interesse não está nessa economia insignificante. O uso de um using evita que o desenvolvedor precise fechar a conexão manualmente. No entanto, esquecer de fechar uma conexão pode passar despercebido e “travar” o aplicativo de uma forma que parecerá aleatória, sempre que o SGBD atingir o número máximo de conexões abertas que ele pode suportar.
  • Linha 11: procede-se de maneira análoga para fechar o objeto IDataReader.

9.3.5. Configuração do projeto de exemplo

O projeto final ficará da seguinte forma:

  • [1]: o projeto terá um arquivo de configuração [App.config]
  • [2]: ele utiliza classes de dois arquivos DLL que não são referenciados por padrão e, portanto, devem ser adicionados às referências do projeto:
    • [System.Configuration] para utilizar o arquivo de configuração [App.config]
    • [System.Data.SqlServerCe] para utilizar o banco de dados SQL Server Compact
  • [3, 4]: explica como adicionar referências a um projeto.
  • [5, 6]: relembra como adicionar o arquivo [App.config] a um projeto.

O arquivo de configuração [App.config] será o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
  <add name="dbSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
 </connectionStrings>
</configuration>
  • linhas 3-5: a tag <connectionStrings>, no plural, define cadeias de conexão a bancos de dados. Uma cadeia de conexão tem o formato “parâmetro1=valor1;parâmetro2=valor2;...”. Ela define todos os parâmetros necessários para estabelecer uma conexão com um banco de dados específico. Essas cadeias de conexão variam a cada SGBD. O site [http://www.connectionstrings.com/] fornece o formato dessas cadeias para os principais SGBD.
  • linha 4: define uma string de conexão específica, neste caso a do banco de dados SQL Server Compact dbarticles.sdf que criamos anteriormente:
    • name = nome da cadeia de conexão. É por meio desse nome que uma cadeia de conexão é recuperada pelo programa C#
    • connectionString: a cadeia de conexão para um banco de dados SQL Server Compact
    • DataSource: indica o caminho do banco de dados. A sintaxe |DataDirectory| indica a pasta de execução do projeto.
    • Senha: a senha do banco de dados. Este parâmetro fica em branco se não houver senha.

O código C# para recuperar a string de conexão anterior é o seguinte:


string connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
  • ConfigurationManager é a classe de DLL [System.Configuration] que permite utilizar o arquivo [App.config].
  • ConnectionsStrings["nom"].ConnectionString: designa o atributo connectionString da tag < add name="nome" connectionString="..."> da seção <connectionStrings> de [App.config]

O projeto já está configurado. Passaremos agora a analisar a classe [Program.cs], cujo exemplo de execução vimos anteriormente.

9.3.6. O programa de exemplo

O programa [program.cs] é o seguinte:


using System;
using System.Collections.Generic;
using System.Data.SqlServerCe;
using System.Text;
using System.Text.RegularExpressions;
using System.Configuration;

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {

            // console do aplicativo — executa consultas SQL digitadas no teclado
            // em um banco de dados cuja string de conexão é obtida em um arquivo de configuração

            // processamento do arquivo de configuração [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // exibição da string de conexão
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // é criado um dicionário dos comandos SQL aceitos
            string[] commandesSQL = new string[] { "select", "insert", "update", "delete" };
            Dictionary<string, bool> dicoCommandes = new Dictionary<string, bool>();
            for (int i = 0; i < commandesSQL.Length; i++) {
                dicoCommandes.Add(commandesSQL[i], true);
            }

            // Leitura e execução dos comandos SQL digitados no teclado
            string requête = null;    // texto da consulta SQL
            string[] champs;            // os campos da consulta    
            Regex modèle = new Regex(@"\s+");    // sequência de espaços

            // ciclo de digitação e execução dos comandos SQL digitados no teclado
            while (true) {
                // solicitação da consulta
                Console.Write("\nRequête SQL (rien pour arrêter) : ");
                requête = Console.ReadLine().Trim().ToLower();
                // concluído?
                if (requête == "")
                    break;
                // decompondo a consulta em campos
                champs = modèle.Split(requête);
                // consulta válida?
                if (champs.Length == 0 || ! dicoCommandes.ContainsKey(champs[0])) {
                    // mensagem de erro
                    Console.WriteLine("Requête invalide. Utilisez select, insert, update, delete ou rien pour arrêter");
                    // próxima consulta
                    continue;
                }
                // execução da consulta
                if (champs[0] == "select") {
                    ExecuteSelect(connectionString, requête);
                } else
                    ExecuteUpdate(connectionString, requête);
            }
        }

        // execução de uma consulta de atualização
        static void ExecuteUpdate(string connectionString, string requête) {
    ...
        }

        // execução de uma consulta Select
        static void ExecuteSelect(string connectionString, string requête) {
....
        }
    }
}
  • linhas 1-6: os namespaces utilizados na aplicação. O gerenciamento de um banco de dados SQL Server Compact requer o namespace [System.Data.SqlServerCe] da linha 3. Há aqui uma dependência de um espaço de nomes proprietário do SGBD. Pode-se deduzir que o programa precisará ser modificado caso haja uma mudança no SGBD.
  • linha 18: a string de conexão com o banco de dados é lida no arquivo [App.config] e exibida na linha 25. Ela será utilizada para estabelecer uma conexão com o banco de dados.
  • linhas 28-32: um dicionário que armazena os nomes dos quatro comandos SQL autorizados: select, insert, update, delete.
  • linhas 40-62: o ciclo de entrada dos comandos SQL digitados no teclado e sua execução no banco de dados
  • linha 48: a linha digitada no teclado é dividida em campos para identificar o primeiro termo, que deve ser: select, insert, update, delete
  • linhas 50-55: se a consulta for inválida, é exibida uma mensagem de erro e passa-se para a consulta seguinte.
  • linhas 57-61: executa-se o comando SQL digitado. Essa execução assume uma forma diferente dependendo se se trata de um comando select ou de um comando insert, update, delete. No primeiro caso, a ordem retorna dados do banco de dados sem alterá-lo; no segundo, ela o atualiza sem retornar dados. Em ambos os casos, a execução é delegada a um método que requer dois parâmetros:
    • a cadeia de conexão que permitirá que ele se conecte ao banco de dados
    • a instrução SQL a ser executada nessa conexão

9.3.7. Execução de uma consulta SELECT

A execução de ordens SQL requer as seguintes etapas:

  1. Conexão com o banco de dados
  2. Envio das ordens SQL para o banco de dados
  3. Processamento dos resultados da ordem SQL
  4. Encerramento da conexão

As etapas 2 e 3 são realizadas repetidamente, sendo que o encerramento da conexão ocorre somente ao final da exploração do banco de dados. As conexões abertas são recursos limitados de um SGBD. É preciso economizá-las. Por isso, sempre se buscará limitar o tempo de vida de uma conexão aberta. No exemplo analisado, a conexão é encerrada após cada ordem SQL. Uma nova conexão é aberta para a ordem SQL seguinte. A abertura/encerramento de uma conexão é onerosa. Para reduzir esse custo, alguns SGBD oferecem o conceito de pools de conexões abertas: durante a inicialização do aplicativo, N conexões são abertas e atribuídas ao pool. Elas permanecerão abertas até o fim do aplicativo. Quando a aplicação abre uma conexão, ela recebe uma das N conexões já abertas do pool. Quando ela fecha a conexão, esta é simplesmente devolvida ao pool. A vantagem desse sistema é que ele é transparente para o desenvolvedor: o programa não precisa ser modificado para utilizar o pool de conexões. A configuração do pool de conexões depende do SGBD.

Primeiramente, vamos nos concentrar na execução das ordens SQL e Select. O método ExecuteSelect do nosso programa de exemplo é o seguinte:


// execução de uma consulta Select
        static void ExecuteSelect(string connectionString, string requête) {
            // tratamento de possíveis exceções
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                    // abertura da conexão
                    connexion.Open();
                    // executa sqlCommand com consulta SELECT
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    SqlCeDataReader reader= sqlCommand.ExecuteReader();
                    // exibição dos resultados
                    AfficheReader(reader);
                }
            } catch (Exception ex) {
                // mensagem de erro
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // exibição do leitor
        static void AfficheReader(IDataReader reader) {
...
        }
  • linha 2: o método recebe dois parâmetros:
    • a string de conexão [connectionString], que permitirá que ele se conecte ao banco de dados
    • a instrução SQL Select [requête] a ser executada nessa conexão
  • linha 4: qualquer operação com um banco de dados pode gerar uma exceção que talvez seja necessário tratar. Isso é ainda mais importante aqui, pois os comandos SQL fornecidos pelo usuário podem conter erros de sintaxe. É preciso que possamos avisá-lo disso. Portanto, todo o código está dentro de um try/catch.
  • linha 5: há vários pontos a serem observados aqui:
    • a conexão com o banco de dados é inicializada com a string de conexão [connectionString]. Ela ainda não está aberta. Será aberta na linha 7.
    • A cláusula using (Recurso) {...} é uma facilidade sintática que garante a liberação do recurso Ressource — neste caso, uma conexão — ao sair do bloco controlado pelo using.
    • A conexão é de um tipo proprietário: SqlCeConnection, específico do SGBD SQL Server Compact.
  • linha 7: a conexão está aberta. É nesse momento que os parâmetros da cadeia de conexão são utilizados.
  • linha 9: um comando SQL é emitido por meio de um objeto proprietário SqlCeCommand. A linha 9 inicializa esse objeto com duas informações: a conexão a ser utilizada e o comando SQL a ser emitido por meio dele. O objeto SqlCeCommand serve tanto para executar uma ordem Select quanto uma ordem Update, Insert ou Delete. Suas propriedades e métodos foram apresentados no parágrafo 9.3.3.
  • linha 10: uma ordem SQL Select é executada por meio do método ExecuteReader doobjeto SqlCeCommand, que retorna um objeto IDataReader, cujos métodos e propriedades foram apresentados no parágrafo 9.3.3.
  • linha 12: a exibição dos resultados é realizada pelo método AfficheReader a seguir:

        // exibição do leitor
        static void AfficheReader(IDataReader reader) {
            using (reader) {
                // análise dos resultados
                // -- colunas
                StringBuilder ligne = new StringBuilder();
                int i;
                for (i = 0; i < reader.FieldCount - 1; i++) {
                    ligne.Append(reader.GetName(i)).Append(",");
                }
                ligne.Append(reader.GetName(i));
                Console.WriteLine("\n{0}\n{1}\n{2}\n", "".PadLeft(ligne.Length, '-'), ligne, "".PadLeft(ligne.Length, '-'));
                // -- dados
                while (reader.Read()) {
                    // análise da linha atual
                    ligne = new StringBuilder();
                    for (i = 0; i < reader.FieldCount; i++) {
                        ligne.Append(reader[i].ToString()).Append(" ");
                    }
                    Console.WriteLine(ligne);
                }
            }
}
  • linha 2: o método recebe um objeto IDataReader. Observe-se que, neste caso, utilizamos uma interface e não uma classe específica.
  • linha 3: a cláusula using é utilizada para gerenciar automaticamente o fechamento do objeto IDataReader.
  • linhas 8-10: exibem-se os nomes das colunas da tabela de resultados do Select. São as colunas coli da consulta `select col1, col2, ... from table ...`
  • linhas 14-21: percorre-se a tabela de resultados e exibem-se os valores de cada linha da tabela.
  • linha 18: não se conhece o tipo da coluna nº i do resultado, pois não se conhece a tabela consultada. Portanto, não é possível utilizar a sintaxe reader.GetXXX(i), em que XXX é o tipo da coluna nº i, já que esse tipo é desconhecido. Utiliza-se, então, a sintaxe reader.Item[i].ToString() para obter a representação da coluna nº i na forma de uma cadeia de caracteres. A sintaxe reader.Item[i].ToString() pode ser abreviada para reader[i].ToString().

9.3.8. Execução de uma ordem de atualização: INSERT, UPDATE, DELETE

O código do método ExecuteUpdate é o seguinte:


        // execução de uma solicitação de atualização
        static void ExecuteUpdate(string connectionString, string requête) {
            // gerenciamento de possíveis exceções
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                    // abertura da conexão
                    connexion.Open();
                    // executa sqlCommand com solicitação de atualização
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // exibição do resultado
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                // mensagem de erro
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
}

Já mencionamos que a execução de uma ordem de consulta Select não difere da execução de uma ordem de atualização Update, Insert, Delete apenas pelo método do objeto SqlCeCommand utilizado: ExecuteReader para Select, ExecuteNonQuery para Update, Insert, Delete. Comentamos apenas este último método no código acima:

  • linha 10: a ordem Update, Insert, Delete é executada pelo método ExecuteNonQuery do objeto SqlCeCommand. Se for bem-sucedido, esse método retorna o número de linhas atualizadas (update), inseridas (insert) ou excluídas (delete).
  • linha 12: esse número de linhas é exibido na tela

Recomenda-se ao leitor que consulte um exemplo de execução desse código, no parágrafo 9.3.2.

9.4. Outros conectores ADO.NET

O código que estudamos é proprietário: ele depende do namespace [System.Data.SqlServerCe] destinado ao SGBD SQL Server Compact. Agora, vamos construir o mesmo programa com diferentes conectores .NET e ver o que muda.

9.4.1. Conector SQL Server 2005

A arquitetura utilizada será a seguinte:

A instalação do SQL Server 2005 está descrita nos anexos, no parágrafo 1.1.

Criamos um segundo projeto na mesma solução de antes e, em seguida, criamos o banco de dados SQL Server 2005. O SGBD SQL Server 2005 deve ser iniciado antes das operações a seguir:

  • [1]: criar um novo projeto na solução atual e torná-lo o projeto ativo.
  • [2]: criar uma nova conexão
  • [3]: selecionar o tipo de conexão
  • [4]: selecionar o SGBD SQL Server
  • [5]: resultado da escolha anterior
  • [6]: use o botão [Browse] para indicar onde criar o banco de dados SQL Server 2005. O banco de dados está encapsulado em um arquivo .mdf.
  • [7]: escolha a raiz do novo projeto e nomeie o banco de dados como [dbarticles.mdf].
  • [8]: use a autenticação do Windows.
  • [9]: confirmar a página de informações
  • [11]: o banco de dados SQL Server
  • [12]: criar uma tabela. Ela será idêntica ao banco de dados SQL Server Compact criado anteriormente.
  • [13]: o campo [id]
  • [14]: o campo [id] é do tipo Identity.
  • [15,16]: o campo [id] é a chave primária
  • [17]: os demais campos da tabela
  • [18]: nomeie a tabela como [articles] ao salvá-la (Ctrl+S).

Resta inserir dados na tabela:

Incluímos o banco de dados no projeto:

As referências do projeto são as seguintes:

O arquivo de configuração [App.config] é o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
  <add name="connectString1" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True;" />
     <add name="connectString2" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;Connect Timeout=30;" />
 </connectionStrings>
</configuration>
  • linha 4: a string de conexão com o banco de dados [dbarticles.mdf] com autenticação do Windows
  • linha 5: a string de conexão com o banco de dados [dbarticles.mdf] com autenticação no servidor SQL. [sa,msde] é o par (login, senha) do administrador do servidor SQL Server, conforme definido no parágrafo 1.1.

O programa [Program.cs] evolui da seguinte forma:


using System.Data.SqlClient;
...

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {
...
            // processamento do arquivo de configuração [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["connectString2"].ConnectionString;
            } catch (Exception e) {
...
            }
...
            // leitura e execução dos comandos SQL digitados no teclado
...
        }

        // execução de uma solicitação de atualização
        static void ExecuteUpdate(string connectionString, string requête) {
            // gerenciamento de possíveis exceções
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                    // abertura da conexão
                    connexion.Open();
                    // executa sqlCommand com solicitação de atualização
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // exibição do resultado
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
....
            }
        }

        // execução de uma consulta SELECT
        static void ExecuteSelect(string connectionString, string requête) {
            // tratamento de possíveis exceções
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                    // abertura da conexão
                    connexion.Open();
                    // executa sqlCommand com consulta SELECT
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    SqlDataReader reader = sqlCommand.ExecuteReader();
                    // análise dos resultados
...
                }
            } catch (Exception ex) {
...
            }
        }
    }
}
  • linha 1: o espaço de nomes [System.Data.SqlClient] contém as classes que permitem gerenciar um SQL Server 2005
  • linha 24: a conexão é do tipo SQLConnection
  • linha 28: o objeto que encapsula as instruções SQL é do tipo SQLCommand
  • linha 47: o objeto que encapsula o resultado de uma instrução SQL Select é do tipo SQLDataReader

O código é idêntico ao utilizado com o SGBD SQL Server Compact, exceto pelos nomes das classes. Para executá-lo, pode-se usar (linha 11) qualquer uma das duas cadeias de conexão definidas no [App.config].

9.4.2. Conector MySQL5

A arquitetura utilizada será a seguinte:

A instalação do MySQL5 está descrita nos anexos, no parágrafo 1.2, e a do conector Ado.Net, no parágrafo 1.2.5.

Criamos um terceiro projeto na mesma solução de antes e adicionamos a ele as referências necessárias:

  • [1]: o novo projeto
  • [2]: ao qual adicionamos referências
  • [3]: DLL, [MySQL.Data] do conector Ado.Net de MySql5, bem como a de [System.Configuration], [4].

Agora criamos o banco de dados [dbarticles] e sua tabela [articles]. O SGBD e o MySQL5 devem ser executados. Além disso, executa-se o cliente [Query Browser] (ver parágrafo 1.2.3).

  • [1]: no [Query Browser], clique com o botão direito do mouse na área [Schemata] [2] para criar [3], um novo esquema, termo que designa um banco de dados.
  • [4]: o banco de dados se chamará [dbarticles]. Em [5], ele pode ser visualizado. Por enquanto, ele não possui tabelas. Vamos executar o seguinte script SQL:
/* seleção do banco de dados atual */
USE dbarticles;
/* criação da tabela de itens */
CREATE TABLE ARTICLES (
    ID            INTEGER PRIMARY KEY AUTO_INCREMENT,
    NOM           VARCHAR(20) NOT NULL,
    PRIX          DOUBLE PRECISION NOT NULL,
    STOCKACTUEL   INTEGER NOT NULL,
    STOCKMINIMUM  INTEGER NOT NULL
);
/* inserção de dados na tabela */
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article1', 100, 10, 1);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article2', 200, 20, 2);
INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article3', 300, 30, 3);
/* adição de restrições */
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0);
ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>'');
ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);
  • linha 1: o banco de dados [dbarticles] passa a ser o banco de dados atual. Os comandos SQL que se seguem serão executados nele.
  • linhas 4-10: definição da tabela [ARTICLES]. Observe que a tabela SQL é proprietária da tabela MySQL. Os tipos das colunas e a geração automática da chave primária (atributo AUTO_INCREMENT) diferem do que foi observado com as tabelas SGBD e SQL do Server Compact e do Server Express.
  • linhas 12-14: inserção de três linhas
  • linhas 16-21: adição de restrições de integridade nas colunas.

Este script é executado no [MySQL Query Browser]:

  • no [MySQL Query Browser] [6], carrega-se o script [7]. Isso pode ser visto no [8]. No [9], ele é executado.
  • em [10], a tabela [articles] foi criada. Clicamos duas vezes nela. Isso faz com que apareça a janela [11] com a consulta [12] dentro dela, pronta para ser executada pelo [13]. No [14], o resultado da execução. Temos, de fato, as três linhas esperadas. Observe que os valores do campo [ID] foram gerados automaticamente (atributo AUTO_INCREMENT do campo).

Agora que o banco de dados está pronto, podemos voltar ao desenvolvimento do aplicativo no Visual Studio.

Em [1], o programa [Program.cs] e o arquivo de configuração [App.config]. Este é o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
 </connectionStrings>
</configuration>

Na linha 4, os elementos da cadeia de conexão são os seguintes:

  • Servidor: nome da máquina na qual se encontram os arquivos SGBD, MySQL, aqui localhost e c.a.d. É a máquina na qual o programa será executado.
  • Banco de dados: o nome do banco de dados gerenciado, neste caso, dbarticles
  • Uid: o login do usuário, neste caso root
  • Pwd: sua senha, neste caso root. Essas duas informações referem-se ao administrador criado no parágrafo 1.2.

O programa [Program.cs] é idêntico ao das versões anteriores, com as seguintes diferenças:

espace de noms
MySql.Data.MySqlClient
classe Connection
MySqlConnection
classe Command
MySqlCommand
classe DataReader
MySqlDataReader

O programa utiliza a cadeia de conexão denominada dbArticlesMySql5 no arquivo [App.config]. A execução produz os seguintes resultados:

Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

9.4.3. Conector ODBC

A arquitetura utilizada será a seguinte:

A vantagem dos conectores ODBC é que eles oferecem uma interface padrão para as aplicações que os utilizam. Assim, a nova aplicação poderá, com um único código, interagir com qualquer SGBD que possua um conector ODBC, c.a.d e a maioria dos SGBD. O desempenho dos conectores ODBC é inferior ao dos conectores “proprietários”, que sabem explorar todas as características de um SGBD específico. Em contrapartida, obtém-se uma grande flexibilidade da aplicação: é possível trocar de SGBD sem alterar o código.

Analisamos um exemplo em que a aplicação utiliza um banco de dados MySQL5 ou um banco de dados SQL Server Express, dependendo da cadeia de conexão fornecida. A seguir, supomos que:

  • os servidores SGBD, SQL (Server Express) e MySQL5 foram iniciados
  • que o driver ODBC do MySQL5 está presente na máquina (ver parágrafo 1.2.6). O driver do SQL Server 2005 está presente por padrão.
  • Os bancos de dados utilizados são os do parágrafo 9.4.2 para o banco MySQL5 e os do parágrafo 9.4.1 para o banco SQL Server Express.

O novo projeto do Visual Studio é o seguinte:

Acima, o banco de dados SQL Server [dbarticles.mdf], criado no parágrafo 9.4.1, foi copiado para a pasta do projeto.

O arquivo de configuração [App.config] é o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;" />
     <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
 </connectionStrings>
</configuration>
  • linha 4: a string de conexão da fonte ODBC MySQL5. Trata-se de uma string já analisada, na qual encontramos um novo parâmetro Driver que define o driver ODBC a ser utilizado.
  • linha 5: a string de conexão da fonte ODBC SQL Server Express. Trata-se da string já utilizada em um exemplo anterior, à qual foi adicionado o parâmetro Driver.

O programa [Program.cs] é idêntico ao das versões anteriores, com as seguintes diferenças:

espace de noms
System.Data.Odbc
classe Connection
OdbcConnection
classe Command
OdbcCommand
classe DataReader
OdbcDataReader

O programa utiliza uma das duas cadeias de conexão definidas no arquivo [App.config]. A execução produz os seguintes resultados:

Com a cadeia de conexão [dbArticlesOdbcSqlServer2005]:

Chaîne de connexion à la base : [Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 500,0000 10 5
2 pompe 10,0000 10 2
3 arc 610,0000 4 1
4 flèches - lot de 6 100,0000 12 20
5 combinaison de plongée 300,0000 8 2
6 Bouteilles d'oxygène 120,0000 10 5

Com a cadeia de conexão [dbArticlesOdbcMySql5]:

Chaîne de connexion à la base : [Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

9.4.4. Conector OLE DB

A arquitetura utilizada será a seguinte:

Assim como os conectores ODBC, os conectores OLE e DB (Object Linking and Embedding DataBase) apresentam uma interface padrão para os aplicativos que os utilizam. Os drivers ODBC permitem o acesso a bancos de dados. As fontes de dados para os drivers OLE e DB são mais variadas: bancos de dados, sistemas de mensagens, diretórios, etc. Qualquer fonte de dados pode ser objeto de um driver Ole DB, caso o desenvolvedor assim o decida. Dessa forma, obtém-se acesso padrão a uma grande variedade de dados.

Analisaremos um exemplo em que o aplicativo utiliza um banco de dados ACCESS ou um banco de dados SQL Server Express, dependendo da cadeia de conexão fornecida. A seguir, supomos que o SGBD SQL Server Express tenha sido iniciado e que o banco de dados utilizado seja o do exemplo anterior.

O novo projeto do Visual Studio é o seguinte:

  • em [1]: o espaço de nomes necessário para os conectores OLE e DB é [System.Data.OleDb], presente na referência [System.Data] acima. O banco de dados SQL Server [dbarticles.mdf] foi copiado do projeto anterior. O banco de dados [dbarticles.mdb] foi criado com o Access.
  • Em [2]: assim como o banco de dados SQL Server, o banco de dados ACCESS possui a propriedade [Copy to Output Directory=Copy Always] para que seja automaticamente copiado para a pasta de execução do projeto.

O banco de dados ACCESS [dbarticles.mdb] é o seguinte:

Em [1], a estrutura da tabela [articles] e, em [2], seu conteúdo.

O arquivo de configuração [App.config] é o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <connectionStrings>
     <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;"/>
     <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
 </connectionStrings>
</configuration>
  • linha 4: a string de conexão da fonte OLE DB ACCESS. Nele está o parâmetro Provider, que define o driver OLE DB a ser utilizado, bem como o caminho do banco de dados
  • linha 5: a string de conexão da fonte OLE DB Server Express.

O programa [Program.cs] é idêntico ao das versões anteriores, com as seguintes diferenças:

espace de noms
System.Data.OleDb
classe Connection
OleDbConnection
classe Command
OleDbCommand
classe DataReader
OleDbDataReader

O programa utiliza uma das duas cadeias de conexão definidas no arquivo [App.config]. A execução produz os seguintes resultados com a cadeia de conexão [dbArticlesOleDbAccess]:

Chaîne de connexion à la base : [Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
id,nom,prix,stockactuel,stockminimum
------------------------------------

1 vélo 1202 5 2
2 arc 5000 10 2
3 canoé 1502 12 6
4 fusil 3000 10 2
5 skis nautiques 1800 5 2
6 essai3 3 3 3
7 cachalot 200000 1 0
8 léopard 500000 1 1
9 panthère 800000 1 1

9.4.5. Conector genérico

A arquitetura utilizada será a seguinte:

Assim como os conectores ODBC, OLE e DB, o conector genérico oferece uma interface padrão para as aplicações que o utilizam, mas melhora o desempenho sem sacrificar a flexibilidade. De fato, o conector genérico se baseia nos conectores proprietários SGBD. O aplicativo utiliza classes do conector genérico. Essas classes atuam como intermediárias entre o aplicativo e o conector proprietário.

No exemplo acima, quando a aplicação solicita, por exemplo, uma conexão com o conector genérico, este retorna uma instância IDbConnection, a interface de conexões descrita no parágrafo 9.3.3, implementada por uma classe MySQLConnection ou SQLConnection, dependendo da natureza da solicitação que lhe foi feita. Diz-se que o conector genérico possui classes do tipo factory: utiliza-se uma classe factory para solicitar que ele crie objetos e forneça referências (ponteiros) a eles. Daí seu nome (factory = fábrica, fábrica de produção de objetos).

Não existe um conector genérico para todos os SGBD (abril de 2008). Para saber quais estão instalados em uma máquina, pode-se utilizar o seguinte programa:


using System;
using System.Data;
using System.Data.Common;

namespace Chap7 {
    class Providers {
        public static void Main() {
            DataTable dt = DbProviderFactories.GetFactoryClasses();
            foreach (DataColumn col in dt.Columns) {
                Console.Write("{0}|", col.ColumnName);
            }
            Console.WriteLine("\n".PadRight(40, '-'));
            foreach (DataRow row in dt.Rows) {
                foreach (object item in row.ItemArray) {
                    Console.Write("{0}|", item);
                }
                Console.WriteLine("\n".PadRight(40, '-'));
            }
        }
    }
}
  • linha 8: o método estático [DbProviderFactories.GetFactoryClasses()] retorna a lista de conectores genéricos instalados, na forma de uma tabela de banco de dados armazenada na memória (DataTable).
  • linhas 9-11: exibem os nomes das colunas da tabela dt:
    • dt.Columns é a lista das colunas da tabela. Uma coluna C é do tipo DataColumn
    • [DataColumn]. ColumnName é o nome da coluna
  • linhas 13-18: exibem as linhas da tabela dt:
    • dt.Rows é a lista das linhas da tabela. Uma linha L é do tipo DataRow
    • [DataRow]. ItemArray é uma matriz de objetos, em que cada objeto representa uma coluna da linha

O resultado da execução no meu computador é o seguinte:

Name|Description|InvariantName|AssemblyQualifiedName|
---------------------------------------
Odbc Data Provider|.Net Framework Data Provider for Odbc|System.Data.Odbc|System.Data.Odbc.OdbcFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OleDb Data Provider|.Net Framework Data Provider for OleDb|System.Data.OleDb|System.Data.OleDb.OleDbFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
OracleClient Data Provider|.Net Framework Data Provider for Oracle|System.Data.OracleClient|System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
SqlClient Data Provider|.Net Framework Data Provider for SqlServer|System.Data.SqlClient|System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|
---------------------------------------
Microsoft SQL Server Compact Data Provider|.NET Framework Data Provider for Microsoft SQL Server Compact|System.Data.SqlServerCe.3.5|System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=3.5.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91|
---------------------------------------
MySQL Data Provider|.Net Framework Data Provider for MySQL|MySql.Data.MySqlClient|MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=5.2.1.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d|
  • linha 1: a tabela tem quatro colunas. As três primeiras são as mais úteis para nós neste caso.

A exibição a seguir mostra que dispomos dos seguintes conectores genéricos:

Nome
Identificador
Odbc Data Provider
System.Data.Odbc
OleDb Data Provider
System.Data.OleDb
OracleClient Data Provider
System.Data.OracleClient
SqlClient Data Provider
System.Data.SqlClient
Microsoft SQL Server Compact Data Provider
System.Data.SqlServerCe.3.5
MySQL Data Provider
MySql.Data.MySqlClient

Um conector genérico pode ser acessado em um programa C# por meio de seu identificador.

Analisaremos um exemplo em que o aplicativo utiliza os diversos bancos de dados que construímos até agora. O aplicativo receberá dois parâmetros:

  • o primeiro parâmetro especifica o tipo de SGBD utilizado, para que a biblioteca de classes correta seja utilizada
  • o segundo parâmetro especifica o banco de dados a ser gerenciado, por meio de uma string de conexão.

O novo projeto do Visual Studio é o seguinte:

  • em [1]: o namespace necessário para os conectores genéricos é [System.Data.common], presente na referência [System.Data].

O arquivo de configuração [App.config] é o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
        <add name="dbArticlesSqlServer" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" />
        <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;Option=3;" />
        <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" />
        <add name="dbArticlesOleDbAccess"  connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;Persist Security Info=True"/>
    </connectionStrings>
    <appSettings>
        <add key="factorySqlServerCe" value="System.Data.SqlServerCe.3.5"/>
        <add key="factoryMySql" value="MySql.Data.MySqlClient"/>
        <add key="factorySqlServer" value="System.Data.SqlClient"/>
        <add key="factoryOdbc" value="System.Data.Odbc"/>
        <add key="factoryOleDb" value="System.Data.OleDb"/>
    </appSettings>
</configuration>
  • linhas 3-11: as cadeias de conexão dos diversos bancos de dados utilizados.
  • linhas 13-17: os nomes dos conectores genéricos a serem utilizados

O programa [Program.cs] é o seguinte:


...
using System.Data.Common;

namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {

            // aplicativo de console — executa consultas SQL digitadas no teclado
            // em um banco de dados cuja string de conexão é obtida em um arquivo de configuração, assim como o nome do conector associado SGBD

            // verificação de parâmetros
            if (args.Length != 2) {
                Console.WriteLine("Syntaxe : pg factory connectionString");
                return;
            }

            // análise do arquivo de configuração
            string factory = null;
            string connectionString = null;
            DbProviderFactory connecteur = null;
            try {
                // fábrica
                factory = ConfigurationManager.AppSettings[args[0]];
                // cadeia de conexão
                connectionString = ConfigurationManager.ConnectionStrings[args[1]].ConnectionString;
                // obtém-se um conector genérico para o SGBD
                connecteur = DbProviderFactories.GetFactory(factory);
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // exibições
            Console.WriteLine("Provider factory : [{0}]\n", factory);
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

...
                // execução da consulta
                if (champs[0] == "select") {
                    ExecuteSelect(connecteur,connectionString, requête);
                } else
                    ExecuteUpdate(connecteur, connectionString, requête);
            }
        }

        // execução de uma consulta de atualização
        static void ExecuteUpdate(DbProviderFactory connecteur, string connectionString, string requête) {
            // gerenciamento de possíveis exceções
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // configuração da conexão
                    connexion.ConnectionString = connectionString;
                    // abertura da conexão
                    connexion.Open();
                    // configuração do comando 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // execução da consulta
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                    // exibição do resultado
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                // mensagem de erro
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // execução de uma consulta Select
        static void ExecuteSelect(DbProviderFactory connecteur, string connectionString, string requête) {
            // gerenciamento de possíveis exceções
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // configuração da conexão
                    connexion.ConnectionString = connectionString;
                    // abertura da conexão
                    connexion.Open();
                    // configuração do comando 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // execução da consulta
                    DbDataReader reader = sqlCommand.ExecuteReader();
                    // exibição dos resultados
...
                }
            } catch (Exception ex) {
                // mensagem de erro
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
    }
}
  • linhas 12-14: o aplicativo recebe dois parâmetros: o nome do conector genérico e a string de conexão com o banco de dados na forma de chaves do arquivo [App.config].
  • linhas 23, 25: são recuperados no [App.config] o nome do conector genérico e a string de conexão
  • linha 27: o conector genérico é instanciado. A partir desse momento, ele é associado a um SGBD específico.
  • linhas 39-43: a execução do comando SQL digitado no teclado é delegada a dois métodos, aos quais são passados:
    • a consulta a ser executada
    • a cadeia de conexão que identifica o banco de dados no qual a consulta será executada
    • o conector genérico que identifica as classes a serem utilizadas para interagir com o SGBD responsável pela base de dados.
  • linhas 50-54: é estabelecida uma conexão com o método CreateConnection (linha 50) do conector genérico e, em seguida, configurada com a string de conexão do banco de dados a ser gerenciado (linha 52). Em seguida, ela é aberta (linha 54).
  • linhas 56-58: o objeto Command, necessário para a execução do comando SQL, é criado com o método CreateCommand do conector genérico. Em seguida, ele é configurado com o texto da ordem SQL a ser executada (linha 57) e a conexão na qual ela será executada (linha 58).
  • linha 60: o comando de atualização SQL é executado
  • linhas 74-87: há um código semelhante. A novidade está na linha 84. O objeto Reader obtido pela execução da ordem Select é do tipo DbDataReader, que é utilizado da mesma forma que os objetos OleDbDataReader, OdbcDataReader, ... que já encontramos anteriormente.

Aqui estão alguns exemplos de execução.

Com a base MySQL5:

Abrimos a página de propriedades do projeto [1] e selecionamos a guia [Debug] [2]. Em [3], a chave do conector da linha 14 de [App.config]. Em [4], a chave da cadeia de conexão da linha 6 de [App.config]. Os resultados da execução são os seguintes:

Provider factory : [MySql.Data.MySqlClient]
Chaîne de connexion à la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 article1 100 10 1
2 article2 200 20 2
3 article3 300 30 3

Com o banco de dados SQL no Server Compact:

Em [1], a chave do conector da linha 13 de [App.config]. No [2], a chave da cadeia de conexão da linha 4 do [App.config]. Os resultados da execução são os seguintes:

Provider factory : [System.Data.SqlServerCe.3.5]
Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Requête SQL (rien pour arrêter) : select * from articles

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

1 vélo 500 10 5
2 pompe 10 10 2
3 arc 600 4 1
4 flèches - lot de 6 100 12 20
5 combinaison de plongée 300 8 2
6 bouteilles d'oxygène 120 10 5

Sugere-se ao leitor que teste os demais bancos de dados.

9.4.6. Qual conector escolher?

Voltemos à arquitetura de um aplicativo com bancos de dados:

Vimos diversos tipos de conectores ADO.NET:

  • os conectores proprietários são os mais eficientes, mas tornam a camada [dao] dependente de classes proprietárias. Alterar o SGBD implica alterar a camada [dao].
  • Os conectores ODBC, OLE e DB permitem trabalhar com múltiplas bases de dados sem alterar a camada [dao]. Eles apresentam desempenho inferior ao dos conectores proprietários.
  • O conector genérico baseia-se nos conectores proprietários, ao mesmo tempo em que apresenta uma interface padrão para a camada [dao].

Portanto, parece que o conector genérico seja o conector ideal. Na prática, porém, o conector genérico não consegue ocultar todas as particularidades de um SGBD por trás de uma interface padrão. Veremos, no parágrafo a seguir, o conceito de consulta parametrizada. Com o SQL Server, uma consulta parametrizada na seguinte forma:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Com o MySQL5, a mesma consulta seria escrita da seguinte forma:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Portanto, há uma diferença de sintaxe. A propriedade da interface IDbCommand descrita no parágrafo 9.3.3, relacionada aos parâmetros, é a seguinte:

Parameters
a lista de parâmetros de uma ordem SQL configurada. A ordem “update articles set prix=prix*1.1 where id=@id” possui o parâmetro @id.

A propriedade Parameters é do tipo IDataParameterCollection, uma interface. Ela representa o conjunto de parâmetros da ordem SQL CommandText. A propriedade Parameters possui um método Add para adicionar parâmetros do tipo IDataParameter, que, mais uma vez, é uma interface. Esta possui as seguintes propriedades:

  • ParameterName: nome do parâmetro
  • DbType: o tipo SQL do parâmetro
  • Value: o valor atribuído ao parâmetro
  • ...

O tipo IDataParameter é adequado para os parâmetros da ordem SQL

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

, pois nela há parâmetros nomeados. A propriedade ParameterName pode ser utilizada.

O tipo IDataParameter não é adequado para a ordem SQL

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

pois os parâmetros não têm nomes. Nesse caso, é a ordem em que os parâmetros são adicionados à coleção [IDbCommand.Parameters] que é levada em consideração. Neste exemplo, será necessário inserir os 4 parâmetros na ordem nom, prix, stockactuel, stockminimum. Na consulta com parâmetros nomeados, a ordem de adição dos parâmetros não importa. No fim das contas, o desenvolvedor não pode ignorar totalmente o SGBD que utiliza ao inicializar os parâmetros de uma consulta parametrizada. Essa é uma das limitações atuais do conector genérico.

Existem frameworks que superam essas limitações e, além disso, trazem novas funcionalidades para a camada [dao]:

Um framework é um conjunto de bibliotecas de classes que visa facilitar uma determinada forma de arquitetar a aplicação. Existem vários que permitem a criação de camadas [dao] que são ao mesmo tempo eficientes e insensíveis a mudanças no SGBD:

  • Spring.Net [http://www.springframework.net/], já apresentado neste documento, oferece o equivalente ao conector genérico estudado, sem suas limitações, além de diversos recursos que simplificam o acesso aos dados. Existe uma versão em Java.
  • iBatis.Net [http://ibatis.apache.org] é mais antigo e mais completo que o Spring.Net. Existe uma versão em Java.
  • NHibernate [http://www.hibernate.org/] é uma adaptação da versão Java do Hibernate, muito conhecida no mundo Java. O NHibernate permite que a camada [dao] se comunique com o SGBD sem emitir comandos SQL. A camada [dao] trabalha com objetos Hibernate. Uma linguagem de consultas HBL (Hibernate Query Language) permite consultar os objetos gerenciados pelo Hibernate. São esses objetos que emitem os comandos SQL. O Hibernate sabe se adaptar aos SQL proprietários dos SGBD.
  • LINQ (Linguagem de Consulta INtegrated), integrada à versão 3.5 .NET e disponível no C# 2008. O LINQ segue os passos do NHibernate, mas, por enquanto (maio de 2008), apenas o servidor SGBD SQL é compatível. Isso deve mudar com o tempo. O LINQ vai além do NHibernate: sua linguagem de consultas permite consultar, de maneira padronizada, três tipos diferentes de fontes de dados:
    • coleções de objetos (LINQ para Objetos)
    • um arquivo XML (LINQ para XML)
    • um banco de dados (LINQ para SQL)

Essas estruturas não serão abordadas neste documento. No entanto, recomenda-se fortemente que sejam utilizadas em aplicações profissionais.

9.5. Consultas parametrizadas

Mencionamos no parágrafo anterior as consultas parametrizadas. Apresentamos aqui um exemplo para o SGBD SQL Server Compact. O projeto é o seguinte

  • em [1], o projeto. Apenas [App.config], [Article.cs] e [Parametres.cs] são utilizados. Observe-se também a base SQL Server Ce [dbarticles.sdf].
  • em [2], o projeto está configurado para executar [Parametres.cs]
  • no [3], as referências do projeto

O arquivo de configuração [App.config] define a string de conexão com o banco de dados:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbArticlesSqlServerCe" connectionString="Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;" />
    </connectionStrings>
</configuration>

O arquivo [Article.cs] define uma classe [Article]. Um objeto Article será utilizado para encapsular as informações de uma linha da tabela ARTICLES do banco de dados [dbarticles.sdf]:


namespace Chap7 {
    class Article {
        // propriedades
        public int Id { get; set; }
        public string Nom { get; set; }
        public decimal Prix { get; set; }
        public int StockActuel { get; set; }
        public int StockMinimum { get; set; }

        // construtores
        public Article() { 
        }

        public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
            Id = id;
            Nom = nom;
            Prix = prix;
            StockActuel = stockActuel;
            StockMinimum = stockMinimum;
        }

    }
}

O aplicativo [Parametres.cs] implementa as consultas parametrizadas:


using System;
using System.Data.SqlServerCe;
using System.Text;
using System.Data;
using System.Configuration;

namespace Chap7 {
    class Parametres {
        static void Main(string[] args) {

            // análise do arquivo de configuração
            string connectionString = null;
            try {
                // cadeia de conexão
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // exibições
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // criação de uma tabela de itens
            Article[] articles = new Article[5];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i-1] = new Article(0, "article" + i, i * 100, i * 10, i);
            }

            // gerenciamento de possíveis exceções
            try {

                // exclusão dos itens existentes do banco de dados
                ExecuteUpdate(connectionString, "delete from articles");

                // exibição dos itens da tabela
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

                // inserir a tabela de itens no banco de dados
                InsertArticles(connectionString, articles);

                // exibe os itens da tabela
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                // mensagem de erro
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // inserção de uma tabela de itens
        static void InsertArticles(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // abertura da conexão
                connexion.Open();
                // configuração do pedido
                string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
                SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                sqlCommand.Parameters.Add("@nom",SqlDbType.NVarChar,30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                // compilação do pedido
                sqlCommand.Prepare();
                // inserção de linhas
                for (int i = 0; i < articles.Length; i++) {
                    // inicialização de parâmetros
                    sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
                    sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
                    sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
                    sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
                    // execução da consulta
                    sqlCommand.ExecuteNonQuery();
                }
            }
        }

        // execução de uma consulta de atualização
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }

        // execução de uma consulta SELECT
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // exibição do leitor
        static void AfficheReader(IDataReader reader) {
...
    }
}

A novidade em relação ao que foi visto anteriormente é o procedimento [InsertArticles] nas linhas 51-75:

  • linha 51: o procedimento recebe dois parâmetros:
    • a string de conexão connectionString, que permitirá que o procedimento se conecte ao banco de dados
    • um array de objetos Article que deve ser adicionado à tabela Articles do banco de dados
  • linha 56: a consulta de inserção de um objeto [Article]. Ela possui quatro parâmetros:
    • @nom: o nome do artigo
    • @prix: seu preço
    • @sa: seu estoque atual
    • @sm: seu estoque mínimo

A sintaxe dessa consulta parametrizada é exclusiva do SQL Server Compact. Vimos no parágrafo anterior que, com o MySQL5, a sintaxe seria a seguinte:

insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Com o SQL Server Compact, cada parâmetro deve ser precedido pelo caractere @. O nome dos parâmetros é livre.

  • linhas 58-61: definem-se as características de cada um dos 4 parâmetros e elas são adicionadas, uma a uma, à lista de parâmetros do objeto SqlCeCommand, que encapsula a ordem SQL a ser executada.

Utilizamos aqui o método [SqlCeCommand].Parameters.Add, que possui seis assinaturas. Utilizamos as duas seguintes:

Add(string parameterName, SQLDbType type)

adiciona e configura o parâmetro denominado parameterName. Esse nome deve ser um dos nomes da consulta parametrizada configurada: (@nome, ...). type designa o tipo SQL da coluna à qual o parâmetro se refere. Existem vários tipos disponíveis, entre os quais os seguintes:

tipo SQL
tipo C#
comentário
BigInt
Int64
 
DateTime
DateTime
 
Decimal
Decimal
 
Float
Duplo
 
Int
Int32
 
Money
Decimal
 
NChar
String
cadeia de comprimento fixo
NVarChar
String
cadeia de comprimento variável
Real
Single
 

Add(string parameterName, SQLDbType type, int size)

o terceiro parâmetro size define o tamanho da coluna. Essa informação só é útil para determinados tipos, como o SQL e o NVarChar, por exemplo.

  • linha 63: compila-se a consulta parametrizada. Também se diz que ela é preparada, daí o nome do método. Essa operação não é indispensável. Ela existe para melhorar o desempenho. Quando um SGBD executa uma ordem SQL, ele realiza um certo trabalho de otimização antes de executá-la. Uma consulta parametrizada destina-se a ser executada várias vezes com parâmetros diferentes. O texto da consulta, por sua vez, não muda. O trabalho de otimização pode, portanto, ser feito apenas uma vez. Alguns SGBD têm a capacidade de “preparar” ou “compilar” consultas parametrizadas. É então definido um plano de execução para essa consulta. Essa é a fase de otimização de que falamos. Uma vez compilada, a consulta é executada repetidamente, cada vez com novos parâmetros efetivos, mas sempre com o mesmo plano de execução.

A compilação não é a única vantagem das consultas parametrizadas. Voltemos à consulta analisada:

insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Pode-se querer construir o texto da consulta por meio de um programa:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('"+nom+"',"+prix+","+sa+","+sm+")";

No exemplo acima, se (nome, preço, sa, sm) for igual a (“artigo1”, 100, 10, 1), a consulta anterior passa a ser:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('article1',100,10,1)";

Agora, se (nome, preço, sa, sm) for igual a (“artigo1”, 100, 10, 1), a consulta anterior passa a ser:

string requête="insert into articles(nom,prix,stockactuel,stockminimum) values('l'article1',100,10,1)";

e torna-se sintaticamente incorreta devido ao apóstrofo no nome l'article1. Se nom for resultado de uma entrada do usuário, isso significa que precisamos verificar se a entrada não contém apóstrofos e, caso contenha, neutralizá-los. Essa neutralização depende do SGBD. A vantagem da consulta preparada é que ela mesma realiza esse trabalho. Essa facilidade, por si só, já justifica o uso de uma consulta preparada.

  • linhas 65-73: os itens da tabela são inseridos um por um
  • linhas 67-70: cada um dos quatro parâmetros da consulta recebe seu valor por meio de sua propriedade Value.
  • linha 72: a consulta de inserção, agora completa, é executada da maneira habitual.

Veja um exemplo de execução:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------


------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

117 article1 100 10 1
118 article2 200 20 2
119 article3 300 30 3
120 article4 400 40 4
121 article5 500 50 5
  • linha 3: mensagem após a exclusão de todas as linhas da tabela
  • linhas 5-7: mostram que a tabela está vazia
  • linhas 10-18: mostram a tabela após a inserção dos 5 itens

9.6. Transactions

9.6.1. Informações gerais

Uma transação é uma sequência de ordens SQL executada de forma “atômica”:

  • ou todas as operações são bem-sucedidas
  • ou uma delas falha e, nesse caso, todas as anteriores são canceladas

No final, as operações de uma transação ou foram todas aplicadas com sucesso, ou nenhuma foi aplicada. Quando o próprio usuário tem o controle da transação, ele a valida por meio de um comando COMMIT ou a cancela por meio de um comando ROLLBACK.

Em nossos exemplos anteriores, não utilizamos nenhuma transação. No entanto, havia transações, pois em um SGBD, um comando SQL é sempre executado dentro de uma transação. Se o cliente .NET não iniciar por conta própria uma transação explícita, o SGBD utiliza uma transação implícita. Há, então, dois casos comuns:

  1. cada ordem SQL individual é objeto de uma transação, iniciada pelo SGBD antes da ordem e encerrada em seguida. Diz-se que estamos no modo autocommit. Tudo ocorre, portanto, como se o cliente .NET realizasse transações para cada ordem SQL.
  2. O SGBD não está no modo autocommit e inicia uma transação implícita na primeira ordem SQL, que o cliente .NET emite fora de uma transação, permitindo que o cliente a feche. Todas as ordens SQL emitidas pelo cliente .NET passam, então, a fazer parte da transação implícita. Essa transação pode ser encerrada por diversos eventos: o cliente encerra a conexão, inicia uma nova transação, etc., mas, nesse caso, estamos em uma situação dependente do SGBD. Esse é um modo a ser evitado.

O modo padrão é geralmente definido pela configuração do SGBD. Alguns SGBD estão, por padrão, no modo autocommit, outros não. O SQLServer Compact está, por padrão, no modo autocommit.

Os comandos SQL dos diferentes usuários são executados simultaneamente em transações que operam em paralelo. As operações realizadas por uma transação podem afetar as realizadas por outra transação. Distinguem-se quatro níveis de isolamento entre as transações dos diferentes usuários:

  • Leitura não confirmada
  • Leitura confirmada
  • Leitura repetível
  • Serializable

Leitura não confirmada

Esse modo de isolamento também é chamado de “Leitura suja”. Veja um exemplo do que pode acontecer nesse modo:

  1. um usuário U1 inicia uma transação na tabela T
  2. um usuário U2 inicia uma transação na mesma tabela T
  3. o usuário U1 altera linhas da tabela T, mas ainda não as confirma
  4. o usuário U2 “vê” essas modificações e toma decisões com base no que vê
  5. o usuário cancela sua transação por meio de um ROLLBACK

Vemos que, no passo 4, o usuário U2 tomou uma decisão com base em dados que se revelarão incorretos posteriormente.

Leitura confirmada

Esse modo de isolamento evita a armadilha anterior. Nesse modo, o usuário U2, na etapa 4, não “verá” as alterações feitas pelo usuário U1 na tabela T. Ele só as verá depois que U1 tiver concluído sua transação.

Nesse modo, também chamado de “Unrepeatable Read”, podem ocorrer, no entanto, as seguintes situações:

  1. um usuário U1 inicia uma transação na tabela T
  2. um usuário U2 inicia uma transação na mesma tabela T
  3. o usuário U2 executa um SELECT para obter a média da coluna C das linhas de T que atendem a uma determinada condição
  4. o usuário U1 altera (UPDATE) determinados valores da coluna C de T e os valida (COMMIT)
  5. o usuário U2 repete a mesma operação SELECT descrita no item 3. Ele perceberá que a média da coluna C mudou devido às alterações feitas por U1.

Agora, o usuário U2 vê apenas as alterações “validadas” por U1. Mas, embora ele permaneça na mesma transação, duas operações idênticas (3 e 5) produzem resultados diferentes. O termo “Unrepeatable Read” se refere a essa situação. É uma situação incômoda para quem deseja ter uma visão estável da tabela T.

Leitura Repetível

Nesse modo de isolamento, um usuário tem a garantia de obter os mesmos resultados em suas leituras do banco de dados, desde que permaneça na mesma transação. Ele trabalha com uma imagem na qual as alterações feitas por outras transações, mesmo que validadas, nunca são refletidas. Ele só verá essas alterações quando ele próprio encerrar sua transação com um COMMIT ou ROLLBACK.

Esse modo de isolamento, no entanto, ainda não é perfeito. Após a operação 3 acima, as linhas consultadas pelo usuário U2 ficam bloqueadas. Durante a operação 4, o usuário U1 não poderá modificar (UPDATE) os valores da coluna C dessas linhas. Ele pode, no entanto, adicionar linhas (INSERT). Se algumas das linhas adicionadas atenderem à condição testada na etapa 3, a operação 5 resultará em uma média diferente daquela encontrada na etapa 3 devido às linhas adicionadas. Essas linhas são às vezes chamadas de “linhas fantasmas”.

Para resolver esse novo problema, é necessário mudar para o nível de isolamento “Serializable”.

Serializable

Nesse modo de isolamento, as transações são completamente isoladas umas das outras. Ele garante que o resultado de duas transações realizadas simultaneamente será o mesmo que se fossem realizadas uma após a outra. Para alcançar esse resultado, durante a operação 4, em que o usuário U1 deseja adicionar linhas que alterariam o resultado da transação SELECT do usuário U1, ele será impedido de fazê-lo. Uma mensagem de erro informará que a inserção não é possível. Ela se tornará possível quando o usuário U2 tiver validado sua transação.

Os quatro níveis de isolamento de transações SQL não estão disponíveis em todos os SGBD. O nível de isolamento padrão é, geralmente, o nível Committed Read. O nível de isolamento desejado para uma transação pode ser especificado explicitamente durante a criação de uma transação explícita por um cliente .NET.

9.6.2. O API de gerenciamento de transações

Uma conexão implementa a interface IDbConnection apresentada no parágrafo 9.3.3. Essa interface possui o seguinte método:

BeginTransaction
M
inicia uma transação.

Esse método possui duas assinaturas:

  1. IDbTransaction BeginTransaction(): inicia uma transação e retorna o objeto IDbTransaction, que permite controlá-la
  2. IDbTransaction BeginTransaction(IsolationLevel nível): especifica ainda o nível de isolamento desejado para a transação. level assume seus valores na seguinte enumeração:
ReadUncommitted
a transação pode ler dados gravados por outra transação que ainda não tenham sido validados por esta — a ser evitado
ReadCommitted
a transação não pode ler dados gravados por outra transação que ainda não tenham sido validados por ela. No entanto, os dados lidos duas vezes consecutivas na transação podem mudar (leituras não repetíveis), pois outra transação pode tê-los modificado nesse intervalo (as linhas lidas não estão bloqueadas — apenas as linhas atualizadas estão). Além disso, outra transação pode ter adicionado linhas (linhas fantasmas) que serão incluídas na segunda leitura.
RepeatableRead
As linhas lidas pela transação são bloqueadas, assim como as linhas atualizadas. Isso impede que outra transação as altere. No entanto, isso não impede a adição de novas linhas.
Serializable
As tabelas utilizadas pela transação são bloqueadas, impedindo que outra transação adicione novas linhas. É como se a transação estivesse sozinha. Isso reduz o desempenho, pois as transações não trabalham mais em paralelo.
Snapshot
A transação trabalha com uma cópia dos dados feita no momento T. Utilizada quando a transação é somente para leitura. Proporciona o mesmo resultado que serializable, evitando seu custo.

Uma vez iniciada, a transação é controlada pelo objeto do tipo IDbTransaction, uma interface cujas propriedades P e métodos M a seguir utilizaremos:

Nome
Tipo
Função
Connection
P
a conexão IDbConnection que suporta a transação
Commit
M
valida a transação — os resultados das ordens SQL emitidas na transação são copiados para o banco de dados.
Rollback
M
invalida a transação — os resultados das ordens SQL emitidas na transação não são copiados para o banco de dados.

9.6.3. O programa de exemplo

Retomamos o projeto anterior para nos concentrarmos agora no programa [Transactions.cs]:

  • em [1], o projeto.
  • em [2], o projeto está configurado para executar [Transactions.cs]

O código de [Transactions.cs] é o seguinte:


using System;
using System.Configuration;
using System.Data;
using System.Data.SqlServerCe;
using System.Text;

namespace Chap7 {
    class Transactions {
        static void Main(string[] args) {

            // análise do arquivo de configuração
            string connectionString = null;
            try {
                // cadeia de conexão
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }

            // exibições
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // criação de uma tabela com 2 itens com o mesmo nome
            Article[] articles = new Article[2];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i - 1] = new Article(0, "article", i * 100, i * 10, i);
            }
            // gerenciamento de possíveis exceções
            try {
                Console.WriteLine("Insertion sans transaction...");
                // inserimos a tabela de itens no banco de dados primeiro, sem transação
                ExecuteUpdate(connectionString, "delete from articles");
                try {
                    InsertArticlesOutOfTransaction(connectionString, articles);
                } catch (Exception ex) {
                    // mensagem de erro
                    Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
                }
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

                // repetimos o mesmo procedimento, mas desta vez em uma transação
                Console.WriteLine("\n\nInsertion dans une transaction...");
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                // mensagem de erro
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // inserção de uma tabela de artigos sem transação
        static void InsertArticlesOutOfTransaction(string connectionString, Article[] articles) {
....
        }

        // inserção de uma tabela de artigos em uma transação
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
....
        }

        // execução de uma consulta de atualização
        static void ExecuteUpdate(string connectionString, string requête) {
....
        }

        // execução de uma consulta SELECT
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // exibição do leitor
        static void AfficheReader(IDataReader reader) {
...
            }
        }
    }
}
  • linhas 12-19: a string de conexão com o banco de dados SQLServer é lida em [App.config]
  • linhas 25-28: é criada uma tabela com dois objetos Article. Esses dois itens têm o mesmo nome, “item”. No entanto, o banco de dados [dbarticles.sdf] possui uma restrição de exclusividade na coluna [nom] (ver parágrafo 9.3.1). Portanto, esses dois itens não podem estar presentes ao mesmo tempo no banco de dados. Os dois artigos com o nome “artigo” são adicionados à tabela articles. Portanto, ocorrerá um problema: c.a.d. Uma exceção lançada pelo SGBD e retransmitida por seu conector ADO.NET. Para demonstrar o efeito da transação, os dois itens serão inseridos em dois ambientes diferentes:
    • primeiro, fora de qualquer transação. É importante lembrar aqui que, nesse caso, o SQLServer Compact opera no modo autocommit, e o c.a.d insere cada ordem SQL em uma transação implícita. O primeiro item será inserido. O segundo, não.
    • Em seguida, em uma transação explícita que engloba as duas inserções. Como a segunda inserção falhará, a primeira será revertida. No final, nenhuma inserção será realizada.
  • linha 33: a tabela articles é esvaziada
  • linha 35: inserção dos dois itens sem transação explícita. Como sabemos que a segunda inserção causará uma exceção, ela é tratada por um try/catch
  • linha 46: exibição da tabela articles
  • linhas 44-46: repetimos a mesma sequência, mas, desta vez, uma transação explícita é utilizada para realizar as inserções. A exceção encontrada é, neste caso, tratada pelo método InsertArticlesInTransaction.
  • linhas 54-56: o método InsertArticlesOutOfTransaction é o método InsertArticles do programa [Parametres.cs] analisado anteriormente.
  • linhas 64-66: o método ExecuteUpdate é o mesmo que o anterior. A ordem SQL é executada em uma transação implícita. Isso é possível aqui porque sabemos que, nesse caso, o SQLServer Compact opera no modo autocommit.
  • linhas 69-71: o mesmo se aplica ao método ExecuteSelect.

O método InsertArticlesInTransaction é o seguinte:


// inserção de uma tabela de artigos em uma transação
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // abertura de conexão
                connexion.Open();
                // configuração do comando
                string requête = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)";
                SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30);
                sqlCommand.Parameters.Add("@prix", SqlDbType.Money);
                sqlCommand.Parameters.Add("@sa", SqlDbType.Int);
                sqlCommand.Parameters.Add("@sm", SqlDbType.Int);
                // compilação do comando
                sqlCommand.Prepare();
                // transação
                SqlCeTransaction transaction = null;
                try {
                    // início da transação
                    transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
                    // o comando SQL deve ser executado nesta transação
                    sqlCommand.Transaction = transaction;
                    // inserção das linhas
                    for (int i = 0; i < articles.Length; i++) {
                        // inicialização de parâmetros
                        sqlCommand.Parameters["@nom"].Value = articles[i].Nom;
                        sqlCommand.Parameters["@prix"].Value = articles[i].Prix;
                        sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel;
                        sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum;
                        // execução da consulta
                        sqlCommand.ExecuteNonQuery();
                    }
                    // a transação é validada
                    transaction.Commit();
                    Console.WriteLine("transaction validée...");
                } catch {
                    // reverte-se a transação
                    if (transaction != null)transaction.Rollback();
                    Console.WriteLine("transaction invalidée...");
                }
            }
        }

Detalhamos apenas o que o diferencia do método InsertArticles do programa [Parametres.cs] analisado anteriormente:

  • linha 16: é declarada uma transação SqlCeTransaction.
  • linhas 17, 35: o try/catch para lidar com a exceção que ocorrerá após a segunda inserção
  • linha 19: a transação é criada. Ela pertence à conexão atual.
  • linha 21: o comando SQL, com os parâmetros definidos, é inserido na transação
  • linhas 23-31: as inserções são realizadas
  • linha 33: tudo correu bem — a transação é validada — as inserções serão definitivamente integradas ao banco de dados.
  • linha 37: ocorreu um problema. A transação é revertida, caso existisse.

A execução produz os seguintes resultados:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Insertion sans transaction...
Il y a eu 0 ligne(s) modifiée(s)
Erreur d'accès à la base de données (A duplicate value cannot be inserted into a unique index. [ Table name = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ])

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

126 article 100 10 1


Insertion dans une transaction...
Il y a eu 1 ligne(s) modifiée(s)
transaction invalidée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------
  • linha 4: exibida pelo ExecuteUpdate (“delete from articles”) — não havia linhas na tabela
  • linha 5: a exceção causada pela segunda inserção. A mensagem indica que a restrição UQ__ARTICLES__0000000000000010 não foi verificada. É possível obter mais informações consultando as propriedades do banco de dados:
  • em [1], na visualização [Database Explorer] do Visual Studio, foi criada uma conexão [2] com o banco de dados [dbarticles.sdf]. Essa conexão possui um índice UQ__ARTICLES__0000000000000010. Ao clicar com o botão direito desse índice, tem-se acesso às suas propriedades (Index properties)
  • em [3,4], percebe-se que o índice UQ__ARTICLES__0000000000000010 corresponde a uma restrição de exclusividade na coluna [NOM]
  • linhas 7-11: exibição da tabela articles após as duas inserções. Ela não está vazia: o primeiro artigo foi inserido.
  • linha 15: exibida pela tabela ExecuteUpdate (“delete from articles”) — havia uma linha na tabela
  • linha 16: mensagem exibida pelo InsertArticlesInTransaction quando a transação falha.
  • linhas 18-20: mostram que nenhuma inserção foi realizada. O Rollback da transação reverteu a primeira inserção.

9.7. O método ExecuteScalar

9.7.1. Entre os métodos da interface IDbCommand descrita no parágrafo 9.3.3, havia o seguinte método:

ExecuteScalar
M
para executar uma ordem SQL Select que retorna apenas um único resultado, como em: select count(*) from articles.

Apresentamos aqui um exemplo de uso desse método. Voltemos ao projeto:

  • em [1], o projeto.
  • em [2], o projeto está configurado para executar [ExecuteScalar.cs]

O programa [ExecuteScalar.cs] é o seguinte:


...
namespace Chap7 {
    class Scalar {
        static void Main(string[] args) {

            // análise do arquivo de configuração
            string connectionString = null;
...

            // exibições
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);

            // criação de uma tabela com 5 itens
            Article[] articles = new Article[5];
            for (int i = 1; i <= articles.Length; i++) {
                articles[i - 1] = new Article(0, "article" + i, i * 100, i * 10, i);
            }

            // gerenciamento de possíveis exceções
            try {
                // inserção da tabela de itens em uma transação
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
                // calcula-se a média dos preços dos itens
                decimal prixMoyen = (decimal)ExecuteScalar(connectionString, "select avg(prix) from articles");
                Console.WriteLine("Prix moyen des articles={0}", prixMoyen);
                // ou o número de itens
                int nbArticles = (int)ExecuteScalar(connectionString, "select count(id) from articles");
                Console.WriteLine("Nombre d'articles={0}", nbArticles);
            } catch (Exception ex) {
                // mensagem de erro
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }

        // inserção de uma tabela de itens em uma transação
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
...
        }


        // execução de uma consulta de atualização
        static object ExecuteScalar(string connectionString, string requête) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                // abertura de conexão
                connexion.Open();
                // execução de consulta
                return new SqlCeCommand(requête, connexion).ExecuteScalar();
            }
        }

        // execução de uma consulta de atualização
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }

        // execução de uma consulta SELECT
        static void ExecuteSelect(string connectionString, string requête) {
...
        }

        // exibição do leitor
        static void AfficheReader(IDataReader reader) {
...
        }
    }
}
  • linhas 14-17: criação de uma tabela com 5 itens
  • linha 22: a tabela articles é esvaziada
  • linha 23: ela é preenchida com os 5 itens
  • linha 24: a tabela é exibida
  • linha 26: solicita o preço médio dos itens
  • linha 29: solicita o número de itens
  • linha 49: uso do método [IDbCommand].ExecuteScalar() para calcular cada um desses valores.

Os resultados da execução são os seguintes:

Chaîne de connexion à la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Il y a eu 5 ligne(s) modifiée(s)
transaction validée...

------------------------------------
ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM
------------------------------------

145 article1 100 10 1
146 article2 200 20 2
147 article3 300 30 3
148 article4 400 40 4
149 article5 500 50 5
Prix moyen des articles=300
Nombre d'articles=5

As linhas 15 e 16 mostram os dois valores retornados pelo método ExecuteScalar.

9.8. Aplicativo de exemplo — versão 7

Retomamos o exemplo de aplicação IMPOTS. A versão mais recente foi analisada no parágrafo 7.6. Tratava-se da seguinte aplicação de três camadas:

  • a camada [ui] era uma interface gráfica [A] e a camada [dao] obtinha seus dados de um arquivo de texto [B].
  • A instanciação das camadas e sua integração na aplicação eram garantidas pelo Spring.

Estamos modificando a camada [dao] para que ela busque seus dados em um banco de dados.

9.8.1. A do banco de dados

O conteúdo do arquivo de texto [B] anterior é inserido em um banco de dados MySQL5. Mostramos como proceder:

  • [1]: O MySQL Administrator foi iniciado
  • [2,3]: na área [Schemata], clique com o botão direito do mouse e selecione a opção [Create Schema] para criar um novo banco de dados
  • [4]: a base de dados se chamará [bdimpots]
  • [5]: ela foi adicionada às bases da área [Schemata].
  • [6,7]: clique com o botão direito do mouse na tabela e selecione a opção [Create New Table] para criar uma tabela
  • [8]: a tabela se chamará [tranches]. Ela terá as colunas [id, limite, coeffR, coeffN].
  • [9,10]: [id] é a chave primária do tipo INTEGER e possui o atributo AUTO_INCREMENT [10]: é o SGBD que se encarregará de preencher essa coluna ao adicionar linhas.
  • As colunas [limite, coeffR, coeffN] são do tipo DOUBLE.
  • [11,12]: a nova tabela aparece na aba [Schema Tables] do banco de dados.
  • [13,14]: para inserir dados na tabela
  • [15]: o processo [Query Browser] foi iniciado
  • [16]: os dados foram inseridos e validados para as colunas [limite, coeffR, coeffN]. A coluna [id] foi preenchida pelo SGBD. A validação ocorreu com o [17].
  • Ainda em [Query Browser] [18], executa-se [20], a consulta [19]. Essa consulta cria um usuário 'admimpots' com a senha 'mdpimpots' e concede a ele todos os privilégios (grant all privileges) sobre todos os objetos do banco de dados bdimpots (on bdimpots.*). Isso nos permitirá trabalhar no banco de dados [bdimpots] com o usuário [admimpots], em vez de com o administrador [root].

9.8.2. A solução do Visual Studio

Seguiremos a abordagem estudada para a versão 5 do aplicativo de exemplo (ver parágrafo 6.4). Vamos construir gradualmente a seguinte solução do Visual Studio:

  • em [1]: a solução ImpotsV7 é composta por três projetos, um para cada uma das três camadas do aplicativo
  • em [2]: o projeto [dao] da camada [dao], que passará a utilizar um banco de dados
  • em [3]: o projeto [metier] da camada [metier]. Retomamos aqui a camada [metier] da versão 5, descrita no parágrafo 6.4.4.
  • em [4]: o projeto [ui] da camada [ui]. Retomamos aqui a camada [ui] da versão 6, descrita no parágrafo 7.6.

Aproveitamos o que já foi desenvolvido para recuperar duas camadas já escritas: as camadas [ui] e [metier]. Isso é possível graças à arquitetura em camadas escolhida. No entanto, precisaremos dos códigos-fonte das camadas [ui] e [metier]. De fato, não é possível nos contentarmos apenas com as camadas DLL. Quando, na versão 5, o DLL da camada [metier] foi criado, ele tinha uma dependência do DLL da camada [dao]. Essa dependência foi fixada na DLL da camada [metier] (nome da DLL da camada [dao], versão, token de identidade, ...). Assim, a DLL da versão 5 [ImpotsV5-metier.dll] só aceita funcionar com a DLL [ImpotsV5-dao.dll] com a qual foi compilada. Se alterarmos o DLL da camada [dao], será necessário recompilar a camada [metier] para criar um novo DLL para ela. O mesmo se aplica à camada [ui]. As camadas [ui] e [metier], portanto, não serão modificadas, mas serão recompiladas para funcionar com a DLL da nova camada [dao].

9.8.3. A camada [dao]

As referências do projeto (ver [1] no projeto)

  • nunit.framework: para o teste NUnit
  • System.Configuration: para processar o arquivo de configuração [App.config]
  • System.Data: porque estamos utilizando um banco de dados.

As entidades (ver [2] no projeto)

As classes [TrancheImpot] e [ImpotException] são as das versões anteriores.

A camada [dao] (ver [3] no projeto)

A interface [IImpotDao] não sofreu alterações:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // faixas de imposto
        TrancheImpot[] TranchesImpot{get;}
    }
}

A classe de implementação [DataBaseImpot] dessa interface é a seguinte:


using System;
using System.Collections.Generic;
using System.Data.Common;
using Entites;

namespace Dao {
    public class DataBaseImpot : IImpotDao {
        // faixas de imposto
        private TrancheImpot[] tranchesImpot;
        public TrancheImpot[] TranchesImpot { get { return tranchesImpot; } }

        // fabricante
        public DataBaseImpot(string factory, string connectionString, string requête) {
            // fábrica: a fábrica do SGBD de destino
            // connectionString: a string de conexão à base de dados das faixas de imposto
            // gerenciamos eventuais exceções
            try {
                // recupera-se um conector genérico para o SGBD
                DbProviderFactory connecteur = DbProviderFactories.GetFactory(factory);
                using (DbConnection connexion = connecteur.CreateConnection()) {
                    // configuração da conexão
                    connexion.ConnectionString = connectionString;
                    // abertura da conexão
                    connexion.Open();
                    // configuração do comando 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                    // execução da consulta
                    List<TrancheImpot> listTrancheImpot = new List<TrancheImpot>();
                    using (DbDataReader reader = sqlCommand.ExecuteReader()) {
                        while (reader.Read()) {
                            // cria-se uma nova faixa de imposto
                            listTrancheImpot.Add(new TrancheImpot() { Limite = reader.GetDecimal(0), CoeffR = reader.GetDecimal(1), CoeffN = reader.GetDecimal(2) });
                        }
                    }
                    // insere-se as faixas de imposto na instância
                    tranchesImpot = listTrancheImpot.ToArray();
                }
            } catch (Exception ex) {
                // encapsulando a exceção em um tipo ImpotException
                throw new ImpotException("Erreur de lecture des tranches d'impôt", ex) { Code = 101 };
            }

        }
    }
}
  • linha 7: a classe [DataBaseImpot] implementa a interface [IImpotDao].
  • linha 10: a implementação do método [TranchesImpot] da interface. Ela se limita a retornar uma referência à tabela de faixas de imposto da linha 9. Essa tabela será construída pelo construtor da classe.
  • linha 13: o construtor. Ele utiliza um conector genérico (ver parágrafo 9.4.5) para consultar o banco de dados das faixas de imposto. O construtor recebe três parâmetros:
    • o nome da “factory” à qual ele solicitará as classes para se conectar ao banco de dados, emitir comandos SQL e processar o resultado de uma consulta SELECT.
    • a string de conexão que deve utilizar para se conectar ao banco de dados
    • o comando SQL Select que deve ser executado para obter as faixas de imposto.
  • linha 19: solicita um conector à “factory”
  • linha 20: cria uma conexão com esse conector. Ela é criada, mas ainda não está operacional
  • linha 22: a string de conexão é inicializada. Agora é possível se conectar.
  • linha 24: realiza-se a conexão
  • linha 26: solicita ao conector um objeto [DbCommand] para executar uma ordem SQL
  • linha 27: define a ordem SQL a ser executada
  • linha 28: define a conexão na qual a ordem será executada
  • linha 30: é criada uma lista [listTrancheImpot] de objetos do tipo [TrancheImpot], inicialmente vazia.
  • linha 31: a ordem SQL Select é executada
  • linhas 32-35: o objeto [DbDataReader], resultado do Select, é processado. Cada linha da tabela de resultados do Select é usada para instanciar um objeto do tipo [TrancheImpot], que é adicionado à lista [listTrancheImpot].
  • linha 38: a lista de objetos do tipo [TrancheImpot] é transferida para a matriz da linha 9.
  • linhas 40-43: uma eventual exceção é encapsulada em um tipo [ImpotException] e recebe o código de erro 101 (arbitrário).

O teste [Test1] (ver [4] no projeto)

A classe [Test1] limita-se a exibir as faixas de imposto na tela. Trata-se da mesma classe já utilizada na versão 5 (parágrafo 6.4.3), exceto pela instrução que instancia a camada [dao] (linha 14).


using System;
using Dao;
using Entites;
using System.Configuration;

namespace Tests {
    class Test1 {
        static void Main() {

            // cria-se a camada [dao]
            IImpotDao dao = null;
            try {
                // criação da camada [dao]
                dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
            } catch (ImpotException e) {
                // exibição de erro
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // encerramento do programa
                Environment.Exit(1);
            }
            // exibição das faixas de imposto
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
        }
    }
}

A linha 14 utiliza o seguinte arquivo de configuração [App.config]:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="dbImpotsMySql5" connectionString="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;" />
    </connectionStrings>
    <appSettings>
        <add key="requete" value="select limite, coeffr, coeffn from tranches"/>
        <add key="factoryMySql5" value="MySql.Data.MySqlClient"/>
    </appSettings>
</configuration>
  • linha 4: a string de conexão com o banco de dados MySQL5. Observe que será o usuário [admimpots] quem estabelecerá a conexão.
  • linha 8: a “factory” para trabalhar com o SGBD MySQL5
  • linha 7: a consulta SQL Select para obter as faixas de imposto.

O projeto está configurado para executar o [Test1.cs]:

Image

A execução do teste apresenta os seguintes resultados:

1
2
3
4
5
6
7
4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

O teste NUnit [NUnit1] (ver [4] no projeto)

O teste unitário [NUnit1] é o mesmo já utilizado na versão 5 (parágrafo 6.4.3), exceto pela instrução que instancia a camada [dao] (linha 16).


using System;
using System.Configuration;
using Dao;
using Entites;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1 : AssertionHelper{
        // camada [dao] a ser testada
        private IImpotDao dao;

        // fabricante
        public NUnit1() {
            // inicialização da camada [dao]
            dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
        }

        // teste
        [Test]
        public void ShowTranchesImpot(){
            // exibição das faixas de imposto
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
            // alguns testes
            Expect(tranchesImpot.Length,EqualTo(7));
            Expect(tranchesImpot[2].Limite,EqualTo(14753).Within(1e-6));
            Expect(tranchesImpot[2].CoeffR, EqualTo(0.191).Within(1e-6));
            Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92).Within(1e-6));
        }
    }
}

Para executar este teste unitário, o projeto deve ser do tipo [Class Library]:

  • em [1]: a natureza do projeto foi alterada
  • em [2]: o DLL gerado será chamado de [ImpotsV7-dao.dll]
  • em [3]: após a geração (F6) do projeto, a pasta [dao/bin/Release] contém o DLL e o [ImpotsV7-dao.dll]. Ele também contém o arquivo de configuração [App.config], renomeado como [nom DLL].config. Isso é padrão no Visual Studio.

Em seguida, o DLL e o [ImpotsV7-dao.dll] são carregados no framework NUnit e executados:

  • no [1]: os testes foram bem-sucedidos. Consideramos agora a camada [dao] operacional. Seu DLL contém todas as classes do projeto, incluindo as classes de teste. Estas são desnecessárias. Estamos reconstruindo a DLL para excluir as classes de teste.
  • em [2]: a pasta [tests] é excluída do projeto
  • em [3]: o novo projeto. Este é regenerado pelo F6 para gerar um novo DLL. É esse DLL que será utilizado pelas camadas [metier] e [ui] do aplicativo.

9.8.4. A camada [metier]

  • em [1], o projeto [metier] tornou-se o projeto ativo da solução
  • em [2]: as referências do projeto. Observe-se a referência no DLL à camada [dao] criada anteriormente. Esse procedimento de adição de referência foi descrito na versão 5, no parágrafo 6.4.4.
  • em [3]: a camada [metier]. Trata-se da camada da versão 5, descrita no parágrafo 6.4.4.

O projeto [metier] está configurado para gerar um DLL:

  • [1]: o projeto é do tipo “biblioteca de classes”
  • [2]: a geração do projeto produzirá os arquivos DLL, [ImpotsV7-metier.dll] e [3].

O projeto foi gerado (F6).

9.8.5. A camada [ui]

  • em [1], o projeto [ui] tornou-se o projeto ativo da solução
  • em [2]: as referências do projeto. Observe-se as referências em DLL das camadas [dao] e [metier].
  • em [3]: a camada [ui]. Trata-se da versão 6 descrita no parágrafo 7.6.
  • em [4], o arquivo de configuração [App.config] é análogo ao da versão 6. Ele difere apenas na forma como a camada [dao] é instanciada pelo Spring:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
                <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
                <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • linhas 11-25: a configuração do Spring
  • linhas 15-24: os objetos instanciados pelo Spring
  • linhas 16-20: instanciação da camada [dao]
  • linha 16: a camada [dao] é instanciada pela classe [Dao.DataBaseImpot], que se encontra na DLL [ImpotsV7-Dao]
  • linhas 17-19: os três parâmetros (fábrica do SGBD utilizada, string de conexão, consulta SQL) a serem fornecidos ao construtor da classe [Dao.DataBaseImpot]
  • linhas 21-23: instanciação da camada [metier]. É a mesma configuração da versão 6.

Testes

O projeto [ui] está configurado da seguinte forma:

  • [1]: o projeto é do tipo “Aplicativo Windows”
  • [2]: a geração do projeto produzirá o executável [ImpotsV7-ui.exe]

Um exemplo de execução é fornecido em [3].

9.8.6. Alterar o banco de dados

A camada [dao] acima foi escrita com um conector genérico e uma base MySQL5. Propomos aqui mudar para uma base SQL Server Compact, a fim de mostrar que apenas a configuração será alterada.

A base SQL Server Compact será a seguinte:

  • [1]: o banco de dados [dbimpots.sdf] na visualização [DataBase Explorer] do Visual Studio [2]. Ele foi criado sem senha.
  • [3]: a tabela [data], que contém os dados. Escolhemos deliberadamente nomes diferentes para a tabela e as colunas em relação aos utilizados no banco de dados MySQL5, a fim de reforçar novamente a importância de incluir esse tipo de detalhe no arquivo de configuração, em vez de no código.
  • [4]: a coluna [id] é a chave primária e possui o atributo Identity; é a coluna SGBD que lhe atribuirá seus valores.
  • [5]: o conteúdo da tabela [data].
  • [6]: a base de dados [dbimpots.sdf] foi colocada na pasta do projeto [ui] e integrada a esse projeto.
  • [7]: o banco de dados [dbimpots.sdf] será copiado para a pasta de execução do projeto.

O arquivo de configuração [App.config] para o novo banco de dados é o seguinte:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <!--
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="MySql.Data.MySqlClient"/>
                <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/>
                <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/>
            </object>
            -->
            <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
                <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
                <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
                <constructor-arg index="2" value="select data1, data2, data3 from data"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • linhas 23-27: a configuração da camada [dao] para utilizar o banco de dados [dbimpots.sdf].

Os resultados da execução são idênticos aos anteriores. Vale ressaltar a vantagem de utilizar um conector genérico para tornar a camada [dao] imune à alteração de SGBD. Vimos, no entanto, que esse conector não é adequado para todas as situações, especialmente aquelas em que são utilizadas consultas parametrizadas. Existem, portanto, outras soluções, como a mencionada, que envolvem o uso de frameworks de terceiros para acesso a dados (Spring, iBatis, NHibernate, LINQ, ...).

9.9. Para saber mais...

  • O LINQ é apresentado em várias obras, principalmente no livro: C# 3.0 in a Nutshell, de Joseph e Ben Albahari, editora O’Reilly, já citado na introdução deste documento.
  • iBatis é apresentado no livro: “iBatis in Action”, de Clinton Begin, editora Manning
  • O livro “Nhibernate in Action”, da editora Manning, está previsto para julho de 2008

Spring, iBatis e NHibernate possuem manuais de referência disponíveis nos sites desses diferentes frameworks.