【Spring Boot】いいね機能を実装する|簡単な掲示板アプリの作成

Spring Bootで開発中の掲示板アプリに「いいね機能」を追加します。いいねの情報をデータベースに保存する仕組みや、ユーザーがいいねを付けたり解除したりするサーバー側の処理、そしていいねの数や状態をウェブページに表示する方法まで、初心者にもわかりやすく解説します。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • Java: OpenJDK 23
  • Spring Boot: 3.4.0
  • Lombok: 1.18.30
  • Thymeleaf Layout Dialect: 3.3.0
  • Spring Security: 6.1.1
  • Thymeleaf Extras Spring Security: 3.1.2.RELEASE
  • maven-compiler-plugin: 3.8.1

サンプルコード

/src/main/java/com/example/bbs/controller/LikeController.java

/src/main/java/com/example/bbs/controller/LikeController.java
1+package com.example.bbs.controller;
2+
3+import com.example.bbs.model.Post;
4+import com.example.bbs.model.User;
5+import com.example.bbs.service.LikeService;
6+import com.example.bbs.service.PostService;
7+import com.example.bbs.service.UserService;
8+
9+import org.springframework.stereotype.Controller;
10+import org.springframework.web.bind.annotation.PathVariable;
11+import org.springframework.web.bind.annotation.PostMapping;
12+import org.springframework.web.bind.annotation.RequestMapping;
13+
14+@Controller
15+@RequestMapping("/posts")
16+public class LikeController {
17+
18+    private final LikeService likeService;
19+    private final PostService postService;
20+    private final UserService userService;
21+
22+    public LikeController(LikeService likeService, PostService postService, UserService userService) {
23+        this.likeService = likeService;
24+        this.postService = postService;
25+        this.userService = userService;
26+    }
27+
28+    @PostMapping("/{postId}/like")
29+    public String toggleLike(@PathVariable Long postId) {
30+        // ログインユーザーを取得
31+        User loggedInUser = userService.getCurrentUser();
32+
33+        // 投稿を取得
34+        Post post = postService.findById(postId).orElseThrow(() -> new RuntimeException("Post not found"));
35+
36+        // いいねの切り替え処理
37+        likeService.toggleLike(loggedInUser, post);
38+
39+        return "redirect:/posts/" + postId;
40+    }
41+}
42

/src/main/java/com/example/bbs/controller/PostController.java

