質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.46%
Spring Boot

Spring Bootは、Javaのフレームワークの一つ。Springプロジェクトが提供する様々なフレームワークを統合した、アプリケーションを高速で開発するために設計されたフレームワークです。

Q&A

解決済

1回答

3761閲覧

Springboot JPAを使用しオブジェクト間の関連を設定。ブログにコメントが投稿できるようにしたい。

yt07

総合スコア7

Spring Boot

Spring Bootは、Javaのフレームワークの一つ。Springプロジェクトが提供する様々なフレームワークを統合した、アプリケーションを高速で開発するために設計されたフレームワークです。

0グッド

0クリップ

投稿2020/06/15 08:45

編集2020/06/16 08:42

SpringBoot初心者です。
@ManyToOne,@OneToManyを利用し、ブログ(blogクラス)に対してのコメント(commentクラス)機能を追加できるようにしたいのですが「フィールド「ブログ」のオブジェクト「コメント」のフィールドエラー」という内容でエラーが生じコメントが表示されません。

①ブログは複数のコメントを持ち(1対多)、コメントは1つのブログに結びついている(多対1)。
②blog,commentに各リポジトリインタフェースを作成。
③コントローラにblogページを表示する際にcommentオブジェクトを作成しModelに追加。commentリポジトリインタフェースを使用し、フォーム送信された内容に投稿日時を付加して保存する。

XAMPPのMySQLを使用してます。commentテーブルにblog_idは生成されてリレーションはできているはずですが、結果表示が思うようにいきません。
フィールドの誤字や引数に間違いがあるのではと思い確認しましたが特に間違いはありませんでした。
ご教授願います。

application.properties

spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://localhost:3306/データベース名?serverTimezone=JST spring.datasource.username=ユーザー名 spring.datasource.password=パスワード

blog.java

package com.example.demo; import java.time.LocalDateTime; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import lombok.Data; @Entity //JPAにテーブルに保存するクラス(エンティティ)であることを示す。 @Data public class Blog { @Id //対応するテーブルのプライマリーキー(主キー)となるフィールド @GeneratedValue(strategy=GenerationType.AUTO) //プライマリーキーの値を連番で自動生成。 private Integer id; private String title; private LocalDateTime postDateTime; @Column(length=1000) //テーブルの列を制御したい場合は@Columnをつける。フィールドの長さをデフォルトの255から1000にする。 private String contents; @OneToMany(mappedBy = "blog", cascade = CascadeType.ALL) //mappedBy引数には「相手側が自分を参照する名前」を指定。 private List<Comment> comments; public void addComment(Comment comment) { comment.setBlog(this); comments.add(comment); } }

comment.java

