As implementações mostradas nesse documento fazem uso da biblioteca sql2o.

Como os dados são acessados?

Mesmo usando bibliotecas como sql2o para facilitar a escrita de código de acesso ao banco de dados, ainda é necessário o uso de abstração para termos um código mais organizado e menos propenso a erros.


Ao criar telas de cadastro de livros, o desenvolvedor poderia fazer os seguintes questionamentos:

O desenvolvedor deveria precisar de tudo isso para proceder com o trabalho?

Data Access Object (DAO)

O padrão DAO é, provavelmente, o mais utilizado para acesso a bancos de dados em linguagens de programação orientada a objetos. Em português, significa Objeto de Acesso a Dados.

A ideia é oferecer uma ou mais classes com métodos amigáveis para adicionar, modificar e excluir dados, sem que o desenvolvedor precise saber detalhes do esquema físico dos dados.

Engana-se quem acha que isso é relevante somente para projetos onde existe uma equipe de desenvolvedores, cada um com uma responsabilidade. Mesmo em “euquipes”, com apenas um desenvolvedor faz-tudo, a aplicação de DAO é muito benéfica.

Código desorganizado 🙁

Observe, a seguir, o método doPost() de um servlet que tem como objetivo a inserção de livros no banco de dados:

protected void doPost(HttpServletRequest request,
                      HttpServletResponse response) throws IOException {
    // Parâmetros obtidos do usuário
    String paramTitulo = request.getParameter("titulo");
    String paramAutor  = request.getParameter("autor");
    int paramEdicao    = Integer.parseInt(request.getParameter("edicao"));

    // Query de inserção
    String query = "INSERT INTO Livros (titulo, autor, edicao) " +
                   "VALUES (:titulo, :autor, :edicao)";

    // Executa query e avalia a resposta
    try (Connection con = sql2o.open()) {
        con.createQuery(query)
            .addParameter("titulo", paramTitulo)
            .addParameter("autor", paramAutor)
            .addParameter("edicao", paramEdicao)
            .executeUpdate();

        // Número de linhas alteradas, se não é maior que 0, houve erros...
        int result = con.getResult();
        if (result > 0) {
            response.sendRedirect(request.getRequestURI() + "?adicionado=true");
        } else {
            response.sendError(400, "Erro inesperado ao tentar inserir");
        }
    }
}

Código mais organizado 😀

Agora veja o mesmo método com o acesso ao banco de dados abstraída:

protected void doPost(HttpServletRequest request,
                      HttpServletResponse response) throws IOException {
    // Parâmetros obtidos do usuário como atributos
    Livro livro = new Livro();
    livro.setTitulo(request.getParameter("titulo"));
    livro.setAutor(request.getParameter("autor"));
    livro.setEdicao(Integer.parseInt(request.getParameter("edicao")));

    // Objeto de acesso a dados
    LivroDao dao = new LivroDao();

    // Chama o método de adicionar livro
    boolean ok = dao.adicionar(livro);
    
    // Mostra mensagem de sucesso ou de falha da operação
    if (ok) {
        response.sendRedirect(request.getRequestURI() + "?adicionado=true");
    } else {
        response.sendError(400, "Erro inesperado ao tentar inserir");
    }
}

No código acima, a classe Livro é um POJO e a classe LivroDao mantém um código de acesso a dados que poderia ser implementada da seguinte forma:

public class LivroDao {

    public boolean adicionar(Livro livro) {
        String query = "INSERT INTO Livros (titulo, autor, edicao) " +
                       "VALUES (:titulo, :autor, :edicao)";

        try (Connection con = sql2o.open()) {
            con.createQuery(query)
                .addParameter("titulo", livro.getTitulo())
                .addParameter("autor", livro.getAutor())
                .addParameter("edicao", livro.getEdicao())
                .executeUpdate();

            // Número de linhas alteradas, se não é maior que 0, houve erros...
            return con.getResult() > 0;
        }
    }
    
    // Outros métodos, para obter, modificar, excluir livros, etc.
    ...
}

Com uma classe separada para acesso a dados, temos o código do servlet bem mais direto e objetivo, fazendo apenas o que é responsabilidade de um servlet.

Temos, assim, um mandamento para o servlet (ou camada de controller):

Não lidarás com o acesso ao banco de dados.

Isso, de uma forma geral, é o padrão DAO!

Sobre as classes DAO

O padrão DAO não estabelece qual deverá ser os nomes das classes de acesso a dados, bem como a quantidade de classes que um sistema deve ter.

Nomes das classes

É comum que as classes de acesso a dados tenham nomes como:

Ou seja, um nome relacionado ao negócio, tal como Livro sufixado por Dao, dando a entender que se trata de uma classe DAO. O uso desse esquema é tão comum que é recomendado o seguir.

Quantidade de classes

Imagine que temos um sistema de biblioteca. Quantas classes DAO eu devo ter?

Em geral, existem duas formas de pensar: a primeira seria ter apenas uma classe BibliotecaDao para obter, adicionar, modificar e excluir livros, autores, locatários, empréstimos, etc.

A segunda forma seria ter uma classe DAO para cada entidade do banco de dados:

Sobre os métodos DAO

Adotando a segunda forma, com classes para cada entidade, a classe LivroDao teria a seguinte API:

public class LivroDao {
    public Livro obter(int id) { ... }

    public List<Livro> obterTodos() { ... }
    
    public List<Livro> obterPorTitulo(String titulo) { ... }

    public boolean adicionar(Livro livro) { ... }

