Skip to content

9. Acesso à base de dados

9.1. Conector ADO.NET

Vamos dar uma nova olhadela à arquitetura em camadas utilizada em várias ocasiões

Nos exemplos estudados, a camada [dao] tem explorado, até agora, dois tipos de fonte de dados:

  • dados codificados
  • dados de ficheiros de texto

Neste capítulo, estudamos o caso em que os dados provêm de uma base de dados. A arquitetura de 3 camadas evolui então para uma arquitetura multicamadas. Existem vários tipos de arquitetura multicamadas. Iremos estudar os conceitos básicos com o seguinte:

No diagrama acima, a camada [dao] [1] interage com o SGBD [3] através de uma biblioteca de classes específica para o SGBD utilizado e fornecida juntamente com este. Esta camada implementa funções padrão conhecidas como ADO (Active X Data Objects). Uma camada deste tipo é designada por «provedor» (provedor de acesso à base de dados, neste caso) ou mesmo «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], pelo que esta última tem os nomes das classes do conector no seu código. Se alterar o SGBD, altera o conector e as classes, e tem de alterar a camada [dao] . Isto é simultaneamente eficiente, porque o conector .NET, tendo sido escrito para um SGBD específico, sabe como melhor utilizá-lo, e rígido, porque alterar o SGBD implica alterar a camada [dao]. Este segundo argumento deve ser relativizado: as empresas não alteram o seu SGBD com muita frequência. 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. As duas formas de utilizar uma fonte de dados

A plataforma .NET permite-lhe explorar uma fonte de dados de duas formas diferentes:

  1. modo conectado
  2. modo offline

No modo conectado, a aplicação

  1. abre uma ligação à fonte de dados
  2. trabalha com a fonte de dados de leitura/gravação
  3. fecha a ligação

No modo offline, a aplicação

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

Aqui, iremos estudar apenas o modo conectado.

9.3. Conceitos básicos de funcionamento de bases de dados

Vamos demonstrar os principais conceitos de utilização de bases de dados com uma base de dados SQL Server Compact 3.5. Este SGBD é fornecido com o Visual Studio Express. É um SGBD leve que só consegue gerir um utilizador de cada vez. No entanto, é suficiente para introduzir a programação de bases de dados. Numa data posterior, apresentaremos outros SGBD.

A arquitetura utilizada será a seguinte:

Uma aplicação de consola [1] irá operar uma base de dados SqlServer Compact [3,4] através do conector Ado.Net deste SGBD [2].

9.3.1. Visite o banco de dados de exemplo em

Vamos criar a base de dados diretamente no Visual Studio Express. Para tal, criamos um novo projeto do tipo consola.

  • [1]: o projeto
  • [2]: abre a vista «Database Explorer»
  • [3]: cria uma nova ligação
  • [4]: Seleciona o tipo de SGBD
  • [5,6]: selecione o SGBD SQL Server Compact
  • [7]: crie a base de dados
  • [8]: uma base de dados SQL Server Compact é encapsulada num único ficheiro .sdf. Indicamos onde a criar, neste caso na pasta do projeto C#.
  • [9]: foi atribuído o nome [dbarticles.sdf] à nova base de dados
  • [10]: o idioma francês está selecionado. Isto tem impacto nas operações de ordenação.
  • [11,12]: a base de dados pode ser protegida por palavra-passe. Aqui, «dbarticles».
  • [13]: validar a página de informações. A base de dados está agora fisicamente criada:
  • [14]: o nome da base de dados que acabou de criar
  • [15]: marque a opção «Guardar a minha palavra-passe» para não ter de a voltar a introduzir sempre
  • [16]: verifique a ligação
  • [17]: está tudo bem
  • [18]: valida a página de informações
  • [19]: a ligação aparece no explorador de bases de dados
  • [20]: por enquanto, a base de dados não tem tabelas. Vamos criar uma. Um artigo terá os seguintes campos:
    • id: um identificador único - chave primária
    • nome: nome do item - único
    • preço: preço do artigo
    • stockactuel: o seu stock atual
    • stockminimum: o nível mínimo de stock abaixo do qual o artigo deve ser reabastecido
  • [21]: o campo [id] é do tipo inteiro e constitui a chave primária da tabela [22].
  • [23]: esta chave primária é do tipo Identity. Esta noção, específica dos SGBD SQL Servers, indica que a chave primária será gerada pelo próprio SGBD. Aqui, a chave primária será um inteiro que começa em 1 e é incrementado em 1 para cada nova chave.
  • [24]: os outros campos são criados. Note-se que o campo [name] tem uma restrição de unicidade do tipo « » [25].
  • [26]: é atribuído um nome à tabela
  • [27]: assim que a estrutura da tabela for validada, esta aparece na base de dados.
  • [28]: pedido para ver o conteúdo da tabela
  • [29]: atualmente vazia
  • [30]: isto é preenchido com alguns dados. Uma linha é validada assim que a linha seguinte é introduzida. O campo [id] não é introduzido: é gerado automaticamente quando a linha é validada.

Precisamos agora de configurar o projeto para que esta base de dados, atualmente na raiz do projeto, seja automaticamente copiada para a pasta de execução do projeto:

  • [1]: pedido para visualizar todos os ficheiros
  • [2]: aparece a base [dbarticles.sdf]
  • [3]: incluímo-la no projeto
  • [4]: a operação de adicionar uma fonte de dados a um projeto inicia um assistente que não precisamos aqui [5].
  • [6]: a base faz agora parte do projeto. Voltamos ao modo normal [7].
  • [8]: o projeto com a sua base
  • [9]: nas propriedades da base de dados, podemos ver [10] que a base de dados será automaticamente copiada para a pasta de execução do projeto. É aí que o programa que estamos prestes a escrever irá procurá-la.

Agora que temos uma base de dados disponível, poderemos explorá-la. Primeiro, vamos dar uma olhadela ao SQL.

9.3.2. Os quatro comandos básicos da linguagem SQL

O SQL (Structured Query Language) é uma linguagem parcialmente padronizada para consultar e atualizar bases de dados. Todos os SGBD respeitam a parte padronizada do SQL, mas acrescentam extensões proprietárias à linguagem que exploram certas funcionalidades do SGBD. Já vimos dois exemplos: a geração automática de chaves primárias e os tipos permitidos para as colunas das tabelas dependem frequentemente do SGBD.

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

select col1, col2,...
from table1, table2,...
where condição
order by expressão
...
A consulta que recupera os dados contidos numa base de dados. Apenas as palavras-chave na primeira linha são obrigatórias, as restantes são opcionais. Outras palavras-chave não apresentadas aqui.
  1. É feita uma junção com todas as tabelas atrás da palavra-chave from
  2. Apenas as colunas atrás da palavra-chave select são mantidas
  3. Apenas as linhas que satisfazem a condição where são retidas
  4. As linhas resultantes, ordenadas pela expressão da palavra-chave order by, formam o resultado da consulta. Este 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 inicializadas com os valores (val1, val2, ...).
atualizar tabela
definir col1=val1, col2=val2
onde condição
Atualiza a tabela verificando a condição (todas as linhas se não houver where). Para estas linhas, a col1 recebe o valor val1
eliminar da tabela
onde condição
Elimina todas as condições de verificação da tabela

Vamos escrever uma aplicação de consola para emitir comandos SQL para a base de dados [dbarticles] que criámos anteriormente. Aqui está um exemplo de execução . Convidamos o leitor a compreender os comandos SQL emitidos e os 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 cadeia de ligação: contém todos os parâmetros necessários para se ligar à base de dados.
  • linha 3: solicita o conteúdo da tabela [articles]
  • linha 16: é inserida uma nova linha. Note-se que o id não é inicializado nesta operação, uma vez que é o SGBD que irá gerar o valor deste campo.
  • linha 19: verificação. Linha 28, a linha foi adicionada.
  • linha 30: o preço do item que acabou de ser adicionado é aumentado em 10%.
  • linha 33: verificação
  • linha 42: o aumento de preço ocorreu
  • linha 44: elimina o artigo adicionado anteriormente
  • linha 47: verificar
  • linhas 53-55: o artigo já não está presente.

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

Voltemos ao diagrama de uma aplicação que utiliza uma base de dados através de um conector ADO.NET:

No modo conectado, o :

  1. abre uma ligação à fonte de dados
  2. trabalha com a fonte de dados de leitura/gravação
  3. fecha a ligação

Três interfaces ADO.NET estão principalmente envolvidas nestas operações:

  • IDbConnection, que encapsula as propriedades e métodos da ligação.
  • IDbCommand, que encapsula as propriedades e os métodos do comando SQL que está a ser executado.
  • IDataReader, que encapsula as propriedades e métodos do resultado de uma instrução SQL Select.

A interface IDbConnection

Utilizada para gerir a ligação à base de dados. Os métodos M e as propriedades P desta interface serão os seguintes:

Nome
Tipo
Função
ConnectionString
P
Cadeia de ligação à base. Especifica todos os parâmetros necessários para estabelecer uma ligação com uma base específica.
Abrir
M
abre a ligação à base definida por ConnectionString
Fechar
M
fecha a ligação
BeginTransaction
M
inicia uma transação.
Estado
P
estado da ligação: ConnectionState.Closed, ConnectionState.Open, ConnectionState.Connecting, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken

Se Connection for uma classe que implementa a interface IDbConnection, a ligação pode ser aberta da seguinte forma:

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

A interface IDbCommand

Utilizada para executar um comando SQL ou um procedimento armazenado. Os métodos M e as propriedades P desta interface serão os seguintes:

