DSI - SIM - Universidade do Minho
Transcrição
DSI - SIM - Universidade do Minho
Universidade do Minho Escola de Engenharia Departamento de Informática DSI LESI/LMCC 2002/03 António Ramires Fernandes Índice 1 Introdução..........................................................................................................3 1.1 Descrição Sumária da API JDBC 2.0 ............................................................................. 3 1.2 Drivers JDBC.................................................................................................................. 6 Tipos de Drivers ..................................................................................................................... 7 1.3 Introdução ao SQL.......................................................................................................... 8 Operações Sobre Tabelas........................................................................................................ 9 Operações Sobre Dados - Selecção...................................................................................... 10 Operações Sobre Dados - Manutenção ................................................................................. 17 2 Utilização Básica da API JDBC .....................................................................21 2.1 Conectividade com Bases de Dados ............................................................................. 21 Estabelecer uma Ligação ...................................................................................................... 21 Terminar a ligação ................................................................................................................ 28 Exemplo ................................................................................................................................ 29 2.2 Definição e Envio de comandos SQL........................................................................... 30 Método específico para Envio de comandos de selecção ..................................................... 31 Método Específico para Envio de Comandos de Actualização ............................................ 32 Método Genérico .................................................................................................................. 33 2.3 Análise de Resultados de Comandos SELECT ............................................................ 35 Exemplo Completo ............................................................................................................... 39 2.4 Meta Dados dos Resultados .......................................................................................... 40 2.5 Excel via JDBC............................................................................................................. 43 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 3 Definição Avançada de Comandos SQL .......................................................46 3.1 Pré-processamento de Comandos SQL......................................................................... 46 3.2 Comandos SQL Parametrizados ................................................................................... 50 3.3 Invocação de Stored Procedures .................................................................................. 54 Stored Procedures em Microsoft Access .............................................................................. 60 3.4 4 Envio de Comandos em Batch...................................................................................... 62 Manipulação Avançada de Resultados de Comandos SELECT ................67 4.1 Resultados Navegáveis Aleatoriamente........................................................................ 70 Navegação Relativa .............................................................................................................. 70 Navegação Absoluta ............................................................................................................. 71 Exemplos............................................................................................................................... 72 Posição Actual do Cursor...................................................................................................... 74 5 Transacções......................................................................................................76 6 Referências.......................................................................................................81 2 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 1 IN TRO D U Ç Ã O Na sua primeira versão, a linguagem JAVA não incluía nenhuma forma de acesso a bases de dados. O JDBC, Java DataBase Connectivity, surgiu, inicialmente, como uma API adicional. No entanto cedo se tornou claro que esta API era essencial. O acesso a bases de dados abria um leque extenso de novas aplicações para a linguagem JAVA. O desenvolvimento da Internet, com a proliferação de sites com conteúdos dinâmicos, também veio reforçar a necessidade desta API. O JDBC tornou-se parte integrante do Java SDK, Software Development Kit, na versão 1.2. A sua evolução não foi tão dramática como a do próprio JAVA, em parte por se tratar desde o início de uma API bastante completa. No entanto as alterações implementadas justificam plenamente esta nova edição. Algumas das novidades mais relevantes da versão 2.0 são as seguintes: • Maior flexibilidade ao lidar com resultados de comandos SQL; • Possibilidade de realizar tarefas em modo Batch; • Capacidade de trabalhar com os tipos de dados introduzidos em SQL3. 1.1 D ESC R IÇ Ã O SU M Á R IA D A A PIJD BC 2.0 Em termos de divisão lógica, a API JDBC 2.0 encontra-se dividida em dois pacotes: java.sql javax.sql O primeiro pacote, java.sql, encontra-se incluído na edição Standard da linguagem JAVA 2.0. O segundo pacote, javax.sql, encontra-se incluído na edição Enterprise, podendo no entanto ser obtido separadamente. Ambas as versões estão disponíveis na secção JDBC do site da SUN (http://java.sun.com/products/jdbc). 3 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Em termos de funcionalidade pode-se considerar que o segundo pacote é uma extensão ao primeiro por forma a aumentar a robustez e escalabilidade das aplicações. Neste livro só será tratado o pacote java.sql. O pacote java.sql contêm um conjunto de classes e interfaces que permitem definir ligações a bases de dados e realizar operações sobre a mesma. Para estabelecer ligações a uma base de dados, e obter informação sobre as ligações activas, são disponibilizadas as seguintes classes e interfaces: • classe DriverManager • interface Driver • classe DriverPropertyInfo • interface Connection Uma vez estabelecida a ligação a uma base de dados é possível o envio de comandos SQL à base de dados. A API JDBC 2.0 permite a definição de comandos SQL de diversas formas, incluindo a pré-compilação de comandos, e o envio em modo batch de um conjunto de comandos. As seguintes classes e interfaces são disponibilizadas para este efeito: • interface Statement • interface PreparedStatement A invocação de stored procedures (rotinas definidas na própria base de dados pelos utilizadores, gestores de base de dados ou até pelos próprios fabricantes), também está prevista na API JDBC 2.0. Para este efeito a API disponibiliza a seguinte interface: • interface CallableStatement 4 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Uma vez enviado um comando à base de dados é necessário processar o seu resultado. Os métodos para consulta e manipulação de resultados dos comandos SQL encontram-se definidos na seguinte interface: • interface ResultSet Esta interface serve de uma forma geral para todos os métodos que retornem resultados não atómicos. Para análise dos meta dados são fornecidas duas interfaces. Uma para os meta dados da base de dados propriamente dita, e uma outra para resultados não atómicos, i.e. para objectos que implementem a interface ResultSet. • interface DataBaseMetaData • interface ResultSetMetaData A interface DataBaseMetaData também define um conjunto de métodos para inquirir o driver sobre o suporte a certas funcionalidades previstas pela API JDBC 2.0 Devido à especificidade desta API, quatro excepções foram definidas. As seguintes excepções são de tratamento obrigatório: • classe SQLException • classe BatchUpdateException As restantes excepções são meramente avisos e podem ser ignoradas pelo programador. Note-se no entanto que esta liberdade oferecida pela API deve ser usada conscientemente uma vez que o lançamento destas excepções implica sempre a presença de algum problema, embora não fatal para a aplicação. Os avisos definidos pela API são: 5 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI • class SQLWarning • class DataTruncation Existem ainda um conjunto de interfaces e classes que definem novos tipos de dados em JAVA de forma a facilitar a utilização de certos tipos de dados em SQL como por exemplo BLOB (Binary Large Objects), Date e Array. A API prevê também a definição de tipos de dados criados pelo utilizador da base de dados. 1.2 D R IVER S JD BC A API JDBC 2.0 não implementa a ligação à base de dados. Tal como se pode observar na listagem de conteúdos da API apresentada anteriormente, a maior parte das suas componentes são interfaces, e não classes. Quer isto dizer que a API define a assinatura dos métodos, e por consequência define implicitamente o modo de interacção programática com a API, mas deixa a parte da implementação propriamente dita dos métodos definidos nas interfaces a cargo dos drivers JDBC. A distribuição da API inclui somente um driver para bases de dados acessíveis via ODBC, Open DataBase Conectivity. A inclusão deste driver é estratégica, uma vez que isso garantia à priori, e sem depender de terceiros para desenvolver drivers específicos, que a comunidade Windows podia trabalhar com um grande número de bases de dados só com a distribuição da API JDBC. Actualmente, encontra-se disponível um grande número de drivers para as mais variadas bases de dados. No entanto, convém referir que a implementação dos drivers não tem necessariamente de ser completa, por consequência nem todos os drivers implementam todas as funcionalidades descritas neste capítulo. Pode também dar-se o caso de a própria base de dados não suportar toda a funcionalidade presente na API JDBC 2.0. 6 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Os requisitos a que um driver deve obedecer são os seguintes: 1. As seguintes interfaces devem ser completamente implementadas; • • • Driver DatabaseMetaData ResultSetMetaData 2. As seguintes interfaces devem ser implementadas: • • • • • CallableStatement Connection PreparedStatement ResultSet Statement No entanto, a implementação destas interfaces, por ser dependente da funcionalidade da própria base de dados, não tem forçosamente de ser completa. Os métodos relativos a funcionalidades não disponíveis na base de dados podem ser implementados de forma a lançarem uma excepção, aquando da sua invocação. Através dos métodos definidos nas interfaces relativas aos meta dados é possível verificar se uma determinada funcionalidade é disponibilizada pelo driver. Todas as restantes interfaces definidas na API JDBC são opcionais, podendo inclusive ser totalmente omitidas. T IPO S D E D R IVER S Os drivers para efectuar uma ligação a uma base de dados através do JDBC podem-se dividir em quatro grandes categorias: • JDBC-ODBC: Este tipo de drivers permite a ligação a base de dados cujo driver OBDC esteja instalado. Permite aceder a todas as bases de dados registadas em ODBC. Este driver está incluído no Java 2.0. 7 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI • API nativa da Base de Dados: Drivers que permitem a ligação directa a uma base de dados local. Estes drivers são fornecidos pelas entidades responsáveis pelas próprias bases de dados ou por terceiros. • JDBC-Net: Este tipo de drivers converte as instruções JDBC para um protocolo independente do sistema de base de dados. Para completar o circuito de ligação é necessário instalar um segundo driver que traduza este protocolo para o protocolo especifico da base de dados desejada. • Protocolo de Rede Nativo: Através deste tipo de drivers consegue-se uma ligação directa à base de dados utilizando o protocolo de rede proprietário da base de dados. Desta forma não é necessário instalar nenhum software adicional para bases de dados locais ou remotas. 1.3 IN TR O D U Ç Ã O A O SQ L O SQL, Structured Query Language, é utilizado na maior parte dos sistemas de bases de dados para realizar operações de consulta, manutenção e actualização. A API JDBC2.0 também recorre ao SQL como forma de comunicação com as bases de dados. Nesta secção será apresentada uma curta introdução ao SQL. O pequeno tutorial que se segue não cobre de forma alguma todas as potencialidades do SQL. A sua inclusão neste capítulo deve-se à forte dependência do JDBC em relação ao SQL. A curta introdução aqui apresentada inclui: • criação, alteração e remoção de tabelas; • selecção de dados presentes nas tabelas. • inserção, remoção e alteração de dados nas tabelas; 8 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O PER A Ç Õ ES SO BR E T A BELA S A sintaxe SQL para definir uma tabela é a seguinte: CREATE TABLE nomeDaTabela ( listaDeEspecificaçãoDeAtributos ); Nota: O simbolo ";" não é necessário quando se utiliza SQL em JDBC. Na sua forma mais simples, a especificação de um atributo consiste no nome e tipo de dados. Por exemplo, para definir um atributo modelo, com tipo de dados texto, de comprimento 50 escrevemos: modelo CHAR(50) A palavra reservada CHAR define o tipo de dados como sendo texto. O comprimento do texto é definido entre parêntesis. Para definir uma tabela para armazenar informação sobre produtos informáticos contendo para cada produto o código, modelo, e preço podemos então escrever: CREATE TABLE Produtos ( código CHAR(9), modelo VARCHAR(50), preço FLOAT ); O tipo de dados VARCHAR introduzido acima, representa um texto de comprimento variável, tendo como comprimento máximo o valor indicado entre parêntesis. A chave primária de uma tabela é definida através das palavras chave PRIMARY KEY. No exemplo acima, caso pretendêssemos definir o atributo código como uma chave primária, poderíamos reescrever o comando da seguinte forma : CREATE TABLE Produtos ( código CHAR(9) PRIMARY KEY, modelo VARCHAR(50), preço FLOAT ); 9 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O SQL permite ainda definir valores a aplicar por omissão aos através da palavra-chave DEFAULT. Por exemplo, se considerarmos um atributo com uma só letra para indicar se o produto está em promoção, cujo valor por omissão é 'N'. A nova definição da tabela Produtos é agora: CREATE TABLE Produtos ( código CHAR(9) PRIMARY KEY, modelo VARCHAR(50), preço FLOAT, promoção CHAR(1) DEFAULT 'N' ); Devido à estrutura dinâmica dos problemas que nos rodeiam, é por vezes necessário alterar a estrutura de tabelas já existentes, quer adicionando, ou removendo atributos. A sintaxe para estas operações envolve as palavras-chave ALTER TABLE. Para adicionarmos um atributo com a marca do produto poderíamos escrever o seguinte comando SQL: ALTER TABLE Produtos ADD marca CHAR(20); A remoção de um atributo tem uma sintaxe semelhante. Por exemplo para remover o atributo promoção, previamente definido na tabela Produtos definiríamos o seguinte comando SQL: ALTER TABLE Produtos DROP promoção; Por vezes o número de alterações a uma tabela é de tal forma grande que mais vale reescrever a tabela do principio. Sendo assim, torna-se por vezes necessário remover tabelas previamente criadas. O SQL fornece o comando DROP para este fim. Por exemplo, o comando SQL que se segue remove a tabela Produtos da base de dados: DROP TABLE Produtos; O PER A Ç Õ ES SO BR E D A D O S -SELEC Ç Ã O Uma das operações mais utilizadas em SQL, senão a mais utilizada, é a selecção de dados das tabelas. O comando SELECT, utilizado para este efeito, tem uma sintaxe extremamente rica, sendo aqui apresentada apenas uma pequena parte desta. 10 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI No seu formato mais simples o comando SELECT permite seleccionar colunas de uma tabela. Por exemplo, o seguinte comando SELECT permite seleccionar o modelo e preço dos itens existentes na tabela Produtos: SELECT modelo, preço FROM Produtos; Neste caso a selecção actua como uma projecção sobre as colunas da tabela, devolvendo somente para cada linha da tabela, ou registo, as colunas indicadas na clausula SELECT. Considerando que a tabela Produtos contêm os seguintes dados: código marca modelo categoria preço promoção 01 ASUS V8100 De Luxe Placa Gráfica 547 N 02 ASUS NoteBook L8400C Portátil 1995 S 03 Toshiba Satelite 3100 Portátil 1950 N 04 Creative Audigy 5.1 OEM Placa Audio 103 S 05 Compaq IPaq Pocket PC PDA 745 N 06 IBM DeskStar 60GB Disco IDE 283 N O resultado do comando SELECT apresentado seria: modelo preço V8100 De Luxe 547 L8400C 1995 Satelite 3100 1950 Audigy 5.1 OEM 103 IPaq Pocket PC 2343 DeskStar 60GB 283 A sintaxe do comando SELECT na sua versão mais simples é SELECT listaDeAtributos FROM listaDeTabelas A lista de atributos pode ser definida pelo simbolo '*' que representa todos os atributos. 11 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Consideremos o seguinte comando SQL: SELECT marca FROM Produtos; Neste caso o resultado seria apenas coluna marca da tabela Produtos: marca A SU S ASUS Toshiba Creative Compaq IBM Como podemos verificar existem linhas duplicadas no resultado do comando SQL anterior. Através da utilização da palavra-chave DISTINCT, podemos eliminar as repetições dos resultados. Ao especificar DISTINCT na clausula SELECT são eliminadas da resposta todas as linhas repetidas. Por exemplo, o seguinte comando SQL já não apresenta repetições: SQL: SELECT DISTINCT marca FROM Produtos; Resultado: marca ASUS Toshiba Creative Compaq IBM 12 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Realizar projecções sobre as colunas de uma tabela é apenas uma parte do que podemos obter utilizando a comando SELECT. Através da clausula WHERE é possível especificar condições que as linhas da tabela tem de obedecer para constarem do resultado. A titulo de exemplo consideremos que só pretendíamos as marcas que contêm produtos que se encontram em promoção. Podemos então adicionar a seguinte condição ao comando SQL anterior promoção='S' O comando completo será: SELECT DISTINCT marca FROM Produtos WHERE promoção='S'; Resultado: marca ASUS Creative A clausula WHERE permite-nos especificar um conjunto de condições que podem ser ligadas através das palavras-chave AND (conjunção lógica) e OR (disjunção lógica). Por exemplo, o resultado do seguinte comando devolve todas as marcas e modelos de artigos em promoção e cuja categoria seja 'Portátil'. SQL: SELECT marca, modelo FROM Produtos WHERE categoria = 'Portátil' AND promoção = 'S'; 13 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Resultado: marca modelo ASUS L8400C Os operadores AND e OR podem ser usados em conjunto, sendo no entanto necessário observar que o operador AND tem prioridade sobre o operador OR. Por exemplo, no seguinte comando iremos obter todos os produtos em promoção cuja categoria é 'Placa Gráfica' ou 'Placa Audio': SQL: SELECT marca,modelo FROM Produtos WHERE ( categoria = 'Placa Gráfica' OR categoria = 'Placa Audio' ) AND promoção= 'S'; Resultado: marca Creative Note-se que a existência dos parêntesis no comando anterior é fundamental. Caso os parêntesis não existissem o significado seria: - Todas os produtos em promoção da categoria 'Placa Audio' e todos os produtos da categoria 'Placa Gráfica', independentemente destes últimos se encontrarem ou não em promoção. 14 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Ou seja, devido à prioridade anteriormente mencionada relativamente aos operadores AND e OR, escrever o comando sem parêntesis é equivalente a escrever o seguinte: SELECT marca,modelo FROM Produtos WHERE categoria = 'Placa Gráfica' OR ( categoria = 'Placa Audio' AND promoção= 'S' ); Resultado: marca ASUS Creative O resultado deste comando SELECT é como podemos ver diferente da versão em que os parêntesis estão correctamente colocados. A complexidade inerente mesmo ao mais simples dos problemas, leva a que uma base de dados seja normalmente composta por muitas tabelas. A combinação de informação das tabelas contidas numa base de dados é fundamental para a análise da informação sob um perspectiva global do sistema de informação. Iremos agora adicionar uma nova tabela e apresentar a forma de realizar consultas de informação que envolvam mais que uma tabela. Consideremos, então, uma tabela em que se especifica quem são os fornecedores de cada marca de produtos. 15 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Esta nova tabela, baptizada de Fornecedores, terá dois campos: marca do produto e nome do fornecedor: marca nome ASUS BomBit Creative ChipChip IBM BitChip Toshiba MegaChip Compaq AGPBit ASUS ChipChip Creative BomBit IBM BomBit Iremos agora analisar o processo de construção de um comando SELECT que nos diga quais as categorias de produtos fornecidas pelo fornecedor 'BomBit'. A primeira condição que teremos de especificar resulta do próprio problema: Fornecedores.nome = 'BomBit' Note-se que na especificação do nome do atributo foi acrescentado, como prefixo, o nome da tabela. Esta prática é recomendável para auxiliar a leitura de comandos SELECT com mais de uma tabela, sendo imperativa no caso em que possa existir ambiguidade em relação à origem de um atributo. Por exemplo o atributo marca existe em ambas as tabelas, logo é necessário especificar o prefixo para remover ambiguidades. A segunda condição necessária para a resolução do nosso problema está relacionada com a forma como a informação entre as tabelas deve ser relacionada. Neste caso, a semântica do problema diz-nos que os dados na coluna marca da tabela Produtos estão relacionadas com os dados na coluna homónima da tabela Fornecedores. Sendo assim a condição que relaciona as duas tabelas é: Produtos.marca = Fornecedores.marca Podemos agora escrever o comando para a obter a informação desejada: 16 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI SELECT Produtos.categoria FROM Produtos, Fornecedores WHERE Produtos.marca = Fornecedores.marca AND Fornecedores.nome = “BomBit”; O resultado do comando aplicado as tabelas anteriormente definidas é: categoria Placa Gráfica Portátil Placa Audio Disco IDE O PER A Ç Õ ES SO BR E D A D O S -M A N U TEN Ç Ã O Caso se pretenda realizar a manutenção de uma base de dados a partir de uma aplicação JAVA, é necessário conhecer também os comandos para inserir, remover e alterar dados. A operação de inserção de dados numa tabela em SQL é realizada através do comando INSERT, e tem duas variantes possíveis. A primeira insere um registo especificando os valores de cada atributo. A segunda insere o resultado de um comando SELECT numa tabela. Na primeira versão pode-se utilizar a seguinte sintaxe: INSERT INTO nomeDaTabela VALUES (listaDeValores); A título de exemplo vamos criar um comando SQL para inserir um novo registo na tabela Fornecedores. INSERT INTO Fornecedores VALUES ('IBM', 'ChipChip'); Note-se que a lista de valores tem de respeitar a ordem em que os atributos são definidos na tabela, e também o seu número. Ou seja tem de existir tantos valores como atributos. Caso se pretenda inserir registos incompletos, ou não especificar os campos que tem valores por omissão é necessário utilizar uma sintaxe mais completa, onde é definido de forma explicita quais os atributos a inserir, e qual a sua ordem: 17 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI INSERT INTO nomeDaTabela(listaDeAtributos) VALUES (listaDeValores); Por exemplo, o comando INSERT definido anteriormente para inserir um registo na tabela Fornecedores, utilizando a nova sintaxe, podia ser escrito das seguintes formas: INSERT INTO Fornecedores(marca,nome) VALUES ('IBM', 'ChipChip'); ou INSERT INTO Fornecedores(nome,marca) VALUES ('ChipChip', 'IBM'); Utilizando esta sintaxe, a ordem da especificação dos valores dos atributos é independente das posições dos respectivos atributos na base de dados. Uma questão que surge naturalmente é a seguinte: "Caso não se especifique os atributos todos, qual o valor registado na tabela para os atributos não especificados?". Como foi mencionado anteriormente, pode-se especificar um valor por omissão para um determinado atributo. Por exemplo, recordando a definição da tabela Produtos, caso não se especificasse um valor para o atributo promoção, o valor registado na tabela seria 'N', ou seja o valor especificado a ser utilizado por omissão. Para os atributos que não se definiu valores por omissão, o valor registado é NULL. Tal como mencionado anteriormente, o comando INSERT possibilita a inserção de múltiplos registos de uma só vez. No entanto, uma vez que se utiliza o comando SELECT para definir quais os dados a inserir, estes têm de ser derivados de dados já existentes na base de dados. A sintaxe para esta versão do comando INSERT é: INSERT INTO nomeDaTabela(listaDeAtributos) comandoSELECT; 18 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Por exemplo, assumindo que a tabela Fornecedores ainda não continha nenhum registo, caso se pretendesse criar um registo para cada marca presente na tabela Produtos poderiamos escrever: INSERT INTO Fornecedores(marca) SELECT marca FROM Produtos; Os atributos presentes na clausula SELECT têm de coincidir com a lista de atributos definida após o nome da tabela. Neste caso, para cada registo inserido, o valor do atributo nome seria NULL, a não ser que a definição da tabela Fornecedores especificasse um valor por omissão para este atributo. A remoção de registos de uma tabela em SQL é realizada através do comando DELETE. A especificação deste comando implica a definição do nome da tabela e de um conjunto de condições a que os registos a remover têm de obedecer. A sintaxe para o comando DELETE é: DELETE FROM nomeDaTabela WHERE condições; As condições a especificar no comando DELETE seguem as regras definidas para o comando SELECT. Por exemplo, para remover todos os produtos cuja marca seja 'ASUS' da tabela Fornecedores, podemos escrever: DELETE FROM Fornecedores WHERE marca='ASUS'; Para terminar esta curta introdução ao SQL vamos apresentar o comando UPDATE. Através deste comando é possível alterar valores presentes numa tabela. A sintaxe é a seguinte: UPDATE nomeDaTabela WHERE condições; SET listaDeAtribuições Mais uma vez, a definição das condições é semelhante à definida para o comando SELECT. Por exemplo para alterar o atributo promoção em todos os produtos da marca IBM, para 'S' poderíamos escrever: 19 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI UPDATE Produtos SET promoção = 'S' WHERE marca = 'IBM'; 20 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 2 U TILIZA Ç Ã O B Á SIC A D A A PIJD BC Neste capitulo será explorada a forma de utilização mais simples da API JDBC para comunicação com bases de dados. Em primeiro lugar o processo de ligação à base de dados será analisado, incluindo a forma de carregar o driver necessário e a definição da localização da base de dados utilizando exemplos para bases de dados locais e remotas. A ligação efectiva a uma base de dados é exemplificada com diversos tipos de drivers. Uma vez estabelecida a ligação a uma base de dados torna-se possível o envio de comandos SQL para realizar operações de alteração ou consulta. O JDBC permite vários métodos de acesso à base de dados de forma a prever um leque abrangente de situações. Neste capitulo será abordado o método mais simples que consiste em criar uma string com o texto de um comando SQL e enviá-lo à base de dados. Dependendo do tipo de comando SQL, o JDBC fornece os métodos apropriados para o envio destes à base de dados. Os métodos a invocar para os diversos tipos de comandos serão apresentados. Ainda dependente do tipo de comandos enviados, é o tipo de resposta obtida. Para comandos SQL do tipo SELECT é necessário processar o resultado devolvido pela base de dados. Neste capítulo é apresentada a forma mais simples de extrair os resultados deste tipo de comandos. 2.1 C O N EC TIVID A D E C O M B A SES D E D A D O S E STA BELEC ER U M A L IG A Ç Ã O O primeiro passo consiste em especificar o driver a utilizar. Como foi mencionado anteriormente, o JDBC inclui o driver necessário para ligações via o protocolo ODBC. Este driver será utilizado nos exemplos que se seguem, juntamente com um driver tipo 4 (protocolo de rede nativo) para a base de dados MySQL. 21 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O JDBC está definido de forma modular no sentido em que o driver a utilizar só influencia a definição da ligação. Posteriormente, após a ligação estar estabelecida, é indiferente qual o driver utilizado, excepto, claro está, no grau de implementação de cada um dos drivers. O primeiro passo consiste em especificar qual o driver a utilizar e torná-lo disponível para posterior utilização. Para tal é necessário carregar o driver, por exemplo, através da invocação do seguinte método da classe java.lang.Class: void forName(String nomeClasse) throws ClassNotFoundException; O parâmetro deste método especifica a classe que implementa o driver. A documentação do driver especifico para a base de dados deve fornecer o nome da classe. A titulo de exemplo, caso se pretenda utilizar o driver JDBC-ODBC, a classe respectiva é sun.jdbc.odbc.JdbcOdbcDriver, sendo o código para a sua invocação: try { Class.forName(“sun.jdbc.odbc.JdbcOdbcDriver”); } catch (ClassNotFoundException e) { } // tratamento do erro Para aceder a uma base dados MySQL, pode utilizar-se o driver tipo 4 (gratuito) MM.MySQL. A denominação do driver resulta das iniciais do seu autor, Mark Matthews. A classe a especificar no método forName para este driver é: org.gjt.mm.mysql.Driver. try { Class.forName(“org.gjt.mm.mysql.Driver”); } catch (ClassNotFoundException e) { } // tratamento do erro Como se pode inferir do código acima apresentado, caso o driver não esteja disponível o método forName lança a excepção ClassNotFoundException. Esta excepção é subclasse de RunTimeException, logo não é de tratamento obrigatório. No entanto, deve prever-se o seu tratamento, uma vez que existe a possibilidade do driver não existir no sistema. 22 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Um processo alternativo para disponibilizar um driver JDBC numa aplicação consiste em adicionar às propriedades do sistema, o texto com o nome da classe associado à propriedade jdbc.drivers. Tal pode ser conseguido de duas formas: através da linha de comando, ao invocar a aplicação; ou utilizando o método setProperty da classe java.lang.System. É possível adicionar vários drivers a esta propriedade utilizando o simbolo ":" para separar os nomes dos drivers. Por exemplo para definir que uma aplicação xpto pode usar o driver JDBC-ODBC pode-se especificar a seguinte linha de comando: > java -Djdbc.drivers=sun.jdbc.odbc.JdbcOdbcDriver xpto Como foi mencionado anteriormente, também se pode utilizar o método setProperty para definir a propriedade jdbc.driver. A assinatura deste método é em seguida apresentada: public static String setProperty( String chave, String valor); Por exemplo, para atribuir o valor sun.jdbc.odbc.JdbcOdbcDriver à chave jdbc.drivers, pode-se escrever: System.setProperty("jdbc.drivers", "sun.jdbc.odbc.JdbcOdbcDriver"); Note-se que a especificação da propriedade jdbc.drivers é uma alternativa à utilização do método forName. Logo, caso se opte pela definição de jdbc.drivers, não é necessário utilizar o método forName para carregar o driver. Ao utilizar a propriedade jdbc.drivers, a aplicação não carrega o driver tal como acontece quando se utiliza o método forName. No entanto, ao executar pela primeira vez um método da classe DriverManager, esta classe irá verificar se está definida a propriedade jdbc.drivers. Em caso afirmativo, os drivers especificados na propriedade serão disponibilizados para a aplicação. 23 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Uma vez que o driver esteja disponível, é possível criar a ligação à base de dados. A classe DriverManager é a responsável pelo estabelecimento da ligação. Todos os métodos desta classe são declarados como static, sendo portanto métodos de classe. Esta classe não permite a criação de instâncias pelo programador, sendo portanto um serviço fornecido aos utilizadores da API. A classe DriverManager disponibiliza os seguintes métodos para estabelecer a ligação: Connection getConnection(String url) throws SQLException; Connection getConnection(String url, String login, String password) throws SQLException; Connection getConnection(String url, java.util.Properties info) throws SQLException; O resultado da invocação do método getConnection é um objecto cuja classe implementa a interface Connection. A interface Connection define as assinaturas dos métodos necessários para criar objectos que contêm comandos SQL. A diferença entre a primeira e as restantes versões do método getConnection reside na forma de especificar um login e password para acesso à base de dados. Caso a base de dados tenha associado um login, pode-se utilizar a segunda ou terceira versões. Em certos drivers, os parâmetros de ligação, nomeadamente login e password, podem ser definidos no parâmetro url como iremos ver mais adiante. Nos casos em que não é necessário especificar um par (login, password), pode-se também utilizar a segunda ou terceira versões, em que, por exemplo, na segunda versão os parâmetros login e password são definidos com a string vazia "". A última versão do método getConnection, em que o segundo parâmetro é uma instância de java.util.Properties, permite que se especifiquem propriedades da ligação através de pares (chave,valor). Por exemplo, este parâmetro poderia conter o login e password do 24 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI utilizador. Outras propriedades da ligação são dependentes do driver em causa, podendo, por exemplo, definir o tempo máximo de espera para obter a ligação. O parâmetro url necessita de uma explicação mais detalhada. Este parâmetro está dividido logicamente em três componentes separadas pelo símbolo ":". As componentes do parâmetro url são: • protocolo: O protocolo em ligações a bases de dados através de JDBC é constante, sendo representado pela string “jdbc”. • sub protocolo: Quanto ao sub protocolo, este é dependente do driver em causa. Por exemplo, caso exista um driver apropriado uma base de dados pode ser acedida directamente; caso a base de dados em questão seja suportada por ODBC, esta também pode ser acedida via ODBC. A documentação do driver deve mencionar qual o sub protocolo a utilizar. A título de exemplo para um driver JDBC-ODBC o sub protocolo é “odbc”, e para o driver MM.MySQL é "mysql". • identificador da base de dados: A identificação da base de dados têm como objectivo permitir a localização da base de dados. A sua sintaxe é dependente do sub protocolo utilizado. Para ligações através do driver JDBC-ODBC o identificador é o definido em Data Source Name aquando do registo da base de dados no administrador de ODBC. Para bases de dados remotas o identificador pode indicar o endereço de rede, utilizando a notação standard de descrição de URL1, e o nome pelo qual a base de dados é conhecida. A especificação do identificador pode também incorporar os parâmetros da ligação em certos drivers. Adicionalmente, em alguns drivers, é possível especificar propriedades da ligação no parâmetro url. A especificação destas propriedades é no entanto dependente do driver em causa. Para o 1 Uniform Resource Location 25 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI driver MM.MySQL serão apresentados exemplos de definições do parâmetro url que tiram partido desta funcionalidade. O excerto do código JAVA para realizar a ligação a uma base de dados com o identificador "bitShop" utilizando o sub-protocolo "odbc" é em seguida apresentado a titulo de exemplo: try { con = DriverManager.getConnection(“jdbc:odbc:bitShop”); } catch (SQLException e) { //tratamento do erro } Caso seja necessário especificar um login e password, pode-se substituir a linha onde se realiza a invocação ao método getConnection, por uma das outras versões do mesmo: con = DriverManager.getConnection( "jdbc:odbc:bitShop", "gestor","bitbit"); ou ainda, java.util.Properties info = new java.util.Properties(); info.add("login","gestor"); info.add("password", "bitbit"); con =DriverManager.getConnection( "jdbc:odbc:bitShop", info); Considerando o driver MM-MySQL, e uma vez que este driver é do tipo 4, protocolo de rede nativo, o parâmetro url é naturalmente diferente, já que é necessário especificar qual a localização do servidor MySQL. Por exemplo, assumindo que o servidor está alojado na máquina bitShop.com, a responder a pedidos na porta 3306, e que a base de dados tem por identificador "bitShop", o código para efectuar a ligação seria: 26 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI String id; id = "jdbc:mysql://bitShop.com:3306/bitShop" try { con = DriverManager.getConnection(id); } catch (SQLException e) { //tratamento do erro } A especificação de parâmetros como login e password pode, neste caso, ser feita também através do parâmetro url. Por exemplo, id = " jdbc:mysql://bitShop.com:3306/bitShop?" + "user=gestor&password=bitbit"; Como se pode verificar neste exemplo, a construção do parâmetro url é em tudo semelhante à construção de um URL parametrizado na WWW. TEMPO MÁXIMO DE ESPERA Através da classe DriverManager é possível estabelecer o tempo máximo de espera para efectuar a ligação a uma base de dados. Note-se que, à partida, existe um tempo máximo prédefinido pelo driver. No entanto, por vezes pode ser necessário alterar a temporização máxima desta operação. Uma situação em que esta necessidade pode surgir resulta da utilização de bases de dados remotas com tempos de acesso prolongados devido a ligações de rede lentas ou congestionadas. A classe DriverManager fornece um método para interrogar qual o valor definido, e um outro método para alterar este valor. O método para interrogar a classe DriverManager sobre o valor definido para o tempo máximo de espera tem a seguinte assinatura: public static int getLoginTimeout() throws SQLException; 27 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O valor devolvido por este método indica o número máximo de segundos para a obtenção da ligação. Este método pode ser utilizado para determinar qual o valor pré-definido desde que o valor ainda não tenha sido alterado. Caso seja necessário, é também possível alterar o valor pré-definido pelo driver. O método para definir o tempo máximo de espera para efectuar a ligação tem a seguinte assinatura: public static void setLoginTimeout(int segundos) throws SQLException; O parâmetro deste método é o número máximo de segundos permitido para estabelecimento da ligação. Certos drivers, como por exemplo MM.MySQL, permitem que o tempo máximo para efectuar uma ligação seja também especificado no parâmetro url. Por exemplo: url = "jdbc:mysql://bitShop.com:3306/bitShop?" + "user=gestor&password=bitbit" + "&initialTimeout=30"; T ER M IN A R A LIG A Ç Ã O Uma ligação a uma base de dados consome recursos tanto ao nível do JAVA como da própria base de dados. Para libertar esses recursos deve-se terminar a ligação logo que esta já não seja necessária. Para tal efeito a interface Connection define um método com a seguinte assinatura: void close() throws SQLException; Para verificar se uma ligação foi efectivamente terminada, ou para averiguar se uma determinada ligação está activa, a interface Connection define o seguinte método: boolean isClosed() throws SQLException; 28 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI E X EM PLO Nesta secção é apresentado um exemplo para definir uma ligação a uma base de dados, e posteriormente terminá-la. O exemplo contem código para apanhar possíveis excepções geradas durante a invocação dos métodos. Assume-se que a seguinte variável foi previamente definida: Connection con; O exemplo apresenta um excerto de código para realizar uma ligação a uma base de dados "bitShop" utilizando o driver JDBC-ODBC. // Carregar o driver JDBC try { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); } catch (ClassNotFoundException e) { System.out.println("Driver não disponível"); } // Abrir a ligação com a base de dados "bitShop" // Não esquecer de registar a base de dados em odbc try { con = DriverManager.getConnection("jdbc:odbc:bitShop"); } catch (SQLException e) { System.out.println("Ligação não efectuada"); } // Efectuar dialogo com a base de dados aqui // ... // Terminar a ligação try { con.close(); } catch (SQLException e) { System.out.println("Erro ao terminar a ligação"); } 29 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 2.2 D EFIN IÇ Ã O E E N VIO D E C O M A N D O S SQ L Nesta secção será apresentada a forma mais simples de dialogar com a base de dados: o envio de um texto com um comando SQL à base de dados. O primeiro passo consiste em criar um Statement, ou seja um objecto do tipo Statement. É este objecto que será utilizada para enviar o comando SQL à base de dados. A criação de um Statement implica a ligação prévia à base de dados. Para criar um Statement deve-se invocar o método createStatement, definido na interface Connection, cuja assinatura se apresenta a seguir: Statement createStatement() throws SQLException; Como foi mencionado anteriormente, ao efectuar a ligação à base de dados através do método getConnection, é criado um objecto cuja classe implementa a interface Connection. É este objecto que irá permitir a criação de Statement. Assumindo que a variável con é o resultado do método getConnection, o código para criar um Statement é o seguinte: Statement s; try { s = con.createStatement(); } catch (SQLException e) { // tratamento do erro } Após a execução do código apresentado é possível enviar comandos SQL à base de dados. O método a invocar para este efeito depende do tipo de comando a enviar. 30 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O JDBC fornece três métodos para o envio de comandos SQL: • executeQuery: para comandos de selecção; • executeUpdate: para todos os outros comandos; • execute: método genérico para todos os tipos de comando. Esta alternativa embora mais simples à partida, implica um esforço adicional para a análise de resultados. A invocação destes métodos para comandos que não sejam os especificados acima provoca o lançamento de uma excepção. Por exemplo, a invocação de executeQuery com um comando INSERT, ou a invocação do método executeUpdate com um comando SELECT. Seguidamente estes três métodos serão apresentados em detalhe. M ÉTO D O ESPEC ÍFIC O PA R A E N VIO D E C O M A N D O S D E SELEC Ç Ã O O método executeQuery permite realizar consultas aos dados armazenados em tabelas na base de dados. Este método tem como argumento uma String com o texto correspondente a um comando SQL SELECT. Este método encontra-se definido na interface Statement, e apresenta a seguinte sintaxe: ResultSet executeQuery(String sql) throws SQLException; O resultado deste método é um ResultSet. Este objecto contém os dados que correspondem à execução do comando SQL enviado à base de dados. Posteriormente serão analisados os métodos para extrair essa informação. A título de exemplo considere-se o seguinte comando SQL: SELECT modelo FROM produtos WHERE preço > 100 Assumindo que a variável con representa uma ligação activa à base de dados, o excerto de código que se segue exemplifica a invocação do método executeQuery: 31 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI ResultSet res; Statement st; String sql; sql = "SELECT modelo FROM Produtos WHERE preço > 100"; try { st = con.createStatement(); res = st.executeQuery(sql); } catch (SQLException e) { System.err.println(e.getMessage()); } M ÉTO D O E SPEC ÍFIC O PA R A E N VIO DE C O M A N D O S D E A C TU A LIZA Ç Ã O A interface Statement fornece um método especifico para comandos SQL que envolvam alterações da estrutura da base de dados ou dos valores contidos nas tabelas propriamente ditas. O método indicado para este tipo de operações é o seguinte: int executeUpdate(String sql) throws SQLException; O parâmetro deste método é uma String com o texto do comando SQL pretendido. O valor devolvido depende do tipo do comando SQL. Para actualizações ao conteúdo das tabelas da base de dados, ou seja para comandos SQL UPDATE, INSERT e DELETE, o resultado do método é o número de linhas afectadas. Por exemplo, considerando um comando DELETE que remova três linhas de uma tabela, o resultado seria três. No caso de comandos que alterem a estrutura da base de dados, como por exemplo a criação de uma nova tabela, o resultado é sempre zero. Se a alteração especificada no comando não for permitida é lançada uma excepção do tipo SQLException. A título de exemplo considere-se o seguinte comando SQL: UPDATE Produtos SET promoção ='S' WHERE marca = 'ASUS'; 32 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O seguinte excerto de código envia o comando à base de dados e apresenta o número de registos alterados. int cont; String s; Statement st; s = "UPDATE Produtos " + "SET promoção = 'S' " + "WHERE marca = 'ASUS'"; try { st = con.createStatement(); cont = st.executeUpdate(s); System.out.println("Registos Alterados: " + cont); } catch (SQLException e) { System.err.println(e.getMessage()); } M ÉTO D O G EN ÉR IC O A API JDBC fornece um terceiro método para envio de comandos SQL à base de dados. O método execute, definido na interface Statement permite o envio de qualquer comando SQL, sem as restrições definidas previamente. Uma utilização típica deste método pode surgir numa aplicação que contenha uma caixa de texto para o utilizador definir um comando SQL. Embora seja possível escrever o código que analise o comando introduzido pelo utilizador para determinar qual o método a invocar, tal não é necessário com a utilização deste comando. O método para envio de comandos SQL genéricos tem a seguinte assinatura: boolean execute(String sql) throws SQLException; O resultado devolvido por este comando é função do tipo de comando enviado. Caso o comando seja SELECT o resultado será TRUE. Para todos os outros comandos o resultado é FALSE. Deste forma é possível saber qual o tipo de comando à posteriori sem ser necessário implementar qualquer código para análise do comando. Resta agora saber como aceder ao resultado real da operação. Dois métodos são fornecidos pela API JDBC consoante o tipo de comando SQL. Para comandos SELECT o método 33 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI getResultSet, definido na interface Statement, permite obter o conjunto de linhas devolvidas pelo comando. A assinatura deste método é a seguinte: ResultSet getResultSet() throws SQLException; Caso o resultado do método execute seja FALSE, ou seja não estamos perante um comando de selecção, este método não deve ser invocado. A invocação indevida deste método produz como resultado null não sendo lançada nenhuma excepção. Para comandos SQL do tipo INSERT, UPDATE e DELETE, o método getUpdateCount, da interface Statement, produz informação sobre o número de registos alterados. Relativamente aos comandos SQL para alteração da estrutura da base de dados, o resultado do método getUpdateCount será sempre zero. A assinatura deste método é em seguida apresentada. int getUpdateCount() throws SQLException; Caso se invoque este método indevidamente este método, ou seja o comando SQL previamente enviado é um comando de selecção, o resultado será –1 não sendo lançada nenhuma excepção. Ambos os métodos apresentados lançam a excepção SQLException em caso de erro no acesso à base de dados. O exemplo que se segue ilustra a utilização destes métodos. Assume-se que a variável con representa uma ligação activa à base de dados. 34 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI int cont; boolean res; String s; ResultSet sel; Statement st; s = "UPDATE Produtos " + "SET promoção = 'S' " + "WHERE marca = 'ASUS'"; try { st = con.createStatement(); res = st.execute(s); if (res) sel = st.getResultSet() else cont = st.getUpdateCount(); } catch (SQLException e) { System.err.println(e.getMessage()); } Como é ilustrado pelo exemplo acima, a utilização deste método genérico exige um esforço adicional para a obtenção do resultado, em comparação com os métodos específicos apresentados nas secções anteriores. A sua utilização só se justifica quando a natureza do comando SQL a enviar à base de dados não é conhecida à priori. 2.3 A N Á LISE D E R ESU LTA D O S D E C O M A N D O S SELEC T Nas secções anteriores foi apresentada a forma de efectuar uma ligação a uma base de dados, e enviar individualmente comandos SQL. Ao enviar um comando SELECT, utilizando os métodos apresentados nas secções anteriores, é criado um ResultSet. Neste contexto, um ResultSet contem os dados resultantes da resposta da base de dados a um comando SELECT. Um ResultSet tem uma estrutura semelhante a uma tabela, sendo composto por uma sequência de linhas, correspondentes às linhas devolvidas pelo comando SELECT. Cada linha por sua vez contem uma sequência de valores relativos aos campos indicados na clausula SELECT. A forma de navegação na tabela de resultados de um ResultSet depende da forma como o comando SQL é criado. Na sua forma mais simples, e por omissão, a navegação é sequencial, e 35 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI unidirecional, i.e. só permite percorrer o ResultSet numa direcção. Mais à frente veremos como construir um ResultSet com possibilidade de acesso aleatório. Devido à unidirecionalidade implícita do ResultSet, os seus dados devem ser obtidos iterativamente de uma forma semelhante à da interface Enumeration. Tal como na interface Enumeration, um ResultSet contem um cursor para as linhas cuja posição inicial se encontra antes da primeira linha. O acesso às linhas é realizado sequencialmente, avançando o cursor para a linha seguinte, através do método next cuja assinatura se apresenta a seguir: boolean next() throws SQLException; O método next, caso existam mais linhas, avança o cursor para a linha seguinte e devolve true. Se o cursor estiver posicionado na última linha, a invocação deste método produz false como resultado. Note-se que neste caso não é lançada nenhuma excepção. A excepção só é lançada quando há erros relacionados com o acesso à base de dados. Uma vez que o método next tem como resultado um valor booleano, a iteração dos dados de um ResultSet pode ser implementada utilizando um ciclo while. Assumindo que rs representa um ResultSet criado como resultado do envio de um comando SELECT à base de dados, podese então escrever: try { while( rs.next() ) { } // processar linha } catch (SQLException e) { // processar erro } Uma vez o cursor posicionado numa linha válida torna-se necessário aceder aos dados relativos às colunas seleccionadas pelo comando SELECT. Os dados das colunas podem ser acedidos utilizando a família de métodos getXXX, sendo XXX correspondente ao tipo de dados da coluna em JAVA. O parâmetro destes métodos pode ser a posição relativa da coluna, ou o seu nome, tal como definido na base de dados. 36 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Por exemplo, considere-se o seguinte comando SELECT: SELECT modelo, marca FROM Produtos WHERE preço < 1000; Neste caso ambas as colunas são do tipo VARCHAR, o que corresponde ao tipo String em JAVA. Dado um objecto rs, que obedeça à interface ResultSet, para obter o valor da coluna "marca", qualquer uma das opções a seguir apresentadas é válida: rs.getString(2); rs.getString("marca"); Note-se que o índice das colunas começa em 1 e não em 0, contrariamente ao que acontece com os dados indexados em praticamente todas as classes JAVA. Para colunas cujo tipo de dados corresponda a uma String JAVA, estão definidos os métodos: String getString(int índiceColuna) throws SQLException; String getString(String nomeColuna) throws SQLException; Estes métodos também podem ser usados para quase todos os tipos de dados, sendo a conversão para String realizada automaticamente. Outros métodos da família getXXX são, por exemplo: Float getFloat Date getDate Int getInt O parâmetro pode ser sempre, ou o nome da coluna, ou o índice. A excepção SQLException será lançada em caso de invocação inapropriada do método, i.e. o tipo de dados do atributo não é passível de conversão no tipo de dados respectivo do método, ou em caso de erro no acesso à base de dados. Consideremos então o seguinte comando SQL aplicado à tabela Produtos: Tabela Produtos 37 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI código marca modelo categoria preço promoção 01 ASUS V8100 De Luxe Placa Gráfica 547 N 02 ASUS NoteBook L8400C Portátil 1995 S 03 Toshiba Satelite 3100 Portátil 1950 N 04 Creative Audigy 5.1 OEM Placa Audio 103 S 05 Compaq IPaq Pocket PC PDA 745 N 06 IBM DeskStar 60GB Disco IDE 283 N SELECT marca,modelo,preço FROM Produtos WHERE preço < 1000.0 O seguinte excerto de código ilustra a invocação destes métodos. Assume-se que a variável rs contem uma instancia de ResultSet com o resultado do comando SQL acima apresentado. System.out.println("Marca \tModelo \tPreço"); System.out.println("----\t-----\t-----"); try { while (rs.next()) { System.out.println(rs.getString(1) + " \t" + rs.getString(2) + "\t" + rs.getFloat(3)); } } catch (SQLException e) { System.out.println(e.getMessage()); } O resultado do código acima apresentado seria: Marca ----ASUS Creative IBM Modelo ---V8100 De Luxe Audigy 5.1 OEM DeskStar 60GB Preço ----547.0 103.0 283.0 Como foi mencionado anteriormente, para cada tipo de dados, existem dois métodos getXXX. A diferença entre estes dois métodos reside unicamente no seu argumento. O argumento tanto pode ser, um int representativo do índice da coluna, como vimos no exemplo acima, ou então uma String com o nome da coluna. Desta forma, poderíamos alternativamente ter escrito, para a linha que imprime os dados, o seguinte código: 38 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI System.out.println(rs.getString("marca") + " \t" + rs.getString("modelo") + "\t" + rs.getFloat("preço")); Alternativamente, também poderíamos ter utilizado o método getString para obter os valores do atributo preço. Como foi mencionado anteriormente o método getString é um método genérico que pode ser utilizado para a maioria dos tipos de dados. E X EM PLO C O M PLETO import java.sql.*; public class SimpleResultSet { static Connection con; static ResultSet rs; static Statement st; public static void main(String[] args) { // Carregar o driver JDBC try { Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); } catch (ClassNotFoundException e){ System.out.println("Driver não disponível"); System.exit(-1); } // Abrir a ligação com a base de dados "bitShop" // Não esquecer de registar a base de dados no odbc try { con = DriverManager.getConnection( "jdbc:odbc:bitShop", "gestor", "bitbit"); } catch (SQLException e) { System.out.println("Ligação não Efectuada"); System.exit(-1); } try { // Definição e envio do comando SQL String sql = "SELECT marca,nome,preço " + "FROM Produtos " + 39 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI "WHERE preço < 1000.0"; st = con.createStatement(); rs = st.executeQuery(sql); // Mostrar resultados System.out.println("Marca \tNome System.out.println("----\t---- \tPreço"); \t-----"); while (rs.next()) { System.out.println(rs.getString(1) + " \t" + rs.getString(2) + "\t" + rs.getFloat(3)); } } catch (SQLException e) { System.out.println(e.getMessage()); } try { con.close(); } catch (SQLException e) { System.out.println e.getMessage()); } } // fim da função main } // fim da classe 2.4 M ETA D A D O S D O S R ESU LTA D O S Na secção anterior foi apresentada uma forma simples de extrair resultados de um ResultSet. O processo apresentado no entanto pressupõe à priori um conhecimento do comando SELECT, nomeadamente qual o número de colunas, qual o seu tipo e nome, etc. Como lidar então com casos em que este conhecimento não existe? Por exemplo, uma aplicação interactiva em que o utilizador introduz comandos SQL para enviar à base de dados. Nestes casos ao receber um ResultSet como resultado do envio de um comando SELECT torna-se necessário determinar em primeiro lugar qual a estrutura do resultado propriamente dito de forma a se poder apresentar convenientemente os resultados. A API JDBC contem uma interface que define um conjunto de métodos para obter informações a estrutura sobre um ResultSet: a interface ResultSetMetaData. Através dos métodos definidos nesta interface é possível determinar o número de colunas presentes num ResultSet, 40 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI o nome das colunas, o seu tipo de dados, e outras informações relativas à estrutura de um ResultSet. Dado um ResultSet rs, para se obter a sua meta data, deve-se invocar o seguinte método: ResultSetMetaData getMetaData() throws SQLException; A partir deste ponto podem-se invocar os métodos definidos na interface ResultSetMetaData utilizando o resultado deste método. Esta interface define um espectro largo de funcionalidades das quais apenas algumas são aqui apresentadas. Ao receber o resultado de um comando SELECT cujo conteúdo é desconhecido é necessário determinar o número de colunas que este contem. Para tal pode-se invocar o seguinte método definido na interface ResultSetMetaData: int getColumnCount() throws SQLException; Dado um índice de uma coluna, o seu nome pode ser obtido através do seguinte método: String getColumnName(int índice) throws SQLException; O nome da classe JAVA que corresponde ao tipo de dados de uma coluna também se pode obter através da interface ResultSetMetaData, invocando o método: String getColumnClassName(int índice) throws SQLException; O nome da tabela de onde foram retirados os valores da coluna pode-se obter através do método: String getTableName(int índice) throws SQLException; Consideremos agora o seguinte comando SQL: SELECT marca,modelo,preço FROM Produtos WHERE preço < 1000.0 41 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Em seguida apresenta-se um excerto de código que ilustra a invocação destes métodos e respectivo resultado. Seja rs um ResultSet que contem o resultado da invocação do comando apresentado. try { ResultSetMetaData rsmd = rs.getMetaData(); System.out.println("Total de colunas : " + rsmd.getColumnCount()); System.out.println("Nome da coluna 1 : " + rsmd.getColumnName(1)); System.out.println("Classe Java da coluna 1 : " + rsmd.getColumnClassName(1)); System.out.println("Nome da tabela da coluna 1 : " + rsmd.getTableName(1) ); } catch (SQLException e) { System.out.println(e.getMessage()); } Resultado: Total de colunas : 3 Nome da coluna 1 : marca Classe Java da coluna 1 : java.lang.String Nome da tabela da coluna 1 : Produtos Note-se que muitos destes métodos não se encontram implementados em alguns drivers, podendo lançar excepções na altura da sua invocação. Através da análise dos meta dados de um ResultSet é possível definir um método genérico para apresentação de resultados. De seguida, apresenta-se um exemplo simples de um método que recebe como parâmetro um ResultSet, que ilustra a potencial da interface ResultSetMetaData. static void display(ResultSet rs) throws SQLException { ResultSetMetaData rsmd; int cont,i; // acesso à meta data 42 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI rsmd = rs.getMetaData(); // número de colunas cont = rsmd.getColumnCount(); // para cada coluna imprimir o nome for (i = 1; i <= cont; i++) System.out.print(rsmd.getColumnName(i) + "\t"); System.out.println(); } // imprimir linhas do resultado while (rs.next()) { for (i = 1; i <= cont; i++) System.out.print(rs.getString(i) + "\t"); System.out.println(); } 2.5 E X C EL VIA JD BC Uma consulta rápida ao administrador de ODBC revela-nos que este protocolo suporta a folha de cálculo Excel. O processo de registo de uma folha de cálculo em ODBC é semelhante ao registo de uma base de dados. Uma vez que o JDBC inclui um driver JDBC-ODBC, então é possível trabalhar com uma folha de cálculo de forma semelhante à apresentada para bases de dados convencionais. Do ponto de vista do ODBC, cada folha de trabalho (Worksheet) corresponde a uma tabela. A primeira linha da tabela especifica os atributos da tabela, sendo as restantes linhas utilizadas para os dados. O nome da folha de trabalho especifica o nome da tabela. Via ODBC, é possível realizar consultas, inserções, e alterações sobre os dados de uma folha de cálculo. No entanto, a ligação via ODBC não permite a remoção de linhas de uma folha de cálculo. Operações sobre de tabelas em folhas de cálculo Excel, via ODBC, também são permitidas embora com uma semântica particular. A inserção de folhas de trabalho não é possível, mas a criação de uma tabela numa folha existente é permitida. Ao realizar uma operação DROP TABLE a folha de trabalho não é eliminada mas os seus conteúdos são apagados. 43 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI A Figura 1apresenta duas folhas de trabalho de uma folha de cálculo gravada em ficheiro com o nome produtos.xls. Como podemos observar pela barra inferior das folhas de trabalho apresentadas na Figura 1, estão definidas duas tabelas: Produtos e Fornecedores. Os nomes dos atributos aparecem na primeira linha de cada uma das tabelas. O sombreado visível na primeira linha de cada folha de trabalho tem somente um fim estético para separar os nomes dos atributos dos seus valores. 44 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Figura 1 Bases de dados em folhas de trabalho Excel Em relação ao código JAVA, o procedimento para efectuar uma ligação à folha de cálculo é exactamente idêntico ao apresentado anteriormente para base de dados, sendo utilizado o driver JDBC-ODBC. A única diferença reside na definição do comando SQL, que apresenta um pouco de açúcar sintáctico. A referência às tabelas é realizada utilizando o nome da tabela com o sufixo '$'. Uma vez que este símbolo é reservado em SQL, torna-se ainda necessário adicionar parêntesis rectos ao nome da tabela. Consideremos, por exemplo, o seguinte comando SELECT SELECT Produtos.marca, modelo, preço FROM Produtos, Fornecedores WHERE nome = 'BomBit' AND Produtos.marca = Fornecedores.marca; A versão do comando acima apresentado, para utilização numa ligação via JDBC a uma folha de cálculo Excel, é a seguinte: SELECT [Produtos$].marca, modelo, preço FROM [Produtos$], [Fornecedores$] WHERE nome = 'BomBit' AND [Produtos$].marca = [Fornecedores$].marca; 45 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 3 D EFIN IÇ Ã O A VA N Ç A D A D E C O M A N D O S SQ L No capítulo anterior foi apresentada a forma mais simples de definição e envio de comandos SQL a uma base de dados. Tendo em vista optimizar a performance de aplicações que utilizem esta API, estão previstas outras formas quer para a definição quer para o envio de comandos SQL. Neste capítulo iremos explorar exactamente estas funcionalidades avançadas da API JDBC 2.0. 3.1 PR É -PR O C ESSA M EN TO D E C O M A N D O S SQ L De acordo com o processo definido para envio de comandos SQL apresentado anteriormente, o comando SQL só é passado ao JDBC na altura da sua execução. Só nessa altura o comando é processado, e enviado. A ideia por detrás do pré-processamento de comandos SQL reside exactamente na separação destes dois passos. O objectivo é permitir reutilizar o processamento de comandos SQL que sejam utilizados frequentemente. Ao utilizar esta funcionalidade o comando SQL é pré-processado uma só vez e executado potencialmente múltiplas vezes, sendo o processo de execução teoricamente mais rápido. Para efectuar o pré-processamento, o comando SQL é enviado à base de dados antes da sua execução. Note-se que nem todos os drivers permitem realizar este pré-processamento, apesar de não lançarem excepções, ou avisos ao utilizar esta funcionalidade. No entanto, como veremos mais à frente, as situações criadas em termos de excepções potenciais são diferentes. O pedido de pré-processamento de um comando SQL é realizado através do método prepareStatement da interface Connection: PreparedStatement prepareStatement(String sql) throws SQLException; 46 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Note-se a diferença em relação ao método createStatement apresentado anteriormente. O método createStatement não tem argumentos, enquanto que o método prepareStatement tem como argumento a String correspondente ao comando SQL. Como se pode observar da assinatura do método, o resultado é um objecto que obedece à interface PreparedStatement. Esta interface é uma especialização da interface Statement, sendo sub-interface desta. No entanto os métodos da super-interface Statement, relativos à execução dos comandos SQL, não devem ser invocados para objectos do tipo PreparedStatemnt. A interface PreparedStatement define um conjunto próprio de métodos para execução de comandos. A execução de um comando pré-compilado é realizada através de um dos três métodos seguintes: ResultSet executeQuery() throws SQLException; int executeUpdate() throws SQLException; boolean execute() throws SQLException; A escolha do método para a execução do comando SQL é dependente do seu tipo. Tal como anteriormente temos o método executeQuery para comandos SELECT, o método executeUpdate para todos os outros comandos, e finalmente o método execute que pode ser utilizado com qualquer tipo de comando. Note-se mais uma vez a diferença em relação aos métodos homónimos apresentados no capítulo anterior. Agora todas as variante para execução de comandos SQL não tem argumentos, contrariamente ao que sucedia anteriormente, onde o comando SQL era um argumento. As diferenças são mais facilmente perceptíveis ao observar o código para as duas versões. O código que se segue assume que a variável con, do tipo Connection, representa uma ligação activa a uma base de dados, e que a variável sql, do tipo String, contem o texto de um comando SQL. Segundo o capítulo anterior tínhamos: Statement st; boolean res; 47 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI st = con.createStatement(); res = st.execute(sql); Com a introdução da funcionalidade para pré-compilação de comandos podemos escrever: PreparedStatement preparedSt; boolean res; preparedSt = con.prepareStatement(sql); res = preparedSt.execute(); Na primeira versão, o método createStatement, limita-se a criar um container vazio para um comando SQL. O objecto resultante de createStatement só é preenchido na ocasião da execução do comando. Nesta altura o comando é compilado e executado. A segunda versão utiliza o método prepareStatement, que já leva como argumento o comando SQL a utilizar posteriormente. A pré-compilação, caso seja permitida, é efectuada nesta altura enviando o comando à base de dados para este fim. Posteriormente ao executar o comando não é necessária mais nenhuma informação, logo o método execute não necessita de argumentos. Note-se que os ganhos potenciais ao nível de performance só serão obtidos caso exista précompilação e o comando seja utilizado repetidamente. O número exacto a partir do qual existem benefícios ao nível de performance é obviamente dependente do driver e da base de dados. Devido à sobrecarga requerida pela pré-compilação, não se justifica a utilização de comandos pré-compilados para uma só execução. Como foi mencionado anteriormente nem todos os drivers e/ou bases de dados permitem a précompilação. Embora não sejam lançadas excepções pela utilização desta funcionalidade quando o pré-processamento não se realiza, existem implicações em relação a qual dos métodos lança excepções caso haja problemas como comando SQL a enviar. 48 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Caso o driver e a base de dados suportem pré-compilação, e por consequência envie o comando à base de dados para este efeito, as excepções relativas à construção sintáctica do comando SQL serão imediatamente lançadas na invocação do método prepareStatement. Se a précompilação não for suportada, então mesmo com comandos SQL incorrectos não é lançada nenhuma excepção na invocação de prepareStatement. Neste último caso a excepção, será lançada por ocasião da invocação do método execute. Podemos aproveitar este facto para facilmente determinar se existe suporte para préprocessamento de comandos SQL. Consideremos o seguinte excerto de código, onde con, do tipo Connection representa uma ligação activa a uma base de dados: String sql = "Isto não é um comando SQL"; try { st = con.prepareStatement(sql); } catch (SQLException e) { System.out.println("Pré-compilação suportada"); } O código acima apresentado lançará uma excepção caso exista pré-compilação. Neste caso a excepção é lançada porque a base de dados informou o driver que o comando em causa continha erros sintácticos. Note-se no entanto que de um ponto de vista semântico nem todas as restrições são aplicadas na fase da pré-compilação do comando. Por exemplo, inserir uma linha na base de dados com um valor duplicado para um campo que seja chave primária pode não lançar uma excepção na fase compilação. No entanto operações de selecção sobre uma tabela inexistente na fase de précompilação podem não ser permitidas e provocar o lançamento de uma excepção. É importante lembrar que, dada a existência de uma fase prévia de pré-compilação, o contexto na pré-compilação não é necessariamente o mesmo que na fase de execução. Sendo assim, o facto de um determinado comando SQL não lançar uma excepção na fase de précompilação, não implica que estamos livres de perigo ao mandar executar o respectivo comando SQL. Mesmo que consideremos comandos que são válidos sintáctica e semanticamente na altura 49 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI de compilação, podemos obter excepções na altura da execução. Por exemplo um comando que insira uma linha numa tabela pode conter uma chave válida, i.e. não existente na tabela, na fase de compilação. No entanto nada nos garante que na fase de execução essa chave não tenha sido entretanto criada, resultando portanto uma excepção ao executar o comando. A título de exemplo, utilizando o driver JDBC-ODBC, as bases de dados Microsoft SQL Server permitem a pré-compilação de um comando que insira uma valor duplicado para uma chave primária, mas não permite uma operação de inserção sobre uma tabela inexistente. O mesmo driver JDBC-ODBC, com bases de dados Access já não suporta pré-compilação de comandos, logo não lança nenhuma excepção caso o comando esteja incorrecto na fase de pré-compilação. 3.2 C O M A N D O S SQ L PA R A M ETR IZA D O S Com foi mencionado anteriormente, a pré-compilação de comandos SQL permite optimizar, caso o driver o implemente esta funcionalidade, a execução posterior dos respectivos comandos. No entanto, para obter ganhos reais de performance é necessário que o comando SQL seja executado um determinado número de vezes para compensar o esforço adicional da pré-compilação. A possibilidade de pré-compilação de comandos SQL parametrizados alarga claramente este espectro de utilização. Por exemplo, quais os livros requisitados pelo utente X? Qual os produtos em promoção da categoria Y? Quais os produtos em stock da marca Z? Quais os alunos matriculados na disciplina W? Os comandos apresentados são claramente executados inúmeras vezes Vejamos agora um exemplo concreto. Quais os produtos distribuídos pelo fornecedor X? SELECT marca FROM Fornecedores WHERE nome = ?; ou, quais os modelos da categoria Y, cujo preço seja menor que Z, que se encontram em promoção? 50 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI SELECT marca, modelo FROM Produtos WHERE categoria = ? AND preço < ? AND promoção = 'S'; O símbolo '?' é a notação usada pelo JDBC para especificar um parâmetro num comando SQL. Uma vez pré-compilado, o comando pode ser instanciado múltiplas vezes atribuindo um valor ao parâmetro antes de cada execução. A criação de comandos pré-compilados com parâmetros é obtida através do método prepareStatement introduzido na secção anterior. A atribuição de valores aos parâmetros é realizada através da família de métodos setXXX da interface PreparedStatement. Consideremos o primeiro comando apresentado nesta secção. O seguinte excerto de código cria uma versão pré-compilada deste comando, e em seguida cria duas instâncias do comando e envia-as à base de dados. Assume-se que a variável con representa uma ligação activa à base de dados. String sql; PreparedStatement preparedSt; ResultSet rs1,rs2; try { sql = "SELECT marca FROM Formecedores WHERE nome = ?"; preparedSt = con.prepareStatement(sql); preparedSt.setString(1,"Bombit"); rs1 = preparedSt.executeQuery(); preparedSt.setString(1,"BitChip"); rs2 = preparedSt.executeQuery(); } catch (SQLException e) { System.out.println(e.getMessage()); } As variáveis rs1 e rs2, após a execução do código acima apresentado, contêm os resultados dos respectivos comandos SQL. O código acima apresentado é equivalente à seguinte versão em que não são utilizados comandos SQL parametrizados. 51 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI String sql; PreparedStatement preparedSt; ResultSet rs1,rs2; try { sql = "SELECT marca FROM Fornecedores " + "WHERE nome = Bombit"; preparedSt = con.prepareStatement(sql); rs1 = preparedSt.executeQuery(); sql = "SELECT marca FROM Fornecedores " + "WHERE nome = BitChip"; preparedSt = con.prepareStatement(sql); rs2 = preparedSt.executeQuery(); } catch (SQLException e) { System.out.println(e.getMessage()); } Com podemos ver no código apresentado na primeira versão, com comandos SQL parametrizados, o método setString tem dois argumentos. A sintaxe deste método é de seguida apresentada: void setString(int índice, String valor) throws SQLException; O primeiro argumento indica qual o índice do parâmetro no comando SQL, o segundo a valor a atribuir ao parâmetro. Note-se que o primeiro índice é 1 e não 0. O segundo comando SQL apresentado nesta secção têm dois parâmetros, o primeiro relativo à categoria, e o segundo ao preço. SELECT marca, modelo FROM Produtos WHERE categoria = ? AND preço < ? AND promoção = 'S'; 52 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Um exemplo do código para a criação da sua versão pré-compilada, e instanciação, é em seguida apresentado: String sql; PreparedStatement preparedSt; ResultSet rs1; try { sql = "SELECT marca, modelo" + "FROM Produtos" + "WHERE categoria = ? AND preço "AND promoção = 'S'"; < ? " + preparedSt = con.prepareStatement(sql); preparedSt.setString(1,"Portátil"); preparedSt.setFloat(2,2000.0); rs1 = preparedSt.executeQuery(); } catch (SQLException e) { System.out.println(e.getMessage()); } Relativamente a este exemplo, vamos rever as instruções para instanciar os parâmetros do comando SQL: preparedSt.setString(1,"Portátil"); preparedSt.setFloat(2,2000.0); Como podemos observar foi utilizado o método correspondente para cada tipo de dados, ou seja o método setString para o atributo categoria, e o método setFloat para o atributo preço. Alternativamente, poderíamos ter escrito, o seguinte código: preparedSt.setString(1,"Portátil"); preparedSt.setString(2,”2000.0”); O método setString pode ser utilizado como um método genérico. 53 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 3.3 IN VO C A Ç Ã O D E STO R ED PR O C ED U R ES Stored Procedures é o nome dado a métodos armazenados na própria base de dados. Estes métodos são semelhantes a métodos convencionais das linguagens de programação no sentido em que podem ter argumentos de entrada e produzir um resultado. Teoricamente, a execução de um stored procedure será uma das formas mais eficientes para obter resultados de comandos SQL, uma vez que estes já se encontram pré-compilados. Um stored procedure pode incluir conjuntos de comandos SQL, reduzindo assim o tráfego entre a aplicação com JDBC e a base de dados, uma vez que só é necessário enviar o nome do stored procedure para executar múltiplos comandos. De um ponto de vista de organização da aplicação, a utilização de stored procedures também é uma forma de garantir a separação total entre a camada de dados e a camada computacional. Ao embeber comandos SQL numa aplicação, estamos a estabelecer uma ligação explicita entre a aplicação e a estrutura da base de dados. Caso a estrutura da base de dados seja alterada torna-se necessário editar o código da aplicação, de forma a alterar todos os comandos SQL afectados. Com a utilização de stored procedures só é necessário realizar alterações na base de dados, sem ser necessário alterar e recompilar a aplicação. Na sua forma mais simples, os stored procedures representam um comando SQL. Por exemplo, vamos supor que o gestor da bitShop pretendia um método para reduzir o preço de todos os seus produtos por uma determinada percentagem, devido aos saldos da época Natalícia. Neste caso o gestor pode optar por criar um comando pré-compilado no código da sua aplicação, ou por definir um stored procedure na própria base de dados. A título de exemplo, é agora apresentada um stored procedure, criado na base de dados MS SQL Server 7.0, para este efeito. CREATE PROCEDURE spSaldos AS UPDATE Produtos SET preço = preço * 0.95 54 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O stored procedure apresentado reduz os preços de todos os produtos, aplicando um desconto de 5% ao preço. A terminar a época dos saldos, o gestor pretende retornar os preços ao seu valor original, tendo para este efeito definido o seguinte stored procedure: CREATE PROCEDURE spResetSaldos AS UPDATE Produtos SET preço = preço * (1 / 0.95) Como foi mencionado anteriormente, um stored procedure também pode ter argumentos. Por exemplo podemos escrever um stored procedure para colocar os produtos de uma determinada marca em promoção. CREATE PROCEDURE spSetPromo (@marca_1 varchar) AS UPDATE Produtos SET promoção = 'Y' WHERE marca = @marca_1 A utilização de stored procedures permite a execução de uma sequência de comandos SQL. Por exemplo, o gestor da bitShop pretende regularmente obter informação sobre marcas, nomeadamente quais são os fornecedores e quais as categorias disponibilizadas pela marca. Neste caso, podemos construir um stored procedure parametrizado e com múltiplos comandos SQL de forma a realizar todas as tarefas pretendidas. CREATE PROCEDURE spInfoMarca (@marca1 VARCHAR(15)) AS SELECT nome FROM Fornecedores WHERE marca = @marca1 SELECT categoria FROM Produtos WHERE marca = @marca1 De facto, na maior parte dos sistemas de bases de dados, a sintaxe para escrita de stored procedures é muito mais rica, não se limitando a permitir a invocação sequências de comandos SQL. No entanto, a descrição detalhada das potencialidades de stored procedures sai fora do âmbito deste livro. 55 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Como podemos inferir dos exemplos acima, a sintaxe para estes casos mais simples é: CREATE PROCEDURE nome (listaDeParâmetros) AS sequênciaDeComandosSQL Para podermos executar um stored procedure armazenado numa base de dados é necessário primeiro invocar o método prepareCall. Este método está definido na interface Connection, a semelhança do que ocorre com os métodos para comandos SQL. O método cria um objecto, que implementa a interface CallableStatement, com uma referência ao stored procedure para posterior invocação. A interface CallableStatement é sub-interface de PreparedStatement. A sua sintaxe é: CallableStatement prepareCall(String nomeSP) throws SQLException; Caso o stored procedure tenha parâmetros estes podem ser instanciados da mesma forma que os comandos SQL parametrizados. Após a criação da referência, e instanciação de possíveis parâmetros, podemos então executar o stored procedure respectivo. Os métodos para este efeito são herdados da interface PreparedStatement, a saber: boolean execute() throws SQLException; ResultSet executeQuery() throws SQLException; int executeUpdate() throws SQLException; O método executeQuery deve ser utilizado nos casos em que o stored procedure contem um único comando SELECT. De igual modo o método executeUpdate também deve ser executado quando o stored procedure contem um único comando SQL de actualização, i.e. um comando que não seja uma selecção. O método execute deve ser invocado quando o stored procedure contem múltiplos comandos SQL. Consideremos então o stored procedure spSaldos, apresentado anteriormente. O seguinte excerto de código ilustra a sua execução. Assume-se que a variável con, do tipo Connection, 56 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI representa uma ligação activa à base de dados. Uma vez que o stored procedure spSaldos contem um único comando SQL do tipo UPDATE, iremos utilizar o método executeUpdate para a sua execução. CallableStatement callSt; try { callSt = con.prepareCall("{ call spSaldos }"); n = callSt.executeUpdate(); System.out.println("actualizações = " + n); } catch (SQLException e) { System.out.println(e.getMessage()); } O próximo exemplo é baseado no stored procedure spInfoMarca definido acima. Este stored procedure é parametrizado e contem dois comandos SELECT. Uma vez que estamos perante múltiplos comandos SQL, iremos utilizar o método execute para a execução do stored procedure. O resultado deste método é um booleano que nos indica qual o tipo de comando que foi executado em primeiro lugar: true para comandos SELECT; false para todos os outros comandos. Os métodos para aceder aos resultados dos comandos SQL são os mesmos que foram apresentados quando se discutiu a forma de aceder a resultados utilizando comandos o método execute para comandos SQL simples, a saber: ResultSet getResultSet() throws SQLException; int getUpdateCount() throws SQLException; O método getResultSet deve ser utilizado para comandos SELECT, enquanto que o método getUpdateCount é indicado para todos os outros comandos. 57 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O excerto de código que se segue ilustra a execução do stored procedure spInfoMarca e a obtenção do resultado relativo ao primeiro comando. Assume-se que a variável con representa uma ligação activa à base de dados. CallableStatement callSt; ResultSet rs; try { callSt = con.prepareCall("{call spInfoMarca(?)}"); callSt.setString(1,"ASUS"); boolean b = callSt.execute(); rs = callSt.getResultSet(); // apresentação de resultados displayRS(rs); } catch (SQLException e) { System.out.println(e.getMessage()); } Uma vez que sabemos que o stored procedure contem dois comandos SELECT não é necessário verificar o valor devolvido pelo método execute. Note-se a presença do símbolo '?' na invocação ao stored procedure. Tal como nos comandos parametrizados, a presença de um '?' indica um parâmetro. O acesso aos resultados seguintes é obtido iterativamente através do método getMoreResults definido na interface Statement. A sintaxe deste método é agora apresentada: boolean getMoreResults() throws SQLException; O resultado deste método indica-nos o tipo de comando SQL: true para comandos SELECT; false para todos os outros comandos, ou caso não existam mais comandos. Note-se que quando o resultado é false a situação é ambígua. No entanto no nosso exemplo, sabemos que temos dois comandos SELECT e portanto sabemos à priori qual o número de resultados esperados. Iremos ver mais à frente como desambiguar a situação num caso genérico. Por enquanto vamos continuar com o nosso exemplo anterior e apresentar a forma de obter o segundo ResultSet. 58 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI CallableStatement callSt; ResultSet rs; try { callSt = con.prepareCall("{call spInfoMarca(?)}"); callSt.setString(1,"ASUS"); boolean b = callSt.execute(); rs = callSt.getResultSet(); displayRS(rs); b = callSt.getMoreResults(); rs = callSt.getResultSet(); displayRS(rs); } catch (SQLException e) { System.out.println(e.getMessage()); } Vamos agora analisar um caso genérico, queremos executar um stored procedure de nome spXpto, em relação ao qual desconhecemos o tipo e número de comandos SQL. Como foi mencionado anteriormente, caso o resultado de getMoreResults seja false, ou estamos perante um comando de actualização, ou terminou a lista de resultados do stored procedure. Para desambiguar esta situação é necessário recordar qual o resultado do método getUpdateCount. Este método devolve um número maior ou igual a zero caso o comando SQL seja um comando de actualização. Em todas as outras situações o resultado é -1. A forma de desambiguar esta situação pode agora ser resumida na seguinte expressão. A lista de resultados terminou se: b = callSt.getMoreResults(); (!b && (callSt.getUpdateCount() == -1) O seguinte excerto de código ilustra uma forma de iterar nos resultados obtidos para stored procedures com múltiplos comandos SQL. CallableStatement callSt; 59 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI ResultSet rs; int cont; try { callSt = con.prepareCall("{call spXpto }"); boolean b = callSt.execute(); do { if (b) { // comandos SELECT rs = callSt.getResultSet(); displayRS(rs); } else { // outros comandos cont = callSt.getUpdateCount(); System.out.println("actualizações : " + cont); } b = callSt.getMoreResults(); } while ( b || callSt.getUpdateCount() != -1); } catch (SQLException e) { System.out.println(e.getMessage()); } STO R ED PR O C ED U R ES EM M IC R O SO FT A C C ESS Na base de dados Microsoft Access a invocação de um stored procedure equivale à execução de uma consulta (query) definida no ambiente da aplicação Access. 60 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Figura 2 - Consultas definidas no ambiente Access. Como se pode observar, na base de dados encontra-se já definida uma consulta. O código SQL relativo à consulta porMarca é o seguinte: SELECT modelo, preço FROM Produtos WHERE marca=m; Note-se que no código SQL aparece um parâmetro m. No ambiente Access, todas as referências que não correspondam a colunas das tabelas definidas na base de dados são automaticamente consideradas como parâmetros. O código para invocar as consultas definidas no ambiente Access é equivalente ao apresentado para o caso geral de um stored procedure numa base de dados. O código que se segue poderia ser utilizado para invocar, e apresentar a consulta porMarca. Considera-se que a variável con representa uma ligação activa à base de dados. static ResultSet rs; static CallableStatement callSt; try { 61 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI callSt = con.prepareCall("{call porMarca(?)}"); callSt.setString(1,"ASUS"); rs = callSt.executeQuery(); displayRS(rs); } catch (SQLException e) { System.out.println(e.getMessage()); 3.4 E N VIO D E C O M A N D O S EM B A TC H Até agora foram analisadas formas de enviar à base de dados comandos SQL, ou executar stored procedures. O processo descrito até agora implica a submissão individual de cada um destes pedidos. Tendo em vista a melhor performance possível, a API JDBC 2.0 permite-nos enviar uma sequência de comandos em simultâneo. Desta forma a comunicação entre o driver e a base de dados é reduzida e, teoricamente, é obtido um ganho de performance. Ao falar de performance, é sempre preciso lembrar que a implementação dos drivers é um dos factores a ter em conta. Para algumas bases de dados existem literalmente dezenas de drivers, cujo nível de eficiência e totalidade de implementação varia significativamente. A base de dados também é um factor importante. Por muito eficiente que um driver seja, está sempre limitado pela performance da própria base de dados. As seguintes interfaces suportam o envio de comandos em batch: • Statement 62 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI • PreparedStatement • CallableStatement A funcionalidade no modo batch está restrita a comandos de actualização, ou seja, todos os comandos SQL, excepto comandos SELECT. Em termos de stored procedures a restrição aplicase ao tipo de comandos, e também ao seu número. Um stored procedure só pode ter um único comando SQL, e este não pode ser um comando SELECT. As interfaces Statement, PreparedStatement e CallableStatement definem os métodos necessários para adicionar comandos SQL, ou referências a stored procedures a uma sequência. Para comandos totalmente especificados, i.e. sem parâmetros, e não pré-compilados utiliza-se o seguinte método definido na interface Statement: void addBatch(String sql) throws SQLException; Para comandos SQL, ou stored procedures parametrizados utiliza-se o seguinte método definido na interface PreparedStatement: void addBatch() throws SQLException; Para executar a sequência de comandos invoca-se o método executeBatch definido na interface Statement. A sintaxe para este método é: int[] executeBatch() throws SQLException; Conforme especificado na assinatura este método lança uma excepção do tipo SQLException caso ocorra algum problema na ligação à base de dados, ou o driver não suporte . Este método pode ainda lançar a excepção BatchUpdateException, subclasse de SQLException, caso a execução de algum dos comandos SQL falhe. Neste caso cabe ao driver decidir se continua ou não a executar os restantes comandos da sequência. O resultado devolvido por este método é um array de inteiros, em que o valor de cada posição do array indica o número de linhas afectadas pelo comando SQL respectivo. Note-se que a ordem 63 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI dos comandos é a ordem pela qual são adicionados à sequência. Os valores possíveis para cada posição do array são: • um número maior ou igual a zero para comandos executados sem problemas e cujo número de linhas afectadas é conhecido • -2 caso o comando seja executado sem problemas mas o driver não consiga descobrir qual o número de linhas afectadas • -3 para os comandos cuja execução falhou. O seguinte excerto de código apresenta a construção de uma sequência de comandos simples, ou seja sem parâmetros e sem pré-compilação: try { Statement st = con.createStatement(); String sql = "UPDATE Produtos " + "SET promoção='S' WHERE marca='ASUS'"; st.addBatch(sql); sql= "UPDATE Produtos " + "SET preço=preço*0.7 WHERE marca='Toshiba'"; st.addBatch(sql); sql= "INSERT INTO Fornecedores " + "VALUES ('Toshiba','Qwerty')"; st.addBatch(sql); int res[] = st.executeBatch(); // Mostrar resultados for (int i=0; i < res.length; i++) { System.out.println("O comando " + i + " afectou " + res[i] + " linhas"); } } catch (BatchUpdateException e) { System.out.println(e.getMessage()); } catch (SQLException e) { System.out.println(e.getMessage()); } 64 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Recordemos o conteúdo da tabelas Produtos. Tabela Produtos código marca modelo categoria preço promoção 01 ASUS V8100 De Luxe Placa Gráfica 547 N 02 ASUS NoteBook L8400C Portátil 1995 S 03 Toshiba Satelite 3100 Portátil 1950 N 04 Creative Audigy 5.1 OEM Placa Audio 103 S 05 Compaq IPaq Pocket PC PDA 745 N 06 IBM DeskStar 60GB Disco IDE 283 N O resultado do código acima apresentado seria: O comando 0 afectou 2 linhas O comando 1 afectou 1 linhas O comando 2 afectou 1 linhas O resultado do último comando será sempre 1 caso a execução seja bem sucedida, uma vez que consiste na inserção de uma linha. Caso a linha a inserir introduzisse uma chave repetida seria lançada uma excepção do tipo BatchUpdateException e o resultado seria -3. No caso de comandos SQL ou stored procedures parametrizados, as sequências tem de ser constituídas pelo mesmo comando, instanciado múltiplas vezes. Caso se pretenda utilizar comandos variados a única opção é utilizar a interface Statement para criar os comandos, ou seja comandos não parametrizados e sem pré-compilação. O excerto de código que se segue apresenta um exemplo utilizando comandos parametrizados. try { String sql = "INSERT INTO Fornecedores " + "VALUES (?,?) "; PreparedStatement preparedSt; preparedSt = con.prepareStatement(sql); 65 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI preparedSt.setString(1,"ASUS"); preparedSt.setString(2,"BomBit"); preparedSt.addBatch(); preparedSt.setString(1,"Creative"); preparedSt.setString(2,"ChipChip"); preparedSt.addBatch(); preparedSt.setString(1,"IBM"); preparedSt.setString(2,"BitChip"); preparedSt.addBatch(); preparedSt.setString(1,"Toshiba"); preparedSt.setString(2,"MegaChip"); preparedSt.addBatch(); preparedSt.setString(1,"Compaq"); preparedSt.setString(2,"AGPBit"); preparedSt.addBatch(); preparedSt.executeBatch(); } catch (BatchUpdateException e) { System.out.println(e.getMessage()); } catch (SQLException e) { System.out.println(e.getMessage()); } Como podemos observar no código apresentado, o comando é instanciado múltiplas vezes e cada instância é adicionada à sequência. Para finalizar esta secção apresenta-se de seguida o método clearBatch, definido na interface Statement. O seu propósito é eliminar os comandos previamente inseridos numa sequência com o método addBatch. A sintaxe para este comando é a seguinte: void clearBatch() throws SQLException; 66 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 4 M A N IPU LA Ç Ã O A VA N Ç A D A D E R ESU LTA D O S D E C O M A N D O S SELEC T A execução de um comando SQL SELECT retorna como resultado um ResultSet. Como vimos anteriormente, por omissão, um ResultSet só permite a navegação sequencial e unidirecional, ou seja a única operação de navegação permitida é avançar o cursor para a linha seguinte. Para se obter um ResultSet, resultado da execução de um comando SELECT, é necessário primeiro criar um objecto do tipo Statement. O mesmo se aplica a objectos do tipo PreparedStatement e CallableStatemet. Como estas interfaces são sub-interfaces de Statement, um objecto de qualquer um destes últimos tipos também é um Statement. Sendo assim, ao mencionar que um objecto é do tipo Statement, está-se implicitamente a considerar também que o objecto pode ser um PreparedStatement ou um CallableStatement. Nos capítulos anteriores foram exploradas diversas alternativas para executar um comando SELECT numa base de dados. Os métodos utilizados para criar um Statement foram os seguintes: Statement createStatement() throws SQLException; PreparedStatement prepareStatement() throws SQLException; CallableStatement prepareCall() throws SQLException; Como foi mencionado anteriormente, ao criar objectos do tipo Statement com estes métodos, o ResultSet resultante da execução do respectivo comando só permite a navegação sequencial e unidirecional. No entanto, a API JDBC permite criar um ResultSet de acesso aleatório, i.e. com acesso directo a qualquer linha, e por consequência com navegação bidirecional. Neste caso, o cursor do 67 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI ResultSet pode-se mover livremente em qualquer direcção.De facto, até é possível utilizar um ResultSet para alterar os dados presentes na base de dados, eliminando parcialmente a necessidade de executar comandos SQL de actualização de dados. Para se obter um ResultSet com este nível de versatilidade utilizam-se os métodos para criação de Statements da interface Connection, homónimos aos anteriormente apresentados, desta vez com parâmetros. Apresentam-se em seguida os métodos na sua versão com parâmetros: Statement createStatement(int tipo, int concorrência) throws SQLException; PreparedStatement prepareStatement(String sql, int tipo, int concorrência) throws SQLException; CallableStatement prepareCall(String sp, int tipo, int concorrência) throws SQLException; O parâmetro tipo pode ter os seguintes valores: • TYPE_FORWARD_ONLY • TYPE_SCROLL_INSENSITIVE • TYPE_SCROLL_SENSITIVE As constantes apresentadas encontram-se definidas na interface ResultSet. O valor TYPE_FORWARD_ONLY implica que o ResultSet resultante da execução do comando SQL é acessível somente de forma sequencial e unidirecional. Quando um ResultSet é criado com o valor TYPE_SCROLL_INSENSITIVE ou TYPE_SCROLL_SENSITIVE obtemos um ResultSet com acesso aleatório, e por consequência bidirecional. A diferença entre estes dois últimos valores reside na forma como é encarado o problema do acesso concorrente à base de dados. No primeiro caso o ResultSet não é sensível à alterações na base de dados sobre dados que se encontram replicados no ResultSet. Ou seja, se um 68 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI ResultSet for resultado de um comando SELECT, e posteriormente os dados seleccionados forem alterados na base de dados, o ResultSet não irá reflectir estas alterações. No segundo caso o ResultSet pode ser "refrescado" de forma a reflectir alterações realizadas por terceiros. O segundo parâmetro dos métodos apresentados, define se o ResultSet pode ser utilizado para actualizar directamente os valores na base de dados. Os valores possíveis para este parâmetro são: • CONCUR_READ_ONLY • CONCUR_UPDATABLE As constantes acima apresentadas encontram-se definidas na interface ResultSet. Caso um Statement seja criado com o parâmetro CONCUR_UPDATABLE, torna-se possível utilizar o ResultSet resultante para actualizar os valores na base de dados directamente sem ser necessário recorrer a comandos SQL INSERT, DELETE, ou UPDATE. Desta forma, as actualizações registadas num ResultSet são automaticamente replicadas na base de dados. Se um Statement for criado com o parâmetro CONCUR_READ_ONLY, só é possível realizar operações de leitura no ResultSet. A utilização dos métodos apresentados sem parâmetros, tal como apresentados nos capítulos anteriores é equivalente a utilizar os seguintes valores para os parâmetros: • tipo = TYPE_FORWARD_ONLY • concorrência = CONCUR_READ_ONLY Por exemplo, a criação de um Statement utilizando o método sem parâmetros, st = con.createStatement(); 69 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI é equivalente a st = con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); sendo con uma Connection e st um Statement. Nas secções seguintes será apresentada a forma de navegação aleatória e actualização de um ResultSet e por consequência a actualização dos valores presentes na base de dados. 4.1 R ESU LTA D O S N A VEG Á VEIS A LEA TO R IA M EN TE Como foi mencionado anteriormente, a forma de navegação de um ResultSet é definida na altura da criação do objecto tipo Statement. Caso o valor especificado para o parâmetro tipo seja ResultSet.TYPE_SCROLL_INSENSITIVE ou ResultSet.TYPE_SCROLL_SENSITIVE o ResultSet resultante da execução de um comando SELECT é navegável de forma aleatória. Um ResultSet nestas condições contem um cursor, apontador para a linha actual do resultado, que pode ser movido livremente em ambas as direcções. A API JDBC permite que a navegação seja realizada de forma absoluta ou relativa. A excepção SQLException será lançada se o tipo do ResultSet for TYPE_FORWARD_ONLY. N A VEG A Ç Ã O R ELA TIVA Dentro de um ResultSet é possível avançar ou recuar o cursor uma linha através dos seguintes métodos: boolean next() throws SQLException; boolean previous() throws SQLException; 70 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O resultado destes métodos será true se o cursor não ficar posicionado para alem da última linha, ou antes da primeira linha, respectivamente. A API JDBC fornece ainda um método de deslocamento relativo em relação à linha actual. boolean relative(int deslocamento) throws SQLException; Um valor negativo desloca o cursor para trás, enquanto que um valor positivo avança o cursor. Se o deslocamento exceder os limites do ResultSet, o cursor ficará posicionado antes da primeira linha, para valores negativos, ou depois da última linha, para valores positivos. Neste caso o valor devolvido por este método será false. A invocação deste método com o valor zero não surte qualquer efeito. O método relative lança a excepção SQLException caso o cursor não se encontre numa linha do ResultSet, ou seja caso se encontre antes da primeira linha ou depois da última. N A VEG A Ç Ã O A BSO LU TA O posicionamento do cursor de um ResultSet pode ser feito através de instruções de posicionamento absoluto. A API JDBC fornece um método para posicionar o cursor na primeira ou ultima linha: boolean first() throws SQLException; boolean last() throws SQLException; O resultado destes método é true, excepto no caso particular em que o ResultSet seja vazio, i.e. não contenha nenhuma linha, tendo neste caso o valor false. Também é possível posicionar o cursor directamente numa linha através do seu índice: boolean absolute(int linha) throws SQLException; Este método aceita valores negativos como parâmetro, sendo o deslocamento efectuado a partir da última linha do ResultSet. Por exemplo absolute(-1) é equivalente a last(). O valor zero não é considerado um parâmetro válido para este método, sendo neste caso lançada uma 71 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI excepção do tipo SQLException. Caso o valor exceda o número de linhas do ResultSet, o cursor ficará posicionado a seguir à última linha. Para o método absolute o valor devolvido será true se o índice corresponder a uma linha do ResultSet. Os métodos que se seguem permitem colocar o cursor antes da primeira linha, ou depois da última, respectivamente. void afterLast() throws SQLException; void beforeFirst() throws SQLException; Estes métodos são úteis no caso em que se pretende iterar sobre todos as linhas do ResultSet. E X EM PLO S O seguinte excerto de código itera sobre todas as linhas de um ResultSet desde a primeira até à última. A variável con representa uma ligação activa à base de dados. try { sql = "SELECT marca, modelo" + "FROM Produtos" + "WHERE categoria = 'Portátil' " + "AND promoção = 'S'"; st = con.createStatement( ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); rs = st.executeQuery(sql); rs.beforeFirst(); while (rs.next()) { // apresentar linha actual } } catch (SQLException e) { System.out.println(e.getMessage()); } 72 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O mesmo efeito pode ser conseguido através do seguinte excerto de código que utiliza a método relative: try { sql = "SELECT marca, modelo" + "FROM Produtos" + "WHERE categoria = 'Portátil' " + "AND promoção = 'S'"; st = con.createStatement( ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); rs = st.executeQuery(sql); if (rs.first()) while (rs.relative(1)) { // apresentar linha actual } } catch (SQLException e) { System.out.println(e.getMessage()); } Embora mais complicado, também é possível iterar sobre as linhas de um ResultSet utilizando o método absolute, como é ilustrado no seguinte excerto de código: try { sql = "SELECT marca, modelo" + "FROM Produtos" + "WHERE categoria = 'Portátil' " + "AND promoção = 'S'"; st = con.createStatement( ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); rs = st.executeQuery(sql); i = 1; while (rs.absolute(i)) { // apresentar linha actual i++; } } catch (SQLException e) { System.out.println(e.getMessage()); } 73 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI Nota: Utilizando o driver JDBC-ODBC em ligações às bases de dados SQLServer e Access, o método absolute não consegue chegar à ultima linha. Neste caso o excerto de código acima apresentado imprime desde a primeira até à penúltima linha. Caso se pretenda, pode-se ainda apresentar as linhas pela ordem inversa à presente no ResultSet: try { sql = "SELECT marca, modelo" + "FROM Produtos" + "WHERE categoria = 'Portátil' " + "AND promoção = 'S'"; st = con.createStatement( ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY); rs = st.executeQuery(sql); rs.afterLast(); while (rs.previous()) { // apresentar linha actual } } catch (SQLException e) { System.out.println(e.getMessage()); } PO SIÇ Ã O A C TU A L D O C U R SO R A API JDBC fornece ainda um conjunto de métodos para recolher informação sobre a posição actual do cursor. Entre estes encontram-se os métodos para indagar se o cursor se encontra antes da primeira linha, ou depois da última, respectivamente. boolean isBeforeFirst() throws SQLException; boolean isAfterLast() throws SQLException; 74 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI O par de métodos que se segue fornece indicação sobre se o cursor se encontra na primeira ou última linha, respectivamente: boolean isFirst() throws SQLException; boolean isLast() throws SQLException; Finalmente, o método getRow indica em que linha o cursor se encontra. int getRow() throws SQLException; Caso o cursor esteja posicionado antes da primeira linha, ou depois da última, o resultado é zero. 75 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 5 T R A N SA C Ç Õ ES Por omissão, quando um comando que altere a estrutura da base de dados ou os dados contidos em tabelas é enviado à base de dados, esta é actualizada automaticamente e as alterações realizadas são definitivas. Neste caso a única forma de retroceder a base de dados ao estado anterior ao envio do comando consiste em enviar um ou mais comandos que realizem a operação inversa. Este tipo de procedimento nem sempre é conveniente. Por vezes é necessário garantir que as alterações realizadas só são registadas definitivamente caso todo um grupo de comandos SQL seja executado com sucesso. Caso ocorra um erro num dos comandos SQL, os efeitos dos comandos do mesmo grupo anteriormente executados devem então ser anulados, i.e. o estado da base de dados retorna ao estado anterior à execução do primeiro comando do grupo. As transacções são uma solução elegante para este problema, permitindo agrupar uma sequência de alterações. As alterações provocadas por comandos SQL dentro de uma transacção só são tornados definitivos quando a transacção é encerrada. Enquanto a transacção se encontrar aberta todas as alterações são provisórias. Ao terminar a transacção a sequência de alterações realizadas é registada na base de dados como definitiva. Por omissão, em JDBC uma transacção é constituída por um único comando SQL. Diz-se que a ligação à base de dados está em modo auto-commit, ou seja cada alteração individual é imediatamente registada na base de dados definitivamente. A interface Connection define um método para desactivar este modo, permitindo assim a especificação de transacções com sequências de comandos SQL. A sua assinatura é agora apresentada: void setAutoCommit(boolean commit) throws SQLException; 76 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI A invocação deste método com o parâmetro false, para além de desactivar o modo autocommit, inicia também uma transacção. A interface Connection define ainda um método para inquirir sobre o modo actual. A sua assinatura é: boolean getAutoCommit() throws SQLException; Quando a ligação não se encontra no modo auto-commit, as alterações realizadas são provisórias podendo a qualquer momento a aplicação pedir à base de dados que retorne ao estado anterior ao inicio da transacção. Sendo assim será necessário indicar à base de dados quando se pretende terminar a transacção, e, por consequência, registar definitivamente as alterações realizadas. A interface Connection define um método para este fim, cuja assinatura se apresenta em seguida: void commit() throws SQLException; A invocação deste método tem um efeito duplo: termina a transacção actual, e inicia uma nova transacção. Como foi mencionado anteriormente as alterações provocadas durante uma transacção são provisórias até ao término da transacção, i.e. antes de se terminar uma transacção é sempre possível pedir à base de dados que retorne ao estado imediatamente anterior ao inicio da transacção. Esta operação denomina-se por rollback. A interface Connection define o seguinte método para este fim: void rollback(); Apresenta-se em seguida um exemplo em que se pretende realizar uma transacção com uma sequência de comandos SQL parametrizados. O objectivo é registar as alterações na base de dados como definitivas só se todas as alterações forem realizadas com sucesso, i.e. se nenhum dos comandos SQL envolvidos lançar uma excepção. 77 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI try{ // desactivação do modo auto-commit con.setAutoCommit(false); // criação de um comando précompilado parametrizável PreparedStatement s = con.prepareStatement(“UPDADE Produtos "+ "SET preço = ? WHERE código = ?”); // execução da primeira instância do comando s.setFloat(1,1000.0); s.setString(2,"01"); s.executeUpdate(); // execução da segunda instância do comando s.setFloat(1,2000.0); s.setString(2,"02"); s.executeUpdate(); // se tudo correr bem tornar as alterações definitivas con.commit(); } // em caso de erro // anular quaisquer alterações já efectuadas catch (SQLException e) { try { con.rollback(); } catch (SQLException e) { System.out.println(e.getMessage()); } } 78 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI As transacções também podem ser utilizadas ao enviar sequências de comandos SQL em modo batch. try{ // desactivação do modo auto-commit con.setAutoCommit(false); // criação de um comando précompilado parametrizável PreparedStatement s = con.prepareStatement(“UPDADE Produtos "+ "SET preço = ? WHERE código = ?”); // adicionar à sequência a primeira instância do comando s.setFloat(1,1000.0); s.setString(2,"01"); s.addBatch(); // adicionar à sequência a segunda instância do comando s.setFloat(1,2000.0); s.setString(2,"02"); s.addBatch(); // envio da sequência de comandos à base de dados s.executeBatch(); // se tudo correr bem tornar as alterações definitivas con.commit(); } // em caso de erro // anular quaisquer alterações já efectuadas catch (SQLException e) { try { con.rollback(); } catch (SQLException e) { System.out.println(e.getMessage()); } } É importante realçar que após a execução do método commit as alterações são tornadas definitivas, e que o método rollback não pode anular os comandos SQL executados antes da invocação do método commit. O exemplo seguinte assume a existência de dois grupos de comandos SQL e ilustra uma situação em que o método commit é invocado após a execução de cada grupo. 79 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI try{ // desactivação do modo auto-commit con.setAutoCommit(false) // colocar aqui a execução do primeiro grupo de comandos ..... // se tudo correr bem tornar as alterações definitivas con.commit(); // colocar aqui a execução do segundo grupo de comandos ..... // se tudo correr bem tornar as alterações definitivas con.commit(); } // em caso de erro anular // as alterações desde o último commit catch (SQLException e) { try { con.rollback(); } catch (SQLException e) { System.out.println(e.getMessage()); } } No exemplo acima, caso ocorra uma excepção no segundo grupo de comandos o método rollback é executado. Uma vez que as alterações realizadas pelo primeiro grupo de comandos já foram tornadas definitivas pelo método commit, o método rollback só anula as alterações realizadas pelos métodos executados por comandos do segundo grupo anteriores ao comando em que ocorreu o erro. 80 JDBC – APONTAMENTOS PEDAGÓGICOS PARA DSI 6 R EFER ÊN C IA S Seth White, Maydene Fisher, Rick Cattell, Graham Hamilton, Mark Hapner, JDBC(TM) API Tutorial and Reference: Universal Data Access for the Java(TM) 2 Platform (2nd Edition), Addison-Wesley, 1999. Maydene Fisher, The JDBC 2.0 Optional Package, http://java.sun.com/products/jdbc/articles/package2.html. 81
Documentos relacionados
JDBC – Trabalhando com banco de dados em Java
banco de dados. A classe Driver proporciona à JDBC um ponto de partida para a conectividade de banco de dados respondendo às requisições de conexão de DriverManager e fornecendo informações sobre a...
Leia maisAcesso a Banco de Dados com JDBC
É mais utilizado para banco de dados antigos como estatais de governos. Aplicação JDBC Driver Manager
Leia mais