20210107のJavaに関する記事は6件です。

[java] listのsort

javaで配列やlistをsortする方法を集めてみました。自分用メモ。

配列

昇順
int[] array = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8 };
Arrays.sort(array);
降順
Integer[] array = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8 };
Arrays.sort(array, Collections.reverseOrder());

※Collectionsクラスを使用しているため、Integer配列で宣言している。

List

昇順
ArrayList<Integer> al = new ArrayList<>();
al.add(2);
al.add(7);
al.add(1);
al.add(8);
al.add(2);
al.add(8);
Collections.sort(al);
降順
ArrayList<Integer> al = new ArrayList<>();
al.add(2);
al.add(7);
al.add(1);
al.add(8);
al.add(2);
al.add(8);
Collections.sort(al, Collections.reverseOrder());

応用(ユーザ定義クラス、複数値)

応用
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class Main {

    public static void main(String[] args) {

        List<Item> itemList = new ArrayList<>();
        itemList.add(new Item(4, "Book", 1000));
        itemList.add(new Item(2, "Bag", 3000));
        itemList.add(new Item(3, "Calendar", 2000));
        itemList.add(new Item(1, "pen", 200));

        // id昇順ソート
        itemList.sort(Comparator.comparing(Item::getId));

        // id降順ソート
        itemList.sort(Comparator.comparing(Item::getId).reversed());

        // kind昇順ソート
        itemList.sort(Comparator.comparing(Item::getKind));

        // kind降順ソート
        itemList.sort(Comparator.comparing(Item::getKind).reversed());

    }
}

class Item {
    int id;
    String kind;
    int price;

    Item(int id, String kind, int price) {

        this.id = id;
        this.kind = kind;
        this.price = price;
    }

    public int getId() {
        return id;
    }

    public String getKind() {
        return kind;
    }

}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ドミニクシステムオンライン

記憶術ドミニクシステムを今すぐ体験できるwebアプリをリリースしました。
ドミニクシステムオンライン

ドミニクシステムとは、トランプ1組を一分で丸暗記するのに使われる記憶術です。
細かなことはについては、使い方を読んでください。
実際のコードは、Github:Dominicです。
コメントがそのまま残っているの仕様です。
消す作業が安全ではないのと、時間感の制約から残しています。

実際に動作しているものとは、コードが多少違います。
どうしても、早く動作させたい場合は、eclipsなどから、TomcatServerで起動するとオンライン版より早く安定的に動作します。
動作には、MySQLデータベースの制作と接続が必要です。大した構造ではないので安心してください。
データベースの構造は、この記事の下の方にあります。

ローカル環境で試す方法

開発環境はosはWindows10、サーバーは、tomcat:9、開発データベースは、MySQL8.0.21を使用しています。
webサーバーは、heroku、データベースはjawsDBを利用しています。webサーバーはローカルで試す場合不要です。
環境を揃えたほうが、確実に動作しますが、おそらく、tomcat9以上が入っていれば大丈夫なはずです。


詳細?な手順
1. GitHubから、ソースをダウンロードする
Pleiades All in One Eclipse(2020 java Full Edition)などで、プロジェクトを読み込む。
tomcatが入っていることが重要です。
3.MySQLをインストールする
4.データベースを製作する。
製作には、web-content内のSQLフォルダに、SQL(createDatabase.sql)が用意してあるので、MySQLWorkBenchなどから、インポートして利用してください。
もちろん、下の方にあるデータベースのカラムから作っても大丈夫です
5.データベース接続に接続する。
c3p0を利用しています。なので、resources/environment.propertiesに、データベース名、ユーザー名、パスワードを登録してください。
'#'でコメントアウトしてあるので、'#'を削除をすると有効にできます。
6.Eclipseから、プロジェクトを実行>1.サーバーで実行する。

オンラインでサービスしているものは、jawsDBとの相性が悪いようで、ログインして利用すると、サーバーエラーが頻発するので、ゲストで利用してもらえるとありがたいです。
単語登録や人名とアクションの変更は、アクセス数の少ないときなら使えるようですが、あまり利用しないようにお願いします。データベースサーバーが接続を拒否してきます。