/src/main/java/com/example/bbs/controller/PostController.java
1 import com.example.bbs.service.PostService;
2 import com.example.bbs.service.UserService;
3 import com.example.bbs.service.CommentService;
4+import com.example.bbs.service.LikeService;
5 
6 import org.springframework.data.domain.Page;
7 import org.springframework.stereotype.Controller;
8 public class PostController {
9     private final PostService postService;
10     private final CommentService commentService;
11-    private final UserService userService; // ユーザーサービスを追加
12-
13-    public PostController(PostService postService, CommentService commentService, UserService userService) {
14+    private final UserService userService;
15+    private final LikeService likeService;
16+
17+    public PostController(
18+            PostService postService,
19+            CommentService commentService,
20+            UserService userService,
21+            LikeService likeService) {
22         this.postService = postService;
23         this.commentService = commentService;
24         this.userService = userService;
25+        this.likeService = likeService;
26     }
27 
28     // 一覧表示
29         // モデルに渡す
30         model.addAttribute("loggedInUserId", loggedInUser.getId());
31 
32-        model.addAttribute("post", postService.findById(id).orElseThrow());
33+        // 投稿情報をモデルに渡す
34+        Post post = postService.findById(id).orElseThrow();
35+        model.addAttribute("post", post);
36+
37+        // コメント情報をモデルに渡す
38         model.addAttribute("comments", commentService.findByPostId(id));
39+
40+        // ログインユーザーがこの投稿に「いいね」しているかどうかを判定
41+        boolean isLiked = likeService.isLikedByUser(post, loggedInUser);
42+        model.addAttribute("isLiked", isLiked);
43+
44+        // この投稿の「いいね」の数を取得
45+        int likeCount = likeService.countLikesForPost(post);
46+        model.addAttribute("likeCount", likeCount);
47         return "posts/detail";
48     }
49 
50

/src/main/java/com/example/bbs/model/Like.java

/src/main/java/com/example/bbs/model/Like.java
1+package com.example.bbs.model;
2+
3+import jakarta.persistence.*;
4+import lombok.Getter;
5+import lombok.Setter;
6+import org.hibernate.annotations.CreationTimestamp;
7+
8+import java.time.LocalDateTime;
9+
10+@Entity
11+@Getter
12+@Setter
13+@Table(name = "likes")
14+public class Like {
15+    @Id
16+    @GeneratedValue(strategy = GenerationType.IDENTITY)
17+    private Long id;
18+
19+    @ManyToOne
20+    @JoinColumn(name = "user_id")
21+    private User user;
22+
23+    @ManyToOne
24+    @JoinColumn(name = "post_id")
25+    private Post post;
26+
27+    @Column(name = "created_at", nullable = false, updatable = false)
28+    @CreationTimestamp
29+    private LocalDateTime createdAt;
30+}
31

/src/main/java/com/example/bbs/model/Post.java

/src/main/java/com/example/bbs/model/Post.java
1 
2 import java.util.ArrayList;
3 import java.util.List;
4+import java.util.Set;
5 
6 import java.time.LocalDateTime;
7 
8     @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
9     private List<Comment> comments = new ArrayList<>();
10 
11+    @OneToMany(mappedBy = "post")
12+    private Set<Like> likes;
13+
14     private LocalDateTime createdAt = LocalDateTime.now();
15     private LocalDateTime updatedAt = LocalDateTime.now();
16 
17

/src/main/java/com/example/bbs/model/User.java

/src/main/java/com/example/bbs/model/User.java
1 
2 import jakarta.persistence.*;
3 import lombok.*;
4-import lombok.Getter;
5 
6 import java.util.List;
7+import java.util.Set;
8 
9 @Entity
10 @Getter
11 
12     @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
13     private List<Comment> comments;
14+
15+    @OneToMany(mappedBy = "user")
16+    private Set<Like> likes;
17 }
18

/src/main/java/com/example/bbs/repository/LikeRepository.java

/src/main/java/com/example/bbs/repository/LikeRepository.java
1+package com.example.bbs.repository;
2+
3+import com.example.bbs.model.Like;
4+import com.example.bbs.model.Post;
5+import com.example.bbs.model.User;
6+import org.springframework.data.jpa.repository.JpaRepository;
7+import org.springframework.stereotype.Repository;
8+
9+import java.util.Optional;
10+
11+@Repository
12+public interface LikeRepository extends JpaRepository<Like, Long> {
13+    Optional<Like> findByUserAndPost(User user, Post post);
14+
15+    // 投稿とユーザーに関連付けられた「いいね」が存在するかを判定するメソッド
16+    boolean existsByPostAndUser(Post post, User user);
17+
18+    // 投稿に対する「いいね」の数をカウントするメソッド
19+    int countByPost(Post post);
20+
21+}
22

/src/main/java/com/example/bbs/service/LikeService.java

