JDBC com Groovy
1. Introdução
Neste artigo, veremos como consultar bancos de dados relacionais comJDBC, usando Groovy idiomático.
O JDBC, embora seja de nível relativamente baixo, é a base da maioria dos ORMs e outras bibliotecas de acesso a dados de alto nível na JVM. E podemos usar o JDBC diretamente no Groovy, é claro; no entanto, ele possui uma API bastante complicada.
Felizmente para nós, a biblioteca padrão do Groovy baseia-se no JDBC para apresentar uma interface limpa, simples e poderosa. Então, vamos explorar o módulo Groovy SQL.
Vamos olhar para JDBC em Groovy simples, sem considerar qualquer estrutura como Spring, para a qual temosother guides.
2. Configuração JDBC e Groovy
Temos que incluir o módulogroovy-sql entre nossas dependências:
org.codehaus.groovy
groovy
2.4.13
org.codehaus.groovy
groovy-sql
2.4.13
Não é necessário listá-lo explicitamente se estivermos usando groovy-all:
org.codehaus.groovy
groovy-all
2.4.13
Podemos encontrar a versão mais recente degroovy, groovy-sqlegroovy-all no Maven Central.
3. Conectando ao banco de dados
A primeira coisa que precisamos fazer para trabalhar com o banco de dados é conectar-se a ele.
Vamos apresentar a classegroovy.sql.Sql, que usaremos para todas as operações no banco de dados com o módulo SQL Groovy.
Uma instância deSql representa um banco de dados no qual desejamos operar.
No entanto,an instance of Sqlisn’t a single database connection. Falaremos sobre as conexões mais tarde, não vamos nos preocupar com elas agora; vamos apenas assumir que tudo funciona magicamente.
3.1. Especificando Parâmetros de Conexão
Ao longo deste artigo, vamos usar um banco de dados HSQL, que é um banco de dados relacional leve que é usado principalmente em testes.
Uma conexão com o banco de dados precisa de uma URL, um driver e credenciais de acesso:
Map dbConnParams = [
url: 'jdbc:hsqldb:mem:testDB',
user: 'sa',
password: '',
driver: 'org.hsqldb.jdbc.JDBCDriver']
Aqui, escolhemos especificar aqueles usandoMap, embora não seja a única escolha possível.
Podemos então obter uma conexão da classeSql:
def sql = Sql.newInstance(dbConnParams)
Veremos como usá-lo nas seções a seguir.
Quando terminarmos, devemos sempre liberar quaisquer recursos associados:
sql.close()
3.2. Usando umDataSource
É comum, especialmente em programas executados dentro de um servidor de aplicativos, usar uma fonte de dados para conectar-se ao banco de dados.
Além disso, quando queremos agrupar conexões ou usar JNDI, uma fonte de dados é a opção mais natural.
A classeSql do Groovy aceita fontes de dados perfeitamente:
def sql = Sql.newInstance(datasource)
3.3. Gestão Automática de Recursos
Lembrar de chamarclose() quando terminarmos com uma instânciaSql é entediante; afinal, as máquinas se lembram das coisas muito melhor do que nós.
ComSql, podemos envolver nosso código em um encerramento e fazer o Groovy chamarclose() automaticamente quando o controle o deixar, mesmo em caso de exceções:
Sql.withInstance(dbConnParams) {
Sql sql -> haveFunWith(sql)
}
4. Emissão de declarações contra o banco de dados
Agora, podemos continuar com as coisas interessantes.
A maneira mais simples e não especializada de emitir uma declaração no banco de dados é o métodoexecute:
sql.execute "create table PROJECT (id integer not null, name varchar(50), url varchar(100))"
Em teoria, funciona tanto para instruções DDL / DML quanto para consultas; no entanto, o formulário simples acima não oferece uma maneira de recuperar os resultados da consulta. Vamos deixar dúvidas para depois.
O métodoexecute tem várias versões sobrecarregadas, mas, novamente, veremos os casos de uso mais avançados desse e de outros métodos em seções posteriores.
4.1. Inserindo dados
Para inserir dados em pequenas quantidades e em cenários simples, o métodoexecute discutido anteriormente é perfeitamente adequado.
No entanto, para os casos em que geramos colunas (por exemplo, com sequências ou incremento automático) e queremos saber os valores gerados, existe um método dedicado:executeInsert.
Quanto aexecute, veremos agora a sobrecarga de método mais simples disponível, deixando as variantes mais complexas para uma seção posterior.
Portanto, suponha que tenhamos uma tabela com uma chave primária de incremento automático (identidade na linguagem HSQLDB):
sql.execute "create table PROJECT (ID IDENTITY, NAME VARCHAR (50), URL VARCHAR (100))"
Vamos inserir uma linha na tabela e salvar o resultado em uma variável:
def ids = sql.executeInsert """
INSERT INTO PROJECT (NAME, URL) VALUES ('tutorials', 'github.com/eugenp/tutorials')
"""
executeInsert se comporta exatamente comoexecute, mas o que ele retorna?
Acontece que o valor de retorno é uma matriz: suas linhas são as linhas inseridas (lembre-se de que uma única instrução pode fazer com que várias linhas sejam inseridas) e suas colunas são os valores gerados.
Parece complicado, mas no nosso caso, que é de longe o mais comum, há uma única linha e um único valor gerado:
assertEquals(0, ids[0][0])
Uma inserção subsequente retornaria um valor gerado 1:
ids = sql.executeInsert """
INSERT INTO PROJECT (NAME, URL)
VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
"""
assertEquals(1, ids[0][0])
4.2. Atualização e exclusão de dados
Da mesma forma, existe um método dedicado para modificação e exclusão de dados:executeUpdate.
Novamente, isso difere deexecute apenas em seu valor de retorno, e vamos olhar apenas para sua forma mais simples.
O valor de retorno, nesse caso, é um número inteiro, o número de linhas afetadas:
def count = sql.executeUpdate("UPDATE PROJECT SET URL = 'https://' + URL")
assertEquals(2, count)
5. Consultando o banco de dados
As coisas começam a ficar Groovy quando consultamos o banco de dados.
Lidar com a classe JDBCResultSet não é exatamente divertido. Felizmente para nós, o Groovy oferece uma boa abstração sobre tudo isso.
5.1. Iterando sobre os resultados da consulta
Embora os loops sejam tão antigos ... nós todos gostamos de encerramentos hoje em dia.
E Groovy está aqui para se adequar ao nosso gosto:
sql.eachRow("SELECT * FROM PROJECT") { GroovyResultSet rs ->
haveFunWith(rs)
}
O métodoeachRow emite nossa consulta no banco de dados e chama um encerramento sobre cada linha.
Como podemos ver,a row is represented by an instance of GroovyResultSet, que é uma extensão do antigoResultSet com alguns itens adicionais. Leia para saber mais sobre isso.
5.2. Acessando conjuntos de resultados
Além de todos os métodosResultSet,GroovyResultSet oferece alguns utilitários convenientes.
Principalmente, expõe propriedades nomeadas que correspondem aos nomes das colunas:
sql.eachRow("SELECT * FROM PROJECT") { rs ->
assertNotNull(rs.name)
assertNotNull(rs.URL)
}
Observe como os nomes de propriedades não diferenciam maiúsculas de minúsculas.
GroovyResultSet também oferece acesso a colunas usando um índice baseado em zero:
sql.eachRow("SELECT * FROM PROJECT") { rs ->
assertNotNull(rs[0])
assertNotNull(rs[1])
assertNotNull(rs[2])
}
5.3. Paginação
Podemos facilmente paginar os resultados, ou seja, carregar apenas um subconjunto, começando de algum deslocamento até um número máximo de linhas. Essa é uma preocupação comum em aplicativos da web, por exemplo.
eachRow e métodos relacionados têm sobrecargas aceitando um deslocamento e um número máximo de linhas retornadas:
def offset = 1
def maxResults = 1
def rows = sql.rows('SELECT * FROM PROJECT ORDER BY NAME', offset, maxResults)
assertEquals(1, rows.size())
assertEquals('REST with Spring', rows[0].name)
Aqui, o métodorows retorna uma lista de linhas ao invés de iterar sobre elas comoeachRow.
6. Consultas e declarações parametrizadas
Mais frequentemente, consultas e instruções não são totalmente corrigidas no momento da compilação; eles geralmente têm uma parte estática e uma parte dinâmica, na forma de parâmetros.
Se você está pensando em concatenação de string, pare agora e leia sobre injeção de SQL!
Mencionamos anteriormente que os métodos que vimos nas seções anteriores têm muitas sobrecargas para vários cenários.
Vamos apresentar as sobrecargas que lidam com parâmetros em consultas e instruções SQL.
6.1. Strings with Placeholders
Em estilo semelhante ao JDBC simples, podemos usar parâmetros posicionais:
sql.execute(
'INSERT INTO PROJECT (NAME, URL) VALUES (?, ?)',
'tutorials', 'github.com/eugenp/tutorials')
ou podemos usar parâmetros nomeados com um mapa:
sql.execute(
'INSERT INTO PROJECT (NAME, URL) VALUES (:name, :url)',
[name: 'REST with Spring', url: 'github.com/eugenp/REST-With-Spring'])
Isso funciona paraexecute,executeUpdate,rows eeachRow. executeInsert também oferece suporte a parâmetros, mas sua assinatura é um pouco diferente e mais complicada.
6.2. Strings Groovy
Também podemos optar por um estilo Groovier usando GStrings com espaços reservados.
Todos os métodos que vimos não substituem marcadores de posição em GStrings da maneira usual; em vez disso, eles os inserem como parâmetros JDBC, garantindo que a sintaxe SQL seja preservada corretamente, sem a necessidade de citar ou escapar de nada e, portanto, sem risco de injeção.
Isso é perfeitamente bom, seguro e Groovy:
def name = 'REST with Spring'
def url = 'github.com/eugenp/REST-With-Spring'
sql.execute "INSERT INTO PROJECT (NAME, URL) VALUES (${name}, ${url})"
7. Transações e conexões
Até agora, ignoramos uma preocupação muito importante: as transações.
Na verdade, também não falamos sobre comoSql do Groovy gerencia as conexões.
7.1. Conexões de curta duração
Nos exemplos apresentados até agora,each and every query or statement was sent to the database using a new, dedicated connection.Sql fecha a conexão assim que a operação termina.
Claro, se estivermos usando um pool de conexão, o impacto no desempenho pode ser pequeno.
Ainda assim,if we want to issue multiple DML statements and queries as a single, atomic operation, precisamos de uma transação.
Além disso, para que uma transação seja possível em primeiro lugar, precisamos de uma conexão que abranja várias instruções e consultas.
7.2. Transações com uma conexão em cache
O Groovy SQL não permite criar ou acessar transações explicitamente.
Em vez disso, usamos o métodowithTransaction com um encerramento:
sql.withTransaction {
sql.execute """
INSERT INTO PROJECT (NAME, URL)
VALUES ('tutorials', 'github.com/eugenp/tutorials')
"""
sql.execute """
INSERT INTO PROJECT (NAME, URL)
VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
"""
}
Dentro do fechamento, uma única conexão com o banco de dados é usada para todas as consultas e instruções.
Além disso, a transação é confirmada automaticamente quando o fechamento é encerrado, a menos que saia antes devido a uma exceção.
No entanto, também podemos confirmar ou reverter manualmente a transação atual com métodos na classeSql:
sql.withTransaction {
sql.execute """
INSERT INTO PROJECT (NAME, URL)
VALUES ('tutorials', 'github.com/eugenp/tutorials')
"""
sql.commit()
sql.execute """
INSERT INTO PROJECT (NAME, URL)
VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
"""
sql.rollback()
}
7.3. Conexões em cache sem uma transação
Finalmente, para reutilizar uma conexão de banco de dados sem a semântica de transação descrita acima, usamoscacheConnection:
sql.cacheConnection {
sql.execute """
INSERT INTO PROJECT (NAME, URL)
VALUES ('tutorials', 'github.com/eugenp/tutorials')
"""
throw new Exception('This does not roll back')
}
8. Conclusões e leituras adicionais
Neste artigo, vimos o módulo Groovy SQL e como ele aprimora e simplifica o JDBC com fechamentos e strings Groovy.
Podemos concluir com segurança que o JDBC antigo parece um pouco mais moderno com uma pitada de Groovy!
Não falamos sobre todos os recursos do Groovy SQL; por exemplo, deixamos de forabatch processing, procedimentos armazenados, metadados e outras coisas.
Para obter mais informações, consultethe Groovy documentation.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emthe GitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.