「なんでこれ表示されないの?」と向き合う日々…
最近、Webアプリ開発の学習の一環として、Spring BootとThymeleafを使ってシンプルな投稿アプリを作っています。
最初は「テンプレートエンジンってなんか難しそう…」と身構えていたものの、一覧・詳細・フォーム画面を地道に組み立てていく中で、少しずつ理解が深まってきました。
でも、そんな中でも「これはなんで動かないの?」「あ、Optionalってそういうことか…!」と、いろんな壁にぶつかりました。
この記事は、そんな**“初心者ならではのつまずき”とその解決プロセス**をまとめた備忘録です。
同じようにこれから投稿機能を作る人の助けになればうれしいです。
今日やったことメモ
今日は、Spring Boot × Thymeleafで作っているブログ風Webアプリの「記事詳細画面」を実装しました。
- 投稿一覧からタイトルをクリック → 詳細ページへ遷移
-
そのためのURL設計とテンプレートの調整
-
コントローラーで投稿を取得 → モデルに渡して表示
途中でリンクがうまく動かない、 テンプレートが更新されない、 Optionalから値が取り出せない…などなど、 いろんな「初心者あるある」に出会った一日でした。
✅ 投稿詳細ページのリンクってどう書く?
まず、<h2>
タグにリンクをつけたい場合、以下のように書けばOKです。
<h2><a th:href="@{/post/list}" th:text="${post.title}"></a></h2>
-
th:href
でリンク先を設定 -
th:text
で中身のタイトルを表示
✅ Thymeleafで動的リンクを作るには?
投稿詳細ページに遷移するリンクを、Thymeleafではこんなふうに書きます:
<a th:href="@{'/post/' + ${post.id}}" th:text="${post.title}"></a>
またはより推奨される書き方:
<a th:href="@{/post/{id}(id=${post.id})}" th:text="${post.title}"></a>
✅ URL設計はどうする?
シンプルに /post/{id}
でもいいですが、SEOやユーザーの視認性を意識するなら…
/post/123-spring-boot-intro
のように id
+ slug
がセットになった構成がオススメです。
✅ コントローラーを編集したら再起動が必要?
- テンプレートだけの変更 →
devtools
があれば再起動不要 - Javaファイルを変更したときは基本的に再起動が必要
再起動をラクにしたいなら、build.gradle
に以下を追加:
developmentOnly 'org.springframework.boot:spring-boot-devtools'
そして:
spring.thymeleaf.cache=false
を application.properties
に書けば、テンプレートの変更が即反映されやすくなります。
✅ 投稿詳細画面で発生したエラーと原因
以下のようなテンプレートで…
<h2 th:text="${post.title}"></h2>
こんなエラーが出ました:
Caused by: org.attoparser.ParseException: Exception evaluating SpringEL expression: "post.title"
❌ 原因:
model.addAttribute("post", postRepository.findById(id));
→ findById()
の戻り値は Optional<Post>
。そのままテンプレートに渡すと、post.title
にアクセスできない!
✅ 解決方法:Optionalから取り出す
Post post = postRepository.findById(id)
.orElseThrow(() -> new RuntimeException("投稿が見つかりません"));
model.addAttribute("post", post);
-
Optional
から安全に中身を取り出して渡す必要がある - そのままだとテンプレート側でアクセスできずエラーになる
✅ Optionalって何?
Optionalとは「値があるかもしれないし、ないかもしれない」ことを表す入れ物です。
nullの代わりに安全に「なさ」を扱えるJavaの仕組みです。
よく使うメソッド:
メソッド | 意味 |
---|---|
isPresent() |
値があるかどうか確認 |
get() |
中の値を取り出す(注意が必要) |
orElse(...) |
なければデフォルト値を使う |
orElseThrow() |
なければ例外を出す(←これが安全) |
✅ 一覧画面ではなぜ問題なかった?
model.addAttribute("posts", postRepository.findAll());
→ findAll()
の戻り値は List<Post>
。そのままテンプレートで使える。
th:each="post : ${posts}"
と繰り返しても問題なし。
✅ 今日の学びまとめ
内容 | ポイント |
---|---|
<h2> にリンクをつける |
<a> を中に入れて th:href を使う |
詳細ページに遷移するリンク |
/post/{id} や /post/{id}-{slug} を使う |
findById() の戻り値は? |
Optional<Post> !直接渡さず中身を取り出すこと |
テンプレート編集の反映 |
devtools +spring.thymeleaf.cache=false が便利 |
Optionalの扱い方 |
orElseThrow() が安全で実践的 |
💬 おまけ:この記事を書いた理由
今回のようなちょっとした初学者のつまずきって、実はすごく大事な学びのチャンス。
自分のメモとしても、誰かの参考になればという気持ちでまとめてみました!