Do JDBC ao Spring Data (ou: é possível reduzir código?)
Rodrigo Vieira
Posted on March 28, 2022
Há tempos que venho querendo escrever este post. Mais precisamente, desde quando comentei sobre o tema deste artigo numa live sobre o livro de spring boot, escrito pelo @boaglio
Bem, finalmente saiu :D
No vídeo, cito que o Spring Data é uma das ferramentas mais interessantes do Mundo Java, dada a sua facilidade de utilização. Mas, outra coisa que me também me chama a atenção é a quantidade de código que deixamos de escrever quando usamos esse framework.
Isso me fez lembrar como criávamos nossos DAOs no passado. E acho que vale a pena falar um pouco sobre isso aqui. Divirtam-se! ;)
No princípio, era o JDBC
Java Database Connectivity foi o primeiro mecanismo que conheci para conectar com um banco de dados e enviar instruções SQL para ele (insert
, select
, etc).
Mais do que isso, é uma API que nos permite conectar com qualquer banco de dados, bastando que tenhamos apenas o driver do banco em mãos.
Isso gerou diversas vantagens. Entre elas, o fato de podermos usar as mesmas classes/interfaces para que nossos sistemas se conectassem em diversos bancos de dados relacionais.
Bem, isso não significa que nosso código era simples, muito menos de fácil manutenção, como podemos ver na classe abaixo. Ela representa um DAO de usuários, com todas as operações básicas que podemos fazer num banco:
@Repository
public class RepositorioUsuariosJdbc implements RepositorioUsuarios {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String usuario;
@Value("${spring.datasource.password}")
private String senha;
private Connection conexao;
@PostConstruct
void posConstrutor(){
try {
conexao = DriverManager.getConnection(url, usuario, senha);
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void save(Usuario umUsuario) {
if(!existe(umUsuario)) salvar(umUsuario);
else atualizar(umUsuario);
}
private boolean existe(Usuario umUsuario) {
return umUsuario.getId() != null;
}
private void atualizar(Usuario usuario) {
try(var statement = conexao.prepareStatement("update usuarios set nome = ? where id = ?")){
statement.setString(1, usuario.getNome());
statement.setLong(2, usuario.getId());
statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void deleteById(Long id) {
try(var statement = conexao.prepareStatement("delete from usuarios where id = ?")){
statement.setLong(1, id);
statement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public List<Usuario> findAll() {
var usuarios = new ArrayList<Usuario>();
try(var statement = conexao.prepareStatement("select * from usuarios");
var resultset = statement.executeQuery()){
while(resultset.next()){
var id = resultset.getLong("id");
var nome = resultset.getString("nome");
var dataNascimento = resultset.getObject("data_nascimento", LocalDate.class);
var usuario = new Usuario(nome, dataNascimento);
usuario.setId(id);
usuarios.add(usuario);
}
} catch (SQLException e) {
e.printStackTrace();
}
return usuarios;
}
@Override
public void close() {
try {
conexao.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
private boolean idEhNulo(Usuario umUsuario) {
return umUsuario.getId() == null;
}
private void salvar(Usuario usuario){
var sql = "insert into usuarios (nome, data_nascimento) values (?, ?)";
try(var statement = conexao.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)){
statement.setString(1, usuario.getNome());
statement.setObject(2, usuario.getDataNascimento());
statement.executeUpdate();
try(var ids = statement.getGeneratedKeys()){
ids.next();
usuario.setId(ids.getLong(1));
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
Escrevi este código num pequeno projeto, criado apenas para este artigo. Ele pode ser consultado aqui. E, para provar que esse código (e os demais que veremos a seguir) funciona, há testes automatizados no projeto.
Outras observações:
- o código foi compilado usando Java 17 (queria testar alguns recursos novos poxa :P)
- na época que usávamos o que é chamado hoje de "JDBC puro" (o código acima) não tínhamos recursos como o try-with-resources. Então, pode ter certeza de que o DAO acima poderia ser muito maior.
Também não tive a preocupação de controlar a conexão com o banco de dados (hoje, usamos connection pools e não temos que nos preocupar com isso), nem de capturar corretamente as exceções.
Sobre o DAO acima... bem, acho que não preciso dizer muita coisa. Tínhamos que nos preocupar com o objeto de conexão com o banco, mas mais que isso: haviam também os objetos ResultSet
e PreparedStatement
(ou Statement
, quando estávamos aprendendo a usar o JDBC. Depois que aprendíamos os problemas de uso do Statement
, partíamos para o PreparedStatement
). Esses objetos eram criados, e tinham que ser fechados quando não eram mais usados.
Também a construção de queries era trabalhosa, e altamente propensa a erros. O que pode acontecer quando adicionamos um parâmetro a mais no insert
do código acima, por exemplo?
Isso sem contar a quantidade imensa de código repetido.
Mas então veio o Hibernate, e nossa vida melhorou (bastante, eu diria):
Tabelas e objetos são (quase) a mesma coisa - Hibernate
Dizem que o Gavin King é uma pessoa muito parecida com o Linus Torwalds em termos de simpatia e carisma.
Se isso é verdade, não sei dizer. Mas posso afirmar sem medo de errar que ele foi o criador de uma das ferramentas mais importantes do Mundo Java. Ela foi criada em 2001 e até hoje usamos ela (a implementação de referência do Spring Data usa Hibernate por baixo dos panos).
Ele entendeu que, de certa forma, podíamos fazer um paralelo entre atributos de objetos e registros em tabelas, com o conjunto de atributos de um objeto sendo comparado a uma linha de uma tabela. Pode ser que seja algo meio óbvio hoje em dia, mas não sei se em 2001 isso era tão claro assim (alguns de nós nem eram nascidos em 2001 :D).
Utilizando o Hibernate, nosso código fica consideravelmente menor:
@Repository
public class RepositorioUsuariosHibernate implements RepositorioUsuarios {
@PersistenceContext
private EntityManager em;
private Session sessao;
@PostConstruct
void posConstrutor(){
sessao = em.unwrap(Session.class);
}
@Override
public void save(Usuario umUsuario) {
sessao.save(umUsuario);
}
@Override
public void deleteById(Long id) {
var usuario = sessao
.createQuery("from Usuario where id = %d".formatted(id))
.uniqueResult();
sessao.delete(usuario);
}
@Override
public List<Usuario> findAll() {
return sessao.createQuery("from Usuario").getResultList();
}
@Override
public void close() {
sessao.close();
}
}
Não temos mais o objeto Connection
nem mesmo os objetos PreparedStatement
ou o ResultSet
, mas apenas a Session
. Ela era como um Façade para o banco. Toda vez que precisávamos fazer algo nele, era só usar a Session
.
OBS: para obter a Session
, usei o EntityManager
que veio com o JPA. Falarei sobre ele mais tarde.
Para obtermos a Session
do Hibernate, era necessário um pouco mais de código. Como o Hibernate utiliza o JDBC, também precisava da URL de conexão, usuário, senha e o dialeto do banco que se usaria no sistema. E essa Session
podia ser obtida via configuração programática ou por XML (o famoso hibernate.cfg.xml
). Uma boa referência sobre como esse processo era feito é essa aqui.
Isso resolveu boa parte dos nossos problemas. Entretanto, o Hibernate era uma solução muito boa. Tão boa que outros programadores/empresas também criaram suas ferramentas de mapeamento objeto-relacional (como o TopLink, que depois virou EclipseLink).
Por essas e outras razões foi criada a especificação JPA.
A unificação de diversas ferramentas de ORM: Java Persistence API
Com a criação da JPA, ferramentas como o EclipseLink e o Hibernate passaram a compartilhar principalmente de diversas anotações que serviam na maioria das vezes para realizar o mapeamento de objetos para tabelas e vice-versa.
Essas anotações deviam ser inseridas na classe que se pretendia persistir. Abaixo, a classe Usuario
devidamente "anotada":
@Entity
@Table(name = "usuarios")
@DynamicUpdate
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode(of = {"nome", "dataNascimento"})
@Getter
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Setter
private Long id;
private String nome;
private LocalDate dataNascimento;
public Usuario(String nome, LocalDate dataNascimento) {
this.nome = nome;
this.dataNascimento = dataNascimento;
}
public void atualizarNome(String novoNome) {
this.nome = novoNome;
}
}
OBS: essa classe está sendo usada em todos os DAOs deste artigo. Logo, pode haver código necessário para um DAO e desnecessário para outro (por exemplo, os getters e setters são necessários para o DAO JDBC, mas completamente dispensáveis para os demais DAOs)
Abaixo, podemos ver o mesmo DAO, implementado usando o Hibernate segundo a especificação JPA:
@Repository
class RepositorioUsuariosJpa implements RepositorioUsuarios {
@PersistenceContext
private EntityManager em;
@Override
public void save(Usuario umUsuario){
em.persist(umUsuario);
em.flush();
}
@Override
public void deleteById(Long id){
Usuario usuario = em.find(Usuario.class, id);
em.remove(usuario);
}
@Override
public List<Usuario> findAll(){
return em.createQuery("from Usuario").getResultList();
}
@Override
public void close() {
em.close();
}
}
A priori não há praticamente nenhuma diferença. Agora usamos esse tal de EntityManager
no lugar da Session
. De fato, olhando somente o código, pouca coisa mudou.
O EntityManager
nos ajudou a lidar com a persistência de forma padronizada, pois veio com a especificação JPA. Para mais detalhes, veja esse artigo aqui.
De qualquer forma, ainda tínhamos que lidar com uma certa quantidade de código repetido. Pense que os DAOs precisavam receber de forma injetada o EntityManager
, e manipulá-lo em todos os DAOs.
Isso não era um grande problema (pra quem já tinha trabalhado com DAOs usando JDBC). Mas aí veio o pessoal da VMWare dizer pra gente que dava pra fazer melhor ainda. E, normalmente, eles estão certos.
O estado da arte - Spring Data
O Spring Data (mais especificamente o Spring Data JPA, embora todos da família seguem os mesmos princípios) é um framework que facilitou de vez a criação de DAOs. Basicamente não temos que escrever praticamente nada no DAO para ele funcionar.
Veja como fica nosso DAO com a utilização dele:
@Repository
public interface RepositorioUsuariosSpringDataJpa extends JpaRepository<Usuario, Long> {
}
Apenas uma interface que estende de outra do próprio framework
é suficiente. Nada mais!
Ok, tem também as anotações na classe Usuario
, mas você entendeu.
Para quem está começando agora com programação Java, se não pegar nenhum sistema legado para dar manutenção, provavelmente já começa com essa ferramenta. Talvez não veja o que ela traz de bom e suas vantagens. Então, se um dia você reclamar que ela não é boa por alguma razão...bem, já foi bem pior :D
Podem ter certeza que resumi bastante toda a história. Há muitos outros detalhes que valeriam a pena citar. Mas procurei focar no código, e o quanto ele foi reduzindo com o passar do tempo.
Perceberam o quanto evoluímos na escrita de nossos DAOs? Atualmente, quase não precisamos escrever eles. E DAOs já foram as classes que mais tinham código em diversos sistemas!
O que o futuro nos reserva? Será que um dia não precisaremos nem mais escrever essa interface? As anotações no código bastarão?
E você? Ainda acha que não é possível refatorar o código da sua aplicação?
Abraço!
Posted on March 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.