Nome
Tipo
Função
Tipo de comando
P
indica o que deve ser executado - obtém os seus valores de uma enumeração:
- CommandType.Text : executa o comando SQL definido em CommandText. Este é o valor predefinido.
- CommandType.StoredProcedure : executa um procedimento armazenado no
CommandText
P
- texto do comando SQL a ser executado se CommandType= CommandType.Text
- o nome do procedimento armazenado a ser executado se CommandType= CommandType.StoredProcedure
Conexão
P
a ID da ligação IDbConnection a ser utilizada para executar o comando SQL
Transação
P
a transação IDbTransaction na qual o comando SQL deve ser executado
Parâmetros
P
a lista de parâmetros de uma instrução SQL parametrizada. A instrução update articles set price=price*1.1 where id=@id tem o parâmetro @id.
ExecuteReader
M
para executar uma instrução SQL SELECT. O resultado é um objeto IDataReader que representa o resultado da instrução SELECT.
ExecuteNonQuery
M
para executar uma instrução SQL Update, Insert, Delete. Isto fornece o número de linhas afetadas pela operação (atualizadas, inseridas, eliminadas).
ExecuteScalar
M
para executar uma instrução SQL Select que retorne um único resultado, como em: select count(*) from articles.
CreateParameter
M
para criar parâmetros IDbParameter na ordem SQL.
Prepare
M
otimiza a execução de uma consulta parametrizada quando esta é executada várias vezes com parâmetros diferentes.

Se Command for uma classe que implemente a interface IDbCommand, a execução de uma instrução SQL sem transação assumirá a seguinte forma:

// opening connection 
IDbConnection connexion=...
connexion.Open();
// order preparation
IDbCommand commande=new Command();
commande.Connection=connexion;
// select order execution
commande.CommandText="select ...";
IDbDataReader reader=commande.ExecuteReader();
...
// execute update, insert, delete commands
commande.CommandText="insert ...";
int nbLignesInsérées=commande.ExecuteNonQuery();
...
// locking connection
connexion.Close();

A interface a IDataReader

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

Nome
Tipo
Função
FieldCount
P
o número de colunas na tabela IDataReader
GetName
M
GetName(i) devolve o nome da coluna n.º i da tabela IDataReader.
Item
P
Item[i] representa a coluna n.º i na linha atual da tabela IDataReader.
Read
M
avança para a linha seguinte na tabela IDataReader. Render Booleano True se a leitura foi possível, False caso contrário.
Fechar
M
fecha a tabela IDataReader.
GetBoolean
M
GetBoolean(i): devolve o valor booleano da coluna n.º i na linha atual da tabela do IDataReader. Outros métodos semelhantes incluem: GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString.
Getvalue
M
Getvalue(i): devolve o valor da coluna n.º i na linha atual da tabela IDataReader como um objeto de tipo.
IsDBNull
M
IsDBNull(i) retorna True se a coluna n.º i na linha atual do IDataReader não tiver valor, simbolizado por SQL NULL.

A utilização de um objeto IDataReader tem frequentemente o seguinte aspeto:

// opening connection 
IDbConnection connexion=...
connexion.Open();
// order preparation
IDbCommand commande=new Command();
commande.Connection=connexion;
// order execution select
commande.CommandText="select ...";
IDataReader reader=commande.ExecuteReader();
// operation results
while(reader.Read()){
    // operate current line
        ...
}
// lock reader
reader.Close();
// locking connection
connexion.Close();

9.3.4. Gestão de erros

Vamos rever a arquitetura de uma aplicação de base de dados:

A camada [dao] pode encontrar inúmeros erros durante a operação da base de dados. Estes serão levantados como exceções pelo conector ADO.NET. O código da camada [dao] deve tratá-los. Qualquer operação com a base de dados deve ser realizada no modo try / catch / finally, de modo a interceptar e tratar qualquer exceção e libertar os recursos que precisam de ser libertados. Por exemplo, o código visto acima para explorar o resultado de uma consulta Select torna-se o seguinte:

// connection initialization
IDbConnection connexion=...
// operation connection
try{
     // opening
    connexion.Open();
     // order picking
    IDbCommand commande=new Command();
    commande.Connection=connexion;
     // order execution select
    commande.CommandText="select ...";
    IDbDataReader reader=commande.ExecuteReader();
     // operation results
    try{
        while(reader.Read()){
        // operate current line
        ...
    }finally{
         // lock reader
        reader.Close();
    }
}catch(Exception ex){
     // management exception
    ...
}finally{
     // locking connection
    connexion.Close();
}
...

Aconteça o que acontecer, os objetos IDataReader e IDbConnection devem ser fechados. É por isso que este encerramento está incluído nas cláusulas finally.

O encerramento da ligação e do objeto IDataReader pode ser automatizado com um using :

// operation connection
try{
    using(IDbConnection connexion=...){
         // opening
        connexion.Open();
         // order preparation
        IDbCommand commande=new Command();
        commande.Connection=connexion;
         // order execution select
        commande.CommandText="select ...";
        using(IDbDataReader reader=commande.ExecuteReader()){
             // operation results
            while(reader.Read()){
            // operate current line
            ...
         }// using IData
     }//using IDbConnection
}catch(Exception ex){
     // management exception
    ...
}
..
  • A linha 3, a cláusula using garante que a ligação aberta no using(...){...} será fechada fora dela, independentemente de como sair do bloco: normalmente ou pela ocorrência de uma exceção. Isto poupa um finally, mas o interesse não está nesta pequena economia. O uso de um using impede que o programador feche a ligação por conta própria. Ou então, esquecer-se de fechar uma ligação pode passar despercebido e «travar» a aplicação de uma forma que parece aleatória, sempre que o SGBD atingir o número máximo de ligações abertas que pode suportar.
  • Linha 11: proceda da mesma forma para fechar o objeto IDataReader.

9.3.5. Exemplo de configuração do projeto

O projeto final ficará da seguinte forma:

  • [1]: o projeto terá um ficheiro de configuração [App.config]
  • [2]: utiliza duas classes DLL que não são referenciadas por predefinição e que, por isso, devem ser adicionadas às referências do projeto:
    • [System.Configuration] para utilizar o ficheiro de configuração [App.config]
    • [System.Data.SqlServerCe] para operar a base de dados Sql Server Compact
  • [3, 4]: relembre como adicionar referências a um projeto.
  • [5, 6]: relembre como adicionar o ficheiro [App.config] a um projeto.

O ficheiro de configuração [App.config] terá o seguinte conteúdo:


<?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> define as cadeias de ligação à base de dados. Uma cadeia de ligação tem o formato "parâmetro1=valor1;parâmetro2=valor2;...". Define todos os parâmetros necessários para estabelecer uma ligação com uma base de dados específica. Estas cadeias de ligação variam consoante o SGBD. O [http://www.connectionstrings.com/] apresenta o formato destas para os principais SGBD.
  • linha 4: define uma cadeia de ligação específica, neste caso para a base de dados SQL Server Compact dbarticles.sdf que criámos anteriormente:
    • name = nome da cadeia de ligação. É através deste nome que uma cadeia de ligação é recuperada pelo programa C#
    • connectionString: a cadeia de ligação para um SQL Server Compact de base
    • DataSource: indica o caminho base. A sintaxe |DataDirectory| indica a pasta de execução do projeto.
    • Password: palavra-passe base. Este parâmetro está ausente se não houver palavra-passe.

O código C# para recuperar a cadeia de ligação anterior é o seguinte:


string connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
  • ConfigurationManager é a classe da DLL [System.Configuration], que é utilizada para operar o ficheiro [App.config].
  • ConnectionsStrings["nom"].ConnectionString: designa a secção <connectionStrings> da tag connectionString <add name="name" connectionString="..."> do ficheiro [App.config]

O projeto está agora configurado. Estamos agora a analisar a classe [Program.cs], cujo exemplo 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 application - executes SQL requests typed from the keyboard
             // on a database whose connection string is obtained from a configuration file
 
             // use of configuration file [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }
 
             // display connection string
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
 
             // build a dictionary of accepted sql commands
            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);
            }
 
             // read-execute SQL commands typed on the keyboard
             string requête =     nu ll; // query text SQL
             string[] cham            ps; // query fields     
             Regex modèle = new Regex(@"\s+    "); // sequence of spaces
 
             // input-execution loop for SQL commands typed on keyboard
            while (true) {
                 // request for query
                Console.Write("\nRequête SQL (rien pour arrêter) : ");
                requête = Console.ReadLine().Trim().ToLower();
                 // finished?
                if (requête == "")
                    break;
                 // the query is broken down into fields
                champs = modèle.Split(requête);
                 // valid request?
                if (champs.Length == 0 || ! dicoCommandes.ContainsKey(champs[0])) {
                     // error msg
                    Console.WriteLine("Requête invalide. Utilisez select, insert, update, delete ou rien pour arrêter");
                     // following request
                    continue;
                }
                 // query execution
                if (champs[0] == "select") {
                    ExecuteSelect(connectionString, requête);
                } else
                    ExecuteUpdate(connectionString, requête);
            }
        }
 
         // execute an update request
        static void ExecuteUpdate(string connectionString, string requête) {
    ...
        }
 
         // executing a Select query
        static void ExecuteSelect(string connectionString, string requête) {
....
        }
    }
}
  • linhas 1-6: namespaces utilizados na aplicação. A gestão de uma base de dados SQL Server Compact requer o namespace [System.Data.SqlServerCe] na linha 3. Trata-se de uma dependência de um namespace proprietário do SGBD. Isto significa que o programa terá de ser modificado se o SGBD for alterado.
  • linha 18: a cadeia de ligação à base de dados é lida do ficheiro [App.config] e apresentada na linha 25. Será utilizada para estabelecer uma ligação com a base de dados.
  • linhas 28-32: um dicionário que armazena os nomes das quatro ordens SQL autorizadas: select, insert, update, delete.
  • linhas 40-62: o ciclo para introduzir comandos SQL digitados no teclado e executá-los na base de dados
  • linha 48: a linha digitada no teclado é dividida em campos para determinar 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: o comando SQL introduzido é executado. Esta execução assume uma forma diferente dependendo de o comando ser select ou um comando insert, update, delete. No primeiro caso, o comando recupera dados da base de dados sem os modificar; no segundo, atualiza a base de dados sem recuperar dados. Em ambos os casos, a execução é delegada a um método que necessita de dois parâmetros:
    • a cadeia de ligação que permitirá a ligação à base de dados
    • o comando SQL a ser executado nesta ligação