/src/main/java/com/example/bbs/service/LikeService.java
1+package com.example.bbs.service;
2+
3+import com.example.bbs.model.Like;
4+import com.example.bbs.model.Post;
5+import com.example.bbs.model.User;
6+import com.example.bbs.repository.LikeRepository;
7+
8+import org.springframework.stereotype.Service;
9+
10+import java.util.Optional;
11+
12+@Service
13+public class LikeService {
14+
15+    private LikeRepository likeRepository;
16+
17+    public LikeService(LikeRepository likeRepository) {
18+        this.likeRepository = likeRepository;
19+    }
20+
21+    public void toggleLike(User user, Post post) {
22+        Optional<Like> existingLike = likeRepository.findByUserAndPost(user, post);
23+
24+        if (existingLike.isPresent()) {
25+            likeRepository.delete(existingLike.get()); // いいねを解除
26+        } else {
27+            Like like = new Like();
28+            like.setUser(user);
29+            like.setPost(post);
30+            likeRepository.save(like); // いいねを追加
31+        }
32+    }
33+
34+    // ユーザーがこの投稿に対して「いいね」しているかを判定するロジック
35+    public boolean isLikedByUser(Post post, User user) {
36+        return likeRepository.existsByPostAndUser(post, user);
37+    }
38+
39+    // この投稿に対する「いいね」の数をカウントする
40+    public int countLikesForPost(Post post) {
41+        return likeRepository.countByPost(post);
42+    }
43+}
44

/src/main/resources/templates/layout/layout.html

/src/main/resources/templates/layout/layout.html
1     <title layout:fragment="title">掲示板</title>
2     <!-- Bootstrap CSS -->
3     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
4+    <!-- Bootstrap icon -->
5+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
6 </head>
7 <body>
8     <!-- Header -->
9

/src/main/resources/templates/posts/detail.html

/src/main/resources/templates/posts/detail.html
1             <button type="submit" class="btn btn-danger">Delete</button>
2         </form>
3     </span>
4+    <!-- いいねボタン -->
5+    <form th:action="@{/posts/{id}/like(id=${post.id})}" method="POST">
6+        <button type="submit" th:class="${isLiked ? 'btn btn-danger' : 'btn btn-outline-danger'}">
7+            <i th:class="${isLiked ? 'bi bi-heart-fill' : 'bi bi-heart'}"></i>
8+        </button>
9+    </form>
10+
11+    <!-- いいね数の表示 -->
12+    <p><span th:text="${likeCount}"></span> 件のいいね</p>
13 
14     <h2>Comments</h2>
15     <div class="mb-3">
16

コード解説

変更点: いいね機能のデータ構造(Likeエンティティ)の新規作成

/src/main/java/com/example/bbs/model/Like.java
1+package com.example.bbs.model;
2+
3+import jakarta.persistence.*;
4+import lombok.Getter;
5+import lombok.Setter;
6+import org.hibernate.annotations.CreationTimestamp;
7+
8+import java.time.LocalDateTime;
9+
10+@Entity
11+@Getter
12+@Setter
13+@Table(name = "likes")
14+public class Like {
15+    @Id
16+    @GeneratedValue(strategy = GenerationType.IDENTITY)
17+    private Long id;
18+
19+    @ManyToOne
20+    @JoinColumn(name = "user_id")
21+    private User user;
22+
23+    @ManyToOne
24+    @JoinColumn(name = "post_id")
25+    private Post post;
26+
27+    @Column(name = "created_at", nullable = false, updatable = false)
28+    @CreationTimestamp
29+    private LocalDateTime createdAt;
30+}

ここでは、「いいね」の情報をデータベースに保存するための新しいクラス Like.java を作成しました。これは Entity(エンティティ)と呼ばれ、データベースのテーブルと対応するJavaのクラスです。

  • @Entity アノテーションは、このクラスがJPA(Java Persistence API)のエンティティであることを示します。
  • @Table(name = "likes") は、このエンティティがデータベースの likes という名前のテーブルにマッピングされることを指定しています。
  • @Id@GeneratedValue(strategy = GenerationType.IDENTITY) は、id フィールドが主キーであり、データベースが自動的に値を生成(自動採番)することを意味します。
  • @ManyToOne は、多対一のリレーションシップを示します。つまり、1つの「いいね」は1人の User(ユーザー)と1つの Post(投稿)に紐づいていることを表します。
  • @JoinColumn(name = "user_id")@JoinColumn(name = "post_id") は、データベースの likes テーブルに user_id 列や post_id 列が外部キーとして存在し、それぞれ users テーブルと posts テーブルを参照していることを定義します。
  • @CreationTimestamp は、この createdAt フィールドに「いいね」が作成された日時が自動的に記録されるようにするアノテーションです。
  • @Getter@Setter は、Lombokというライブラリのアノテーションで、フィールドのゲッターメソッドとセッターメソッドを自動で生成してくれるため、コードを簡潔に書くことができます。

