IBatis (MyBatis) Joins: Result Mapping Avançados, Association, Collections, Problema Do Select N+1

01 Mar 2011
13 mins read

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: MybatisMySQL 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).
O arquivo Blog.xml ficaria assim usando NestedResult (BlogBestPractice.xml):

[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]

Note que isso é uma boa prática. É melhor evitar o problema N+1, a não ser que essa seja a solução escolhida, mas fique atento à performance.

3 - BlogDAO

Agora que temos tudo configurado, vamos escrever o DAO:

[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]

Existem 2 métodos nesse DAO: o primeiro faz o select usando a primeira abordagem (com problema do select n+1) e o segundo busca os dados usando a query que evita o problema do n+1.

4 - Annotations

Como já publiquei um artigo que explica anotações básicas no MyBatis, vou apenas listas as diferenças, ok?
Vamos escrever 2 selects (um pro Blog, um pro Author e outro para Posts e Tags, assim como fizemos no Blog.xml):

[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]

Vamos tratar os relacionamentos 1-1 e 1-N com as annotations @One e @Many.
Vamos recapitular a annotation @Result:

@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

No arquivo SqlMapConfig.xml definimos os alias e 2 mappers em XML. Mas agora também estamos usando annotation no mesmo projeto.
Precisamos setar o novo mapper no MyBatisConnectionFactory.
Assim podemos usar ambos no projeto: XML e annotation.
Opinião pessoal: use apenas um (XML ou annotations), fica meio confuso usar os dois.

[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! :)