9.3.7. Executar uma consulta SELECT

A execução de comandos SQL requer os seguintes passos:

  1. Ligação à base de dados
  2. Envio de ordens SQL para a base
  3. Processamento dos resultados das instruções SQL
  4. Encerramento da ligação

Os passos 2 e 3 são executados repetidamente, sendo a ligação encerrada apenas quando a base de dados já não estiver a ser utilizada. As ligações abertas são recursos limitados de um SGBD. Devem ser conservadas. É por isso que procuramos sempre limitar o tempo de vida de uma ligação aberta. Neste exemplo, a ligação é encerrada após cada comando SQL. É aberta uma nova ligação para a próxima ordem SQL. Abrir e fechar uma ligação é dispendioso. Para reduzir este custo, alguns SGBD oferecem o conceito de conjuntos de ligações abertas: quando a aplicação é inicializada, são abertas N ligações e atribuídas ao conjunto. Estas permanecem abertas até ao fim da aplicação. Quando a aplicação abre uma ligação, recebe uma das N ligações já abertas no conjunto. Quando fecha a ligação, basta devolvê-la ao conjunto. A vantagem deste sistema é que é transparente para o programador: o programa não precisa de ser modificado para utilizar o conjunto de ligações. A configuração do conjunto de ligações depende do SGBD.

Primeiro, analisamos a execução de ordens SQL Select. O método ExecuteSelect do nosso programa de exemplo é o seguinte:


// execute a Select query
        static void ExecuteSelect(string connectionString, string requête) {
             // handle any exceptions
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                     // opening connection
                    connexion.Open();
                     // executes sqlCommand with select query
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    SqlCeDataReader reader= sqlCommand.ExecuteReader();
                     // displaying results
                    AfficheReader(reader);
                }
            } catch (Exception ex) {
                 // error msg
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
 
         // reader display
        static void AfficheReader(IDataReader reader) {
...
        }
  • linha 2: o método recebe dois parâmetros:
    • a cadeia de ligação [connectionString] que lhe permitirá ligar-se à base de dados
    • a instrução SQL Select [request] a ser executada nesta ligação
  • linha 4: qualquer operação com uma base de dados pode gerar uma exceção que talvez queira tratar. Isto é ainda mais importante aqui, uma vez que os comandos SQL fornecidos pelo utilizador podem conter erros sintáticos. Precisamos de poder informá-lo disso. Todo o código está, portanto, dentro de um try / catch.
  • linha 5: há várias coisas aqui:
    • a ligação à base de dados é inicializada com a cadeia de ligação [connectionString]. Ainda não está aberta. Será aberta na linha 7.
    • A cláusula using (Resource) {...} é um recurso sintático que garante a libertação do recurso Resource, neste caso uma ligação, na saída do bloco controlado pelo using.
    • A ligação é de um tipo proprietário: SqlCeConnection, específico do SGBD SQL Server Compact.
  • linha 7: a ligação é aberta. É aqui que os parâmetros da cadeia de ligação são utilizados.
  • linha 9: é emitida uma instrução SQL através de um objeto SqlCeCommand. A linha 9 inicializa este objeto com duas informações: a ligação a utilizar e o comando SQL a enviar através dela. O objeto SqlCeCommand pode ser utilizado para executar uma instrução Select, uma instrução Update, Insert ou Delete. As suas propriedades e métodos foram apresentados no parágrafo 9.3.3.
  • linha 10: uma instrução SQL Select é executada através do objeto ExecuteReader do SqlCeCommand, o que cria um objeto IDataReader cujos métodos e propriedades foram apresentados no parágrafo 9.3.3.
  • linha 12: a exibição dos resultados é confiada ao AfficheReader a seguir:

         // reader display
        static void AfficheReader(IDataReader reader) {
            using (reader) {
                 // exploitation of results
                 // -- columns
                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, '-'));
                 // -- data
                while (reader.Read()) {
                     // current line operation
                    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. Note que aqui se trata de uma interface e não de uma classe específica.
  • linha 3: a cláusula using é utilizada para gerir automaticamente o encerramento do IDataReader.
  • linhas 8-10: os nomes das colunas da tabela de resultados da instrução Select. Estas são as colunas col1, col2, ... da instrução Select col1, col2, ... from table ...
  • linhas 14-21: percorre a tabela de resultados e apresenta os valores de cada linha da tabela.
  • linha 18: não sabemos o tipo da coluna i no resultado porque não sabemos qual a tabela que está a ser consultada. A sintaxe reader.GetXXX(i), em que XXX é o tipo da coluna n.º i, uma vez que este tipo não é conhecido. Em seguida, usamos a sintaxe reader.Item[i].ToString() para obter a representação em cadeia de caracteres da coluna n.º i. A sintaxe reader.Item[i].ToString() pode ser abreviada para reader[i].ToString().

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

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


         // execute an update request
        static void ExecuteUpdate(string connectionString, string requête) {
             // handle any exceptions
            try {
                using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                     // opening connection
                    connexion.Open();
                     // executes sqlCommand with update request
                    SqlCeCommand sqlCommand = new SqlCeCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                     // result display
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                 // error msg
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
}

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

  • linha 10: os comandos Update, Insert e Delete são executados pelo método ExecuteNonQuery do objeto SqlCeCommand. Se for bem-sucedido, este método devolve o número de linhas atualizadas (update), inseridas (insert) ou eliminadas (delete).
  • linha 12: este número de linhas é apresentado no ecrã

Convidamos o leitor a consultar um exemplo de como executar este código, no parágrafo 9.3.2.

9.4. Outros conectores ADO.NET

O código que estudámos é proprietário: depende do [System.Data.SqlServerCe] para o SGBD SQL Server Compact. Vamos agora compilar 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 é descrita nos anexos, no parágrafo 1.1.

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

  • [1]: criar um novo projeto na solução atual e torná-lo o projeto atual.
  • [2]: criar uma nova ligação
  • [3]: selecionar o tipo de ligação
  • [4]: selecionar o SGBD SQL Server
  • [5]: resultado da escolha anterior
  • [6]: utilize o botão [Procurar] para especificar onde criar a base de dados do SQL Server 2005. A base de dados está encapsulada num ficheiro .mdf.
  • [7]: selecione a raiz do novo projeto e aceda ao ficheiro base [dbarticles.mdf].
  • [8]: utilize a autenticação do Windows.
  • [9]: valide a página de informações
  • [11]: Base de dados SQL Server
  • [12]: criar uma tabela. Esta será idêntica à base de dados SQL Server Compact criada anteriormente.
  • [13]: o campo [id]
  • [14]: o campo [id] é do tipo Identity.
  • [15,16]: o campo [id] é a chave primária
  • [17]: outros campos da tabela
  • [18]: atribua à tabela o nome [articles] ao guardá-la (Ctrl+S).

Ainda precisamos de inserir dados na :

Incluímos a base de dados no :

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

O ficheiro 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: cadeia de ligação à base de dados [dbarticles.mdf] com autenticação do Windows
  • linha 5: cadeia de ligação à base de dados [dbarticles.mdf] com autenticação do SQL Server. [sa,msde] é o par (nome de utilizador, palavra-passe) do administrador do SQL Server, tal como 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) {
...
             // use of configuration file [App.config]
            string connectionString = null;
            try {
                connectionString = ConfigurationManager.ConnectionStrings["connectString2"].ConnectionString;
            } catch (Exception e) {
...
            }
...
             // read-execute SQL commands typed on the keyboard
...
        }
 
         // execute an update request
        static void ExecuteUpdate(string connectionString, string requête) {
             // handle any exceptions
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                     // opening connection
                    connexion.Open();
                     // executes sqlCommand with update request
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                     // result display
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
....
            }
        }
 
         // execute a Select query
        static void ExecuteSelect(string connectionString, string requête) {
             // handle any exceptions
            try {
                using (SqlConnection connexion = new SqlConnection(connectionString)) {
                     // opening connection
                    connexion.Open();
                     // executes sqlCommand with select query
                    SqlCommand sqlCommand = new SqlCommand(requête, connexion);
                    SqlDataReader reader = sqlCommand.ExecuteReader();
                     // exploitation of results
...
                }
            } catch (Exception ex) {
...
            }
        }
    }
}
  • linha 1: o espaço de nomes [System.Data.SqlClient] contém classes para gerir uma base de dados SQL Server 2005
  • linha 24: a ligação é SQLConnection
  • linha 28: o objeto que encapsula as ordens 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 o executar, pode utilizar (linha 11) qualquer uma das duas cadeias de ligação definidas em [App.config].

9.4.2. Conector MySQL5

A arquitetura utilizada será a seguinte:

A instalação do MySQL5 é descrita nos apêndices, 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 as referências necessárias:

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

Criamos agora a base de dados [dbarticles] e a sua tabela [articles]. O SGBD MySQL5 deve estar em execução. Além disso, iniciamos o cliente [Query Browser] (ver parágrafo 1.2.3).

  • [1]: no [Query Browser], clique com o botão direito do rato na zona [Schemata] [2] para criar [3] um novo esquema, o termo utilizado para descrever uma base de dados.
  • [4]: a base de dados será denominada [dbarticles]. Em [5], podemos vê-la. Por enquanto, não possui tabelas. Vamos executar o seguinte script SQL:
/* choice of current database */
USE dbarticles;
/* create an item table */
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
);
/* table data insertion */
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);
/* adding constraints */
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: a base de dados [dbarticles] torna-se a base de dados atual. Os seguintes comandos SQL serão executados nessa base de dados.
  • linhas 4-10: definição da tabela [ARTICLES]. Note-se que o SQL é propriedade do MySQL. Os tipos de coluna e a geração automática da chave primária (atributo AUTO_INCREMENT) diferem dos encontrados no SGBD SQL Server Compact e Express.
  • linhas 12-14: inserção de três linhas
  • linhas 16-21: adicionar restrições de integridade às colunas.