この Like エンティティによって、誰が(User)、どの投稿に(Post)、いつ(createdAt)「いいね」をしたかという情報をデータベースで管理できるようになります。

変更点: 投稿モデル(Post)へのいいね情報の追加

/src/main/java/com/example/bbs/model/Post.java
1 
2 import java.util.ArrayList;
3 import java.util.List;
4+import java.util.Set;
5 
6 import java.time.LocalDateTime;
7 
8     @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
9     private List<Comment> comments = new ArrayList<>();
10 
11+    @OneToMany(mappedBy = "post")
12+    private Set<Like> likes;
13+
14     private LocalDateTime createdAt = LocalDateTime.now();
15     private LocalDateTime updatedAt = LocalDateTime.now();
16 

Post.java ファイルに likes という新しいフィールドを追加しました。

  • @OneToMany(mappedBy = "post") アノテーションは、1つの Post(投稿)が複数の Like(いいね)を持つという「一対多」のリレーションシップ(関係性)を示しています。
  • mappedBy = "post" は、このリレーションシップが Like エンティティ内の post フィールドによってマッピングされていることを意味します。つまり、「いいね」の方が「投稿」に紐付いていることを示し、Post エンティティ側では新しい外部キー列を持つ必要がありません。
  • Set<Like> likes; は、この投稿に関連付けられた「いいね」の集合を保持するためのフィールドです。List ではなく Set を使うことで、同じ「いいね」が重複して登録されるのを防ぐことができます。

この変更により、Post エンティティから、その投稿に付いているすべての「いいね」の情報を簡単に取得できるようになります。

変更点: ユーザーモデル(User)へのいいね情報の追加

/src/main/java/com/example/bbs/model/User.java
1 
2 import jakarta.persistence.*;
3 import lombok.*;
4-import lombok.Getter;
5 
6 import java.util.List;
7+import java.util.Set;
8 
9 @Entity
10 @Getter
11 
12     @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
13     private List<Comment> comments;
14+
15+    @OneToMany(mappedBy = "user")
16+    private Set<Like> likes;
17 }

User.java ファイルに likes という新しいフィールドを追加しました。

  • @OneToMany(mappedBy = "user") アノテーションは、1人の User(ユーザー)が複数の Like(いいね)を持つという「一対多」のリレーションシップを示しています。
  • mappedBy = "user" は、このリレーションシップが Like エンティティ内の user フィールドによってマッピングされていることを意味します。つまり、「いいね」の方が「ユーザー」に紐付いていることを示し、User エンティティ側では新しい外部キー列を持つ必要がありません。
  • Set<Like> likes; は、このユーザーがした「いいね」の集合を保持するためのフィールドです。List ではなく Set を使うことで、同じ「いいね」が重複して登録されるのを防ぐことができます。

この変更により、User エンティティから、そのユーザーがしたすべての「いいね」の情報を簡単に取得できるようになります。

変更点: いいねリポジトリ(LikeRepository)の新規作成

/src/main/java/com/example/bbs/repository/LikeRepository.java
1+package com.example.bbs.repository;
2+
3+import com.example.bbs.model.Like;
4+import com.example.bbs.model.Post;
5+import com.example.bbs.model.User;
6+import org.springframework.data.jpa.repository.JpaRepository;
7+import org.springframework.stereotype.Repository;
8+
9+import java.util.Optional;
10+
11+@Repository
12+public interface LikeRepository extends JpaRepository<Like, Long> {
13+    Optional<Like> findByUserAndPost(User user, Post post);
14+
15+    // 投稿とユーザーに関連付けられた「いいね」が存在するかを判定するメソッド
16+    boolean existsByPostAndUser(Post post, User user);
17+
18+    // 投稿に対する「いいね」の数をカウントするメソッド
19+    int countByPost(Post post);
20+
21+}

