テーブルの複数行を束ねて表示したい場合
例えばこのような画面ですね(ω・
グループ番号の列を結合させたい場合に、Thymeleafでの記述はどうするのだろうか、で少し悩んだため残しておきます。
データとその構造
このテーブルの元データはデータベースにて以下の構成をしています。
テーブル名 | 説明 |
---|---|
item | 商品の情報を格納する。本サンプルでは簡単に商品番号と名前だけを格納している。 |
item_group | 商品グループと商品の関連付けをする。1つのグループには1つ以上の商品を設定できる |
item と item_group は item_id の値で関連付け(リレーション)を定義しています。具体的なデータ例は以下です。
itemテーブル
item_id | name |
---|---|
1 | 商品A |
2 | 商品B |
3 | 商品C |
4 | 商品D |
5 | 商品E |
6 | 商品F |
7 | 商品G |
8 | 商品H |
item_groupテーブル
group_id | item_id |
---|---|
1 | 1 |
2 | 2 |
2 | 3 |
3 | 4 |
3 | 5 |
3 | 6 |
3 | 7 |
3 | 8 |
データの親子関係(リレーション)を図で示すと、このようになるでしょう。
抽出するクエリ(SQL)
冒頭に示したデータを表示するクエリは次のとおりです。
SELECT
item_group.group_id
, item.item_id
, item.name
FROM
item
INNER JOIN
item_group
ON item.item_id = item_group.item_id
ORDER BY item_group.group_id, item.item_id
検索結果をJavaのクラスへマッピングする
データベースからクラスへデータを格納します。
今回は Mybatis を使って、さらにデータの親子関係をもった状態で取得します。
マッピングするクラスは、データベースのテーブル定義とならって、ItemとItemGroupを作成します。
データの親子関係(リレーション)を維持した以下の状態になるようにクラスを構成します。
package com.github.apz.model.item;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor @Getter
public class Item {
private Integer itemId;
private String name;
}
ItemGroup1つに対し1つ以上のItemがある、つまりItemGroupは複数のItemを持ちますので、java.util.List でItemを複数持つと宣言します。
package com.github.apz.model.item;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor @Getter
public class ItemGroup {
private Integer groupId;
private List<Item> items;
}
検索結果を取得し、マッピングするMybatis用の定義xmlは以下です。
<?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="com.github.apz.mapper.ItemMapper">
<resultMap type="com.github.apz.model.item.ItemGroup" id="itemGroup">
<result column="group_id" property="groupId"/>
<collection property="items" ofType="com.github.apz.model.item.Item">
<result column="item_id" property="itemId"/>
<result column="name" property="name"/>
</collection>
</resultMap>
<select id="findBy" resultMap="itemGroup">
SELECT
item_group.group_id
, item.item_id
, item.name
FROM
item
INNER JOIN
item_group
ON item.item_id = item_group.item_id
ORDER BY item_group.group_id, item.item_id
</select>
</mapper>
この検索結果を受け取るMapperインタフェースは以下です。複数のItemGroupを受け取ります。
package com.github.apz.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.github.apz.model.item.ItemGroup;
@Mapper
public interface ItemMapper {
List<ItemGroup> findBy();
}
この検索結果を 「そのままThymeleafへ渡し」ます。特殊な変換処理や、表示用のコード修正はしません。
Thymeleaf
複数の ItemGroup があり、さらに ItemGroup は複数のItemを持ちますので、繰り返し要素が2つあることになります。
先ほどの検索結果を itemGroups の名前で Thymeleaf テンプレートへ格納していた場合、次の手順で繰り返し出力します。
- ItemGroup の数だけ繰り返し
- Itemの数だけ繰り返し
- Itemの先頭だけ、rowspan を出力し、その値は Itemの個数を設定する
これを実現するため、th:each
を2階層出力しますが、Thymeleafは1つの要素に2つの th:each
は使えませんので(実行時エラー)、例えば以下のように1つ出力に影響のない要素に出力します。
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>グループ番号</th>
<th>商品番号</th>
<th>商品名</th>
</tr>
</thead>
<tbody>
<ins th:each="itemGroup: ${itemGroups}" th:remove="tag">
<tr th:each="item: ${itemGroup.items}">
<td th:if="${itemStat.first}" th:inline="text" th:rowspan="${itemStat.size}">[[${itemGroup.groupId}]]</td>
<td th:inline="text">[[${item.itemId}]]</td>
<td th:inline="text">[[${item.name}]]</td>
</tr>
</ins>
</tbody>
</table>
itemStat: th:each
で繰り返し出力する内容を item の名前で指定したとき、Thymeleafのデフォルト設定では、繰り返し要素の状態を扱う変数として、itemStat が利用できます。
- 各Itemの先頭に group_id を出すので
th:if="${itemStat.first}"
- さらに rowspan に itemの数を指定するので、
th:rowspan="${itemStat.size}"
これで実現可能です。
まとめ
- 複数の階層構造をもつデータをThymeleafで出力する方法を実現しました
- MyBatisのマッピング結果で得たデータの階層構造をそのままThymeleafで使えることを示しました
サンプルコード全体
以下にMySQLでの動作サンプルを公開しています。