Este script é executado no [MySQL Query Browser]:

  • no [MySQL Query Browser] [6], carregamos o script [7]. Pode vê-lo em [8]. Em [9], é executado.
  • em [10], a tabela [articles] foi criada. Clique duas vezes nela. Isto abre a janela [11] com a consulta [12] no interior, pronta para ser executada por [13]. Em [14], o resultado da execução. Temos as três linhas esperadas. Note que os valores no campo [ID] foram gerados automaticamente (atributo do campo AUTO_INCREMENT).

Agora que a base de dados está pronta, podemos voltar ao desenvolvimento da aplicação no Visual Studio.

Em [1], o programa [Program.cs] e o ficheiro de configuração [App.config]. Este último é 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 ligação são os seguintes:

  • Server : nome da máquina na qual se encontra o SGBD MySQL, neste caso localhost, ou seja, a máquina na qual o programa será executado.
  • Base de dados: o nome da base de dados gerida, neste caso dbarticles
  • Uid: nome de utilizador, aqui root
  • Pwd: a sua palavra-passe, neste caso root. Estas duas informações designam o administrador criado no parágrafo 1.2.

O programa [Program.cs] é idêntico ao das versões anteriores, exceto pelos seguintes detalhes:

espaço de nomes
MySql.Data.MySqlClient
classe Connection
MySqlConnection
classe Command
MySqlCommand
classe DataReader
MySqlDataReader

O programa utiliza a cadeia de ligação denominada dbArticlesMySql5 no ficheiro [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 apresentam uma interface padrão para as aplicações que os utilizam. Assim, com um único código, a nova aplicação poderá comunicar com qualquer SGBD através de um conector ODBC, c.a.d. ou SGBD. O desempenho dos conectores ODBC não é tão bom quanto o dos conectores «proprietários», que são capazes de explorar todas as funcionalidades de um SGBD específico. Por outro lado, obtém-se uma grande flexibilidade de aplicação: é possível mudar de SGBD sem alterar o código.

Vamos analisar um exemplo em que a aplicação utiliza uma base de dados MySQL5 ou uma base de dados SQL Server Express, dependendo da cadeia de ligação que lhe for fornecida. A seguir, assumimos que:

  • os SGBD SQL Server Express e MySQL5 foram iniciados
  • o controlador ODBC do MySQL5 está presente na máquina (ver parágrafo 1.2.6). O padrão é o SQL Server 2005.
  • as bases de dados utilizadas são as do parágrafo 9.4.2 para a base MySQL5 e as do parágrafo 9.4.1 para a base de dados SQL Server Express.

O novo projeto do Visual Studio é o seguinte:

Acima, a base de dados SQL Server [dbarticles.mdf] criada no parágrafo 9.4.1 foi copiada para o ficheiro do projeto.

O ficheiro 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: cadeia de ligação de origem ODBC MySQL5. Esta é uma cadeia já estudada anteriormente, na qual encontramos um novo parâmetro Driver que define o controlador ODBC a ser utilizado.
  • linha 5: cadeia de ligação de origem ODBC SQL Server Express. Esta é a cadeia já utilizada num exemplo anterior, à qual foi adicionado o parâmetro Driver.

O programa [Program.cs] é idêntico ao das versões anteriores, exceto pelos seguintes detalhes:

espaço de nomes
System.Data.Odbc
classe Connection
OdbcConnection
classe Command
OdbcCommand
classe DataReader
OdbcDataReader

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

Com a cadeia de ligaçã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 ligaçã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:

Tal como acontece com os conectores ODBC, também estão disponíveis conectores OLE DB (Object Linking and Embedding DataBase), cujos controladores fornecem uma interface padrão às aplicações que os utilizam. Os controladores ODBC permitem o acesso a bases de dados. As fontes de dados para os controladores OLE DB são mais variadas: bases de dados, sistemas de mensagens, diretórios, etc. Qualquer fonte de dados pode ser objeto de um controlador OLE DB, se o editor assim o decidir. Isto proporciona acesso padrão a uma ampla variedade de dados.

Vamos analisar um exemplo em que a aplicação utiliza uma base de dados ACCESS ou SQL Server Express, dependendo da cadeia de ligação que lhe for fornecida. No que se segue, assumimos que o SGBD SQL Server Express foi iniciado e que a base de dados utilizada é a do exemplo anterior.

O novo projeto do Visual Studio é o seguinte:

  • em [1]: o namespace necessário para os conectores OLE DB é [System.Data.OleDb], presente na referência [System.Data] acima. A base de dados SQL Server [dbarticles.mdf] foi copiada do projeto anterior. A base [dbarticles.mdb] foi criada com o Access.
  • em [2]: tal como a base de dados do SQL Server, a base de dados do ACCESS tem a propriedade [Copiar para o diretório de saída=Copiar sempre], para que seja automaticamente copiada para a pasta de execução do projeto.

A base de dados ACCESS [dbarticles.mdb] é a seguinte:

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

O ficheiro 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: cadeia de ligação de origem OLE DB ACCESS. Contém o parâmetro Provider, que define o controlador OLE DB a utilizar e o caminho da base de dados
  • linha 5: cadeia de ligação de origem OLE DB Server Express.

O programa [Program.cs] é idêntico ao das versões anteriores, exceto pelos seguintes detalhes:

espaço de nomes
System.Data.OleDb
classe Connection
OleDbConnection
classe Command
OleDbCommand
classe DataReader
OleDbDataReader

O programa utiliza uma das duas cadeias de ligação definidas no ficheiro [App.config]. A execução produz os seguintes resultados com a cadeia de ligaçã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:

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

No exemplo acima, quando a aplicação solicita uma ligação ao conector genérico, este devolve um IDbConnection, a interface de ligação descrita no parágrafo 9.3.3, implementada por um MySQLConnection ou SQLConnection, de acordo com a natureza do pedido que lhe é feito. Diz-se que o conector genérico possui classes do tipo «factory»: utilizamos uma «factory» para lhe pedir que crie objetos e forneça referências aos mesmos (ponteiros). Daí o seu nome («factory» = fábrica, unidade de produção de objetos).

Não existe um conector genérico para todos os SGBD (abril de 2008). Para descobrir quais estão instalados numa determinada máquina, utilize 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()] devolve uma lista de conectores genéricos instalados, sob a forma de uma tabela de base de dados armazenada na memória (DataTable).
  • linhas 9-11: exibe os nomes das colunas da tabela dt :
    • dt.Columns é a lista de 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 de 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 na minha máquina é 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 aqui.

A exibição a seguir mostra que os seguintes conectores genéricos estão disponíveis:

Nome
Identificador
Provedor de dados ODBC
System.Data.Odbc
Provedor de dados OleDb
System.Data.OleDb
Provedor de dados OracleClient
System.Data.OracleClient
Provedor de dados SqlClient
System.Data.SqlClient
Provedor de dados do Microsoft SQL Server Compact
System.Data.SqlServerCe.3.5
Provedor de dados MySQL
MySql.Data.MySqlClient

Um conector genérico é acessível num programa C# através do seu identificador.

Vamos analisar um exemplo em que a aplicação explora as várias bases de dados que construímos até agora. A aplicação receberá dois parâmetros:

  • o primeiro parâmetro especifica o tipo de SGBD utilizado, para que seja utilizada a biblioteca de classes correta
  • o segundo parâmetro especifica a base de dados gerida, através de uma cadeia de ligação.

O novo projeto do Visual Studio é o seguinte:

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

O ficheiro 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: cadeias de ligação para as várias bases de dados utilizadas.
  • linhas 13-17: nomes dos conectores genéricos a utilizar

O programa [Program.cs] é o seguinte:


...
using System.Data.Common;
 
namespace Chap7 {
    class SqlCommands {
        static void Main(string[] args) {
 
             // console application - executes SQL requests typed from the keyboard
             // on a database whose connection string is obtained from a configuration file, along with the connector name of the associated SGBD
 
             // checking parameters
            if (args.Length != 2) {
                Console.WriteLine("Syntaxe : pg factory connectionString");
                return;
            }
 
             // using the configuration file
            string factory = null;
            string connectionString = null;
            DbProviderFactory connecteur = null;
            try {
                 // factory
                factory = ConfigurationManager.AppSettings[args[0]];
                 // connecting chain
                connectionString = ConfigurationManager.ConnectionStrings[args[1]].ConnectionString;
                 // we retrieve a generic connector for the SGBD
                connecteur = DbProviderFactories.GetFactory(factory);
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }
 
             // displays
            Console.WriteLine("Provider factory : [{0}]\n", factory);
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
 
...
                 // query execution
                if (champs[0] == "select") {
                    ExecuteSelect(connecteur,connectionString, requête);
                } else
                    ExecuteUpdate(connecteur, connectionString, requête);
            }
        }
 