ここでは、「いいね」のデータをデータベースから取得したり保存したりするための LikeRepository インターフェースを新しく作成しました。

  • @Repository アノテーションは、このインターフェースがデータアクセス層のコンポーネントであることをSpringに示します。
  • JpaRepository<Like, Long> を継承することで、Like エンティティ(データベースの likes テーブル)に対して、保存、検索、削除といった基本的なデータベース操作メソッドが自動的に提供されます。<Like, Long>Like は対象のエンティティクラス、Long はそのエンティティの主キーの型を示します。
  • Optional<Like> findByUserAndPost(User user, Post post); は、特定のユーザーと特定の投稿に紐づく「いいね」が存在するかどうかを検索するためのカスタムメソッドです。Optional は、値が存在しない可能性を表すために使用され、ヌルポインタ例外を防ぐのに役立ちます。
  • boolean existsByPostAndUser(Post post, User user); は、特定の投稿とユーザーの組み合わせで「いいね」が存在するかどうかを真偽値で判定するためのメソッドです。
  • int countByPost(Post post); は、特定の投稿に付けられた「いいね」の総数をカウントするためのメソッドです。

Spring Data JPAでは、このようにメソッドの名前の付け方によって、SQLクエリを自動的に生成してくれます。これにより、開発者は煩雑なSQLを書く手間を省き、ビジネスロジックに集中できるようになります。

変更点: いいねサービス(LikeService)の新規作成

/src/main/java/com/example/bbs/service/LikeService.java
1+package com.example.bbs.service;
2+
3+import com.example.bbs.model.Like;
4+import com.example.bbs.model.Post;
5+import com.example.bbs.model.User;
6+import com.example.bbs.repository.LikeRepository;
7+
8+import org.springframework.stereotype.Service;
9+
10+import java.util.Optional;
11+
12+@Service
13+public class LikeService {
14+
15+    private LikeRepository likeRepository;
16+
17+    public LikeService(LikeRepository likeRepository) {
18+        this.likeRepository = likeRepository;
19+    }
20+
21+    public void toggleLike(User user, Post post) {
22+        Optional<Like> existingLike = likeRepository.findByUserAndPost(user, post);
23+
24+        if (existingLike.isPresent()) {
25+            likeRepository.delete(existingLike.get()); // いいねを解除
26+        } else {
27+            Like like = new Like();
28+            like.setUser(user);
29+            like.setPost(post);
30+            likeRepository.save(like); // いいねを追加
31+        }
32+    }
33+
34+    // ユーザーがこの投稿に対して「いいね」しているかを判定するロジック
35+    public boolean isLikedByUser(Post post, User user) {
36+        return likeRepository.existsByPostAndUser(post, user);
37+    }
38+
39+    // この投稿に対する「いいね」の数をカウントする
40+    public int countLikesForPost(Post post) {
41+        return likeRepository.countByPost(post);
42+    }
43+}

ここでは、「いいね」に関するビジネスロジックを管理する LikeService クラスを新しく作成しました。

  • @Service アノテーションは、このクラスがビジネスロジックを処理するサービス層のコンポーネントであることをSpringに示します。
  • コンストラクタインジェクションにより LikeRepository を受け取り、このサービス内でデータベース操作を行うために利用します。
  • toggleLike(User user, Post post) メソッドは、「いいね」の追加と解除を切り替える主要な処理です。
    • まず likeRepository.findByUserAndPost(user, post) を使って、指定されたユーザーがその投稿に既に「いいね」しているかどうかを確認します。
    • もし existingLike.isPresent()true であれば、既に「いいね」が存在するため、likeRepository.delete() を呼び出してその「いいね」をデータベースから削除(解除)します。
    • もし existingLike.isPresent()false であれば、まだ「いいね」が存在しないため、新しい Like オブジェクトを作成し、ユーザーと投稿を設定してから likeRepository.save() を呼び出してデータベースに保存(追加)します。
  • isLikedByUser(Post post, User user) メソッドは、特定のユーザーが特定の投稿に「いいね」しているかどうかを判定するために likeRepository.existsByPostAndUser() を利用します。
  • countLikesForPost(Post post) メソッドは、特定の投稿に対する「いいね」の総数を取得するために likeRepository.countByPost() を利用します。