アプリ概要

  • 記憶術ドミニクシステムを誰でもすぐ使えるシステム
    • 数値と人名が、頭の中でつながっていない段階から、記憶訓練を行えるようになる
  • ドミニクシステムの人名リストの表示と編集
    • リストを自分で用意しなくていい
    • 先にリストを作る必要性がなく、訓練をしながらリストを構築できる
  • 記憶文章の検索と自動構文および編集
    • 文章を考える手間が、すくなくなる。
  • ログイン機能
    • ユーザーのリストを保存するために利用。

検索画面

ドミニクタイトル画面.png
数値と言葉を入力することで、記憶文章を生成できます

検索結果

1351黄巾の乱.png
文章を修正して、登録ボタンを押せば、保存することができます。

人名表示画面

編集機能.png
数値に対応する人物の画像と行動が表示されます。
変数ボタンを押して、名前と行動及び画像を変更することが可能です

単語帳画面

文章登録.png
登録した記憶文章を確認・編集することができます。

システム概要

会員(ログインユーザー)とゲスト(非ログインユーザー)ともに、Webブラウザを使用し、本システムにアクセスする。
本システムは、Webサーバー上に配置する。
利用者の利便性向上のため、ゲストであっても保存以外の機能を有効にする。
利用者は、メインページで数字と単語を入力することで、ドミニクシステムの記憶文章を獲得できる。検索結果をその場で単語帳に登録することができる。
人名リスト、単語帳にアクセスし変更することができる。
人名と数字を関連付けさせる練習を行うことができる。
会員のデータは、データベースに保存する
デフォルトのデータは、データベースに保存し、web上で変更可能にする
ブラウザにデータを保存はしない。
ゲストが、データを保存したい場合に会員登録を要求する
会員はログインすることによって、どこからでも自身のリストと単語帳を確認できる

技術目標

フロントエンドとバックエンドをajaxで完全に分離する。
デザインは、頑張らない。HTMLやCSSはメインにしない。ただし、JavaScriptは、例外的に好きにいじる。
5年放置しても安全にする。(メールアドレスの収集やSNS連携をしない)
どのような出来であれ、公開する。

使用言語・ツール

使用言語 使用IDE
java eclipse
MySQL MySQLWorkBench
JavaScript + jQuery Visual Studio Code
html + css Visual Studio Code + Chrome
ER図 draw.io

利用サーバー+データベース
heroku
jawsDB
両方とも無料枠範囲内で利用する。
自分にコードの効率化圧力をかけるため。

使用ライブラリ

ライブラリ 使用目的
Jackson Databind JSON形式への変換
JSTL JSPでのページ構築
javax.servlet サーブレットの展開と維持
jBCrypt パスワードの暗号化
c3p0 データベース接続の構築
mchange-commons  c3p0が必要とするライブラリ
connect-j javaとMySQLの接続
Bootstrap 4 HTMLのデザイン
jQuery AJAX通信の管理
jQuery.inview.js 画面表示の検知

アプリ構造設計

画面のデザインは、機能に絞って書かれたものです。
ニューモフィズムのデザインを取り入れて実装します。
画面接続性は、初期に制作されたもので、実際の構造とは、差異があります。
トレーニングモードは、時間的に実装を凍結しました。
データベースは、仕様を満たす形で作られています。
実装で利用されていないテーブルとカラムが存在します。
カラムを削除しないでください。内部的には利用されている場合があります。
カラム表とER図が違う部分は、更新された部分です。カラム表を正として読んでください。

  • フロント画面
    1_TOP_b_ワード検索後.JPG

  • 単語帳画面
    3_単語帳_b_ワード検索後.JPG

  • 画面遷移
    ドミニク画面設計.png

  • データベースカラム
    データベースカラム.png

  • データベースER図
    ドミニクER図.png

システム導入の背景と目的