         // execute an update request
        static void ExecuteUpdate(DbProviderFactory connecteur, string connectionString, string requête) {
             // handle any exceptions
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                     // connection configuration
                    connexion.ConnectionString = connectionString;
                     // opening connection
                    connexion.Open();
                    // configuration Command 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                     // request execution
                    int nbLignes = sqlCommand.ExecuteNonQuery();
                     // result display
                    Console.WriteLine("Il y a eu {0} ligne(s) modifiée(s)", nbLignes);
                }
            } catch (Exception ex) {
                 // error msg
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
 
         // execute a Select query
        static void ExecuteSelect(DbProviderFactory connecteur, string connectionString, string requête) {
             // handle any exceptions
            try {
                using (DbConnection connexion = connecteur.CreateConnection()) {
                     // connection configuration
                    connexion.ConnectionString = connectionString;
                     // opening connection
                    connexion.Open();
                    // configuration Command 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                     // request execution
                    DbDataReader reader = sqlCommand.ExecuteReader();
                     // display of results
...
                }
            } catch (Exception ex) {
                 // error msg
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
    }
}
  • linhas 12-14: a aplicação recebe dois parâmetros: o nome do conector genérico e a cadeia de ligação à base de dados sob a forma de chaves no ficheiro [App.config].
  • linhas 23, 25: recuperam o nome do conector genérico e a cadeia de ligação do ficheiro [App.config]
  • linha 27: o conector genérico é instanciado. A partir deste ponto, fica associado a um SGBD específico.
  • linhas 39-43: a execução do comando SQL introduzido no teclado é delegada a dois métodos, aos quais passamos:
    • o pedido a ser executado
    • a cadeia de ligação que identifica a base de dados na qual a consulta será executada
    • o conector genérico que identifica as classes a serem utilizadas para comunicar com o SGBD que gere a base de dados.
  • linhas 50-54: obtém-se uma ligação utilizando o CreateConnection (linha 50) do conector genérico, sendo esta posteriormente configurada com a cadeia de ligação da base a gerir (linha 52). Em seguida, é aberta (linha 54).
  • linhas 56-58: o objeto Command necessário para executar a instrução SQL é criado com o método CreateCommand do conector genérico. Em seguida, é configurado com o texto da instrução SQL a ser executada (linha 57) e a conexão na qual executá-la (linha 58).
  • linha 60: o comando SQL de atualização é executado
  • linhas 74-87: é utilizado um código semelhante. A novidade reside na linha 84. O objeto Reader obtido pela execução da ordem do tipo Select é DbDataReader, que pode ser utilizado da mesma forma que OleDbDataReader, OdbcDataReader, ... que já conhecemos.

Aqui estão alguns exemplos.

Com o MySQL5 de base:

Abra a página de propriedades do projeto [1] e selecione o separador [Debug] [2]. Em [3], a chave do conector da linha 14 do ficheiro [App.config]. Em [4], a chave da cadeia de ligação na linha 6 do ficheiro [App.config]. Os resultados 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 SQL Server Compact:

Em [1], a chave do conector na linha 13 do [App.config]. Em [2], a chave da cadeia de ligação na linha 4 do [App.config]. Os resultados 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

Convidamos o leitor a testar as outras bases de dados.

9.4.6. Que conector escolher?

Voltemos à arquitetura de uma aplicação de base de dados:

Vimos vários 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 significa alterar a camada [dao].
  • Os conectores ODBC ou OLE DB permitem-lhe trabalhar com várias bases de dados sem alterar a camada [dao]. São menos potentes do que os conectores proprietários.
  • O conector genérico depende de conectores proprietários, ao mesmo tempo que apresenta uma interface padrão para a camada [dao].

Assim, parece que o conector genérico é 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. No próximo parágrafo, abordaremos o conceito de consultas parametrizadas. Com o SQL Server, uma consulta parametrizada tem a seguinte forma:

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

Com o MySQL5, a mesma consulta seria escrita:

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

Existe, portanto, uma diferença na sintaxe. A propriedade da interface IDbCommand descrita no parágrafo 9.3.3 é a seguinte:

Parâmetros
a lista de parâmetros de uma instrução SQL parametrizada. A instrução update articles set price=price*1.1 where id=@id possui o parâmetro @id.

O tipo da propriedade Parameters é IDataParameterCollection, uma interface. Representa todos os parâmetros da instrução SQL CommandText. A propriedade Parameters possui um método Add para adicionar IDataParameter, novamente uma interface. Possui as seguintes propriedades:

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

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

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

, uma vez que contém parâmetros nomeados. É possível utilizar o ParameterName.

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

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

porque os parâmetros não estão nomeados. A ordem em que os parâmetros são adicionados à coleção [IDbCommand.Parameters] é então tida em conta. Neste exemplo, os 4 parâmetros devem ser inseridos na seguinte ordem: nome, preço, stockactuel, stockminimum. Numa consulta com parâmetros nomeados, a ordem em que os parâmetros são adicionados é irrelevante. No final, o programador não pode ignorar totalmente o SGBD que utiliza ao inicializar os parâmetros de uma consulta parametrizada. Esta é uma das limitações atuais do conector genérico.

Existem frameworks que superam estas limitações e adicionam novas funcionalidades à camada [dao]:

Uma estrutura é um conjunto de bibliotecas de classes concebidas para facilitar uma forma específica de arquitetar uma aplicação. Existem várias estruturas deste tipo, permitindo-lhe escrever camadas [dao] que são simultaneamente de alto desempenho e insensíveis a alterações no SGBD:

  • O Spring.Net [http://www.springframework.net/], já apresentado neste documento, oferece o equivalente ao conector genérico estudado, sem as suas limitações, bem como várias funcionalidades para simplificar o acesso aos dados. Está também disponível uma versão em Java.
  • O iBatis.Net [http://ibatis.apache.org] é mais antigo e mais completo do que o Spring.Net. Está disponível uma versão em Java.
  • O NHibernate [http://www.hibernate.org/] é uma adaptação da versão Java do mundialmente famoso Hibernate. O NHibernate permite que a camada [dao] interaja com o SGBD sem emitir comandos SQL. A camada [dao] trabalha com objetos Hibernate. Utiliza-se uma linguagem de consulta HBL (Hibernate Query Language) para consultar objetos geridos pelo Hibernate. São estes objetos que emitem comandos SQL. O Hibernate consegue adaptar-se aos comandos SQL dos SGBD.
  • O LINQ (Language INtegrated Query), integrado na versão 3.5 do .NET e disponível no C# 2008. O LINQ segue os passos do NHibernate, mas, por enquanto (maio de 2008), apenas o SGBD SQL Server é suportado. Isto deverá evoluir com o tempo. O LINQ vai mais longe do que o NHibernate: a sua linguagem de consulta permite consultas padrão de três tipos diferentes de fontes de dados:
    • coleções de objetos (LINQ to Objects)
    • um ficheiro XML (LINQ to XML)
    • uma base de dados (LINQ to SQL)

Estas estruturas não serão abordadas neste documento. No entanto, recomendamos vivamente a sua utilização em aplicações profissionais.

9.5. Consultas paramétricas

No parágrafo anterior, abordámos as consultas parametrizadas. Apresentamo-las aqui com 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 também a base de dados SQL Server [dbarticles.sdf].
  • em [2], o projeto está configurado para executar [Parametres.cs]
  • em [3], o projeto faz referência a

O ficheiro de configuração [App.config] define a cadeia de ligação à base 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 ficheiro [Article.cs] define uma classe [Article]. Um objeto Article será utilizado para encapsular as informações numa linha da base de dados ARTICLES [dbarticles.sdf] :


namespace Chap7 {
    class Article {
         // properties
        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; }
 
         // manufacturers
        public Article() { 
        }
 
        public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) {
            Id = id;
            Nom = nom;
            Prix = prix;
            StockActuel = stockActuel;
            StockMinimum = stockMinimum;
        }
 
    }
}

A aplicação [Parametres.cs] implementa pedidos parametrizados:


using System;
using System.Data.SqlServerCe;
using System.Text;
using System.Data;
using System.Configuration;
 
namespace Chap7 {
    class Parametres {
        static void Main(string[] args) {
 
             // using the configuration file
            string connectionString = null;
            try {
                 // connecting chain
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }
 
             // displays
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
 
             // create a table of items
            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);
            }
 
             // handle any exceptions
            try {
 
                 // delete existing items from the database
                ExecuteUpdate(connectionString, "delete from articles");
 
                 // table items are displayed
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
 
                 // insert the table of items into the database
                InsertArticles(connectionString, articles);
 
                 // table items are displayed
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
            } catch (Exception ex) {
                 // error msg
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
 
         // insert table of items
        static void InsertArticles(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                 // opening connection
                connexion.Open();
                 // control configuration
                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);
                 // command compilation
                sqlCommand.Prepare();
                 // line insertion
                for (int i = 0; i < articles.Length; i++) {
                     // parameter initialization
                    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;
                     // request execution
                    sqlCommand.ExecuteNonQuery();
                }
            }
        }
 
         // execute an update request
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }
 
         // execute a Select query
        static void ExecuteSelect(string connectionString, string requête) {
...
        }
 
         // reader display
        static void AfficheReader(IDataReader reader) {
...
    }
}

O procedimento [InsertArticles] nas linhas 51-75 é novo em comparação com o que foi visto anteriormente:

  • linha 51: o procedimento recebe dois parâmetros:
    • a cadeia de ligação connectionString que permitirá ao procedimento ligar-se à
    • uma matriz de objetos Article a serem adicionados à base de dados Articles
  • linha 56: o pedido de inserção do objeto [Article]. Tem quatro parâmetros:
    • @name : nome do artigo
    • @price : o seu preço
    • @its : o seu stock atual
    • @sm : o seu stock mínimo

A sintaxe desta 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 @. Os nomes dos parâmetros são livres.

  • linhas 58-61: definem as características de cada um dos 4 parâmetros e adicionam-nos, um a um, à lista de parâmetros do objeto SqlCeCommand, que encapsula o comando SQL a ser executado.

Utilizamos aqui o método [SqlCeCommand].Parameters.Add, que possui seis assinaturas. Utilizamos ambas abaixo:

Add(string parameterName, SQLDbType type)

adiciona e configura o parâmetro denominado parameterName. Este nome deve ser um dos que constam no parâmetro de consulta configurado: (@name, ...). type designa o tipo SQL da coluna a que o parâmetro se refere. Estão disponíveis muitos tipos, incluindo:

tipo SQL
tipo c#
comentário
BigInt
Int64
 
DateTime
DateTime
 
Decimal
Decimal
 
Float
Duplo
 
Inteiro
Int32
 
Moeda
Decimal
 