このようにサービス層でビジネスロジックを集中管理することで、コードの見通しが良くなり、再利用性や保守性が向上します。

変更点: いいね機能用コントローラ(LikeController)の新規作成

/src/main/java/com/example/bbs/controller/LikeController.java
1+package com.example.bbs.controller;
2+
3+import com.example.bbs.model.Post;
4+import com.example.bbs.model.User;
5+import com.example.bbs.service.LikeService;
6+import com.example.bbs.service.PostService;
7+import com.example.bbs.service.UserService;
8+
9+import org.springframework.stereotype.Controller;
10+import org.springframework.web.bind.annotation.PathVariable;
11+import org.springframework.web.bind.annotation.PostMapping;
12+import org.springframework.web.bind.annotation.RequestMapping;
13+
14+@Controller
15+@RequestMapping("/posts")
16+public class LikeController {
17+
18+    private final LikeService likeService;
19+    private final PostService postService;
20+    private final UserService userService;
21+
22+    public LikeController(LikeService likeService, PostService postService, UserService userService) {
23+        this.likeService = likeService;
24+        this.postService = postService;
25+        this.userService = userService;
26+    }
27+
28+    @PostMapping("/{postId}/like")
29+    public String toggleLike(@PathVariable Long postId) {
30+        // ログインユーザーを取得
31+        User loggedInUser = userService.getCurrentUser();
32+
33+        // 投稿を取得
34+        Post post = postService.findById(postId).orElseThrow(() -> new RuntimeException("Post not found"));
35+
36+        // いいねの切り替え処理
37+        likeService.toggleLike(loggedInUser, post);
38+
39+        return "redirect:/posts/" + postId;
40+    }
41+}

ここでは、「いいね」に関するユーザーからのHTTPリクエストを処理するための LikeController を新しく作成しました。

  • @Controller アノテーションは、このクラスがSpring MVCのコントローラであることを示し、HTTPリクエストを処理する役割を持つことを定義します。
  • @RequestMapping("/posts") は、このコントローラ内のすべてのアクションが /posts というURLパスの下で処理されることを指定します。
  • コンストラクタインジェクションにより、LikeServicePostServiceUserService を注入しています。これにより、コントローラはこれらのサービス層の機能を利用してビジネスロジックを実行できます。
  • toggleLike メソッドは、「いいね」の追加・解除リクエストを処理します。
    • @PostMapping("/{postId}/like") は、http://localhost:8080/posts/{postId}/like のようなURLへのPOSTリクエストに対応します。{postId} の部分はURLから取得する値で、@PathVariable Long postId によってJavaの変数 postId にマッピングされます。
    • まず userService.getCurrentUser() を呼び出して、現在ログインしているユーザーの情報を取得します。
    • 次に postService.findById(postId) を使って、リクエストされた postId に対応する投稿を取得します。もし投稿が見つからなければ、orElseThrow で例外を発生させます。
    • 取得したユーザーと投稿の情報を likeService.toggleLike(loggedInUser, post) に渡すことで、「いいね」の追加または解除のビジネスロジックを実行します。
    • return "redirect:/posts/" + postId; は、処理が完了した後、投稿の詳細ページにブラウザをリダイレクトさせることを意味します。これにより、ユーザーは「いいね」の変更結果が反映されたページに戻ります。

このコントローラによって、ユーザーはウェブページから「いいね」ボタンをクリックするだけで、サーバー側で「いいね」の追加や解除を行うことができるようになります。