記憶術「ドミニクシステム」の習得には、2つのものが必要です。100名の顔のわかる人名と数字との関連付けという処理が必要で導入が難しいものです。未経験者が始めるには、リストを構築してからでないと、導入できないという弱点があります。
 そのため、未経験者でも、すぐにドミニクシステムの訓練を始められるオンラインシステムには、必要性と需要がある。

 新システムの導入することによって、ドミニクシステムの浸透を図ると同時に、記憶力の向上を利用者に提供する。リストの構築にかかる手間を極力排除し、違和感のある部分をユーザごとに修正できるようにする。皆が設定した候補を見ながら素早く自分に合ったリストを得られるようにする。

制作過程

javaSliverの試験を優先していたので、即興で作られています。

概ね以下の手順で制作しました
1. 製作資料を制作する。上記のものと画面全部・データベースのカラムとER図
2. フロントを作る。時間の都合上、検索画面のみ先行完成
3. データベースを作る。データベースは、中途半端な分割が難しかったので、先に完成させました
4. javaでデータベースの検索結果を返すwebAPIを制作する。今回はPUTやDELETEを使わず、1つのアドレス=1つの動作にする。
5. javaScriptで、フロント側からAJAXでwebAPIを呼び出す。


フロント画面とデータベースの完成まで、2週間前後。残りの画面を3週間ほどでコーディングしました。
1日8時間計算で換算、実作業は分散している場合も、一日で作業している場合もあります。
開発開始時期11月初旬
企画書を制作する 2日
フロント側のデザインを決定する 1日
データベースを設計する 2日
データベースの制作 0.5日
index画面(検索機能のみがある画面)HTML+CSS 3日
発表会まで一週間を切ったので機能を絞る
index画面のJavaScript 2日
バックエンドAPIの制作 3日
11月下旬 
ドミニクシステムの人名リストの表示と編集
記憶文章の検索と編集
ログイン機能の完成 
この時点では、リスト画面は何も制作されていない。
バックエンドののAPIはほぼ完成している。
12月中旬までは、SPRINGFrameの研修を優先したので制作中断
リスト画面(人名リストのる画面) 3日
リスト画面 JavaScript 2日
単語画面  1日
使い方画面 1日
バックエンドAPIへの接続 2日
各画面のレスポンシブ対応2日
動作確認 0.5日
12月31日、ギリギリ年内で、ローカル完成。
Herokuへの対応を行う
Heroku導入対応・アップロード 1日
ネットワークエラーの原因調査2日
データベースと接続設定に問題があることが判明
c3p0の設定の調整 1日
SQLの実行回数が多すぎることが抜本的原因であることが確定。
ゲストユーザーの場合データベースへの接続をしないように変更1日
この文章を書く1日。
リリース

Q.なぜゲストだけ、データベースにアクセスさせない対応にしたのか

A.現在取れる以下の対応を比較検討した結果です。

エラーの状態

jawsDBのconnnectionが上限に到達し、接続の途中でも通信を終了している。
デーベースサーバーがコネクション上限に達したエラーを返すようになる。
一定期間放置すると解除される。
一つのAJAX通信で複数のSQLの実行する途中で、中断され、ローカルでは見ないレコードが帰ってきています。
TomcatのコネクションプールとC3p0の動作が違うことが表面的原因のようです。
コネクションを内部でリレーして使い回すように変更した結果ある程度動くようになりました。
さらに、C3P0の設定をいじって、だいぶ改善したんですけど、実用には足りない状態です。
もし、2時間位で解決する良い方法があったら教えて下さい。

取れる方法は、大きく分けて3つ

お金を払う方法

jawsDBを有料プランに切り替えて高速化する。
高速化のためには、月119ドル程度のものが必要で、支払いたくない。

環境を変える方法

VPSサーバーを借りて、データベース接続を高速化する。
VPSサーバーはSSDかつ、内部にデータベース・サーバーがあるので、ローカル環境と同等の動作をするはず。
想定作業時間は3日。次の制作物が、SPRINGFrame使うので、本番環境でためしながら開発できるのは、ありがたい。ただし、次の制作物にすぐに移りたいので、3日は痛い。

