MyBatisを用いて、単一テーブル内の親子関係を持つ階層データ(自己参照)を効率的に取得する方法を解説します。例として、ニュースカテゴリのツリー構造(親カテゴリ → 子カテゴリ)を扱います。
テーブル構造は以下の通りです:
CREATE TABLE newslabel (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
pid INT DEFAULT 0 COMMENT '親カテゴリID。0はルートを意味する'
);
データ例:
- id=2, name='体育新闻', pid=0(親カテゴリ)
- id=3, name='NBA', pid=2(子カテゴリ)
- id=5, name='火箭', pid=3(孫カテゴリ)
エンティティクラスの定義
階層構造を表現するため、NewsCategoryクラスに自身のリストを含むフィールドを追加します。
import java.util.List;
public class NewsCategory {
private Integer id;
private String name;
private List<NewsCategory> subCategories;
// ゲッター・セッター
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public List<NewsCategory> getSubCategories() { return subCategories; }
public void setSubCategories(List<NewsCategory> subCategories) { this.subCategories = subCategories; }
@Override
public String toString() {
return "NewsCategory{id=" + id + ", name='" + name + "', subCategories=" + subCategories + "}";
}
}
DAOインターフェース
親カテゴリの子孫を取得するメソッドと、自身を含む全階層を取得するメソッドを定義します。
public interface NewsCategoryDao {
List<NewsCategory> findSubCategoriesByParentId(Integer parentId);
List<NewsCategory> findTreeByCategoryId(Integer categoryId);
}
Mapper XMLの実装
1. 子孫カテゴリのみを取得(再帰的)
findSubCategoriesByParentIdは、指定された親IDの直下の子カテゴリを再帰的に取得します。
<?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="dao.NewsCategoryDao">
<resultMap id="categoryTreeMap" type="model.NewsCategory">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="subCategories"
ofType="model.NewsCategory"
select="findSubCategoriesByParentId"
column="id"/>
</resultMap>
<select id="findSubCategoriesByParentId" resultMap="categoryTreeMap">
SELECT id, name FROM newslabel WHERE pid = #{parentId}
</select>
</mapper>
注意:
column="id"は、親クエリの結果のidが子クエリの#{parentId}に渡されます。
2. 自身を含む全階層を取得
親カテゴリ自身も含む完全なツリーを取得するには、別途ルートクエリを定義し、resultMapを再利用します。
<!-- 自身とそのすべての子孫を取得 -->
<select id="findTreeByCategoryId" resultMap="categoryTreeMap">
SELECT id, name FROM newslabel WHERE id = #{categoryId}
</select>
</mapper>
この構成では:
findTreeByCategoryIdが最初に自身のレコードを1件取得resultMapの<collection>がそのidを引数にfindSubCategoriesByParentIdを再帰的に呼び出す- 子孫が存在しないレベルで再帰が終了
テストコード
import org.apache.ibatis.session.SqlSession;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
public class NewsCategoryTest {
private SqlSession sqlSession;
private NewsCategoryDao dao;
@Before
public void setup() {
sqlSession = MyBatisUtils.getSqlSession();
dao = sqlSession.getMapper(NewsCategoryDao.class);
}
@Test
public void testFindTreeByCategoryId() {
List<NewsCategory> tree = dao.findTreeByCategoryId(2);
tree.forEach(System.out::println);
}
@Test
public void testFindSubCategoriesByParentId() {
List<NewsCategory> children = dao.findSubCategoriesByParentId(2);
children.forEach(System.out::println);
}
@After
public void tearDown() {
if (sqlSession != null) {
sqlSession.close();
}
}
}
実行結果の例
findTreeByCategoryId(2)の出力:
NewsCategory{id=2, name='体育新闻', subCategories=[
NewsCategory{id=3, name='NBA', subCategories=[
NewsCategory{id=5, name='火箭', subCategories=[]},
NewsCategory{id=6, name='湖人', subCategories=[]}
]},
NewsCategory{id=4, name='CBA', subCategories=[
NewsCategory{id=7, name='北京金瓯', subCategories=[]},
NewsCategory{id=8, name='浙江广夏', subCategories=[]},
NewsCategory{id=9, name='青岛双星', subCategories=[]}
]}
]}
ログには、MyBatisが内部で複数のSELECTを再帰的に発行している様子が確認できます。各レベルでpidをパラメータとして渡し、葉ノードに到達するまで処理が続きます。
注意点
- 再帰クエリは、階層が深いほどDBアクセスが増えるため、パフォーマンスに注意
- 大規模なツリーでは、一度に全データを取得するより、Lazy Loadingやページネーションを検討
- MyBatisの
<collection>はデフォルトでEager Fetch(即時取得)なので、必要に応じてfetchType="lazy"を指定可能