As implementações mostradas nesse documento fazem uso da biblioteca sql2o.
- Como os dados são acessados?
- Data Access Object (DAO)
- Sobre as classes DAO
- Sobre os métodos DAO
- Fábrica de conexões
- Cenário de exemplo
- Leitura complementar
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:
- Em qual tabela os livros estão sendo inseridos?
- Qual coluna é a chave primária?
- Existem chaves estrangeiras? Quais?
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:
ProdutoDao
ArmarinhoDao
ClienteDao
LojaDao
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:
LivroDao
AutorDao
LocatarioDao
EmprestimoDao
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 URL de conexão;
- o nome de usuário;
- a senha.
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