コードを変える方法

データベースの構造を抜本的に変更する。
おそらく6日くらい。勉強にはなるが、このまま変更するより
2週間ぐらいかけてSpringBootに移植してしまうほうが、後々楽なのでやりたくない。

ORマッパーのHivernateを導入する
C3p0がうまく動かない原因としてHibernateが入っていないことがあります。そのため、Hrbernateを導入して、データベースのマッピングをすれば、解決する可能性がある。
ただし、Mavenプロジェクトにしていないので、変換作業をするか、自力で導入する必要がある。変換する場合、1から作り直すより時間がかかる。
自力導入は、別のトラブルを引き起こす可能性が高い。同様のやり方をする人が少ないので、ネット検索が使えない。大きなトラブルを自力でなんとかする必要がる。

ゲストモードだけ取りあえず使えるようにする。
すぐ作れる、3時間位。
ユーザーが自分のリストが作るのが難しいままである。
ただし、私はローカルで使えるので、問題が出ない。
想定接続ユーザー数は、私一人だけである。

上記の案を検討した結果、すぐに治せるゲストモードだけ治すことにしました。

今後の修正予定
VPSサーバーを使いだしたら、移動させるかもしれない。1ヶ月くらいかかるかも。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java - Jersey Framework vs Spring Boot

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring Data JPAで動的にクエリを生成する(複数ワード検索)(ページング対応)

JPAでGoogle検索みたいにスペース区切りで複数ワードのAND検索がしたい。
「転職 東京」みたいな。
さらにページングにも対応したい。

色々調べていたらSpecificationに辿り着いたので、実装方法を備忘録として残しておきます。
いくつか粗があるので参考程度に。

Entity

@Data
@Entity
@Table(name="account")
public class AccountEntity implements Serializable{

    /**
     * シリアルバージョンUID
     */
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id")
    private Integer id;

    @Column(name="name")
    private String name;

    @Column(name="age")
    private Integer age;
}

Repository

JpaSpecificationExecutorを継承します。

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountRepository extends JpaRepository<AccountEntity, Integer>, JpaSpecificationExecutor<AccountEntity> {
    Page<AccountEntity> findAll(Pageable pageable);
}

Specification

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;


@Component
public class AccountSpecification {
    /**
     * 指定文字をユーザー名に含むアカウントを検索する。
     */
    public Specification<AccountEntity> nameLike(String name) {

    // 匿名クラス
        return new Specification<AccountEntity>() {
            //CriteriaAPI
            @Override
            public Predicate toPredicate(Root<AccountEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                // 「name LIKE '%name%'」を追加
                return cb.like(root.get("name"), "%" + name + "%");
            }
        };
    }
    /**
     * 指定文字を年齢に含むアカウントを検索する。
     */
    public Specification<AccountEntity> ageEqual(String age) {

        return new Specification<AccountEntity>() {
            //CriteriaAPI
            @Override
            public Predicate toPredicate(Root<AccountEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

                // 数値変換可能かのチェック
                try {
                    Integer.parseInt(age);

                    // 「age = 'age'」を追加
                    return cb.equal(root.get("age"), age);

                } catch (NumberFormatException e) {
                    return null;
                }
            }
        };
    }
}

AND句・OR句で繋げる

ここでもand()やor()で繋げられますが、サービスクラスでwhere()やand()or()メソッド使った方がわかりやすいかも。

                cb.like(root.get("name"), "%" + name + "%");
                cb.and(cb.equal(root.get("age"), age));
                return cb.or(cb.equal(root.get("age"), age));

Service

findAllの引数でwhere()やand()メソッドを呼び出して、Where句やAnd句を追加します。

1語検索バージョン
@Service
public class AccountService {

    @Autowired
    AccountRepository repository;

    @Autowired
    AccountSpecification accountSpecification;