package com.example.demo; import java.time.LocalDateTime; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.ManyToOne; import lombok.Data; @Entity @Data public class Comment { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String text; private LocalDateTime postDateTime; @ManyToOne //多対1の関係。Commentは一つブログに結びついている。 private Blog blog; } }

BlogRepository.java

package com.example.demo; import org.springframework.data.jpa.repository.JpaRepository; public interface BlogRepository extends JpaRepository<Blog, Integer> { }

CommentRepository.java

package com.example.demo; import org.springframework.data.jpa.repository.JpaRepository; public interface CommentRepository extends JpaRepository<Comment, Integer>{ }

SampleController.java

package com.example.demo; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @Controller public class SampleController { @Autowired //リポジトリインターフェース参照、追加、更新などのメソッドがあらかじめ定義されている。findAllメソッドを使用することで単純にテーブル内のレコードを一覧で返す。 private BlogRepository blogRepository; @Autowired private CommentRepository commentRepository; @GetMapping("/") public String index(Model model) { List<Blog> blogs = blogRepository.findAll(); model.addAttribute("blogs", blogs); return "index"; } @GetMapping("/form") public String form(Blog blog) { return "form"; } @PostMapping("/create") public String create(Blog blog) { blog.setPostDateTime(LocalDateTime.now()); blogRepository.save(blog); //エンティティを保存するにはリポジトリインターフェースのsaveメソッドを使用。 //エンティティをsaveすると@Idアノテーションを付けたフィールドに値が設定される。 return "redirect:/blog/" + blog.getId(); } @GetMapping("/blog/{id}") public String blog(@PathVariable Integer id, Model model) { Optional<Blog> blog = blogRepository.findById(id); //返されたオブジェクトがnullでなければこの処理を行うなど、nullの場合の処理を書きやすくするためのもの。 //getメソッドを呼び出して中身を取り出す。 model.addAttribute("blog", blog.get()); Comment comment = new Comment(); comment.setBlog(blog.get()); model.addAttribute("comment", comment); return "/blog"; } @Transactional @PostMapping("/blog/{blogId}/comment") public String createComment(@PathVariable("blogId") Integer id, Comment comment) { comment.setPostDateTime(LocalDateTime.now()); Optional<Blog> optionalBlog = blogRepository.findById(id); Blog blog = optionalBlog.orElseThrow(RuntimeException::new); blog.addComment(comment); blogRepository.save(blog); return "redirect:/blog/" + id; } }

blog.html

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title th:text="${blog.title}">ブログタイトル</title> </head> <body> <p> <a href="/">一覧に戻る</a> </p> <div th:object="${blog}"> <h1 th:text="*{title}">タイトル</h1> <div> 投稿日時 <span th:text="*{postDateTime}">投稿日時</span> </div> <p> <th:block th:each="line : *{contents.split('\n')}"> <th:block th:text="${line}"></th:block><br> </th:block> </p> <form th:action="@{/blog/{blogId}/comment(blogId=${blog.id})}" method="post" th:object="${comment}"> コメントをどうぞ<br> <input type="hidden" name="blog" th:value="*{blog.id}"> <input type="text" size="40" th:field="*{text}"> <input type="submit"> </form> <ul> <li th:each="c : *{comments}" th:object="${c}"> <span th:text="*{postDateTime}"></span> <span th:text="*{text}"></span> </li> </ul> </div> </body> </html>

エラー内容

Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback. Mon Jun 15 17:32:58 JST 2020 There was an unexpected error (type=Bad Request, status=400). Validation failed for object='comment'. Error count: 1 org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors Field error in object 'comment' on field 'blog': rejected value [4]; codes [typeMismatch.comment.blog,typeMismatch.blog,typeMismatch.com.example.demo.Blog,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [comment.blog,blog]; arguments []; default message [blog]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'com.example.demo.Blog' for property 'blog'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.example.demo.Blog' for property 'blog': no matching editors or conversion strategy found]

blogテーブルとcommentテーブル

index.html(トプページ。配列でblogタイトルと日時を出し、aタグでidのページに行きます)

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org/"> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <p> <a href="/form">新規作成</a> </p> <ul> <li th:each="blog : ${blogs}" th:object="${blog}"> <a th:href="@{|/blog/*{id}|}" th:text="*{title}">タイトル</a> <span th:text="*{postDateTime}">投稿日時</span> </li> </ul> </body> </html>

form.html

<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>新規ブログ</title> </head> <body> <form action="/create" method="post" th:object="${blog}"> <div> タイトル<br> <input type="text" size="40" th:field="*{title}"> </div> <div> 内容<br> <textarea rows="5" cols="60" th:field="*{contents}"></textarea> </div> <div> <input type="submit"> </div> </form> </body> </html>

テスト

送信するとエラー
error

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/06/15 09:47

なんのカラムをベースに結合すれとかこれじゃわかんないよ
rubytomato

2020/06/15 09:49

エラーメッセージを見るとコントローラーのエンドポイントを呼んだときにエラーが起きているように思います。 なので、もう少し質問内容を補足してください。例えば以下の点がわかると回答しやすくなるとおもいます。 1) 呼び出したエンドポイント(URL) 2) 呼び出し時のhtmlページのコードと入力データの例 3) 念のために2つのテーブルのDDL文
退会済みユーザー

退会済みユーザー

2020/06/15 12:54

rubytomato エラーは数値を受け取ったが対象のフィールドはブログ型だった。だしな
guest

回答1

0

ベストアンサー

質問内容の追記ありがとうございました。以下にエラーの原因と改修案を回答しましたのでご確認ください。

エラーの原因

blog.html

下記のフォームでhiddenのblogフィールドにblog.idの値をセットしていますが

html

1<form action="/comment" method="post" th:object="${comment}"> 2 コメントをどうぞ<br> 3 <input type="hidden" name="blog" th:value="*{blog.id}"> 4 <input type="text" size="40" th:field="*{text}"> 5 <input type="submit"> 6</form>

SampleController.java

エンドポイントの/commentで受け取るCommentクラスのblogフィールドの型はBlogクラスです。

@PostMapping("/comment") public String createComment(Comment comment) { // 省略 }

Comment.java

@Entity @Data public class Comment { // 省略 @ManyToOne private Blog blog; }

エラーメッセージ

このため、ポストされたデータをCommentクラスへ変換しようとする際に、下記のエラーが発生しているように思います。

default message [Failed to convert property value of type 'java.lang.String' to required type 'com.example.demo.Blog' for property 'blog'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.example.demo.Blog' for property 'blog': no matching editors or conversion strategy found]
タイプ「java.lang.String」のプロパティ値をプロパティ「ブログ」の必須タイプ「com.example.demo.Blog」に変換できませんでした。 ネストされた例外はjava.lang.IllegalStateExceptionです:プロパティ 'blog'のタイプ 'java.lang.String'の値を必要なタイプ 'com.example.demo.Blog'に変換できません:一致するエディターまたは変換戦略が見つかりません

改修案

改修案はいくつか考えられるのですが、そのうちの1つを以下に提示しますので参考になさってください。
あくまでも1案ですので、このように改修しなければならないということはありません。

Blog.java

BlogクラスにaddCommentというメソッドを追加し、ブログにコメントを追加する時の実装を行います。
また@OneToManyアノテーションのcascade属性はリレーションシップのあるテーブルの更新に必要です。

なお、私の環境ではGenerationType.AUTOだと動作しなかったのでGenerationType.IDENTITYに変えていますが、AUTOで問題ないようであればそのままで結構です。

java

1@Entity 2@Data 3public class Blog { 4 5 @Id 6 //@GeneratedValue(strategy = GenerationType.AUTO) 7 @GeneratedValue(strategy = GenerationType.IDENTITY) 8 private Integer id; 9 10 // 省略 11 12 // 修正 13 @OneToMany(mappedBy="blog", cascade = CascadeType.ALL) 14 private List<Comment> comments; 15 16 // 追加 17 public void addComment(Comment comment) { 18 comment.setBlog(this); 19 comments.add(comment); 20 } 21 22}

SampleController.java

次にエンドポイントを/commentから/blog/{blogId}/commentのように変更し、コメントを追加したいブログIDはフォームに埋め込むのではなくURL上に表現します。
同時にハンドラメソッドも以下のように修正します。
@Transactionalアノテーションはトランザクション境界を指定するためのもので、特に更新を行う場合は指定します。

java

1@Transactional 2@PostMapping("/blog/{blogId}/comment") 3public String createComment(@PathVariable("blogId") Integer id, Comment comment) { 4 comment.setPostDateTime(LocalDateTime.now()); 5 6 // コメントを追加するブログがあるかどうか確認する 7 Optional<Blog> optionalBlog = blogRepository.findById(id); 8 // なければ例外をスロー 9 Blog blog = optionalBlog.orElseThrow(RuntimeException::new); 10 11 // 存在するブログに対してコメントを追加して永続化 12 blog.addComment(comment); 13 blogRepository.save(blog); 14 15 return "redirect:/blog/" + id; 16}

なお、データの永続化にBlogRepositoryを使用するようにしているので、CommentRepositoryは使いませんが、CommentRepositoryでデータの永続化を行ってもかまいません。

blog.html

フォームもエンドポイントの変更に対応する修正を行います。

html

1<form th:action="@{/blog/{blogId}/comment(blogId=${blog.id})}" method="post" th:object="${comment}"> 2 コメントをどうぞ<br> 3 <input type="text" size="40" th:field="*{text}"> 4 <input type="submit"> 5</form>

以上のコードは下記の環境にて動作確認済みです。

  • Windows 10
  • OpenJDK 14.0.1
  • Spring Boot 2.3.1
  • MySQL 8.0.19

投稿2020/06/15 14:49

編集2020/06/16 00:54
rubytomato

総合スコア1752

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yt07

2020/06/16 00:43

ご回答ありがとうございます。 上記のように、モデルクラス、html、コントローラの修正を行ってみました。 コントローラのreturnはこのようにしてみて実行してみましたが、404エラーになるのですが、returnの記述は間違ってるのでしょうか? たびたび質問すみません。 https://i.gyazo.com/fd384dd72300ae26600e304e067ad04f.png ``` @Transactional @PostMapping("/blog/{blogId}/comment") public String createComment(@PathVariable("blogId") Integer id, Comment comment) { comment.setPostDateTime(LocalDateTime.now()); Optional<Blog> optionalBlog = blogRepository.findById(id); Blog blog = optionalBlog.orElseThrow(RuntimeException::new); blog.addComment(comment); blogRepository.save(blog); return "redirect:/blog/{blogId}/comment" ; } ```
rubytomato

2020/06/16 00:55

失礼しました。return文が抜けていたので追記しましたのでご確認ください。
yt07

2020/06/16 01:15

迅速なご対応ありがとうございます。 return "redirect:/blog/" + id; とし実行しましたが404エラーは変わりませんでした。 ご教授いただいたのみすみません。 @Transactionalでjavax.transaction.Transactional;とorg.springframework.transaction.annotation.Transactional;どちらもimportし試しましたが404エラー内容は変わりませんでした。
rubytomato

2020/06/16 01:31

ポスト後のリダイレクト先は "/blog/1" のようになっていますか? ブラウザのURLを見てください。 このリダイレクト先は 以下のblogメソッドになっていますので404エラーになることは考えにくいです。"/blog/"へリダイレクトしているということはないでしょうか。 @GetMapping("/blog/{id}") public String blog(@PathVariable Integer id, Model model) { } また、アノテーションは"org.springframework.transaction.annotation.Transactional"が正しい方です。 それとソースコード修正後はアプリケーションを再起動してください。
yt07

2020/06/16 03:12

``` package com.example.demo; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @Controller public class SampleController { @Autowired //リポジトリインターフェース参照、追加、更新などのメソッドがあらかじめ定義されている。findAllメソッドを使用することで単純にテーブル内のレコードを一覧で返す。 private BlogRepository blogRepository; @Autowired private CommentRepository commentRepository; @GetMapping("/") public String index(Model model) { List<Blog> blogs = blogRepository.findAll(); model.addAttribute("blogs", blogs); return "index"; } @GetMapping("/form") public String form(Blog blog) { return "form"; } @PostMapping("/create") public String create(Blog blog) { blog.setPostDateTime(LocalDateTime.now()); blogRepository.save(blog); //エンティティを保存するにはリポジトリインターフェースのsaveメソッドを使用。 //エンティティをsaveすると@Idアノテーションを付けたフィールドに値が設定される。 return "redirect:/blog/" + blog.getId(); } @GetMapping("/blog/{id}") public String blog(@PathVariable Integer id, Model model) { Optional<Blog> blog = blogRepository.findById(id); //返されたオブジェクトがnullでなければこの処理を行うなど、nullの場合の処理を書きやすくするためのもの。 //getメソッドを呼び出して中身を取り出す。 model.addAttribute("blog", blog.get()); Comment comment = new Comment(); comment.setBlog(blog.get()); model.addAttribute("comment", comment); return "/blog"; } @Transactional @PostMapping("/blog/{blogId}/comment") public String createComment(@PathVariable("blogId") Integer id, Comment comment) { comment.setPostDateTime(LocalDateTime.now()); Optional<Blog> optionalBlog = blogRepository.findById(id); Blog blog = optionalBlog.orElseThrow(RuntimeException::new); blog.addComment(comment); blogRepository.save(blog); return "redirect:/blog/" + id; return "redirect:/blog/{id}";こちらも試してみる。 } } ``` ポスト後のリダイレクト先ですが下記のようになります。 https://i.gyazo.com/21a38b1ceabe41ee34cd8cf0640e92e3.mp4 blog.htmlの<form action="@{/blog/{blogId}/comment(blogId=${blog.id})}" method="post" th:object="${comment}">の部分の記述の問題ですか?
rubytomato

2020/06/16 03:58

> blog.htmlの<form action="@{/blog/{blogId}/comment(blogId=${blog.id})}" method="post" th:object="${comment}">の部分の記述の問題ですか? そのようです。 <form action="..." method="post" th:object="${comment}"> の部分は 正しくは <form th:action="..." method="post" th:object="${comment}"> です。 回答欄にもそのように記述してあるのでご確認ください。
yt07

2020/06/16 04:44

何度も何度もすみません。 blog.htmlを修正してURLはhttp://localhost:8080/blog/14/commentと表示されましたが、またblogの型を変換できなかったというエラーが...。 https://i.gyazo.com/fd7fe79f6ebe8d158c70f19eff4dea6d.mp4 コードの変更はご回答いただいた部分のみです。 Blog.java ``` package com.example.demo; import java.time.LocalDateTime; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import lombok.Data; @Entity //JPAにテーブルに保存するクラス(エンティティ)であることを示す。 @Data public class Blog { @Id //対応するテーブルのプライマリーキー(主キー)となるフィールド @GeneratedValue(strategy=GenerationType.AUTO) //プライマリーキーの値を連番で自動生成。 private Integer id; private String title; private LocalDateTime postDateTime; @Column(length=1000) //テーブルの列を制御したい場合は@Columnをつける。フィールドの長さをデフォルトの255から1000にする。 private String contents; @OneToMany(mappedBy = "blog", cascade = CascadeType.ALL) //mappedBy引数には「相手側が自分を参照する名前」を指定。 private List<Comment> comments; public void addComment(Comment comment) { comment.setBlog(this); comments.add(comment); } } ``` コントローラ、htmlは上記のまま変更はありません。
rubytomato

2020/06/16 08:01

ソースコードの修正を行って頂いたと思いますが、現時点での最新のソースコードを質問内容に反映してください。 たぶん、どこかのコードが古いままな気がします。
yt07

2020/06/16 08:45

確認くださりありがとうございます。 全ファイル最新のものを質問内容に反映いたしました。
rubytomato

2020/06/16 08:47

現状、フォームは下記のようになっていますが <form th:action="@{/blog/{blogId}/comment(blogId=${blog.id})}" method="post" th:object="${comment}"> コメントをどうぞ<br> <input type="hidden" name="blog" th:value="*{blog.id}"> <input type="text" size="40" th:field="*{text}"> <input type="submit"> </form> hiddenのフィールドは不要です。なので下記のようにしてください。 <form th:action="@{/blog/{blogId}/comment(blogId=${blog.id})}" method="post" th:object="${comment}"> コメントをどうぞ<br> <input type="text" size="40" th:field="*{text}"> <input type="submit"> </form>
yt07

2020/06/16 08:58

エラーなくコメントできました!! https://i.gyazo.com/9119ce14168897eae24466470ace45d5.mp4 hiddenフィールドでうまくいかないんですね。name="blog"にしていたから呼び出しの型が違ったからですかね。 DBへもリレーションもしっかりとcmmentテーブルのデータも保存されています! すごく勉強になりました!!感動です! ご回答を読み直して完全に理解できるようにします!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.46%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問