Do JDBC ao Spring Data (ou: é possível reduzir código?)
Tags: beginners, programming, java, orm
Published: 2022-03-28 23:54:06

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](https://www.youtube.com/watch?v=TX1NztPbvMw) @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](https://www.tutorialspoint.com/design_pattern/data_access_object_pattern.htm) 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:

```java
@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](https://github.com/rodrigovp/diferentes-daos). 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](https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html)_. 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](https://www.baeldung.com/hikaricp)_ 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](https://www.alura.com.br/conteudo/persistencia-jpa-introducao-hibernate) 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:

```java
@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](https://refactoring.guru/pt-br/design-patterns/facade) 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](https://docs.jboss.org/hibernate/orm/3.5/reference/pt-BR/html/session-configuration.html).

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](https://www.eclipse.org/eclipselink/)).

Por essas e outras [razões](https://www.amazon.com.br/Pro-EJB-Java-Persistence-API/dp/1590596455) 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":

```java
@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:

```java
@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](https://www.theserverside.com/news/2240186700/The-JPA-20-EntityManager-vs-the-Hibernate-Session-Which-one-to-use).

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](https://spring.io/projects/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:

```java
@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!