    public List<AccountEntity> searchAccount(String keyWords, Pageable pageable){

        //前後の全角半角スペースを削除
        String trimedkeyWords = keyWords.strip();

        // 全角スペースと半角スペースで区切る
        String[] keyWordArray = trimedkeyWords.split("[  ]", 0);

        // 「Select * From account」 + 「Where name LIKE '%keyWordArray[0]%'」
        return repository.findAll(Specification
                .where(accountSpecification.ageEqual(keyWordArray[0])), pageable);
    }
}

「Select * From account Where(name='name' OR age='age') AND(name='name' OR age='age')」みたいにWhere句やAnd句の()の中に入れたい時はWhere/Andメソッドの中にAnd/ORメソッドを入れ子にする。

複数ワード検索バージョン
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

@Service
public class AccountService {

    @Autowired
    AccountRepository repository;

    @Autowired
    AccountSpecification accountSpecification;

    public Page<AccountEntity> findAll(Pageable pageable) {
        return repository.findAll(pageable);
    }

    public Page<AccountEntity> searchAccount(String keyWords, Pageable pageable){

        //前後の全角半角スペースを削除
        String trimedkeyWords = keyWords.strip();

        // 全角スペースと半角スペースで区切る
        String[] keyWordArray = trimedkeyWords.split("[  ]", 0);

        //todo ここのisBlankでのnullチェックが効いてない。nullがfalseになる。
        //nullか空文字なら全検索 1語ならWhere句追加 2語以上ならAnd句追加にしたい。
        if(keyWordArray.length == 1 && StringUtils.isBlank(keyWordArray[0])) {
            return repository.findAll(pageable);

        }else if(keyWordArray.length == 1) {
            // 「Select * From account Where (name LIKE '%keyWordArray[0]%' OR age = '%keyWordArray[0]%')
            return repository.findAll(Specification
                    .where(accountSpecification.nameLike(keyWordArray[0])
                    .or(accountSpecification.ageEqual(keyWordArray[0]))), pageable);

        }else {
            Specification<AccountEntity> specification =
                    Specification.where(accountSpecification.nameLike(keyWordArray[0])
                            .or(accountSpecification.ageEqual(keyWordArray[0])));

            // 「Select * From account Where(name LIKE '%keyWordArray[0]%' OR age = '%keyWordArray[0]%') AND(name LIKE '%keyWordArray[i]%' OR age = '%keyWordArray[i]%') AND ・・・
            for(int i = 1; i < keyWordArray.length; i++) {
                specification = specification.and(accountSpecification.nameLike(keyWordArray[i])
                        .or(accountSpecification.ageEqual(keyWordArray[i])));
            }
            return repository.findAll(specification, pageable);
        }
    }
}

Controller

@Controller
public class AccountController {
    //全表示 or 検索の判定用
    boolean isAllOrSearch;

    @Autowired
    AccountService accountService;

    //アカウント全表示
    @GetMapping("/hello")
    public String getHello( @PageableDefault(page=0, size=2)Pageable pageable, Model model) {
        isAllOrSearch = true;
        Page<AccountEntity> accountAll = accountService.findAll(pageable);

        model.addAttribute("isAllOrSearch", isAllOrSearch);
        model.addAttribute("page", accountAll);
        model.addAttribute("accountAll", accountAll.getContent());
        return "hello";
    }

    //アカウント検索
    @GetMapping("/search")
    public String getName(@RequestParam("keyWords")String keyWords, Model model, @PageableDefault(page = 0, size=2)Pageable pageable) {
        isAllOrSearch = false;
        Page<AccountEntity> accountAll = accountService.searchAccount(keyWords, pageable);

        model.addAttribute("keyWords", keyWords);
        model.addAttribute("isAllOrSearch", isAllOrSearch);
        model.addAttribute("page", accountAll);
        model.addAttribute("accountAll", accountAll.getContent());

        return "hello";
    }
}

HTML

