初めてのSpring案件を終えたメモ ~データベース編~


はじめに

ピュアJavaとDBの基本知識のみスタートからのSpring案件が波乱の末ひと段落したので、所感や覚書などを。
記事の内容としてはチュートリアル以上、実践未満のイメージ。
掲載コードは動作確認できていないのであくまで参考で。

開発環境

RedHat(開発環境はCent)7系
Java8
SpringBoot2.1.6
MySQL8系
STS

SpringDataJPAについて

関連用語が難しい。
JPA(仕様)を実装したもの(Hibernate)を利用したフレームワークがSpringDataJPA。自信ない。
動的クエリ作成で使うCriteriaAPIはJPAの機能だから参考にしていい。
サンプルで見かけるEclipseLinkはHibernateとは違う実装なので参考にしないほうがよさそう。
でもHibernateの機能でなくSpringの機能を推奨してたりするから、油断ならない。

エンティティのライフサイクルを知っておくと、親子関係のエンティティの追加や削除、明示的トランザクションとかで不審な動きをするとき解決の助けになるかも。
https://qiita.com/Maruo110/items/4dc4a49aedd6323ebfdb

H2DB

インメモリデータベース。

Springに含まれてるから環境にRDBMSを用意しなくてもデータベースが使える。
アプリを実行するたびに初期化されるる。カジュアルに使えるのでプロトタイプやテストに使える。

SQLの文法はH2DB独自のものだけどmodeオプションで他のRDBMSっぽい文法が使えるけど完全ではない。
http://www.h2database.com/html/features.html#compatibility
mysqlモードでcreate文のインデックスの指定の仕方はこれで吸収できたけど、
ストアドとかトリガー、tinyint型のマッピングなどは吸収できなかった。
そもそも、ここで困るようだとDBに依存しすぎってことなんだろうね。

Entityについて

シンプルにつくるとテーブルの情報だけのクラスになるけど、結構いろいろできる。
データの加工はやってもよさそう。
データの更新(永続化)前のタイミングなどで呼び出せるコールバック系メソッドも便利。
AuditingEntityListenerを使えばDI対象クラスも使えるけど、テストが難しくなる。
継承もできるけど、lombokと相性がよくない。
「とりあえず動かしてみた」系サンプルでたまに見られるformとの兼用なんかをしようものなら瞬く間に複雑になるので、ちゃんとformとEntityは分けること。

Employee.java
@Entity
@Data
class Employee {
    @Id
    private long id;

    private String lastName;

    private String firstName;

    //データ加工
    public String getFullName(){
      return this.lastName + this.firstName;
    }

    private Date updateDate;

    //コールバック系メソッド
    @PreUpdate
    private void preUpdate() {
      setUpdateDate(new Date());
    }
}

関連付けは鬼門。古今東西の情報が入り乱れている。
OneだかManyだか、相互参照、遅延フェッチ、削除時の動作など正しく設定できているか早いうちに確認しないと、後からの修正は影響範囲が泣ける。

Repositoryについて

命名規則通りのメソッドを宣言すると、自動でクエリを実装してくれる。
countやexsist、Entityに関連付けを定義していれば関連テーブルのカラムも指定できる
https://spring.pleiades.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

名前が長くなりがちだから、適切な名前でラップした方が良さそう。

戻り値にはOptionalが使えるので積極的に使うといい。

findAllは検索条件やページング情報、ソート情報を引数に渡せる。
でもページングかつソートのときは、ページング情報にソート条件を入れる。
ページングするときは内部でselect * ~select count ~が発行されているため注意。

動的クエリについて

Specificationを使った。これも鬼門。

基本の使い方はすんなり導入できるけど、少し変わったことしようとすると理解しないといけないことが多い。
具体的には CriteriaAPI とか ラムダ式 とか。
メタモデルも便利そうだけど今回は知るのが遅くて使わなかった。

・distinct

hogeSpecification.java
public Specification<Hoge> distinct() {
    @Override
    public Predicate toPredicate(Root<Users> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        cb.distinct(true);
        return cb.equal(root.get("id"),root.get("id")); //必ずtrue。苦肉の策。
    }
}

・fetch
結合はroot.joinで出来るけど、これ指定しないと


select *
   from a 
    innner join b1 on a.id = b1.id
    innner join b2 on a.id = b2.id
  where b1.id = {1}
    and b2.name = {2};

みたいに何度も同じテーブルと結合するSQLが発行されてしまった。
原因も解決方法も、理解できていない。

また、フェッチしたクエリでページングしたいとき以下の記述を入れる必要がある。
if (query.getResultType() != Long.class && query.getResultType() != long.class)
なぜならページングのときはselect * ~select count ~が発行されるけどcountのときフェッチできないから回避する必要がある。。。えぇ?
https://coderanch.com/t/656073/frameworks/Spring-Data-fetch-join-Specification

それを踏まえてこんなコードになった。

hogeSpecification.java
public Specification<Hoge> fetch() {
    @Override
    public Predicate toPredicate(Root<Users> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
        if (query.getResultType() != Long.class && query.getResultType() != long.class){
            root.fetch("childTable", JoinType.LEFT); //結合するテーブルはEntityで定義しておく
        }
        return cb.equal(root.get("id"),root.get("id")) //必ずtrue。苦肉の策。
    };
}

2つ以上フェッチできないらしく、親子孫関係に使えなかったのでクエリ結果を加工してごまかす羽目になった。むずかしい。。。

・制御(条件分岐とかループとか)
サンプルだとメソッドチェーンで全て繋げるけど、もちろん途中で切ってもいい。
SQLでいうところのIN句の複数カラム指定( where (name,id) in ((hoge,1),(fuga,2)) みたいなこと)はこれで実装した。

hogeService.java
public List<Hoge> serch(HogeForm form) {
    Specification<hoge> 検索条件 = 
        Specification
            .where(検索条件1)
            .and(検索条件2);

    //条件分岐とか
    if(分岐条件) 検索条件 = 検索条件.and(追加検索条件);

    //ループとか
    Specification<hoge> ループ用検索条件 = null;
    for(ループする){
        ループ用検索条件 = ループ用検索条件.or(繰返検索条件);
    }
    検索条件 = 検索条件.and(ループ用検索条件);

    return hogeRepository.findAll(検索条件);
}

他のメモ

初めてのSpring案件を終えたメモ ~Springとは編~
初めてのSpring案件を終えたメモ ~MVC編~