NChar
Cadeia
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. Esta informação só é útil para determinados tipos SQL, como o tipo NVarChar, por exemplo.

  • linha 63: a consulta parametrizada é compilada. Também dizemos que a preparamos, daí o nome do método. Esta operação não é essencial. Existe para melhorar o desempenho. Quando um SGBD executa uma consulta SQL, realiza algum trabalho de otimização antes de a executar. Uma consulta parametrizada destina-se a ser executada várias vezes com parâmetros diferentes. O texto da consulta, no entanto, permanece inalterado. O trabalho de otimização pode, portanto, ser realizado apenas uma vez. Alguns programas SGBD podem «preparar» ou «compilar» consultas parametrizadas. É então definido um plano de execução para esta consulta. Esta é a fase de otimização de que temos vindo a falar. Uma vez compilada, a consulta é executada repetidamente, cada vez com novos parâmetros efetivos, mas com o mesmo plano de execução.

A compilação não é a única vantagem das consultas parametrizadas. Consideremos a consulta que estudámos:

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

Podemos querer construir o texto da consulta programaticamente:

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

Acima, se (nome, preço, sa, sm) for ("item1", 100, 10, 1), a consulta anterior torna-se:

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

Agora, se (nome, preço, stock atual, stock mínimo) for ("item1", 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 incorreto devido ao apóstrofo no substantivo «article1». Se o nome provém de uma entrada do utilizador, isso significa que temos de verificar se a entrada não contém apóstrofos e, caso contenha, neutralizá-los. Esta neutralização depende do SGBD. A vantagem da consulta preparada é que ela própria realiza esse trabalho. Esta facilidade, por si só, justifica o uso de uma consulta preparada.

  • linhas 65-73: os artigos na tabela são inseridos um a um
  • linhas 67-70: cada um dos quatro parâmetros da consulta recebe o seu valor através da sua propriedade Value.
  • linha 72: o pedido de inserção, agora completo, é executado da forma habitual.

Eis um exemplo:

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 eliminaçã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 artigos

9.6. Transações

9.6.1. Geral

Uma transação é uma sequência de ordens SQL executadas «atomicamente»:

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

No final, as operações de uma transação ou foram todas aplicadas com sucesso, ou nenhuma delas. Quando o utilizador controla a transação, valida-a com um comando COMMIT ou cancela-a com um comando ROLLBACK.

Nos nossos exemplos anteriores, não utilizámos uma transação. E, no entanto, utilizámos, porque num SGBD uma ordem SQL é sempre executada dentro de uma transação. Se o cliente .NET não iniciar explicitamente uma transação, o SGBD utiliza uma transação implícita. Existem dois casos comuns:

  1. cada comando SQL individual é objeto de uma transação, iniciada pelo SGBD antes do comando e encerrada posteriormente. Dizemos que estamos em autocommit. Portanto, é como se o cliente .NET estivesse a realizar transações para cada comando 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, deixando que o cliente a feche. Todas as ordens SQL emitidas pelo cliente .NET fazem então parte da transação implícita. Isto pode ser encerrado por vários eventos: o cliente fecha a ligação, inicia uma nova transação, etc., mas fica-se então numa situação dependente do SGBD. Este modo deve ser evitado.

O modo padrão é normalmente definido através da configuração do SGBD. Alguns SGBDs têm o autocommit como padrão, outros não. Por padrão, o SQL Server Compact está em autocommit.

As instruções SQL de diferentes utilizadores são executadas simultaneamente em transações que funcionam em paralelo. As operações realizadas por uma transação podem afetar as realizadas por outra. Existem quatro níveis de isolamento entre as transações de diferentes utilizadores:

  • Leitura não confirmada
  • Leitura confirmada
  • Leitura repetível
  • Serializável

Leitura não confirmada

Este modo de isolamento também é conhecido como «Leitura suja». Aqui está um exemplo do que pode acontecer neste modo:

  1. um utilizador U1 inicia uma transação na tabela T
  2. um utilizador U2 inicia uma transação na mesma tabela T
  3. o utilizador U1 modifica linhas na tabela T, mas ainda não as valida
  4. o utilizador U2 «vê» estas modificações e toma decisões com base no que vê
  5. o utilizador cancela a transação com um ROLLBACK

Podemos ver que, no ponto 4, o utilizador U2 tomou uma decisão com base em dados que mais tarde se revelarão falsos.

Leitura confirmada

Este modo de isolamento evita a armadilha anterior. Neste modo, o utilizador U2, no passo 4, não «verá» as alterações feitas pelo utilizador U1 na tabela T. Só as verá depois de o U1 ter executado um COMMIT da sua transação.

Neste modo, também conhecido como «Leitura Não Repetível», podem, no entanto, ocorrer as seguintes situações:

  1. um utilizador U1 inicia uma transação na tabela T
  2. um utilizador U2 inicia uma transação na mesma tabela T
  3. o utilizador U2 executa um SELECT para obter a média da coluna C das linhas em T que satisfazem uma determinada condição
  4. o utilizador U1 modifica (UPDATE) determinados valores na coluna C de T e valida-os (COMMIT)
  5. o utilizador U2 repete o mesmo SELECT que no ponto 3. Ele irá verificar que a média na coluna C mudou em resultado das modificações feitas por U1.

Agora, o utilizador U2 só vê as modificações «validadas» por U1. Mas, embora permaneça na mesma transação, duas operações idênticas 3 e 5 dão resultados diferentes. Esta situação é conhecida como «Leitura Não Repetível». É uma situação incómoda para quem deseja ter uma imagem estável da tabela T.

Leitura Repetível

Neste modo de isolamento, é garantido que um utilizador obtenha os mesmos resultados nas suas leituras da base de dados, desde que permaneça na mesma transação. Ele trabalha numa imagem na qual as modificações feitas por outras transações, mesmo as validadas, nunca são refletidas. Só as verá quando terminar a sua transação com um COMMIT ou ROLLBACK.

No entanto, este modo de isolamento ainda não é perfeito. Após a operação 3 acima, as linhas consultadas pelo utilizador U2 ficam bloqueadas. Durante a operação 4, o utilizador U1 não poderá modificar (UPDATE) os valores na coluna C dessas linhas. No entanto, pode adicionar linhas (INSERT). Se algumas das linhas adicionadas verificarem a condição testada em 3, a operação 5 dará uma média diferente daquela encontrada em 3, devido às linhas adicionadas. Estas linhas são por vezes referidas como linhas fantasmas.

Para resolver este novo problema, precisamos de mudar para o isolamento «Serializable».

Serializable

Neste modo de isolamento, as transações estão completamente isoladas umas das outras. Isso 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 este resultado, na operação 4, quando o utilizador U1 quiser adicionar linhas que alterariam o resultado do SELECT do utilizador U1, ele será impedido de o fazer. Uma mensagem de erro informá-lo-á de que a inserção não é possível. Tal só será possível assim que o utilizador U2 tiver validado a sua transação.

Os quatro níveis SQL de isolamento de transações não estão disponíveis em todos os SGBD. O nível de isolamento predefinido é normalmente a Leitura Confirmada. O nível de isolamento pretendido para uma transação pode ser explicitamente especificado quando uma transação explícita é criada por um cliente .NET.

9.6.2. O sistema de gestão de transações da API

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

BeginTransaction
M
inicia uma transação.

Este método tem duas assinaturas:

  1. IDbTransaction BeginTransaction() : inicia uma transação e devolve o objeto IDbTransaction para a controlar
  2. IDbTransaction BeginTransaction(IsolationLevel level) : especifica também o nível de isolamento necessário para a transação. level assume os seus valores a partir da seguinte enumeração:
ReadUncommitted
a transação pode ler dados gravados por outra transação que esta última ainda não tenha validado — evite isto
ReadCommitted
a transação não pode ler dados gravados por outra transação que ainda não tenha validado. No entanto, os dados lidos duas vezes seguidas pela transação podem sofrer alterações (leituras não repetíveis), pois outra transação pode tê-los modificado entretanto (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 da mesma forma que as linhas atualizadas. Isto impede que outra transação as modifique. Não impede que sejam adicionadas linhas.
Serializable
as tabelas utilizadas pela transação são bloqueadas, impedindo que novas linhas sejam adicionadas por outra transação. Tudo acontece como se a transação estivesse sozinha. Reduz o desempenho, uma vez que as transações já não funcionam em paralelo.
Snapshot
a transação opera sobre uma cópia dos dados feita no momento T. Utilizado quando a transação é de leitura apenas. Proporciona o mesmo resultado que o modo serializable, evitando o seu custo.

Uma vez iniciada a transação, esta é controlada pelo IDbTransaction, uma interface com as seguintes propriedades P e métodos M:

Nome
Tipo
Função
Conexão
P
a conexão IDbConnection que suporta a transação
Confirmar
M
valida a transação — os resultados das ordens SQL emitidas na transação são copiados para a base de dados .
Rollback
M
invalida a transação — os resultados das instruções SQL emitidas na transação não são copiados para a base de dados.

9.6.3. O programa de exemplo

Voltemos ao projeto anterior e vejamos o programa [Transactions.cs] :

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

O código para [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) {
 
             // using the configuration file
            string connectionString = null;
            try {
                 // connecting chain
                connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString;
            } catch (Exception e) {
                Console.WriteLine("Erreur de configuration : {0}", e.Message);
                return;
            }
 
             // displays
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
 
             // create a table of 2 items with the same name
            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);
            }
             // handle any exceptions
            try {
                Console.WriteLine("Insertion sans transaction...");
                 // the table of items is first inserted into the database without a transaction
                ExecuteUpdate(connectionString, "delete from articles");
                try {
                    InsertArticlesOutOfTransaction(connectionString, articles);
                } catch (Exception ex) {
                     // error msg
                    Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
                }
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
 
                 // we do the same thing again, but in a transaction this time
                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) {
                 // error msg
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
 
         // insert item table without transaction
        static void InsertArticlesOutOfTransaction(string connectionString, Article[] articles) {
....
        }
 
         // insert an array of items into a transaction
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
....
        }
 
         // execute an update request
        static void ExecuteUpdate(string connectionString, string requête) {
....
        }
 
         // execute a Select query
        static void ExecuteSelect(string connectionString, string requête) {
...
        }
 
         // reader display
        static void AfficheReader(IDataReader reader) {
...
            }
        }
    }
}
  • linhas 12-19: a cadeia de ligação à base de dados SQLServer Ce é lida no ficheiro [App.config]
  • linhas 25-28: é criada uma matriz de dois objetos Article. Estes dois artigos têm o mesmo nome "article". Ou, a base de dados [dbarticles.sdf] tem uma restrição de unicidade na sua coluna [name] (cf. parágrafo 9.3.1). Estes dois artigos não podem, portanto, estar presentes na base de dados ao mesmo tempo. Os dois artigos com o nome «article» são adicionados à tabela articles. Por isso, vai haver um problema, ou seja, uma exceção lançada pelo SGBD e retransmitida pelo seu conector ADO.NET. Para demonstrar o efeito da transação, os dois artigos serão inseridos em dois ambientes diferentes:
    • primeiro, sem qualquer transação. Recorde-se aqui que, neste caso, o SQLServer Compact funciona em autocommit, ou seja, insere cada comando SQL numa transação implícita. O primeiro artigo será inserido. O segundo não.
    • depois, numa transação explícita que engloba as duas inserções. Como a segunda inserção irá falhar, a primeira será anulada. No final, nenhuma inserção será efetuada.
  • linha 33: a tabela «articles» é esvaziada
  • linha 35: inserção dos dois artigos sem uma transação explícita. Como sabemos que a segunda inserção irá provocar uma exceção, esta é tratada por um try/catch
  • linha 46: tabela «display articles»
  • linhas 44-46: a mesma sequência é repetida, mas desta vez é utilizada uma transação explícita para realizar as inserções. A exceção encontrada é aqui tratada pelo InsertArticlesInTransaction.
  • linhas 54-56: o método InsertArticlesOutOfTransaction corresponde ao programa InsertArticles [Parametres.cs] analisado anteriormente.
  • linhas 64-66: o método ExecuteUpdate é o mesmo que o anterior. A instrução SQL é executada numa transação implícita. Isto é possível aqui porque sabemos que, neste caso, o SQL Server Compact funciona em modo de autocommit.
  • linhas 69-71: o mesmo se aplica ao método ExecuteSelect.