アカウント全表示用と検索用でほぼ同じページネーションを書いてしまっている。
サービスクラスでのnullチェックが上手くいかないため。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
    <form action="/search" method="get">
        <input type="text" name="keyWords">
        <input type="submit">
    </form>

    <table>
        <tbody>
            <tr>
                <th>ID</th>
                <th>名前</th>
                <th>年齢</th>
            </tr>
            <tr th:each="account : ${accountAll}">
                <td th:text="${account.id}">id</td>
                <td th:text="${account.name}">名前</td>
                <td th:text="${account.age}">年齢</td>
            </tr>
        </tbody>
    </table>
    <!-- ページネーション -->
        <div th:if="${isAllOrSearch}">
            <ul>
                <li style="display: inline;"><span th:if="${page.isFirst}">&lt;&lt;先頭</span>
                    <a th:if="${!page.isFirst}"
                    th:href="@{/hello(page = 0)}">
                        &lt;&lt;先頭 </a></li>
                <li style="display: inline; margin-left: 10px;"><span th:if="${page.isFirst}"></span>
                    <a th:if="${!page.isFirst}"
                    th:href="@{/hello(page = ${page.number} - 1)}"></a></li>
                <li th:if="${!page.empty}" th:each="i : ${#numbers.sequence(0, page.totalPages - 1)}"
                    style="display: inline; margin-left: 10px;"><span
                    th:if="${i} == ${page.number}" th:text="${i + 1}">1</span> <a
                    th:if="${i} != ${page.number}"
                    th:href="@{/hello(page = ${i})}"> <span
                        th:text="${i+1}">1</span></a></li>
                <li style="display: inline; margin-left: 10px;"><span
                    th:if="${page.isLast}"></span> <a th:if="${!page.isLast}"
                    th:href="@{/hello(page = (${page.number} + 1))}"></a></li>
                <li style="display: inline; margin-left: 10px;"><span
                    th:if="${page.last}">最後&gt;&gt;</span> <a th:if="${!page.isLast}"
                    th:href="@{/hello(page = ${page.totalPages - 1})}">
                        最後&gt;&gt; </a></li>
            </ul>
        </div>

        <div th:if="${!isAllOrSearch}">
            <ul>
                <li style="display: inline;"><span th:if="${page.isFirst}">&lt;&lt;先頭</span>
                    <a th:if="${!page.isFirst}"
                    th:href="@{'/search?keyWords=' + ${keyWords} + '&page=0'}">
                        &lt;&lt;先頭 </a></li>
                <li style="display: inline; margin-left: 10px;"><span th:if="${page.isFirst}"></span>
                    <a th:if="${!page.isFirst}"
                    th:href="@{'/search?keyWords=' + ${keyWords} + '&page=' + ${page.number - 1}}"></a></li>
                <li th:if="${!page.empty}" th:each="i : ${#numbers.sequence(0, page.totalPages - 1)}"
                    style="display: inline; margin-left: 10px;"><span
                    th:if="${i} == ${page.number}" th:text="${i + 1}">1</span> <a
                    th:if="${i} != ${page.number}"
                    th:href="@{'/search?keyWords=' + ${keyWords} + '&page=' + ${i}}"> <span
                        th:text="${i+1}">1</span></a></li>
                <li style="display: inline; margin-left: 10px;"><span
                    th:if="${page.isLast}"></span> <a th:if="${!page.isLast}"
                    th:href="@{'/search?keyWords=' + ${keyWords} + '&page=' + ${page.number + 1}}"></a></li>
                <li style="display: inline; margin-left: 10px;"><span
                    th:if="${page.last}">最後&gt;&gt;</span> <a th:if="${!page.isLast}"
                    th:href="@{'/search?keyWords=' + ${keyWords} + '&page=' + ${page.totalPages - 1}}">
                        最後&gt;&gt; </a></li>
            </ul>
        </div>
</body>
</html>

Specificationパターン

Specificationパターンとは、仕様を満たすことを目的にしたデザインパターン。
例えば人材探しで条件と候補者を分ける。
普通ならIF文とかSQLのWhere句とかで実装する。

specificationパターンでは
and() や or() メソッドで、個々の条件判断オブジェクトを、追加していく。

