IBatis (MyBatis) Joins: Result Mapping Avançados, Association, Collections, Problema Do Select N+1
Este tutorial tem como objetivo mostrar como fazer o setup do iBatis (MyBatis) usando um projeto Java simples, e também mostrará exemplos de como configurar mapeamentos avançados (result mapping), association (1 pra 1), collections (1 pra n, n para n), problema do select n+1 usando configuração em XML e annotations.
Pré-Requisitos
Para este tutorial usei:
IDE: Eclipse (você pode usar a sua IDE favorita)
DataBase: MySQL
Libs/jars: Mybatis, MySQL conector e JUnit (para testes)
No Eclipse, a estrutura do seu projeto vai ficar assim:
Dados de Exemplo
Execute o script que está dentro da pasta sql antes de começar o projeto. Este arquivo contém os dados de exemplo usados neste tutorial.
1 - POJOs - Beans
Vou representar os Beans com um diagrama UML para melhor visualização. O código fonte completo pode ser baixado no final do tutorial.
O objetivo desse post é demonstrar como obter os dados do Blog do banco de dados, mas como pode perceber, a classe Blog contém uma association com Autor (Author), e uma coleção de Posts (que por sua vez contém uma coleção de Tags). Vamos tentar popular esses objetos.
Para popular esses objetos, vamos usar as seguintes relação: 1-1, 1-n, n-n usando iBatis/Mybatis.
2 - Mapeamento Avançado - Result Mapping
Como já mencionei em posts anteriores, o resultMap é o elemento mais importante e mais poderoso do Mybatis.
O MyBatis foi criado com uma idéia em mente: os Banco de Dados às vezes não são como você quer ou precisa que eles sejam. Enquanto todos desejam que os bancos estejam na terceira forma normal, muitas vezes não estão. E seria ótimo se fosse possível ter um único mapeamento do banco de dados para todas as aplicações que o utilizam, mas não é assim que as coisas funcionam. Os Result Maps são a resposta do MyBatis para esses problemas.
O elemento resultMap tem alguns sub-elementos. Os elementos abaixo são alguns dos sub-elementos:
- constructor – usado para injeção de resultados no construtor da classe através de instanciação da mesma.
- nested result mappings– associação (association) de resultMaps no próprio elemento, ou podem fazer referência a outro resultMap
- collection – uma coleção de tipos complexos ou coleção de resultMaps, ou ainda referência a outros resultMaps
- discriminator – utiliza uma valor do result para determinar qual ResultMap usar (Vamos ver discriminator mais de perto no próximo post)
- nested result mappings – um resultMap pode fazer referência a outros resultMaps
Boas Práticas: Sempre construa os ResultMaps de forma incremental. Testes unitários pode te ajudar bastante nisso. Se tentar implementar um resultMap de uma vez, pode ser que alguma coisa esteja errada e será difícil de achar o erro. Comece de maneira simples, e dê um passo de cada vez, evolua seu ResultMap aos poucos. A melhor maneira para verificar se você está no caminho certo é om testes unitários.
O nosso objetivo é implementar o seguinte resultMap.
[code lang="xml" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<resultMap id="resultBlog" type="Blog">
<id property="id" column="idBlog" />
<result property="name" column="blogname" />
<result property="url" column="blogurl" />
<association property="author" column="idBlog" javaType="Author"
select="selectAuthor" />
<collection property="posts" column="idBlog" javaType="ArrayList"
ofType="Post" select="selectPosts" resultMap="resultTag" />
</resultMap>
[/code]
Seguindo as boas práticas, vamos dar um passo por vez. Vamos começar buscando no banco de dados as informações do Blog apenas. O ResultMap inicial vai ficar assim:
[code lang="xml" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<resultMap id="resultBlog" type="Blog">
<id property="id" column="idBlog" />
<result property="name" column="blogname" />
<result property="url" column="blogurl" />
</resultMap>
<select id="selectBlog" resultMap="resultBlog">
SELECT idBlog, name as blogname, url as blogurl FROM BLOG
</select>
[/code]
Até agora tudo bem, já vimos um select simples em posts anteriores. Vamos para o próximo passo.
Association
Vamos tentar buscar as informações do Blog e do Autor agora.
O elemento de association trabalha com o relacionamento "tem-um". Por exemplo, neste post, um Blog tem um Autor. Uma mapeamento de association funciona como qualquer outro result. Você precisa especificar a propriedade target (atributo da classe Blog neste caso), a coluna para obter o valor (neste caso o ID do blog para fazer o filtro), o javaType da propriedade (que o MyBatis consegue descobrir sozinho na maioria das vezes) - neste caso é Author, o jdbcType se for necessário, e um typeHandler se precisar substituir os valores do result.
Onde o elemento association é diferente é o que você "falar" pro MyBatis conseguir fazer o load do objeto. O Mybatis pode fazer isso de duas maneiras:
- Nested Select: Select Aninhado - executando um outro mapeamento SQL que retorna o tipo desejado.
- Nested Results: Result aninhado - usando mapeamentos do result (result mapping) para tratar os subconjuntos repetidos da query com join.
Vamos dar uma olhada no Nested Select primeiro.
Aqui está o nosso resultMap com a association do
[code lang="xml" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<resultMap id="resultBlog" type="Blog">
<id property="id" column="idBlog" />
<result property="name" column="blogname" />
<result property="url" column="blogurl" />
<association property="author" column="idBlog" javaType="Author"
select="selectAuthor" />
</resultMap>
<select id="selectBlog" resultMap="resultBlog">
SELECT idBlog, name as blogname, url as blogurl FROM BLOG
</select>
<select id="selectAuthor" parameterType="int" resultType="Author">
SELECT idAuthor as id, name, email FROM AUTHOR WHERE idBlog = #{idBlog}
</select>
[/code]
Dê uma olhada no atributo select="selectAuthor". Significa que o MyBatis irá executar a query para obter os autores que escrever para o Blog. Para fazer o relacionamento entre Blog e Author especificamos o atributo column="idBlog", para filtrar os autores.
Note que também especificamos o javaType="Author". Estamos usando um Alias (Lembra?). Se não tiver um alias, é preciso colocar o nome completo da classe (com o nome do pacote junto: com.loiane.model.Author).
É isso. Temos 2 queries com select statements: uma para carregar os dados do blog, e outra para carregar o autor, e o resultMap do Blog descreve que o MyBatis deve usar o "selectAuthor” statement para obter as informações do Author.
Essa abordagem é bem simples, mas não terá boa performance para grande conjunto de dados ou listas. Este problema é conhecido como o Problema do Select N+1. Este problema é causado:
- Você executa um SQL que retorna uma lista de registros (o "+1").
- Para cada registro retornado, você executa um SELECT que carrega os detalhes (o “N”).
Este problema pode resultar em centenas ou milhares de SELECTs que serão executados. Isso às vezes não é o desejado. A parte boa é que o iBatis pode executar queries como essa, mas você deve estar atento o quando isso é custoso em termos de performance, ou seja, péssima performance.
Vou mostrar aqui no post como evitar esse problema, mas antes vamos ver como lidar com Lists ou coleções.
Collection
Já estamos buscando informações do Blog e do Author no banco de dados. Agora temos que buscar as informações do Post. Um Blog contém uma lista de Posts e estes, por sua vez, contém uma lista de Tags. Estamos lidando com 2 relacionamentos agora: 1-N (Blog-Posts) e N-N (Posts-Tags). Vamos ver como se faz isso usando MyBatis.
Vamos ainda usar um Nested Select para fazer isso.
Dê uma olhada no resultMap com o Post collection:
[code lang="xml" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<resultMap id="resultBlog" type="Blog">
<id property="id" column="idBlog" />
<result property="name" column="blogname" />
<result property="url" column="blogurl" />
<association property="author" column="idBlog" javaType="Author"
select="selectAuthor" />
<collection property="posts" column="idBlog" javaType="ArrayList"
ofType="Post" select="selectPosts" resultMap="resultTag" />
</resultMap>
[/code]
O elemento collection funciona quase da mesma maneira que o association. Vamos focar nas diferenças.
Primeiro, estamos usando o elemento collection. Depois irá notar que existe o atributo "ofType". Este atributo é necessário para notar a diferença entre o tipo de propriedade do JavaBean (atributo) e o tipo que a coleção contém.
Para lidar com o relacionamento N-N entre Post e Tag, vamos usar também um elemento collection, mas não vamos usar nested results para isso.
[code lang="xml" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<resultMap id="resultPosts" type="Post">
<id property="id" column="idPost" />
<result property="title" column="title" />
<collection property="tags" column="idPost" javaType="ArrayList"
ofType="Tag" resultMap="resultTag" />
</resultMap>
<resultMap id="resultTag" type="Tag">
<id property="id" column="idTag" />
<result property="value" column="value" />
</resultMap>
<select id="selectPosts" parameterType="int" resultType="Post"
resultMap="resultPosts">
SELECT
P.idPost as idPost, P.title as title,
T.idTag as idTag, T.value as value
FROM Post P
left outer join Post_Tag PT on P.idPost = PT.idPost
left outer join Tag T on PT.idTag = T.idTag
WHERE P.idBlog = #{idBlog}
</select>
[/code]
Pronto! Vamos ver como o arquivo Blog.xml se parece:
[code lang="xml" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="Blog">
<resultMap id="resultBlog" type="Blog">
<id property="id" column="idBlog"/>
<result property="name" column="blogname"/>
<result property="url" column="blogurl"/>
<association property="author" column="idBlog" javaType="Author" select="selectAuthor"/>
<collection property="posts" column="idBlog" javaType="ArrayList" ofType="Post"
select="selectPosts" resultMap="resultTag"/>
</resultMap>
<resultMap id="resultPosts" type="Post">
<id property="id" column="idPost"/>
<result property="title" column="title"/>
<collection property="tags" column="idPost" javaType="ArrayList" ofType="Tag"
resultMap="resultTag"/>
</resultMap>
<resultMap id="resultTag" type="Tag">
<id property="id" column="idTag"/>
<result property="value" column="value"/>
</resultMap>
<select id="selectBlog" resultMap="resultBlog">
SELECT idBlog, name as blogname, url as blogurl FROM BLOG
</select>
<select id="selectAuthor" parameterType="int" resultType="Author">
SELECT idAuthor as id, name, email FROM AUTHOR WHERE idBlog = #{idBlog}
</select>
<select id="selectPosts" parameterType="int" resultType="Post" resultMap="resultPosts">
SELECT
P.idPost as idPost, P.title as title,
T.idTag as idTag, T.value as value
FROM Post P
left outer join Post_Tag PT on P.idPost = PT.idPost
left outer join Tag T on PT.idTag = T.idTag
WHERE P.idBlog = #{idBlog}
</select>
</mapper>
[/code]
Solução para o Problema de Selects N+1
Como resolver este problem?
Usando Nested Results: - usando mapeamentos aninhados para lidar com subconjuntos repetidos no resultado dos joins.
O que precisamos fazer é escrever uma única query para obter todas as informações de uma vez, e o ResultMap lida com o resultado.
Muito Importante: o elemento ID tem um papel muito importante no nested result mappeing. Você sempre deve especificar uma ou mais propriedades que podem ser usadas para identificar os resultados como únicos. A verdade é que o MyBatis ainda funciona se você não especificar, mas isso vai custar a performance. A chave primária é uma escolha óbvia para essa situação (mesmo que seja uma chave composta).
[code lang="xml" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BlogBestPractice">
<resultMap id="resultBlog" type="Blog">
<id property="id" column="idBlog"/>
<result property="name" column="blogName"/>
<result property="url" column="url"/>
<association property="author" column="idBlog" javaType="Author">
<id property="id" column="idAuthor"/>
<result property="name" column="authorName"/>
<result property="email" column="email"/>
</association>
<collection property="posts" column="idBlog" javaType="ArrayList" ofType="Post">
<id property="id" column="idPost"/>
<result property="title" column="title"/>
<collection property="tags" column="idBlog" javaType="ArrayList" ofType="Tag">
<id property="id" column="idTag"/>
<result property="value" column="value"/>
</collection>
</collection>
</resultMap>
<select id="selectBlogBestPractice" resultMap="resultBlog">
SELECT
B.idBlog as idBlog, B.name as blogName, B.url as url,
A.idAuthor as idAuthor, A.name as authorName, A.email as email ,
P.idPost as idPost, P.title as title,
T.idTag as idTag, T.value as value
FROM BLOG as B
left outer join Author A on B.idBlog = A.idBlog
left outer join Post P on P.idBlog = B.idBlog
left outer join Post_Tag PT on P.idPost = PT.idPost
left outer join Tag T on PT.idTag = T.idTag
</select>
</mapper>
[/code]
3 - BlogDAO
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
package com.loiane.dao;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import com.loiane.model.Blog;
public class BlogDAO {
/**
* Returns the list of all Contact instances from the database.
* @return the list of all Contact instances from the database.
*/
@SuppressWarnings("unchecked")
public List<Blog> select(){
SqlSessionFactory sqlSessionFactory = MyBatisConnectionFactory.getSqlSessionFactory();
SqlSession session = sqlSessionFactory.openSession();
try {
List<Blog> list = session.selectList("Blog.selectBlog");
return list;
} finally {
session.close();
}
}
/**
* Returns the list of all Contact instances from the database avoiding the N + 1
* problem
* @return the list of all Contact instances from the database.
*/
@SuppressWarnings("unchecked")
public List<Blog> selectN1ProblemSolution(){
SqlSessionFactory sqlSessionFactory = MyBatisConnectionFactory.getSqlSessionFactory();
SqlSession session = sqlSessionFactory.openSession();
try {
List<Blog> list = session.selectList("BlogBestPractice.selectBlogBestPractice");
return list;
} finally {
session.close();
}
}
}
[/code]
4 - Annotations
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false"]
package com.loiane.data;
import java.util.List;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.One;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import com.loiane.model.Author;
import com.loiane.model.Blog;
import com.loiane.model.Post;
public interface BlogMapper {
final String SELECT_POSTS = "SELECTP.idPost as idPost, P.title as title, T.idTag as idTag, T.value as value " +
"FROM Post P left outer join Post_Tag PT on P.idPost = PT.idPost " +
"left outer join Tag T on PT.idTag = T.idTag WHERE P.idBlog = #{idBlog}";
/**
* Returns the list of all Blog instances from the database.
* @return the list of all Blog instances from the database.
*/
@Select("SELECT idBlog, name as blogname, url as blogurl FROM BLOG")
@Results(value = {
@Result(property="id", column="idBlog"),
@Result(property="name", column="blogname"),
@Result(property="url", column="blogurl"),
@Result(property="author", column="idBlog", javaType=Author.class, one=@One(select="selectAuthor")),
@Result(property="posts", column="idBlog", javaType=List.class, many=@Many(select="selectBlogPosts"))
})
List<Blog> selectAllBlogs();
/**
* Returns the list of all Author instances from the database of a Blog
* @param idBlog
* @return the list of all Author instances from the database of a Blog
*/
@Select("SELECT idAuthor as id, name, email FROM AUTHOR WHERE idBlog = #{idBlog}")
Author selectAuthor(String idBlog);
/**
* Returns the list of all Post instances from the database of a Blog
* @param idBlog
* @return the list of all Post instances from the database of a Blog
*/
@Select(SELECT_POSTS)
@Results(value = {
@Result(property="id", column="idPost"),
@Result(property="title", column="title"),
@Result(property="tags", column="idPost", javaType=List.class, many=@Many)
})
List<Post> selectBlogPosts(String idBlog);
}
[/code]
@Result
Um único mapeamento entre coluna e atributo.Atributos: id, column, property, javaType, jdbcType, typeHandler, one, many.O atributo id é um valor booleano que indica se a propriedade deve ser usado para comparações. (similar à tag <id> na configuração em XML).O atributo one é para association, similar à tag <association>, e o atributo many é usado para collection, similar à tag <collection>. Foram nomeados assim para evitar conflitos.
@One
Mapeamento entre uma única propriedade de um tipo complexo.Atributos: select, nome da query que deve ser executada para obter o resultado (nome do método do mapper).
@Many
Mapeamento entre uma propriedade que é uma collection de um tipo complexo.Atributos: select, nome da query que deve ser executada para obter o resultado (nome do método do mapper).
5 - SqlMapConfig.xml
O arquivo SqlMapConfig.xml vai ficar assim:
[code lang="xml" firstline="1" toolbar="true" collapse="false" wraplines="false"]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias alias="Blog" type="com.loiane.model.Blog"/>
<typeAlias alias="Author" type="com.loiane.model.Author"/>
<typeAlias alias="Post" type="com.loiane.model.Post"/>
<typeAlias alias="Tag" type="com.loiane.model.Tag"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/blog_ibatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/loiane/data/Blog.xml"/>
<mapper resource="com/loiane/data/BlogBestPractice.xml"/>
</mappers>
</configuration>
[/code]
6 - MyBatisConnectionFactory
[code lang="java" firstline="1" toolbar="true" collapse="false" wraplines="false" highlight="27"]
package com.loiane.dao;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import com.loiane.data.BlogMapper;
public class MyBatisConnectionFactory {
private static SqlSessionFactory sqlSessionFactory;
static {
try {
String resource = "SqlMapConfig.xml";
Reader reader = Resources.getResourceAsReader(resource);
if (sqlSessionFactory == null) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
sqlSessionFactory.getConfiguration().addMapper(BlogMapper.class);
}
}
catch (FileNotFoundException fileNotFoundException) {
fileNotFoundException.printStackTrace();
}
catch (IOException iOException) {
iOException.printStackTrace();
}
}
public static SqlSessionFactory getSqlSessionFactory() {
return sqlSessionFactory;
}
}
[/code]
Download
Se desejar fazer o download do código fonte completo, basta ir até a minha conta no GitHub: https://github.com/loiane/ibatis-handling-joins
Se quiser fazer o download o aqruivo zip com o projeto, clique no botão de Downloads (depois é só importar pro Eclipse ou outra IDE):
Mais tutoriais sobre iBatis/MyBatis estão vindo! Fique ligado! :)
Bons códigos! :)