O método InsertArticlesInTransaction é o seguinte:


// insert an array of items into a transaction
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                 // opening connection
                connexion.Open();
                 // control configuration
                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);
                 // command compilation
                sqlCommand.Prepare();
                 // transaction
                SqlCeTransaction transaction = null;
                try {
                     // start of transaction
                    transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);
                     // the SQL command must be executed in this transaction
                    sqlCommand.Transaction = transaction;
                     // line insertion
                    for (int i = 0; i < articles.Length; i++) {
                         // parameter initialization
                        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;
                         // request execution
                        sqlCommand.ExecuteNonQuery();
                    }
                     // validate the transaction
                    transaction.Commit();
                    Console.WriteLine("transaction validée...");
                } catch {
                     // we undo the transaction
                    if (transaction != null)transaction.Rollback();
                    Console.WriteLine("transaction invalidée...");
                }
            }
        }

Limitar-nos-emos a detalhar as diferenças entre este e o InsertArticles do programa [Parametres.cs] analisado anteriormente:

  • linha 16: é declarada uma transação SqlCeTransaction.
  • linhas 17, 35: try / catch para lidar com a exceção que surgirá no final da 2.ª inserção
  • linha 19: a transação é criada. Pertence à ligação atual.
  • linha 21: o comando SQL é definido na transação
  • linhas 23-31: são efetuadas inserções
  • linha 33: tudo correu bem — a transação foi validada — as inserções serão agora definitivamente integradas na base de dados.
  • linha 37: tivemos um problema. A transação é anulada, 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. Pode obter mais informações consultando as propriedades da base de dados:
  • em [1] na vista [Database Explorer] do Visual Studio, criámos uma ligação [2] à base de dados [dbarticles.sdf]. Esta possui um índice UQ__ARTICLES__0000000000000010. Clique com o botão direito do rato no índice para aceder às suas propriedades
  • em [3,4], podemos ver que o índice UQ__ARTICLES__0000000000000010 corresponde a uma restrição de exclusividade na coluna [NOM]
  • linhas 7-11: a tabela apresenta os artigos após as duas inserções. Não está vazia: o primeiro artigo foi inserido.
  • linha 15: exibida pelo ExecuteUpdate("delete from articles") — havia uma linha na tabela
  • linha 16: mensagem exibida pelo InsertArticlesInTransaction se a transação falhar.
  • linhas 18-20: mostram que não foi feita nenhuma inserção. O Rollback da transação anulou a primeira inserção.

9.7. O método ExecuteScalar

9.7.1. Entre os métodos do IDbCommand descritos no parágrafo 9.3.3, encontrava-se o seguinte método:

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

Apresentamos aqui um exemplo de como utilizar este método. Voltar ao:

  • 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) {
 
             // using the configuration file
            string connectionString = null;
...
 
             // displays
            Console.WriteLine("Chaîne de connexion à la base : [{0}]\n", connectionString);
 
             // creation of a 5-item table
            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);
            }
 
             // handle any exceptions
            try {
                 // insert the item table into a transaction
                ExecuteUpdate(connectionString, "delete from articles");
                InsertArticlesInTransaction(connectionString, articles);
                ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");
                 // average item prices
                decimal prixMoyen = (decimal)ExecuteScalar(connectionString, "select avg(prix) from articles");
                Console.WriteLine("Prix moyen des articles={0}", prixMoyen);
                 // or the number of items
                int nbArticles = (int)ExecuteScalar(connectionString, "select count(id) from articles");
                Console.WriteLine("Nombre d'articles={0}", nbArticles);
            } catch (Exception ex) {
                 // error msg
                Console.WriteLine("Erreur d'accès à la base de données (" + ex.Message + ")");
            }
        }
 
         // insert an array of items into a transaction
        static void InsertArticlesInTransaction(string connectionString, Article[] articles) {
...
        }
 
 
         // execute an update request
        static object ExecuteScalar(string connectionString, string requête) {
            using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) {
                 // opening connection
                connexion.Open();
                 // request execution
                return new SqlCeCommand(requête, connexion).ExecuteScalar();
            }
        }
 
         // execute an update request
        static void ExecuteUpdate(string connectionString, string requête) {
...
        }
 
         // execute a Select query
        static void ExecuteSelect(string connectionString, string requête) {
...
        }
 
         // reader display
        static void AfficheReader(IDataReader reader) {
...
        }
    }
}
  • linhas 14-17: criação de uma matriz com 5 artigos
  • linha 22: a tabela «articles» é esvaziada
  • linha 23: preenchida com os 5 artigos
  • linha 24: é exibida
  • linha 26: solicita o preço médio dos artigos
  • linha 29: solicita o número de artigos
  • linha 49: utilizando o método [IDbCommand].ExecuteScalar() para calcular cada um destes valores.

Os resultados 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 devolvidos pelo ExecuteScalar.

9.8. Aplicação de exemplo - versão 7

Tomamos como exemplo a 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] era um ficheiro de texto [B].
  • A instanciação da camada e a integração na aplicação foram tratadas pelo Spring.

Modificamos a camada [dao] para que ela obtenha os seus dados de uma base de dados.

9.8.1. Visite a base de dados

O conteúdo do ficheiro de texto anterior [B] é inserido numa base de dados MySQL5. Vamos mostrar-lhe como fazer isso:

  • [1] : O MySQL Administrator foi iniciado
  • [2,3]: na área [Esquemas], clique com o botão direito do rato e selecione a opção [Criar Esquema] para criar uma nova base de dados
  • [4]: a base de dados será denominada [bdimpots]
  • [5]: foi adicionado às bases da zona [Esquemas].
  • [6,7]: clique com o botão direito do rato na tabela e selecione a opção [Criar Nova Tabela] para criar uma tabela
  • [8]: a tabela será chamada [slices]. Terá as colunas [id, limit, coeffR, coeffN].
  • [9,10]: [id] é uma chave primária do tipo INTEGER e possui o atributo AUTO_INCREMENT [10]: o SGBD preencherá esta coluna quando forem adicionadas linhas.
  • As colunas [limit, coeffR, coeffN] são do tipo DOUBLE.
  • [11,12]: a nova tabela aparece no separador [Schema Tables] da base de dados.
  • [13,14]: para adicionar dados à tabela
  • [15]: o [Query Browser] foi iniciado
  • [16]: dados introduzidos e validados para as colunas [limit, coeffR, coeffN]. A coluna [id] foi preenchida pelo SGBD. A validação foi realizada com [17].
  • Ainda no [Query Browser] [18], executamos a consulta [20] [19]. Isto cria um utilizador «admimpots» com a palavra-passe «mdpimpots» e atribui-lhe todos os privilégios (conceder todos os privilégios) sobre todos os objetos na base de dados bdimpots (em bdimpots.*). Isto permitir-nos-á trabalhar na base de dados [bdimpots] com o utilizador [admimpots] em vez do administrador [root].

9.8.2. A solução do Visual Studio

Seguiremos a mesma abordagem utilizada na versão 5 da aplicação de exemplo (ver parágrafo 6.4). Iremos 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 da aplicação
  • em [2]: o projeto [dao] na camada [dao], que agora utilizará uma base de dados
  • em [3]: o projeto [metier] para a camada [metier]. Retiramos aqui a camada [metier] da versão 5, descrita no parágrafo 6.4.4.
  • em [4]: projeto [ui] da camada [ui]. Aqui, tomamos a camada [ui] da versão 6, descrita no parágrafo 7.6.