今回の例で言えば、条件判断の元がEntityに入ってて、andやorメソッドで条件を追加していく。
(間違っていたらごめんなさい)

参考

Spring Data JPA の Specificationでらくらく動的クエリー
[JPA] DB検索時の条件を動的に設定する
JPA Specificationで複数キーワードによる絞り込み検索
Specification パターン :複雑な ビジネスルールの表現手段

終わりに

ここのところSpringBootやJPAに触れていてインターフェースの中身を覗くことが多かった。
抽象的なコードを理解するために、デザインパターンを知っておくと良さそう。
Javaのデザインパターンの名著があるらしいので買ってみよう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Spring Data JPAで動的にクエリを生成する(複数ワード検索)

JPAでGoogle検索みたいにスペース区切りで複数ワードのAND検索がしたい。
「転職 東京」みたいな。

色々調べていたらSpecificationに辿り着いたので、実装方法を備忘録として残しておきます。

Entity

@Data
@Entity
@Table(name="account")
public class AccountEntity implements Serializable{

    /**
     * シリアルバージョンUID
     */
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id")
    private Integer id;

    @Column(name="name")
    private String name;

    @Column(name="age")
    private Integer age;
}

Repository

JpaSpecificationExecutorを継承します。

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountRepository extends JpaRepository<AccountEntity, Integer>, JpaSpecificationExecutor<AccountEntity> {}

Specification

name/ageがBlankならnullを返す。
つまり検索ワードが何も入力されてなければWhere句やAnd句を追加しない。
検索ワードが入力されていればWhere句やAnd句を追加する。

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;


@Component
public class AccountSpecification {
    /**
     * 指定文字をnameに含むアカウントを検索する。
     */
    public Specification<AccountEntity> nameLike(String name) {

        // 三項演算子 「条件文 true : false」。 匿名クラス
        return name.isBlank() ? null : new Specification<AccountEntity>() {

            //CriteriaAPI
            @Override
            public Predicate toPredicate(Root<AccountEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

                // 「name LIKE '%name%'」を追加
                return cb.like(root.get("name"), "%" + name + "%");
            }
        };
    }

    /**
     * 指定文字をageに含むアカウントを検索する。
     */
    public Specification<AccountEntity> ageEqual(String age) {

        // 三項演算子 「条件文 true : false」。 匿名クラス
        return age.isBlank() ? null : new Specification<AccountEntity>() {

            //CriteriaAPI
            @Override
            public Predicate toPredicate(Root<AccountEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) {

                // 数値変換可能かのチェック
                try {
                    Integer.parseInt(age);

                    // 「age = 'age'」を追加
                    return cb.equal(root.get("age"), age);

                } catch (NumberFormatException e) {
                    return null;
                }
            }
        };
    }
}

Service

findAllの引数でwhere()やand()メソッドを呼び出してWhere句やAnd句を追加します。

1語検索バージョン
@Service
public class AccountService {

    @Autowired
    AccountRepository repository;

    @Autowired
    AccountSpecification accountSpecification;

    public List<AccountEntity> searchAccount(String keyWords){

        //前後の全角半角スペースを削除
        String trimedkeyWords = keyWords.strip();

        // 全角スペースと半角スペースで区切る
        String[] keyWordArray = trimedkeyWords.split("[  ]", 0);

        // 「Select * From account」 + 「Where name LIKE '%keyWordArray[0]%'」
        return repository.findAll(Specification
                .where(accountSpecification.ageEqual(keyWordArray[0])));
    }
}

「Select * From account Where(name='name' OR age='age') AND(name='name' OR age='age')」みたいにWhere句やAnd句の()の中に入れたい時はWhere/Andメソッドの中にAnd/ORメソッドを入れ子にする。

複数ワード検索バージョン
@Service
public class AccountService {

    @Autowired
    AccountRepository repository;

    @Autowired
    AccountSpecification accountSpecification;