変更点: 投稿コントローラ(PostController)へのいいね関連機能の追加

/src/main/java/com/example/bbs/controller/PostController.java
1 import com.example.bbs.service.PostService;
2 import com.example.bbs.service.UserService;
3 import com.example.bbs.service.CommentService;
4+import com.example.bbs.service.LikeService;
5 
6 import org.springframework.data.domain.Page;
7 import org.springframework.stereotype.Controller;
8 public class PostController {
9     private final PostService postService;
10     private final CommentService commentService;
11-    private final UserService userService; // ユーザーサービスを追加
12-
13-    public PostController(PostService postService, CommentService commentService, UserService userService) {
14+    private final UserService userService;
15+    private final LikeService likeService;
16+
17+    public PostController(
18+            PostService postService,
19+            CommentService commentService,
20+            UserService userService,
21+            LikeService likeService) {
22         this.postService = postService;
23         this.commentService = commentService;
24         this.userService = userService;
25+        this.likeService = likeService;
26     }
27 
28     // 一覧表示
29         // モデルに渡す
30         model.addAttribute("loggedInUserId", loggedInUser.getId());
31 
32-        model.addAttribute("post", postService.findById(id).orElseThrow());
33+        // 投稿情報をモデルに渡す
34+        Post post = postService.findById(id).orElseThrow();
35+        model.addAttribute("post", post);
36+
37+        // コメント情報をモデルに渡す
38         model.addAttribute("comments", commentService.findByPostId(id));
39+
40+        // ログインユーザーがこの投稿に「いいね」しているかどうかを判定
41+        boolean isLiked = likeService.isLikedByUser(post, loggedInUser);
42+        model.addAttribute("isLiked", isLiked);
43+
44+        // この投稿の「いいね」の数を取得
45+        int likeCount = likeService.countLikesForPost(post);
46+        model.addAttribute("likeCount", likeCount);
47         return "posts/detail";
48     }
49 

既存の PostController.java ファイルに、いいね機能に関連する変更を加えました。

  • LikeService の追加:

    • import com.example.bbs.service.LikeService;LikeService クラスをインポートしました。
    • private final LikeService likeService;LikeService のインスタンスをフィールドとして宣言しました。
    • コンストラクタの引数に LikeService likeService を追加し、注入されたインスタンスをフィールドに設定するように変更しました。これにより、PostControllerLikeService のメソッドを使えるようになります。
  • 投稿詳細ページ(/posts/{id})の表示処理の変更:

    • detail メソッド内で、投稿の情報を取得した後、以下の2つの情報を追加で取得し、モデル(model.addAttribute)に渡すようにしました。
      • boolean isLiked = likeService.isLikedByUser(post, loggedInUser);:現在ログインしているユーザーが、この投稿に「いいね」しているかどうかを LikeService を使って判定し、その結果(true または false)を isLiked という名前でモデルに追加します。
      • int likeCount = likeService.countLikesForPost(post);:この投稿に付けられている「いいね」の総数を LikeService を使って取得し、その数を likeCount という名前でモデルに追加します。

これらの変更により、投稿の詳細ページを表示する際に、その投稿に対する「いいね」の数と、ログインユーザーが「いいね」済みかどうかの状態も合わせて取得し、ウェブページに表示できるようになります。

変更点: レイアウトファイルへのBootstrapアイコンの追加

/src/main/resources/templates/layout/layout.html
1     <title layout:fragment="title">掲示板</title>
2     <!-- Bootstrap CSS -->
3     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
4+    <!-- Bootstrap icon -->
5+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
6 </head>
7 <body>
8     <!-- Header -->

ウェブページの共通レイアウトを定義している layout.html ファイルに、Bootstrap Iconsを使用するためのCSSを読み込む <link> タグを追加しました。

  • <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> の行が新しく追加されています。
  • この行は、ウェブページが表示される際に、CDN(Content Delivery Network)経由でBootstrap Iconsのスタイルシートを読み込みます。
  • Bootstrap Iconsは、ウェブサイトでよく使われるアイコン(例:ハート、鉛筆、ゴミ箱など)を簡単に表示できるCSSアイコンライブラリです。
  • この追加により、後述する投稿詳細ページで「いいね」を示すハートマークのアイコンを簡単に表示できるようになります。

変更点: 投稿詳細ページ(detail.html)へのいいねボタンと数の表示

/src/main/resources/templates/posts/detail.html
1             <button type="submit" class="btn btn-danger">Delete</button>
2         </form>
3     </span>
4+    <!-- いいねボタン -->
5+    <form th:action="@{/posts/{id}/like(id=${post.id})}" method="POST">
6+        <button type="submit" th:class="${isLiked ? 'btn btn-danger' : 'btn btn-outline-danger'}">
7+            <i th:class="${isLiked ? 'bi bi-heart-fill' : 'bi bi-heart'}"></i>
8+        </button>
9+    </form>
10+
11+    <!-- いいね数の表示 -->
12+    <p><span th:text="${likeCount}"></span> 件のいいね</p>
13 
14     <h2>Comments</h2>
15     <div class="mb-3">

投稿の詳細ページを表示する detail.html ファイルに、「いいね」ボタンと「いいね」の数を表示するためのHTMLコードを追加しました。

  • いいねボタンのフォーム:

    • <form th:action="@{/posts/{id}/like(id=${post.id})}" method="POST"> という Thymeleaf の構文を使って、このフォームが PostController から渡された post.id を使って /posts/{id}/like というURLにPOSTリクエストを送るように設定しています。このリクエストは先ほど作成した LikeControllertoggleLike メソッドによって処理されます。
    • <button type="submit" th:class="${isLiked ? 'btn btn-danger' : 'btn btn-outline-danger'}"> は、「いいね」ボタンです。th:class はThymeleafの属性で、isLiked(ログインユーザーがいいねしているかどうか)の値に基づいてボタンのCSSクラスを動的に切り替えています。
      • isLikedtrue の場合(いいね済み)は btn btn-danger が適用され、赤色の塗りつぶしボタンになります。
      • isLikedfalse の場合(未いいね)は btn btn-outline-danger が適用され、赤色の枠線だけのボタンになります。
    • <i th:class="${isLiked ? 'bi bi-heart-fill' : 'bi bi-heart'}"></i> は、Bootstrap Iconsを使ってハートのアイコンを表示しています。th:class によって isLiked の値に基づいてアイコンを切り替えています。
      • isLikedtrue の場合は bi bi-heart-fill が適用され、塗りつぶされたハートアイコンが表示されます。
      • isLikedfalse の場合は bi bi-heart が適用され、枠線だけのハートアイコンが表示されます。
  • いいね数の表示:

    • <p><span th:text="${likeCount}"></span> 件のいいね</p> は、投稿に対する「いいね」の総数を表示しています。th:text="${likeCount}"PostController から渡された likeCount の値をこの <span> タグの中に表示します。

これらの変更により、ユーザーは投稿詳細ページで「いいね」の状態を視覚的に確認し、「いいね」の追加や解除を簡単に行えるようになります。

おわりに

おわりに

今回の記事では、掲示板アプリに「いいね機能」を追加する方法を学びました。まず、Likeエンティティと対応するリポジトリ、サービス、コントローラを新しく作成し、データベースでのいいね情報の管理とサーバー側の処理を実装しました。また、既存のPostモデルとUserモデルにはLikeとの関連を追加し、情報連携をスムーズにしました。フロントエンドでは、ThymeleafとBootstrap Iconsを活用して、ログインユーザーの「いいね済み」状態や「いいね数」を動的に表示するUIを構築できました。これにより、Spring Bootアプリケーションの機能拡張の具体的な流れを理解し、一歩先の開発スキルを習得できたことと思います。

関連コンテンツ

関連IT用語