(Tips)Thymeleafとrowspanを使ったテーブル出力は、Mybatisのマッピングと相性がいい

# テーブルの複数行を束ねて表示したい場合

例えばこのような画面ですね（ω・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13268/241541a6-3ca4-5c5b-ba5a-93258aebf5ac.png)

グループ番号の列を結合させたい場合に、Thymeleafでの記述はどうするのだろうか、で少し悩んだため残しておきます。

## データとその構造

このテーブルの元データはデータベースにて以下の構成をしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13268/8b6395f7-f75e-68e2-e3e7-895047c27157.png)

|テーブル名|説明|
|---|---|
|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|

データの親子関係（リレーション）を図で示すと、このようになるでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/13268/493e021c-abb5-808b-29a1-e3101993f0bf.png)

## 抽出するクエリ（SQL）

冒頭に示したデータを表示するクエリは次のとおりです。

```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](https://mybatis.org/mybatis-3/ja/index.html) を使って、さらにデータの親子関係をもった状態で取得します。

https://qiita.com/alpha_pz/items/9c88acd814ffad2f1a36

マッピングするクラスは、データベースのテーブル定義とならって、ItemとItemGroupを作成します。
データの親子関係（リレーション）を維持した以下の状態になるようにクラスを構成します。

```java:Item.java
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を複数持つと宣言します。

```java:ItemGroup.java
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:ItemMapper.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を受け取ります。

```java:ItemMapper.java
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つ出力に影響のない要素に出力します。

```html:item.html
<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での動作サンプルを公開しています。

[https://github.com/A-pZ/springboot-thymeleaf-rowspan-sample](https://github.com/A-pZ/springboot-thymeleaf-rowspan-sample)