    public List<AccountEntity> searchAccount(String keyWords){

        //前後の全角半角スペースを削除
        String trimedkeyWords = keyWords.strip();

        // 全角スペースと半角スペースで区切る
        String[] keyWordArray = trimedkeyWords.split("[  ]", 0);

        //0語なら全検索 1語ならWhere句追加 2語以上ならAnd句追加
        if(keyWordArray.length == 0) {
            return repository.findAll();

        }else if(keyWordArray.length == 1) {
            // 「Select * From account Where (name LIKE '%keyWordArray[0]%' OR age = 'age')
            return repository.findAll(Specification
                    .where(accountSpecification.nameLike(keyWordArray[0])
                    .or(accountSpecification.ageEqual(keyWordArray[0]))));

        }else {
            System.out.println(keyWordArray.length);


            Specification<AccountEntity> specification =
                    Specification.where(accountSpecification.nameLike(keyWordArray[0])
                            .or(accountSpecification.ageEqual(keyWordArray[0])));

            // 「Select * From account Where(name LIKE '%keyWordArray[0]%' OR age = 'age') AND(name LIKE '%keyWordArray[0]%' OR age = 'age') AND ・・・
            for(int i = 1; i < keyWordArray.length; i++) {
                specification = specification.and(accountSpecification.nameLike(keyWordArray[i])
                        .or(accountSpecification.ageEqual(keyWordArray[i])));
            }
            return repository.findAll(specification);
        }
    }
}

Controller

@Controller
public class AccountController {

    @Autowired
    AccountService accountService;

    @GetMapping("/hello")
    public String getHello() {
        return "hello";
    }

    @GetMapping("/name")
    public String getName(@RequestParam("name")String keyWords, Model model) {
        List<AccountEntity> accountAll = accountService.searchAccount(keyWords);

        model.addAttribute("accountAll", accountAll);

        return "hello";
    }
}

Specificationパターン

Specificationパターンとは、仕様を満たすことを目的にしたデザインパターン。
例えば人材探しで条件と候補者を分ける。
普通ならIF文とかSQLのWhere句とかで実装する。

specificationパターンでは
and() や or() メソッドで、個々の条件判断オブジェクトを、追加していく。

今回の例で言えば、条件判断の元がEntityに入ってて、andやorメソッドで条件を追加していく。
(間違っていたらごめんなさい)

参考
Spring Data JPA の Specificationでらくらく動的クエリー
[JPA] DB検索時の条件を動的に設定する
JPA Specificationで複数キーワードによる絞り込み検索
Specification パターン :複雑な ビジネスルールの表現手段

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【thymeleaf】An error happened during template parsingが出たときに考えること

初学者です。
初心者にありがちな、
書く→修正→バグ→修正→バグ・・・
のスパンの短いこと。少しでも短くしたい。

そのためには同じ様なエラーが出たときの対処をテンプレ化できればと思います。

環境

windows10
spring boot 2.4.0
thymeleaf 3.0.11

エラーメッセージ

There was an unexpected error (type=Internal Server Error, status=500).
An error happened during template parsing (template: "class path resource [templates/テンプレ名]")

とにかくこのエラーが起こる。ホンマによく起こる

どこを修正したら起こるのか

  1. コントローラー
  2. html

コントローラーのビューに渡すオブジェクトの名前や格納している中身を変更するとビューでの受け取りに失敗する。

thmlを修正した場合、thymeleafの書き方が間違っていることが多かった。特に
th:text="${customer"
のように最後の}が抜けていることが結構起きます。

別に下記忘れていたということではなく、エディターの補完機能で、{}のどちらかを消すと両方消されてしまうという便利のようで不便な機能で、意図せず消えてしまっていることがあるということです。

HTMLのビューの問題なら検索する

ページに表示されたエラーを全部読むのは大変です

ショートカット
Ctrl + F
// 「line」で検索

そうするとHTMLファイルの何行目にエラーが発生しているか探せます。

他にも適切な対処方があるとは思いますが、初心者の自分にはこれでほぼ解決できるエラーです。

  • コントローラーのオブジェクトの確認
  • lineで検索
  • HTMLの{}を確認する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む