Thymeleafのth:eachの使い方!ループ回数やindexなどの繰り返し処理を学ぼう
生徒
「Thymeleafで複数のデータを表示する方法ってありますか?」
先生
「はい、Thymeleafではth:each属性を使って、リストや配列のデータを簡単にループ表示できます。」
生徒
「どうやって使うんですか?」
先生
「では、基本的な使い方を説明していきましょう!」
1. th:each属性とは?
th:eachは、HTMLの同じタグ(liやtrなど)を「コレクションの件数ぶん」繰り返し出力するための属性です。書き方は「変数 : コレクション」という形で、左側が1件分を受け取る名前、右側が一覧データです。商品一覧、ユーザー一覧、タグの羅列など「リストをそのまま表示したい」場面でまず使います。
最小の例は次のとおりです。ここでは${items}に「文字列のリスト(例:りんご、みかん、ぶどう)」が入っている想定です。
<ul>
<li th:each="item : ${items}">
[[${item}]]
</li>
</ul>
ポイントは2つだけ。①th:eachが<li>をリストの件数ぶん繰り返す、②[[...]]が1件分の値(ここではitem)を画面に表示する、という流れです。まずはこの「1行=1件」の形に慣れればOKです。
もう一歩だけ。オブジェクトの「名前」と「値段」を出すときも考え方は同じです。HTMLの中で必要な場所に[[...]]を置くだけです(特別なクラス指定は不要)。
<ul>
<li th:each="p : ${products}">
[[${p.name}]] / [[${p.price}]]円
</li>
</ul>
まずは「左:1件分の変数」「右:一覧データ」を意識して書ければ十分です。細かな行番号や装飾は、次の章以降で段階的に扱います。
2. 実際の例で学ぶth:eachの使い方
ここでは「一覧データを画面に並べる」一番素直な書き方を確認します。考え方はシンプルで、th:eachで1件ずつ取り出し、取り出した1件を[[...]]で表示するだけです。
まずは最小の例(名前だけ出す)です。${productList}に「商品の配列(nameとpriceを持つ)」が入っている想定です。
<ul>
<li th:each="product : ${productList}">
[[${product.name}]]
</li>
</ul>
次に、名前と価格を2列の表で出す例です。テーブルの行(<tr>)を商品数ぶん繰り返し、列(<td>)に必要な値を埋め込みます。
<table>
<thead>
<tr>
<th>商品名</th>
<th>価格</th>
</tr>
</thead>
<tbody>
<tr th:each="product : ${productList}">
<td>[[${product.name}]]</td>
<td>[[${product.price}]]円</td>
</tr>
</tbody>
</table>
ポイントは「左側が1件分の変数(ここではproduct)、右側が一覧(${productList})」という約束だけです。難しい設定は不要で、見せたい場所に[[...]]を書けば、その位置に値が差し込まれます。まずはこの基本形をしっかり押さえましょう。
3. th:eachの変数を使いこなそう
生徒
「行番号ってどうやって出すんですか?」
先生
「th:eachの第2引数に“状態”を受け取ればOK。たとえばproduct, iterStatのiterStatに番号や奇数・偶数の情報が入ります。」
th:each="要素, 状態 : ${一覧}"の「状態」には、現在の回数や偶数・奇数などの情報がまとまっています。まずは行番号(1始まり)と、偶数行だけに印を付ける例を見てみましょう。
<table>
<thead>
<tr>
<th>No.</th>
<th>商品名</th>
<th>価格</th>
</tr>
</thead>
<tbody>
<!-- 第2引数 iterStat に反復の状態が入る -->
<tr th:each="product, iterStat : ${productList}">
<!-- count は 1,2,3... と増える(1始まり) -->
<td>[[${iterStat.count}]]</td>
<td>
[[${product.name}]]
<!-- 偶数行のときだけ「(偶数)」を表示 -->
<span th:if="${iterStat.even}">(偶数)</span>
</td>
<td>[[${product.price}]]円</td>
</tr>
</tbody>
</table>
押さえる点は2つだけ。①iterStat.countで自然な行番号が出せる、②iterStat.evenやiterStat.oddで行ごとの条件分岐ができる、ということです。まずはこの2つを使いこなせば、番号付けや交互の強調表示が簡単に書けます。
4. th:eachを使うときの注意点
繰り返し処理を行う際、Nullのリストや空のリストに注意が必要です。th:eachで使用するオブジェクトがnullの場合、例外が発生する可能性があります。そのため、事前にデータがあるか確認するか、Thymeleafのth:ifを組み合わせることで安全に表示できます:
<div th:if="${productList != null && !productList.isEmpty()}">
<!-- th:eachによるループ -->
</div>
5. ループ回数を制御する(上位N件・ページング)
「上位N件だけ表示したい」「ページングしたい」ときは、subListやth:withで範囲を切ってからth:eachに渡すのがシンプルです。SEO的にも一覧の表示量を抑えて表示速度を高めるのは有効です。
<!-- 上位5件のみ -->
<ul>
<li th:each="p : ${productList.subList(0, productList.size() >= 5 ? 5 : productList.size())}">
[[${p.name}]] / [[${p.price}]]円
</li>
</ul>
<!-- 簡易ページング:クエリ ?page=0&size=10 を想定(endは排他的) -->
<div th:with="
page=${param.page ?: 0},
size=${param.size ?: 10},
start=${page * size},
end=${start + size},
last=${productList.size()},
from=${start < last ? start : last},
to=${end <= last ? end : last}
">
<table>
<tr th:each="p,st : ${productList.subList(from, to)}">
<td>[[${from + st.index + 1}]]</td>
<td>[[${p.name}]]</td>
<td>[[${p.price}]]円</td>
</tr>
</table>
</div>
6. 反復ステータスを極める(index/count/size/odd・even/first・last)
th:each="item, stat : ${list}"の第2変数statには便利なプロパティが揃っています。行番号や奇数偶数、先頭末尾を使って装飾・区切り・No.付与ができます。
<tr th:each="product, stat : ${productList}"
th:class="${stat.odd} ? 'table-secondary' : ''">
<td>No. [[${stat.count}]] / 全[[${stat.size}]]件</td> <!-- countは1始まり -->
<td>[[${product.name}]]</td>
<td>[[${product.price}]]円
<span th:if="${stat.first}" class="badge bg-info ms-2">NEW</span>
<span th:if="${stat.last}" class="badge bg-secondary ms-1">LAST</span>
</td>
</tr>
<!-- 区切り文字の制御:最後のカンマを出さない -->
<span th:each="tag, s : ${product.tags}">
[[${tag}]]<span th:if="${!s.last}">, </span>
</span>
7. ネストしたループとMapの反復(親子リスト/key・value)
カテゴリごとに商品を出すなどの「親子リスト」や、Mapのkey/valueを回す方法です。親ループのstatで章番号を付けるなど、構造化された一覧に強いです。
<div th:each="cat, cStat : ${categoryList}" class="mb-3">
<h4>[[${cStat.count}]]. [[${cat.name}]]</h4>
<ol>
<li th:each="p, pStat : ${cat.products}">
[[${cStat.count}]].[[${pStat.count}]] [[${p.name}]]([[${p.price}]]円)
</li>
</ol>
</div>
<!-- Mapをループ:エントリ(key/value) -->
<table class="table">
<tr><th>キー</th><th>値</th></tr>
<tr th:each="e : ${priceMap}">
<td>[[${e.key}]]</td>
<td>[[${e.value}]]</td>
</tr>
</table>
大きなコレクションはページングや「上位N件」に分割し、表示速度と可読性を両立しましょう(「Thymeleaf th:each ページング」「index 行番号」などの検索ニーズにもマッチ)。
まとめ
Thymeleafのth:eachは、Spring BootやSpring MVCで受け取った一覧データをHTML上に繰り返し表示するときに欠かせない基本機能です。商品一覧、ユーザー一覧、お知らせ一覧、カテゴリ一覧、検索結果、注文履歴など、Webアプリケーションでは同じ形式のデータを複数件表示する場面が数多くあります。そのようなときに、同じHTMLを件数分だけ手作業で書くのではなく、th:eachを使ってコレクションから一件ずつデータを取り出すことで、短く読みやすいテンプレートを作成できます。
基本となる考え方は、th:each="変数 : ${一覧データ}"という形です。右側にはControllerからModelへ渡したListや配列などを指定し、左側には繰り返し処理の中で一件分のデータを受け取る変数名を書きます。たとえば商品一覧を表示する場合は、th:each="product : ${productList}"とすることで、商品を一件ずつproductとして取り出せます。あとはproduct.nameやproduct.priceのようにプロパティを参照すれば、商品名や価格を画面へ表示できます。
th:eachの基本形をもう一度確認しよう
初心者のうちは、繰り返し処理と聞くとJavaのfor文を思い浮かべることがあります。しかし、Thymeleafのth:eachはHTMLテンプレートの中で表示用の繰り返しを行うための仕組みです。Java側ではControllerやServiceがデータを準備し、Thymeleaf側では受け取ったデータを画面へ並べるという役割分担を意識すると理解しやすくなります。表示処理をテンプレート側へまとめることで、Javaコードの中にHTMLを大量に書く必要がなくなり、画面の修正もしやすくなります。
<table class="table table-striped">
<thead>
<tr>
<th>番号</th>
<th>商品名</th>
<th>価格</th>
</tr>
</thead>
<tbody>
<tr th:each="product, stat : ${productList}">
<td>[[${stat.count}]]</td>
<td>[[${product.name}]]</td>
<td>[[${product.price}]]円</td>
</tr>
</tbody>
</table>
この例では、productが一件分の商品データで、statが繰り返し処理の状態を表す変数です。商品一覧の件数だけtr要素が繰り返されるため、データが三件なら三行、十件なら十行の表が生成されます。ThymeleafではHTMLの構造を保ったまま繰り返し処理を書けるため、画面レイアウトとデータ表示の関係を確認しやすい点も大きな特徴です。
indexとcountの違いを理解しておこう
th:eachでは、二つ目の変数を指定すると反復ステータスを利用できます。特によく使うのがindexとcountです。indexは零から始まり、countは一から始まります。Javaの配列やListの添字と同じ感覚で処理したい場合はindexが便利で、画面上に人が読む番号を表示したい場合はcountが分かりやすいでしょう。
たとえば一件目ではindexが零、countが一になります。二件目ではindexが一、countが二です。初心者がよく迷う部分ですが、内部処理の位置を知りたいならindex、画面上の連番を表示したいならcountと覚えておくと整理しやすくなります。
反復ステータスには、ほかにもsize、first、last、odd、evenなどがあります。sizeでは全件数を確認でき、firstでは最初の要素かどうか、lastでは最後の要素かどうかを判定できます。また、oddとevenを利用すれば奇数行と偶数行で表示を変更できます。表の背景を交互に変えたり、最初の商品だけに目印を付けたり、最後の要素の後には区切り文字を付けないといった画面制御が可能です。
th:ifと組み合わせると一覧画面が使いやすくなる
実際のWebアプリケーションでは、一覧データが必ず存在するとは限りません。検索条件に一致する商品が一件もない場合や、登録直後でデータがまだ存在しない場合もあります。そのため、th:eachだけでなくth:ifやth:unlessを組み合わせ、データがない場合の表示も用意しておくと親切です。
<div th:if="${productList != null and !productList.isEmpty()}">
<div class="card mb-3" th:each="product, stat : ${productList}">
<div class="card-body">
<div class="fw-bold">[[${stat.count}]]件目</div>
<div>商品名:[[${product.name}]]</div>
<div>価格:[[${product.price}]]円</div>
</div>
</div>
</div>
<div class="alert alert-info" th:if="${productList == null or productList.isEmpty()}">
表示できる商品はありません。
</div>
このように一覧が存在するときだけ繰り返し表示し、一覧が空のときには案内文を表示すると、利用者は画面が壊れているのか、検索結果がないだけなのかを判断しやすくなります。Thymeleafで一覧画面を作るときは、正常にデータがある場合だけでなく、零件の場合も考えておくことが重要です。
ネストしたth:eachでは変数名を分かりやすくする
カテゴリごとに商品を表示する場合のように、th:eachの中でさらにth:eachを使うこともできます。これをネストした繰り返し処理と呼びます。親側でカテゴリを一件ずつ取り出し、そのカテゴリが持つ商品一覧を子側のth:eachで繰り返すことで、親子関係のあるデータを自然なHTML構造として表示できます。
ただし、ネストが増えるほど変数の意味が分かりにくくなるため、itemやdataのような曖昧な名前を繰り返し使うより、category、product、categoryStat、productStatのように役割が分かる名前を付けることが大切です。テンプレートは後から修正することが多いため、短さだけではなく読みやすさも意識しましょう。
Mapを繰り返すときはkeyとvalueを使う
th:eachはListや配列だけでなくMapの繰り返しにも利用できます。Mapを繰り返した場合は、一件分の要素からkeyとvalueを参照できます。たとえば商品コードと価格、都道府県コードと都道府県名、設定名と設定値など、キーと値の組み合わせを一覧表示したい場面で便利です。
Listは同じ種類のデータを順番に扱うことが得意で、Mapはキーを使って値を管理することが得意です。Thymeleafの画面側でも、Java側でどのようなコレクションを準備したのかを理解しておくと、th:eachの記述を迷いにくくなります。Spring BootとThymeleafを組み合わせる場合は、ControllerでModelへ渡したデータ型を確認しながらテンプレートを書く習慣を付けると、エラーの原因も見つけやすくなります。
大量データはテンプレートだけで処理しない
一覧が数件から数十件程度であれば、そのままth:eachで表示しても大きな問題にはなりにくいですが、数百件や数千件のデータを一度に画面へ出力すると、HTMLの量が増えて表示に時間がかかります。データベースから取得する件数も増えるため、サーバー側の処理や通信量にも影響します。
そのため、実務では必要な件数だけを取得するページングを利用する方法が一般的です。Spring Data JPAを使用している場合は、Pageableなどを利用してデータベースから一ページ分だけ取得し、その結果をThymeleafで繰り返し表示する設計にすると効率的です。テンプレートの中で複雑な計算や大きなListの切り出しを何度も行うより、ControllerやServiceで表示に必要な形へ整えてからModelへ渡すほうが、処理の役割が明確になり保守しやすくなります。
Thymeleafは画面表示を担当するテンプレートエンジンです。条件分岐や繰り返し処理を記述できますが、業務上の複雑な計算やデータ加工までテンプレートへ詰め込みすぎると、HTMLが読みにくくなります。Java側で行うべき処理と、画面側で行うべき表示制御を分けることが、Spring BootとThymeleafを使ったWebアプリケーション開発では重要です。
th:eachでエラーが出たときに確認するポイント
th:eachが思ったように動かない場合は、最初にControllerからModelへ正しい名前でデータが渡されているか確認しましょう。Java側でmodel.addAttributeに指定した名前と、HTML側の${productList}などの名前が一致していなければ、期待した一覧を参照できません。
次に、一件分のオブジェクトが持つプロパティ名を確認します。Java側がnameというプロパティを持っているのに、HTML側でproduct.titleのような別名を指定していれば、正しい値は表示されません。さらに、ネストしたオブジェクトを参照する場合は、途中の値が存在するかも確認します。エラーが起きたときは、th:eachそのものだけを見るのではなく、Modelの属性名、コレクションの中身、一件分の型、プロパティ名という順番で確認すると原因を絞り込みやすくなります。
一覧表示ではHTMLの意味も大切にする
th:eachはさまざまなHTMLタグに付けられますが、何でも同じタグで繰り返せばよいわけではありません。表形式のデータならtable、thead、tbody、tr、th、tdを適切に使い、単純な箇条書きならulやolとliを使います。商品カードのような表示ならdivやBootstrapのcardクラスを使うと整理しやすくなります。
データの意味に合ったHTMLを使うことで、ソースコードを読んだときの分かりやすさが向上し、画面の保守もしやすくなります。ThymeleafはHTMLを土台として動作するため、繰り返し処理だけでなくHTMLそのものの基本も合わせて理解することが大切です。
th:eachを覚えると作れる画面が大きく広がる
th:eachの基本を身に付けると、商品一覧や会員一覧だけでなく、検索結果、ランキング、注文履歴、コメント一覧、カテゴリ別メニュー、管理画面のデータ一覧など、多くの画面を作れるようになります。さらにth:if、th:unless、th:text、th:href、th:classappendなどと組み合わせることで、繰り返した各行に条件付きの表示やリンク、装飾を追加できます。
最初からすべての機能を覚える必要はありません。まずはth:eachでListを一件ずつ表示し、次にcountで番号を付け、その後にfirstやlast、oddやevenを使って表示を調整するという順番で練習すると理解しやすくなります。さらにMapやネストしたListへ進めば、実務で必要になる多くの一覧表示に対応できるようになります。
Thymeleafの繰り返し処理で大切なのは、単にth:eachの構文を暗記することではありません。どの一覧データを受け取り、一件分をどの変数名で扱い、画面では何を表示し、データがない場合にどう見せるのかまで考えることが重要です。Spring BootのController、Model、Thymeleafテンプレートのつながりを意識しながら練習すると、一覧画面の仕組みがより深く理解できます。
生徒
「今回の記事で、th:eachは一覧データを一件ずつ取り出してHTMLを繰り返し表示するための属性だと分かりました。」
先生
「その理解で大丈夫です。Spring BootではControllerやServiceでデータを準備し、Modelを通してThymeleafへ渡し、テンプレート側でth:eachを使って表示する流れを意識すると分かりやすいですよ。」
生徒
「product, statのように二つ変数を書くと、statから番号や最初と最後の判定も取得できるんですね。」
先生
「はい。画面に一から始まる番号を出すならcount、零から始まる位置を扱うならindexを使います。さらにfirstやlast、oddやevenを使えば、先頭だけ表示を変えたり、行ごとに見た目を変えたりできます。」
生徒
「一覧が空の場合は、th:ifを組み合わせてメッセージを表示しておくと利用者にも分かりやすいということですね。」
先生
「そのとおりです。一覧画面ではデータがある場合だけでなく、零件の場合も考えることが大切です。検索結果がないときに案内文が表示されれば、利用者も状況を理解しやすくなります。」
生徒
「カテゴリの中に商品一覧があるような場合は、th:eachを二重にして表示できるんですよね。」
先生
「できます。ただし、ネストした繰り返しでは変数名が分かりにくくなりやすいので、カテゴリ用と商品用で意味の分かる名前を付けましょう。読みやすいテンプレートにしておくと、後から修正するときも迷いません。」
生徒
「大量の商品がある場合も、全部をth:eachで表示すればよいのでしょうか。」
先生
「大量データを一度に表示するより、実務ではページングを使って必要な件数だけ取得する方法が適しています。Spring Data JPAなどで一ページ分を取得し、Thymeleafでは受け取ったデータを表示する役割に集中させると、処理も分かりやすくなります。」
生徒>
「テンプレートの中で何でも計算するのではなく、Java側で準備する処理とHTML側で表示する処理を分けることも大切なんですね。」
先生
「はい。th:eachはとても便利ですが、役割はあくまで表示のための繰り返し処理です。データ取得や複雑な業務処理はControllerやServiceなどJava側で行い、Thymeleafでは読みやすいHTMLを保つことを意識してください。」
生徒
「まずはListの一覧表示、次にcountやindex、そのあとで条件分岐やネスト、Mapの繰り返しという順番で練習してみます。」
先生
「その順番なら理解しやすいです。th:eachを使った一覧表示は、Spring BootとThymeleafでWebアプリケーションを作るときに何度も登場します。基本構文と反復ステータスをしっかり身に付けておけば、商品一覧や検索結果、管理画面などさまざまな画面へ応用できます。」