    public boolean modificar(Livro livro) { ... }

    public boolean excluir(Livro livro) { ... }
}

Métodos mais complexos

Supondo que a entidade Livro tenha um relacionamento com Autor, então poderia ser interessante a inclusão de outro método à classe DAO:

public class LivroDao {
    ...

    /**
     * Autores de um certo livro
     */
    public List<Autor> obterAutores(int livroId) { ... }
}

Perguntas e respostas

No último exemplo, o método obterAutores() não deveria estar na classe AutorDao?

O retorno do método é uma lista de autores, então poderíamos pensar que a classe AutorDao é o melhor lugar para o colocar. Mas o método está buscando não uma simples lista de autores, mas os autores de um livro em especial. O método obtém informação realmente útil para a entidade Livro. Sendo assim, o melhor lugar dele é realmente em LivroDao.

Imagine o contrário, eu quero obter a lista de livros de um certo autor. Entre as classes de acesso LivroDao e AutorDao qual é o melhor local para implementar esse método?

Toda classe DAO precisa ter métodos para obter, adicionar, modificar e excluir?

Não necessariamente, isso depende do que o software precisa fazer com os dados.

Imagine um sistema de loja em que o gerenciamento de pedidos seja realizada por um sistema de terceiros. Nesse caso, a classe PedidoDao só precisaria, possivelmente, os métodos de buscar.

Os nomes dos métodos importa?

Não, os nomes são normalmente baseados em padrões definidos pela equipe de desenvolvimento.

Por exemplo, a classe LivroDao poderia ter nomes em inglês para os métodos:

public Livro get(int id) { ... }

public List<Livro> getAll() { ... }

public List<Livro> getByTitulo(String titulo) { ... }

public boolean add(Livro livro) { ... }

public boolean update(Livro livro) { ... }

public boolean delete(Livro livro) { ... }

public List<Autor> getAutores(int livroId) { ... }

Fábrica de conexões

Quando se aplica o padrão DAO, é também comum utilizar um outro chamado de padrão factory.

O padrão factory estabelece que devemos ocultar os detalhes de construção de um objeto. No caso desse assunto, é importante que o objeto Connection seja construído sem precisar das informações de acesso ao banco de dados:


A fábrica de conexões é normalmente acessada através de um método getConnection() em uma classe chamada ConnectionFactory.

Outro nome comum para essa classe é Database, que dá um significado melhor.

Uso da fábrica

Podemos obter uma conexão normal:

Connection con = db.getConnection();

Ou podemos obter uma transação:

Connection con = db.getConnection("T");

Em qualquer dos casos, a conexão ainda precisará ser fechada, portanto use o try:

try (Connection con = db.getConnection()) {
    ...
}

Implementação da fábrica

import org.sql2o.Connection;
import org.sql2o.Sql2o;

public class Database {

    /**
     * Constantes das informações de conexão ao SGBD
     */
    private static final String
            JDBC_URL = "jdbc:sqlserver://localhost;databaseName=PROGWEB",
            USER = "sa",
            PASSWORD = "123456";

    /**
     * Instância necessária da biblioteca de acesso a dados
     */
    private final Sql2o sql2o = new Sql2o(JDBC_URL, USER, PASSWORD);

    /**
     * Cria uma conexão padrão
     */
    public Connection getConnection() {
        return getConnection("");
    }

    /**
     * Cria uma conexão com uma dada opção
     */
    public Connection getConnection(String option) {
        switch (option) {
            case "T":
                return sql2o.beginTransaction();
            default:
                return sql2o.open();
        }
    }

    //<editor-fold desc="Instância única da classe (padrão singleton)">
    private Database() {
    }

    private static final Database INSTANCE = new Database();

    public static Database getInstance() {
        return INSTANCE;
    }
    //</editor-fold>
}

O uso da classe acima seria algo assim:

// Obtém instância da fábrica
Database db = Database.getInstance();

// Cria uma conexão com a instância
try (Connection con = db.getConnection()) {
    ...
}

Implementação alternativa

Uma coisa que não dá para deixar de mostrar é que a classe Sql2o é, na verdade, uma implementação de fábrica de conexões métodos de acesso diferentes:

getConnection() ➡️ open()
getConnection("T") ➡️ beginTransaction()

Assim, você pode optar por usar o seguinte design, mais simples, para seu projeto:

import org.sql2o.Sql2o;

public class Database extends Sql2o {

    /**
     * Constantes das informações de conexão ao SGBD
     */
    private static final String
            JDBC_URL = "jdbc:sqlserver://localhost;databaseName=PROGWEB",
            USER = "sa",
            PASSWORD = "123456";

    //<editor-fold desc="Instância única da classe (padrão singleton)">
    private Database() {
        super(JDBC_URL, USER, PASSWORD);
    }

    private static final Database INSTANCE = new Database();

    public static Database getInstance() {
        return INSTANCE;
    }
    //</editor-fold>
}

O uso da fábrica de conexões mudaria ligeiramente para o seguinte código:

// Obtém instância da fábrica
Database db = Database.getInstance();

// Cria uma conexão com a instância
try (Connection con = db.open()) {
    ...
}

Dessa forma, economizamos tempo com o reuso da fábrica de conexões sql2o 👍

Cenário de exemplo

Acesse o link para ver a construção da camada de acesso a dados em um cenário simples.

Leitura complementar

Site

Bancos de dados e JDBC (Apostila da Caelum)

Livro

Capítulo 15: Trabalhando com Banco de Dados