Componentes Fundamentais do MyBatis
Antes de explorar as estratégias de acesso a dados, é crucial compreender o ciclo de vida e a responsabilidade dos objetos centrais do framework:
- SqlSessionFactoryBuilder: Atua como uma classe utilitária para construir a fábrica de sessões. Não requer gerenciamento via Singleton; deve ser instanciada, utilizada para criar a fábrica e, em seguida, descartada.
- SqlSessionFactory: Responsável por gerar instâncias de
SqlSession. Recomenda-se fortemente o uso do padrão Singleton para este objeto em toda a aplicação. Em ambientes integrados com Spring, o próprio container gerencia seu ciclo de vida como um bean único. - SqlSession: Interface principal para interação com o banco de dados, contendo métodos de execução de comandos SQL. Como possui atributos de estado interno, não é thread-safe. A melhor prática arquitetural é instnaciá-la como variável local dentro de métodos de serviço e fechá-la imediatamente após a execução.
Abordagem de DAO Tradicional
No modelo tradicional, o desenvolvedor é responsável por definir a interface de acesso a dados e prover uma implemantação manual, lidando explicitamente com a infraestrutura do MyBatis.
Definição da Interface
package com.sistema.persistencia.dao;
import java.util.List;
import com.sistema.modelo.Cliente;
public interface ClienteDao {
Cliente buscarPorId(int id) throws Exception;
List<Cliente> buscarPorNome(String nome) throws Exception;
void cadastrar(Cliente cliente) throws Exception;
void excluir(int id) throws Exception;
}
Implementação Manual
package com.sistema.persistencia.dao.impl;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import com.sistema.modelo.Cliente;
import com.sistema.persistencia.dao.ClienteDao;
public class ClienteDaoImpl implements ClienteDao {
private final SqlSessionFactory sessionFactory;
// Injeção de dependência via construtor
public ClienteDaoImpl(SqlSessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public Cliente buscarPorId(int id) throws Exception {
try (SqlSession session = sessionFactory.openSession()) {
// Hardcoding do identificador do statement
return session.selectOne("statements.ClienteMapper.buscarPorId", id);
}
}
@Override
public List<Cliente> buscarPorNome(String nome) throws Exception {
try (SqlSession session = sessionFactory.openSession()) {
return session.selectList("statements.ClienteMapper.buscarPorNome", nome);
}
}
@Override
public void cadastrar(Cliente cliente) throws Exception {
try (SqlSession session = sessionFactory.openSession()) {
session.insert("statements.ClienteMapper.cadastrar", cliente);
session.commit();
}
}
@Override
public void excluir(int id) throws Exception {
try (SqlSession session = sessionFactory.openSession()) {
session.delete("statements.ClienteMapper.excluir", id);
session.commit();
}
}
}
Limitações do Modelo Tradicional
- Código Boilerplate: Abertura, commit e fechamento de sessões são repetidos exaustivamente em cada método.
- Acoplamento por Strings: Os identificadores dos mapeamentos XML são passados como strings fixas, dificultando a refatoração e a manutenção.
- Falta de Segurança de Tipos: Como os métodos da
SqlSessionaceitamObjectcomo parâmetro, erros de tipagem nos argumentos não são detectados pelo compilador, gerando falhas apenas em tempo de execução.
Desenvolvimento com Proxy de Interface Mapper
Para mitigar os problemas do DAO tradicional, o MyBatis introduziu o mecanismo de proxy dinâmico. O framework gera auotmaticamente a implementação da interface em tempo de execução. Para que a injeção do proxy funcione, quatro regras de contrato devem ser estritamente obedecidas:
- O atributo
namespaceno arquivo XML de mapeamento deve ser idêntico ao caminho completo (fully qualified name) da interface Java. - O nome do método declarado na interface deve corresponder exatamente ao
idda tag SQL no XML. - O tipo do parâmetro de entrada do método Java deve alinhar-se com o
parameterTypedefinido no XML. - O tipo de retorno do método Java deve coincidir com o
resultType(ouresultMap) especificado.
Interface Mapper
package com.sistema.persistencia.mapper;
import java.util.List;
import com.sistema.modelo.Cliente;
public interface ClienteMapper {
Cliente buscarPorId(int id) throws Exception;
List<Cliente> buscarPorNome(String nome) throws Exception;
void cadastrar(Cliente cliente) throws Exception;
void excluir(int id) throws Exception;
}
Arquivo de Mapeamento XML
<?xml version="1.0" encoding="UTF-8" ?>
<mapper namespace="com.sistema.persistencia.mapper.ClienteMapper">
<select id="buscarPorId" parameterType="int" resultType="com.sistema.modelo.Cliente">
SELECT id, nome_completo, email, data_registro
FROM clientes
WHERE id = #{id}
</select>
<select id="buscarPorNome" parameterType="string" resultType="com.sistema.modelo.Cliente">
SELECT id, nome_completo, email, data_registro
FROM clientes
WHERE nome_completo LIKE CONCAT('%', #{nome}, '%')
</select>
<insert id="cadastrar" parameterType="com.sistema.modelo.Cliente">
<selectKey keyProperty="id" order="AFTER" resultType="int">
SELECT LAST_INSERT_ID()
</selectKey>
INSERT INTO clientes (nome_completo, email, data_registro)
VALUES (#{nomeCompleto}, #{email}, #{dataRegistro})
</insert>
<delete id="excluir" parameterType="int">
DELETE FROM clientes WHERE id = #{id}
</delete>
</mapper>
O arquivo XML deve ser registrado na configuração global do MyBatis:
<mappers>
<mapper resource="mapper/ClienteMapper.xml"/>
</mappers>
Testando o Proxy Mapper
package com.sistema.persistencia.mapper;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.sistema.modelo.Cliente;
public class ClienteMapperTest {
private SqlSessionFactory sessionFactory;
@BeforeEach
public void configurarAmbiente() throws Exception {
String config = "mybatis-config.xml";
InputStream stream = Resources.getResourceAsStream(config);
sessionFactory = new SqlSessionFactoryBuilder().build(stream);
}
@Test
public void validarBuscaPorId() throws Exception {
try (SqlSession session = sessionFactory.openSession()) {
// O MyBatis gera a implementação da interface dinamicamente
ClienteMapper mapper = session.getMapper(ClienteMapper.class);
Cliente cliente = mapper.buscarPorId(10);
System.out.println("Cliente encontrado: " + cliente.getNomeCompleto());
}
}
@Test
public void validarBuscaPorNome() throws Exception {
try (SqlSession session = sessionFactory.openSession()) {
ClienteMapper mapper = session.getMapper(ClienteMapper.class);
List<Cliente> resultados = mapper.buscarPorNome("Silva");
resultados.forEach(c -> System.out.println(c.getEmail()));
}
}
}
Comportamento Interno e Boas Práticas
Resolução de Consultas (Select):
O proxy gerado pelo MyBatis analisa a assinatura do método na interface para determinar a operação interna. Se o tipo de retorno for um objeto único (POJO), o framework roteia a chamada para o método selectOne da SqlSession. Caso o retorno seja uma coleção (como List ou Set), a execução é delegada ao selectList.
Gerenciamento de Múltiplos Parâmetros:
Por padrão, os métodos da interface Mapper aceitam apenas um parâmetro. Quando uma consulta exige múltiplos filtros, a solução arquitetural padrão é encapsular esses critérios em um objeto de transferência (DTO/POJO) ou utilizar um Map<String, Object>.
Nota de Design: Embora a camada de persistência possa depender de objetos de consulta específicos, é altamente recomendável que a camada de Serviço (Service) mantenha uma assinatura baseada em tipos primitivos ou objetos de domínio genéricos, convertendo-os para o DTO do Mapper internamente. Isso evita o acoplamento da lógica de negócios às estruturas do framework de persistência.