Estamos a basear-nos no que aprendemos para recuperar duas camadas que já foram escritas, as camadas [ui] e [metier]. Isto é possível graças à arquitetura em camadas que escolhemos. No entanto, precisaremos do código-fonte das camadas [ui] e [metier]. Não é possível contentar-se com a DLL das camadas. Quando, na versão 5, a DLL da camada [metier] foi criada, ela tinha uma dependência da DLL da camada [dao]. Esta dependência estava codificada na DLL da camada [metier] (nome da DLL da camada [dao], versão, token de identidade, etc.). Por exemplo, a DLL da versão 5 [ImpotsV5-metier.dll] só funcionará com a DLL [ImpotsV5-dao.dll] com a qual foi compilada. Se a DLL da camada [dao] for alterada, a camada [metier] deve ser recompilada para criar uma nova DLL. O mesmo se aplica à camada [ui]. As camadas [ui] e [metier] não serão, portanto, modificadas, mas serão recompiladas para funcionar com a DLL da nova camada [dao].

9.8.3. A camada [dao]

Referências do projeto (ver [1] no projeto)

  • nunit.framework : para testes NUnit
  • System.Configuration : para utilizar o ficheiro de configuração [App.config]
  • System.Data: porque utilizamos uma base 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 {
         // tax brackets
        TrancheImpot[] TranchesImpot{get;}
    }
}

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


using System;
using System.Collections.Generic;
using System.Data.Common;
using Entites;
 
namespace Dao {
    public class DataBaseImpot : IImpotDao {
         // tax brackets
        private TrancheImpot[] tranchesImpot;
        public TrancheImpot[] TranchesImpot { get { return tranchesImpot; } }
 
         // manufacturer
        public DataBaseImpot(string factory, string connectionString, string requête) {
             // factory: the factory of the target SGBD
             // connectionString: connection string to tax bracket base
             // handle any exceptions
            try {
                 // we retrieve a generic connector for the SGBD
                DbProviderFactory connecteur = DbProviderFactories.GetFactory(factory);
                using (DbConnection connexion = connecteur.CreateConnection()) {
                     // connection configuration
                    connexion.ConnectionString = connectionString;
                     // opening connection
                    connexion.Open();
                    // configuration Command 
                    DbCommand sqlCommand = connecteur.CreateCommand();
                    sqlCommand.CommandText = requête;
                    sqlCommand.Connection = connexion;
                     // request execution
                    List<TrancheImpot> listTrancheImpot = new List<TrancheImpot>();
                    using (DbDataReader reader = sqlCommand.ExecuteReader()) {
                        while (reader.Read()) {
                             // a new tax trance is created
                            listTrancheImpot.Add(new TrancheImpot() { Limite = reader.GetDecimal(0), CoeffR = reader.GetDecimal(1), CoeffN = reader.GetDecimal(2) });
                        }
                    }
                     // put the tax brackets in your instance
                    tranchesImpot = listTrancheImpot.ToArray();
                }
            } catch (Exception ex) {
                 // encapsulate the exception in a ImpotException type
                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: implementação do método da interface [TranchesImpot]. Retorna simplesmente uma referência à tabela de escalões fiscais da linha 9. Esta tabela será construída pelo construtor da classe.
  • linha 13: o construtor. Utiliza um conector genérico (ver parágrafo 9.4.5) para avaliar a base de dados de escalões fiscais. O construtor recebe três parâmetros:
    • o nome da «fábrica» à qual solicitará classes para se ligar à base de dados, emitir comandos SQL e avaliar o resultado de um Select.
    • a cadeia de ligação que deve utilizar para se ligar à base de dados
    • o comando SQL Select que deve executar para obter as faixas de imposto.
  • linha 19: solicita um conector de fábrica
  • linha 20: cria uma ligação com este conector. É criado, mas ainda não está operacional
  • linha 22: a cadeia de ligação é inicializada. Agora já é possível estabelecer a ligação.
  • linha 24: ligar
  • linha 26: solicita um objeto [DbCommand] ao conector para executar um comando SQL
  • linha 27: define o comando SQL a ser executado
  • linha 28: define a ligação na qual o comando será executado
  • linha 30: é criada uma lista [listTrancheImpot] de objetos do tipo [TrancheImpot] vazia.
  • linha 31: o comando SQL Select é executado
  • linhas 32-35: o objeto [DbDataReader] resultante do Select é utilizado. Cada linha da tabela de resultados do Select é utilizada para instanciar um objeto do tipo [TrancheImpot], que é adicionado à lista [listTrancheImpot].
  • linha 38: a lista de objetos do tipo [TrancheImpot] é transferida para a tabela na linha 9.
  • linhas 40-43: qualquer exceção é encapsulada no tipo [ImpotException] e atribuído o código de erro 101 (arbitrário).

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

A classe [Test1] simplesmente exibe as faixas de imposto no ecrã. Esta é a 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() {
 
             // create the [dao] layer
            IImpotDao dao = null;
            try {
                 // layer creation [dao]
                dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
            } catch (ImpotException e) {
                 // error display
                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);
                 // program stop
                Environment.Exit(1);
            }
             // display tax brackets
            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 ficheiro 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: cadeia de ligação à base de dados MySQL5. Note-se que é o utilizador [admimpots] que estabelecerá a ligação.
  • linha 8: a «fábrica» para trabalhar com o SGBD MySQL5
  • linha 7: consulta SQL Select para obter faixas de impostos.

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

Image

A execução do teste produz 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{
         // layer [dao] to be tested
        private IImpotDao dao;
 
         // manufacturer
        public NUnit1() {
             // dao] layer initialization
            dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]);
        }
 
         // test
        [Test]
        public void ShowTranchesImpot(){
             // display tax brackets
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
             // some tests
            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]: a DLL gerada será chamada [ ImpotsV7-dao.dll]
  • em [3]: após a geração do projeto (F6), a pasta [dao/bin/Release] contém a DLL [ImpotsV7-dao.dll]. Contém também o ficheiro de configuração [App.config], renomeado para [nome da DLL].config. Isto é padrão no Visual Studio.

A DLL [ImpotsV7-dao.dll] é então carregada na estrutura NUnit e executada:

  • em [1]: testes aprovados. Consideramos agora a camada [dao] operacional. A sua DLL contém todas as classes do projeto, incluindo as classes de teste. Estas já não são necessárias. Recompilamos a DLL para excluir as classes de teste.
  • em [2]: a pasta [tests] é excluída do projeto
  • em [3]: o novo projeto. Este é regenerado premindo F6 para gerar uma nova DLL. Esta DLL será utilizada pelas camadas [metier] e [ui] da aplicação.

9.8.4. A tarefa [ ] da camada

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

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

  • [1]: o projeto é do tipo «biblioteca de classes»
  • [2]: a geração do projeto produzirá a DLL [ImpotsV7-metier.dll] [3].

O projeto é gerado (F6).

9.8.5. A camada [ui]

  • em [1], o projeto [ui] tornou-se o projeto ativo da solução
  • em [2]: referências do projeto. Repare nas referências à DLL das camadas [dao] e [metier].
  • em [3]: a camada [ui]. Esta é a camada da versão 6 descrita no parágrafo 7.6.
  • em [4], o ficheiro de configuração [App.config] é semelhante ao da versão 6, diferindo 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: configuração do Spring
  • linhas 15-24: objetos instanciados pelo Spring
  • linhas 16-20: instanciar a 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 utilizado, string de ligação, pedido SQL) a fornecer ao construtor da classe [Dao.DataBaseImpot]
  • linhas 21-23: instância da camada [metier]. Esta é a mesma configuração da versão 6.

Testes

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

  • [1]: o projeto é do tipo «Aplicação Windows»
  • [2]: a geração do projeto irá produzir o executável [ImpotsV7-ui.exe]

É apresentado um exemplo em [3].

9.8.6. Alterar base de dados

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

A base SQL Server Compact será a seguinte:

  • [1]: a base de dados [dbimpots.sdf] na vista [DataBase Explorer] do Visual Studio [2]. Foi criada sem palavra-passe.
  • [3]: a tabela [data] que contém os dados. Escolhemos deliberadamente nomes diferentes para a tabela e as colunas em relação aos utilizados na base de dados MySQL5, para voltar a enfatizar a importância de colocar esses detalhes no ficheiro de configuração, em vez de no código.
  • [4]: a coluna [id] é a chave primária e possui o atributo Identity: é o SGBD que atribuirá os seus valores.
  • [5]: conteúdo da tabela [data].
  • [6]: a base de dados [dbimpots.sdf] foi colocada na pasta do projeto [ui] e integrada neste projeto.
  • [7]: a base de dados [dbimpots.sdf] será copiada para a pasta de execução do projeto.

O ficheiro de configuração [App.config] para a nova base 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: configuração da camada [dao] para utilizar a base de dados [dbimpots.sdf].

Os resultados são idênticos aos anteriores. Note-se a vantagem de utilizar um conector genérico para tornar a camada [dao] insensível a alterações no SGBD. Vimos, no entanto, que este conector não é adequado para todas as situações, particularmente aquelas que envolvem consultas parametrizadas. Nesses casos, existem outras soluções, como a mencionada acima, frameworks de acesso a dados de terceiros (Spring, iBatis, NHibernate, LINQ, ...).

9.9. Para saber mais...

  • O LINQ é mencionado em inúmeras publicações, incluindo o livro: C# 3.0 in a Nutshell, de Joseph e Ben Albahari, publicado pela O'Reilly, já citado na introdução deste documento.
  • O iBatis é apresentado no livro: iBatis in Action, de Clinton Begin, da Manning Editions
  • Nhibernate in Action, publicado pela Manning, tem lançamento previsto para julho de 2008

O Spring, o iBatis e o NHibernate têm manuais de referência disponíveis nos sites das respetivas estruturas.