【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.java1+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.java1 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.java1+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.java1 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.java1 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.java1+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.java1+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.html1 <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.html1 <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.java1+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.java1 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.java1 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.java1+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.java1+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.java1+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パスの下で処理されることを指定します。- コンストラクタインジェクションにより、
LikeService、PostService、UserServiceを注入しています。これにより、コントローラはこれらのサービス層の機能を利用してビジネスロジックを実行できます。 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.java1 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を追加し、注入されたインスタンスをフィールドに設定するように変更しました。これにより、PostControllerがLikeServiceのメソッドを使えるようになります。
-
投稿詳細ページ(
/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.html1 <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.html1 <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リクエストを送るように設定しています。このリクエストは先ほど作成したLikeControllerのtoggleLikeメソッドによって処理されます。<button type="submit" th:class="${isLiked ? 'btn btn-danger' : 'btn btn-outline-danger'}">は、「いいね」ボタンです。th:classはThymeleafの属性で、isLiked(ログインユーザーがいいねしているかどうか)の値に基づいてボタンのCSSクラスを動的に切り替えています。isLikedがtrueの場合(いいね済み)はbtn btn-dangerが適用され、赤色の塗りつぶしボタンになります。isLikedがfalseの場合(未いいね)はbtn btn-outline-dangerが適用され、赤色の枠線だけのボタンになります。
<i th:class="${isLiked ? 'bi bi-heart-fill' : 'bi bi-heart'}"></i>は、Bootstrap Iconsを使ってハートのアイコンを表示しています。th:classによってisLikedの値に基づいてアイコンを切り替えています。isLikedがtrueの場合はbi bi-heart-fillが適用され、塗りつぶされたハートアイコンが表示されます。isLikedがfalseの場合は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アプリケーションの機能拡張の具体的な流れを理解し、一歩先の開発スキルを習得できたことと思います。