目的
Spring Data JPAにて開発中、ハマったポイントと原因と対処を書き溜めていく
環境
Spring-boot
java8
STS
Spring-data-jpa
hivernate
問題と対処
事象1.0
BeanUtil.copyPropertiesにて、フォームから取得した値をDBへ登録するEntityへ転写する際、値が引き継がれずnullになるプロパティがある
import org.springframework.beans.BeanUtils
BeanUtils.copyProperties(フォーム, エンティティ);//左から右に値をコピー
画面からPOSTされた値をFormクラスを経由しコントローラで取得し、
javax.persistance.Entityクラスに引き継ぎ、
DBへ保存する。
フォームには値が入っているが、
DB側からNULLを許容しないエラーが発生する
画面から値の流れを調査したら、
copyPropatyにて値が引き継がれていなかった
原因と対処 1.0
Formクラスで定義されていたデータ型が、Entityと一致していない。
EntityでDoubleで定義されていた値に、FormクラスでIntegerで定義されたフィールドを割り当てていた。
springframeworkのcopyPropatyの場合、例外とはならず、nullが渡される。
その他 1.0
ちなみにハマったわけではないが、
フォームでは文字列で入力されている値を、Entityへ移動する際に、データ型をDateへ変換する処理
// 明細部
// 文字列の日付をdbのためdate型へ変換してFormからEntityへ
public static MitsumoriDtl setMeisaiDateFormToEntity(MeisaiForm form, MitsumoriDtl entity) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
try {
entity.setSys_create_ymdhm(sdf.parse(form.getSys_create_ymdhm()));
entity.setSys_delete_ymdhm(sdf.parse(form.getSys_delete_ymdhm()));
entity.setSys_update_ymdhm(sdf.parse(form.getSys_update_ymdhm()));
} catch (Exception e) {
e.printStackTrace();
}
return entity;
}
事象2.0
JPQLにて検索処理に失敗する。
@Query("SELECT t FROM t_mitsumori_dtl t WHERE :hdrKey = t.mitsumori_hdr_no")
List<MitsumoriDtl> findByHdrKey(@Param("hdrKey") String hdrKey);
t_mitsumori_dtlというテーブルから、JPQLを利用し、検索した結果をリストで返す。
検索はhdrKeyという変数の値で、mitsumori_hdr_no を絞り込む
しかし、t_mitsumori_dtlがマッピングできないというHibernateのエラーとなる
原因と対処 2.0
@Queryの中に入るのは、Entityクラスで、テーブルではない
package com.example.repository;
import java.util.Date;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import com.example.domain.MitsumoriDtl;
@Repository
@Transactional
public interface MitsumoriDtlRepository extends JpaRepository<MitsumoriDtl, Integer> {
// hdrに紐づく明細を取得する
@Query("SELECT t FROM MitsumoriDtl t WHERE :hdrKey = t.mitsumori_hdr_no")
List<MitsumoriDtl> findByHdrKey(@Param("hdrKey") String hdrKey);
}
@Queryの中に入るのは、Entityクラスで、テーブルではない
上記に修正し解消することができた。
事象3.0
コントローラから、一部値を入れたエンティティを画面へ送り、
画面から残りの値を編集しデータの作成を行いたい
@RequestMapping(value = "editMeisai", method = RequestMethod.GET)
String edit(Model model,@RequestParam Integer id,MeisaiForm form){
// ここで取得しなければならないのは、紐づけるhdrのID番号だけで、そこへ新規の明細データをaddしていく
// フォーム用
// からの明細フォームを送る。hdridをあらかじめ入れておく
MeisaiForm newMeisai = new MeisaiForm();
newMeisai.setMitsumori_hdr_no(String.valueOf(id));
model.addAttribute("newmeisai", newMeisai);
return "views/meisai";
}
この受け渡らせた値を受け取るフォーム側
<form th:action="@{/testdb/editMeisai}" th:object="${newmeisai}"
modelAttribute="meisaiForm" method="post">
<fieldset>
<legend>登録</legend>
しかし、以下のエラーが出る。
dtl ---- : MitsumoriDtl(t_mitsumori_dtl_seq=null, mitsumori_hdr_no=1, mitsumori_dtl_no=123, mitsumori_dtl_mei=123, mitsumori_dtl_suryo=123.0, mitsumori_dtl_suryo_tani=123, mitsumori_dtl_tanka=123, mitsumori_dtl_gaku=123, mitsumori_dtl_biko=123, t_mitsumori_hdr_seq=1, sys_create_user=123, sys_create_ymdhm=Thu Jan 01 00:00:00 JST 2015, sys_create_class=123, sys_update_user=123, sys_update_ymdhm=Thu Jan 01 00:00:00 JST 2015, sys_update_class=123, sys_delete_user=123, sys_delete_ymdhm=Thu Jan 01 00:00:00 JST 2015, sys_delete_class=123, sys_delete_no=123, sys_version=1, headerEntity=null)
2016-03-22 01:20:43.768 ERROR 5996 --- [nio-8080-exec-6] org.thymeleaf.TemplateEngine : [THYMELEAF][http-nio-8080-exec-6] Exception processing template "views/meisai": Exception evaluating SpringEL expression: "#fields.hasErrors('mitsumori_hdr_no')" (views/meisai:29)
2016-03-22 01:20:43.800 ERROR 5996 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "#fields.hasErrors('mitsumori_hdr_no')" (views/meisai:29)] with root cause
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'newmeisai' available as request attribute
画面からmodelのnewmeisaiに紐づく値で
Neither BindingResult nor plain target object for bean name 'newmeisai' available as request attribute
リクエストのnewmeisaiは、BuildingResultでもplainでもないというエラーが発生している
原因と対処 3.0
画面からの入力を行うフォーム項目は、完全な状態かからの状態でなければならない。
中途半端に入力してしまうと、バリデーションが走ってしまい、適切に表示できない
つまり、${newmeisai}、は、直接ここから受け取ることはできず、プレーンなフォームオブジェクトを与えなければならない
<form th:action="@{/testdb/editMeisai}" th:object="${newmeisai}"
modelAttribute="meisaiForm" method="post">
ではどうやって
コントローラから受け取った値を、Formにバインドされた新規のフォームクラスへ受け渡すか
方針として
1. Formの外にコントローラからkey(${newmeisai}に入っているキーとなる値を持っているフィールド)を受け取るだけの要素を作る
2. javascriptで、それぞれのdom要素を取得し、${newmeisai}コントローラから渡ってきた値を渡したい*{mitsumori_hdr_no}に引き渡す
3. この操作はHTML読み込み完了後に呼び出されるfooterでやる、また、値受け取り用のフォームはhiddenタイプにして隠蔽する
以下のように、値受け取り専用のformを作成し、値を取得する
<!-- ここで値を受け取る -->
<form th:object="${newmeisai}" ><input type="text" id="pased_id"
name="mitsumori_hdr_no"
th:field="*{mitsumori_hdr_no}" class="form-control"
th:value="*{mitsumori_hdr_no}" /></form>
<!----------------------->
<h1>明細</h1>
<!-- 登録を行う -->
<div class="col-sm-12">
<!-- th:objectでは、フォームクラス名の頭小文字を指定すると、フォームオブジェクトとHTMLフィールドをバインドでき、
このクラスのフィールド名で *{フィールド名}で値を呼び出せる -->
<form th:action="@{/testdb/editMeisai}" th:object="${meisaiForm}"
modelAttribute="meisaiForm" method="post">
<fieldset>
<legend>登録</legend>
<!-- 見積もり番号 -->
<div class="form-group"
th:classappend="${#fields.hasErrors('mitsumori_hdr_no')}? 'has-errorhas-feedback'">
<label for="mitsumori_hdr_no"
class="col-sm-3control-label">見積もり番号 </label>
<div class="col-sm-11">
<input type="text" id="mitsumori_hdr_no"
name="mitsumori_hdr_no"
th:field="*{mitsumori_hdr_no}" class="form-control"
th:value="${mitsumori_hdr_no}" />
上記は、HTMLでは以下のようにレンダリングされる
<form><input type="text" id="pased_id" name="mitsumori_hdr_no" class="form-control" value="1" /></form>
<h1>明細</h1>
<!-- 登録を行う -->
<div class="col-sm-12">
<!-- th:objectでは、フォームクラス名の頭小文字を指定すると、フォームオブジェクトとHTMLフィールドをバインドでき、
このクラスのフィールド名で *{フィールド名}で値を呼び出せる -->
<form modelAttribute="meisaiForm" method="post" action="/testdb/editMeisai">
<fieldset>
<legend>登録</legend>
<!-- 見積もり番号 -->
<div class="form-group">
<label for="mitsumori_hdr_no" class="col-sm-3control-label">見積もり番号 </label>
<div class="col-sm-11">
<input type="text" id="mitsumori_hdr_no" name="mitsumori_hdr_no" class="form-control" value="" />
</div>
ボディ要素のしたに以下のjavascriptを追加し、
上のdom要素を取得し、コントローラから値を受けとているpassed_idの要素のvalueを
今回値を登録したいmitsumori_hdr_noのvalueへ移し替える
<footer>
<script type="text/javascript">
var passed_key = document.getElementById('pased_id');
var post_key = document.getElementById('mitsumori_hdr_no');
post_key.value = passed_key.value
</script>
</footer>
値が確かに引き渡されている。受け取り側のtypeをhidenにすれば自然に渡せる
事象4.0
関連テーブルのデータを削除できない
- 1対多の関連を持つテーブルがあるこれに対応した以下のドメインクラスがある
hdrEntity (単数)
meisaiDtl (多:関連テーブル)
このクラスhttp://qiita.com/yukihigasi/items/54e224ce346dbd89e981
- 関連テーブル側をrepositoryのdeleteメソッドで直接削除しようとした場合、削除されない
原因と対処 4.0
- 原因
元テーブル側を経由して関連テーブルの削除を行わなければならない
関連テーブルだけを単純に
mitsumoriDtlService.delete(id);
関連テーブル用のリポジトリ.delete(関連テーブルの主キー)
で削除できない
元ドメインクラスが生きている限り、マネージャがまだメモリ上に保持している
- 対処
元テーブル側から削除する
元テーブルにコレクションクラスで保持されている関連テーブルの中から、
今回削除したい関連テーブルオブジェクトを除去し、
元テーブルのドメインクラスをアップデートする
以下で例示する
1. dtl(多)を検索してhdr(単一)の主キーを検索し、
2. hdrが保持するすべてのdtlを洗い、今回のdtlのidを見つけ、リストから削除する
3. 編集されたdtlのリストを持つhdrをアップデートする
@RequestMapping(value = "deleteMeisai", method = RequestMethod.POST)
String deleteMeisai(@RequestParam Integer id) {
// TODO 元エンティティから削除をしないと、マネージャ持されてしまうため、関連エンティティは直接けせない
// dtlを検索してhdrの主キーを検索し、
MitsumoriDtl dtl = mitsumoriDtlService.findOne(id);
// hdrのdtlを繰り返し今回のdtlのidを見つけ、削除する
HeaderEntity hdr = mitsumoriHdrService.findOne(dtl.getT_mitsumori_hdr_seq());
List<MitsumoriDtl> serchDeleteLst = hdr.getMitsumoriDtl();
int i=0;
int index=0;
for (MitsumoriDtl adtl:serchDeleteLst){
if(adtl.getT_mitsumori_dtl_seq() == id){
index=i;
}
i++;
}
serchDeleteLst.remove(index);
hdr.setMitsumoriDtl(serchDeleteLst);
// hdrをアップデートする
mitsumoriHdrService.update(hdr);
// mitsumoriDtlService.delete(id);
return "redirect:/testdb";
}
以上で、関連テーブルのキーを元に、関連テーブルのレコードを削除することができる。
以後、何かあり次第追記いたします。