Javaのバッチ(batch)処理について調べてみた

f:id:ts0818:20211106182801j:plain

www.itmedia.co.jp

 米Googleの親会社である米Alphabetは11月4日(現地時間)、AIを使った創薬事業を手掛ける新会社「Isomorphic Labs」を設立すると発表した。CEOは、遺伝子情報からタンパク質の立体構造を解析するAI「AlphaFold」や、囲碁AI「AlphaGo」を開発したAlphabet傘下の英DeepMind創業者、デミス・ハサビス代表が兼務する。

Google親会社のAlphabet、AI創薬の新会社設立 DeepMind創業者がCEOに - ITmedia NEWS

⇧「創薬事業」にイノベーションが起こるのかしら、何にせよ、良い方向へ進んでいって欲しいですね。

今回は、「バッチ処理」なんかについて調べてみました。

レッツトライ~。

 

バッチ処理って?

Wikipediaさんに聞いてみた。

バッチ処理(バッチしょり)はバッチ単位でおこなわれる処理である。特に以下の2つの意味で用いられる。

  1. コンピュータでひとまとまりのデータを一括して処理する方式。本項で詳述する。
  2. ひとつの設備である程度まとまった時間、または単位操作ごとに処理を区切り、原材料をこの区切りごとにまとめて投入する処理のこと。回分処理(かいぶんしょり)も同義。発酵工程やバッチ殺菌などは典型的なバッチ処理である。反対語は逐次処理または連続処理

バッチ処理 - Wikipedia

⇧ ってな感じの説明なんだけど、

コンピュータでひとまとまりのデータを一括して処理する方式。本項で詳述する。」って言っており、「バッチ(batch)」てのは、

「バッチ (batch)」という言葉は、プログラマプログラミングをする際、コードを書き、紙テープか80カラム仕様のパンチカードにパンチしていった時代の言葉。カードまたは紙テープはシステムオペレーターに渡され、オペレーターはタスクスケジューリングをし、コンピュータにカードまたは紙テープを投入していった。スケジューリングされたタスクは直ちにシステムに入れられたのではなく、一緒にまとめて投入されたので(バッチとは、複数の似ているものを1つのグループとして生産、処理すること、または一緒に集めて1つのユニットとして扱うこと。en:Batch processingより)、これらのタスクの集まりを「バッチジョブ」と呼ぶようになった。

バッチ処理 - Wikipedia

⇧ っていう語源らしい。

英語版のWikipediaによると、

Computerized batch processing is the running of "jobs that can run without end user interaction, or can be scheduled to run as resources permit."

https://en.wikipedia.org/wiki/Batch_processing

⇧ というように、人手による操作を介さずに実行できる、または、「リソース」の許す限り「スケジュール」された「ジョブ」のことですと。

ちなみに、「Process(computing)」のWikipediaの情報によると、

⇧「OS(Operation System)」においても「スケジュール」の仕組みが導入されてることが多いらしい。

「job」については出てこないのだけど、「program」の命令が「process」となって、「process」は複数の「thread」で構成されており、「scheduler」が「CPU」に実行させるタイミングを管理してる感じでしょうか。

で、「schedule」については、

In computerscheduling is the action of assigning resources to perform tasks. The resources may be processorsnetwork links or expansion cards. The tasks may be threadsprocesses or data flows.

https://en.wikipedia.org/wiki/Scheduling_(computing)

⇧「tasks」を実行するために「リソース」を割り当てるアクションということで、「The tasks may be threadsprocesses or data flows.」とあるように、「task」は複数の「prosess」「thread」または、「data flow」で構成されてますと。

「scheduler」がどんなところで使われてるかの例としては、

f:id:ts0818:20211103073142p:plain

A highly simplified structure of the Linux kernel, which includes process schedulers, I/O schedulers, and packet schedulers

https://en.wikipedia.org/wiki/Scheduling_(computing)

⇧「File System」なんかにおける「I/O」の制御とか、「prosess」の制御とかが有名なんですかね?

Windows環境であれば、「タスク マネージャー」とか、

f:id:ts0818:20211103080557p:plain

「タスク スケジューラ」なんかが、

f:id:ts0818:20211103080936p:plain

が「scheduler」の例と言えるんですかね?

で、肝心の「job」って?

In computing, a job is a unit of work or unit of execution (that performs said work). A component of a job (as a unit of work) is called a task or a step (if sequential, as in a job stream). As a unit of execution, a job may be concretely identified with a single process, which may in turn have subprocesses (child processes; the process corresponding to the job being the parent process) which perform the tasks or steps that comprise the work of the job; or with a process group; or with an abstract reference to a process or process group, as in Unix job control.

https://en.wikipedia.org/wiki/Job_(computing)

⇧ ということで、

  • a nuit of work
  • unit of execution(that perfoms said work)

というもので、「a unit of work」のほうは、

  • task
  • step(if sequential, as in job stream)

のどちらかで知られてるということらしい、知らんかったけど。

ここまでの情報を鑑みるに、

 「Job」 \supseteq 「task」 or 「step」 \supseteq 「process」 \supseteq 「thread」

ってことになるんかな?

長々と脱線しましたが、「バッチ処理」というのは、「schedule」された「jobs」ということですと。

ちなみに、「スティーブ・ジョブズ(スティーヴ・ジョブス、英語: Steve Jobs)」は、今回の「jobs」とは全く関係ないので、要注意です。

 

Javaでバッチ(batch)処理ってどうするのか?

その前に、Javaにおいては、「バッチ(batch)処理」の「標準仕様」が策定されたのが、遅かったらしい。

builder.japan.zdnet.com

 jBatchは、Java EE 7で初めて導入されたバッチ処理の標準仕様であり、その詳細はJSR-352(Batch Applications for Java Platform)で規定されています。近年のJava EEでは、オープンソース・ソフトウェアの成果を標準技術として積極的に取り込んでいますが、jBatchでも、基本的な仕組みの多くを「Spring Batch」から継承しています。jBatchは、あくまでもバッチ処理の基本部分を標準化したものであり、Spring Batchのほうが機能は豊富ですが、jBatchに準じたバッチ・アプリケーションを作ることで、Java EEに準拠した環境ならどこでも動かせるというメリットがあります。

Java EE 7 jBatchの使い方──『Java EE 7徹底入門』番外編 第3回 - builder by ZDNet Japan

 現状、Java EEでバッチ・アプリケーションを作る方法は、jBatchを含めて大きく4つあります。

Java EE 7 jBatchの使い方──『Java EE 7徹底入門』番外編 第3回 - builder by ZDNet Japan

⇧ 何か、Javaにおける「バッチ(batch)処理」の「標準仕様」が「jBatch」ということになったらしいんだけど、経緯としては、

builder.japan.zdnet.com

 「バッチ処理」という言葉を聞いて、多くの方は「月次の〆処理」や「銀行勘定系システム」、「在庫管理」、「深夜に動く」、「計算量が膨大」、「ジョブ」、「順序制御」、「並列実行」といったキーワードを連想されるだろう。このことから、「多くの業種やシステムに同様の処理が存在し、求められる機能や用途には共通の点が多い」ことがわかる。この「似通った処理機能が、さまざまな業種で個別に開発/利用されてきた」ことが、Java EEバッチ処理の標準化が求められていた最大の理由だ。

待望のバッチ処理機能がJava EE 7で標準化。「jBatch」で何ができるのか、どう使うのか? - builder by ZDNet Japan

⇧ 開発現場での需要があったってことですかね?

上記のような感じで、各々の開発プロジェクトで自作してたような感じなんですかね?

まぁ、不思議なのが、個別に開発されてたってような「バッチ(Batch)処理」の構築の方法とかって、情報が全く見当たらないんだけど、昔のエンジニアの人の情報発信をGoogleさんはちゃんと上位表示して欲しいんだが...

いや、まぁ、情報発信してくれてるという仮定が矛盾しないとしての話ではありますが...

で、「バッチ(Batch)処理」に関しては、鎖国みたいに閉鎖的な状況が続いていたというのは何となく分かりましたと。

そんなこんなで、個別で開発されてたみたいな「バッチ(Batch)処理」の秘儀みたいな情報とかは、出回っていないので、Javaでレガシーなシステムでなくて「バッチ(batch)処理」をするってなると、「Spring Batch」か「jBatch」が有力候補に挙がってくるってことなんかな?

teratail.com

⇧ 上記サイト様の情報によりますと、「Java EE」と「Spring Framework」は機能を混ぜたら誤動作に繋がることが起こり得るということで、どの「バッチ(Batch)処理」のライブラリを使うかは、開発プロジェクトに依りけりってことですかね。

 

Spring Batchのアーキテクトって?

Java EE」が「Oracle」から「Eclipse Foundation」に寄贈されて、「Jakarta EE」とかになって、いろいろ改善されていくのかもしらんけど、今はまだ「Spring Framework」を使っていくのが無難なんではないかということで、「Spring Batch」を使ってみることに。(すみません、後述しますが今回は「Spring Batch」は使いませんでした...)

その前に、「Spring Batch」のアーキテクトってどんな感じなのか?

docs.spring.io

Spring Batch is designed with extensibility and a diverse group of end users in mind. The figure below shows the layered architecture that supports the extensibility and ease of use for end-user developers.

https://docs.spring.io/spring-batch/docs/current/reference/html/index-single.html#springBatchArchitecture

This layered architecture highlights three major high-level components: Application, Core, and Infrastructure. The application contains all batch jobs and custom code written by developers using Spring Batch. 

https://docs.spring.io/spring-batch/docs/current/reference/html/index-single.html#springBatchArchitecture

The Batch Core contains the core runtime classes necessary to launch and control a batch job. It includes implementations for JobLauncherJob, and Step

https://docs.spring.io/spring-batch/docs/current/reference/html/index-single.html#springBatchArchitecture

Both Application and Core are built on top of a common infrastructure. This infrastructure contains common readers and writers and services (such as the RetryTemplate), which are used both by application developers(readers and writers, such as ItemReader and ItemWriter) and the core framework itself (retry, which is its own library).

https://docs.spring.io/spring-batch/docs/current/reference/html/index-single.html#springBatchArchitecture

⇧ 大きく分けて、「Batch Core」と「Batch Infrastructure」で機能を分けてるらしいですと。

バージョン1.0.xと旧いのですが、

docs.spring.io

⇧ 一般的なシンプルな「バッチ(Batch)処理」の構成ってのが、上記のイメージ図のような感じになるってことなんかな?

って思っていたら、「Spring Batch」には処理方式によって、

  • Tasklet
  • Chunk

の2パターンあるらしく、

dzone.com

Spring Batch Framework offers ‘TaskletStep Oriented’ and ‘Chunk Oriented’ processing style. In this article, Chunk Oriented Processing Model is explained. Also, TaskletStep Oriented Processing in Spring Batch Article is definitely suggested to investigate how to develop TaskletStep Oriented Processing in Spring Batch.

https://dzone.com/articles/chunk-oriented-processing

Chunk Oriented Processing Feature has come with Spring Batch v2.0.

https://dzone.com/articles/chunk-oriented-processing

⇧ 上記サイト様によりますと、「Chunk」って処理方式については、「Spring Batch」のバージョン 2.0 でお披露目されたらしく、バージョン 1.0のドキュメントの内容は「Tasklet」に関してのみの情報ってことになるっぽい。

何て言うか、経緯とか背景を把握してないと公式のドキュメントも意味不明な部分のほうが多くなってくるから困るよね...

NTT DATAの方が公開してくれてるスライドによると、

⇧「リスタート機能」っていうのが、その語句の意味通りと捉えて良いとすると、何か途中で異常とか起きた場合に、処理が再開できるかどうかってのが一番の違いって感じなんですかね?

なので、

spring.pleiades.io

上記の図は、Spring Batch のドメイン言語を構成する重要な概念を強調しています。ジョブには 1 つから多数のステップがあり、各ステップには正確に 1 つの ItemReader、1 つの ItemProcessor、1 つの ItemWriter があります。ジョブを立ち上げる必要があり(JobLauncher を使用)、現在実行中のプロセスに関するメタデータを(JobRepository に)格納する必要があります。

https://spring.pleiades.io/spring-batch/docs/current/reference/html/domain.html#domainLanguageOfBatch

⇧「Step」が、

  • Tasklet
  • Chunk

のどちらかになるにせよ、実際に

  • データのインプット
  • データの加工
  • データのアウトプット

を受け持ってるのは「Step」らしい。

「Spring Batch」のAPIのパッケージ的には、

⇧ みたいな感じになるんかな?(赤枠とか赤字の文言は勝手に加えてます。)

で、「Job」は複数の「Step」を持つことができるというあたりは、この記事の冒頭で「バッチ処理」とは何ぞや?、で調べていて辿り着いた、

 「Job」 \supseteq 「task」 or 「step」 \supseteq 「process」 \supseteq 「thread」

の関係に則していると言えますかね。

で、「Step」が実行されるには「Job」が起動してる必要があり、「Job」を起動させるのは「JobLauncher」らしい。

で、肝心の「JobLauncher」は誰が稼働させんのよ?ってのが、

spring.pleiades.io

⇧ 何かしら、外部からのアクションが必要らしい。

なんだけど、「Client」についての説明が一切無いというね...

で、ドキュメントを読み進めていくと、「JobLauncher」を稼働させる方法としては、

の2つのアプローチがあって、「Web コンテナー内からのジョブの実行」ってほうは、

⇧ 何かしら、外部からリクエストが「Controller」に届いて、「Controller」はリクエストを元に、「JobLauncher」を起動させる想定ですと、つまり「オンライン処理」になるんかな?

で、「コマンドラインからジョブを実行する」については、

コマンドラインからジョブを実行する

エンタープライズスケジューラからジョブを実行するユーザーの場合、コマンドラインがプライマリインターフェースです。これは、ほとんどのスケジューラー(NativeJob を使用する場合を除き、Quartz を除く)は、主にシェルスクリプトで開始されるオペレーティングシステムプロセスで直接動作するためです。PerlRuby などのシェルスクリプト、あるいは ant や maven などの「ビルドツール」など、Java プロセスを起動する方法は多数あります。ただし、ほとんどの人はシェルスクリプトに精通しているため、この例ではシェルスクリプトに焦点を当てます。

https://spring.pleiades.io/spring-batch/docs/current/reference/html/job.html#runningJobsFromCommandLine

⇧ って説明があり、というか、『ただし、ほとんどの人はシェルスクリプトに精通しているため、この例ではシェルスクリプトに焦点を当てます。』って無言のプレッシャーが半端ない...

というか、「エンタープライズスケジューラ」って何なのよ?

www.linkedin.com

An Enterprise Job Scheduler is a computer application for controlling and automating the unattended programmatic execution of jobs. (Jobs could be in the form of ETL, FTP, Backups, Stored Procedures etc). This is commonly called batch scheduling. 

What is an Enterprise Job Scheduler?

⇧『「ジョブの実行」を操作したり自動化したりするアプリケーション』ってことらしく、一般的には、「batch scheduling」と呼ばれる、とあることから、「バッチ処理」界隈では有名らしい、私は初耳でしたけど...。

なので、よく事前に決めておいた日程なんかに「バッチ処理」が実行されるようにする仕組みは、「コマンドラインからジョブを実行する」になるんかな、それで言うと、「オフライン処理」ってことになるんかな?

 

Quartz」と言う名のバッチ処理ライブラリ

職場の先輩に教えていただいたのですが、「Quartz」という「バッチ処理」のライブラリを知りました。

What’s Quartz

www.quartz-scheduler.org

What is the Quartz Job Scheduling Library?

Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components that may execute virtually anything you may program them to do. The Quartz Scheduler includes many enterprise-class features, such as support for JTA transactions and clustering.

Quartz is freely usable, licensed under the Apache 2.0 license.

http://www.quartz-scheduler.org/

⇧ と説明があり、「job scheduling library」というものらしい。

github.com

Quartz is a richly featured, open source job scheduling library that can be integrated within virtually any Java application - from the smallest stand-alone application to the largest e-commerce system.

https://github.com/quartz-scheduler/quartz

GitHubの説明も「job scheduling library」ですね。

我々は、この記事の冒頭で、心許ない綱渡り感のある仮定と推測によって

 「『schedule』された『job』」== 「バッチ処理」

ということを突き詰めていたので、

 「job \ scheduling \ library」== 「『バッチ処理』に関するライブラリ」

という仮定が矛盾しないという前提で話を進めていきます。

で、「『バッチ処理』に関するライブラリ」の1種であると思われる「Quartz」ですが、「エンタープライズスケジューラ」の1種になるんかな?

まぁ、「エンタープライズスケジューラ」の1種であると仮定して矛盾しない前提で話を進めると、「エンタープライズスケジューラ」は一般的に「batch scheduling」すると呼ばれると言っていたので、「バッチ処理」が実装できるライブラリってことがほぼほぼ言えるんじゃないでしょうか?

いやはや、相変わらず、用語の解釈に時間を要しますな...もっと他のことに時間を使いたいところなんですけどね...

 

というわけで、使ってみる

物は試し、というわけで、使ってみる。

blog.y-yuki.net

stackoverflow.com

stackoverflow.com

⇧ 上記サイト様を参考に実施してみました。

「定期実行」みたいなことは、頑張れば、Java SEの標準APIの「ScheduledExecutorService」ってので実装できるらしいのですが、「Quartz」を使えば、簡単に...いかないみたいですね...

どうも、「30日」とか「60日」とか「90日」とか、長いスパンでの「定期実行」に関しては、情報が少ないみたいですね、実際の現場で絶対に用途があるはず技術って、どうにも情報が出回らない感がありますね。

まぁ、stackoverflowを見て頑張りますか。

と思ったら、「Quartz」の「Trigger」についてのチュートリアルで説明されてる模様。

www.quartz-scheduler.org

1つ古いバージョンのチュートリアルで、どんな値を設定できるかとかの説明があるっぽいです。

www.quartz-scheduler.org

⇧ 上記のように、「Trigger」で、どのタイミングで「バッチ処理」が実行されるようにするかを設定できる模様。

というわけで、Eclipseを起動し、「ファイル(F)」>「新規(N)」>「Gradle プロジェクト」を選択。

f:id:ts0818:20211102215444p:plain

「プロジェクト名」を適当に入力し、「次へ(N)>」を押下。

f:id:ts0818:20211102215622p:plain

特に何も変更せずデフォルトの状態で、「次へ(N)>」を押下しました。

f:id:ts0818:20211102215703p:plain

「完了(F)」を押下。

f:id:ts0818:20211102215803p:plain

「Gradle プロジェクト」が作成されたら、「build.gradle」に「Quartz」のライブラリを依存関係として追加していきます。

f:id:ts0818:20211102215903p:plain

で、その前に「Eclipse」で作成された「Gradle プロジェクト」で利用されてる「Gradle」のバージョンが旧かったので、「Gradle プロジェクト」の「Gradle」のバージョンを上げました。

「Gradle Dsitributions Snapshots」と、

services.gradle.org

「Gradle Distirbutions」で、

services.gradle.org

何が違うか分からんけども、今回は「Gradle Dsitribution Snapshots」を利用することに。

「Gradle プロジェクト」内の「gradle」フォルダにある「gradle-wrapper.properties」を開いて、

f:id:ts0818:20211106171110p:plain

「distributionUrl」を編集。「Gradle Distirbution Snapshots」のリンクをコピーしてきて、以下のように「https」と「:」の間に「\」を追記して設定で。

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
#distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-7.4-20211104232430+0000-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

⇧ 今回はバージョンを「7.4」にしてみました。

そしたらば、「Gradle」>「Gradle プロジェクトのリフレッシュ」しときます。

f:id:ts0818:20211106172618p:plain

脱線しましたが、「build.gradle」に追加する「依存関係」は、

github.com

⇧ 上記のページの「Downloads」のページの情報より、「groupId」が「org.quartz-scheduler」のライブラリを追加すれば良いと思ったのですが、「Spring Boot」を使う場合、「spring-boot-starter-quartz」って依存関係があるようなので、「MVN Repository」のサイトで、諸々の依存関係を探して、その他にも、「JDBCドライバ(PostgresSQL用)」、「Spring Boot Starter Data JPA」、「Jackson」の依存関係とか何やかんやを追加して、「build.gradle」は以下のようになりました。

■/QuartzProject/build.gradle

/*
 * This file was generated by the Gradle 'init' task.
 *
 * This generated file contains a sample Java Library project to get you started.
 * For more details take a look at the Java Libraries chapter in the Gradle
 * User Manual available at https://docs.gradle.org/5.6.1/userguide/java_library_plugin.html
 */

plugins {
    // Apply the java-library plugin to add support for Java Library
    id 'java-library'
    id 'org.springframework.boot' version '2.5.6'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}

repositories {
    // Use jcenter for resolving dependencies.
    // You can declare any Maven/Ivy/file repository here.
    jcenter()
}

dependencies {
    // This dependency is exported to consumers, that is to say found on their compile classpath.
    api 'org.apache.commons:commons-math3:3.6.1'

    // This dependency is used internally, and not exposed to consumers on their own compile classpath.
    implementation 'com.google.guava:guava:28.0-jre'

    // Use JUnit test framework
    testImplementation 'junit:junit:4.12'

    // https://mvnrepository.com/artifact/org.quartz-scheduler/quartz
    //implementation group: 'org.quartz-scheduler', name: 'quartz', version: '2.3.2'

    // https://mvnrepository.com/artifact/org.postgresql/postgresql
    implementation group: 'org.postgresql', name: 'postgresql', version: '42.3.1'

    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter', version: '2.5.6'

    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-quartz
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-quartz', version: '2.5.6'

    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa
    implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.5.6'

    // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
    implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.0'

    // https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api
    implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.4.0-b180830.0359'

    // https://mvnrepository.com/artifact/com.mchange/c3p0
    implementation group: 'com.mchange', name: 'c3p0', version: '0.9.5.5'

}

⇧ ちょっと、「Gradle プロジェクト」作成時の「Gradle」が旧かったせいなのか、デフォルトで「build.gradle」に記載されてる「依存関係」のバージョンが旧いような気がしなくもないですが...

で、「PostgreSQL」の「テーブル」なんかについては、

ts0818.hatenablog.com

⇧ 前回の記事で作成したものを使っていこうかと。「テーブル」としては、5つ作ってる感じです。

f:id:ts0818:20211103103623p:plain

⇧ 上記の「テーブル」については、何気なしに、

⇧ 上図を参考に作成したのですが、「User」の「user_email_token_expires」ってのが、「user_email_token」ってものの「有効期限」っぽく、今を時めく「OAuth 2.0」の仕様を見ると、

datatracker.ietf.org

RFC 6749                        OAuth 2.0                   October 2012


4.2.2.  Access Token Response

   If the resource owner grants the access request, the authorization
   server issues an access token and delivers it to the client by adding
   the following parameters to the fragment component of the redirection
   URI using the "application/x-www-form-urlencoded" format, per
   Appendix B:

   access_token
         REQUIRED.  The access token issued by the authorization server.

   token_type
         REQUIRED.  The type of the token issued as described in
         Section 7.1.  Value is case insensitive.

   expires_in
         RECOMMENDED.  The lifetime in seconds of the access token.  For
         example, the value "3600" denotes that the access token will
         expire in one hour from the time the response was generated.
         If omitted, the authorization server SHOULD provide the
         expiration time via other means or document the default value.

https://datatracker.ietf.org/doc/html/rfc6749

⇧ 「expires_in」って項目があって、一般的に「token」の「有効期限」ってのは「second(秒)」で保持するのがデファクトスタンダードらしいですと。

となると、管理する側としては、「token」が作成された「日時」を保持してないと、「有効期限」を過ぎたのかどうかを判定できませんと。

で、「テーブル」を作成するのに参考にした「データモデリング」はと言うと、

f:id:ts0818:20211103105502p:plain

⇧ 作成した日時、どれなんやろ?ってな感じで、こういう時に役立つのがドキュメント、つまり「設計書」とかだとは思うのだけど、そもそも「設計書」なんて無いよ、というような開発現場もあり、推測するしかないがために個々人によって解釈が異なり、認識齟齬の雨霰が今日も各地で起こるのであった...

まぁ、話を元に戻すと、

項目 データ型 格納サイズ
(バイト)
備考
user_email_token_created timestamp 8 user_email_tokenが作成された日時
YYYY/MM/DD hh:mm:ss

みたいな「項目」はあったほうが良い気がするんですが、「User」を作った人の意図が分からんのが困ったところですね...

なので、実際の開発現場では、とりあえず、「自分はこういう風に考えたけど合ってます?」みたいな感じで「認識合わせ」は超重要ってことですね。

有識者の人とかなら、「たぶん、こういうことを聞きたいんだろうな」って感じで情報を正しい方向へ導いてくれることが多いので、自分なりの「ここがよく分からない」ってのをアウトプットしていくのは大事!

もし、誰も分から~んってなったら、その時にチームのみんなで考えれば良いですし。

度々、話が脱線してすみません。

そんなわけで、「user」テーブルに「user_email_token_created」って「カラム」を追加することにします。

f:id:ts0818:20211103115049p:plain

本当は、追加する「カラム」の位置を指定したかったのですが、

wiki.postgresql.org

postgresqlの初心者の多くからよく、テーブル内の列の位置を変更することはサポートされているかどうか聞かれます。 今のところサポートされていません。 列の位置を変更したいのであれば、テーブルを再作成するか、新しい列を追加してデータを移動するかのいずれかを行う必要があります。 列の位置の順序の変更を許すという考えをpostgresql開発者は対象としていません。

  • This page was last edited on 19 December 2009, at 07:12.

https://wiki.postgresql.org/wiki/Alter_column_position/ja

⇧ いまのところ、と言っていたのが「2009年12月19日」だとすると、いまが「2021年11月3日」だから、大体12年ぐらい経過してるっぽいのだが、

teratail.com

RailsPostgresqlの環境です。

テーブルの任意の場所にカラムを追加したいのですが、Postgresqlの場合、afterが有効にならず、テーブルの最後のカラムの後にしかカラムが追加されないのは周知の事実だと思います。

https://teratail.com/questions/275759

⇧ って「問いかけ」に対して、「回答」で特に指摘が無いところを見ると、「PostgreSQL」では「カラム」の追加は「テーブル」の「カラム」の末尾に追加されていく仕様が変わることは無さそうですね。

話を戻して、インストールしていた「PostgreSQL」に接続して、「テーブル」を作成していた「postgres」データベースに接続します。

前回インストールしてたのが「PostgreSQL 14」になります。

f:id:ts0818:20211106165825p:plain

では、接続。

f:id:ts0818:20211103113809p:plain

「user」テーブルに「user_email_token_created」って「カラム」を追加で。(SQL文をいろいろミスって申し訳ない。)

f:id:ts0818:20211103114346p:plain

追加できました。

f:id:ts0818:20211103120218p:plain

データも追加しておくことにします。
ただ、「PostgreSQL」で「テーブル」に「パスワード」とか保存する際に、「パスワード」を「平文」で保存するってことは宜しくないということで、「ハッシュ化」とかしたほうが良いと思われるのですが、

www.postgresql.jp

pgcryptoモジュールはPostgreSQL用の暗号関数を提供します。

このモジュールはtrustedと見なされます。つまり、現在のデータベースに対してCREATE権限を持つ非スーパーユーザがインストールできます。

https://www.postgresql.jp/document/13/html/pgcrypto.html

⇧ という「暗号関数」が「PostgreSQL」には用意されてるらしいのですが、

www.postgresql.jp

この付録と次の付録にはPostgreSQL配布物のcontribディレクトリにあるモジュールに関する情報があります。 ここには、移植用のツール、解析ユーティリティ、限定した利用者を対象にしていること、または、主ソースツリーに含めるには実験的すぎることが主な理由でPostgreSQLのコアシステムにはないプラグイン機能が含まれます。 これはその有用性を妨げるものではありません。

https://www.postgresql.jp/document/13/html/contrib.html

多くのモジュールは新しいユーザ定義関数、演算子、型を提供します。 こうしたモジュールの1つを使用できるようにするためには、コードをインストールした後に、新しいSQLオブジェクトをデータベースサーバに登録する必要があります。 これはCREATE EXTENSIONコマンドを実行することで行われます。

https://www.postgresql.jp/document/13/html/contrib.html

⇧ とあって、「contrib」ディレクトリに格納されてる「モジュール」であれば、「CREATE EXTENTION [モジュール]」って感じのSQL文を実行すれば「モジュール」が利用できるようになるらしい。

で、Windows環境の「PostgreSQL」では、「contrib」ディレクトリなんてものは存在せず、「lib」ディレクトリが該当するらしい...

公式のドキュメントが全く役に立たん...

f:id:ts0818:20211103142444p:plain

で、「モジュール」が有効になってるかどうかは、以下のSQL文で確認できるらしい。

select * from pg_available_extensions;    

f:id:ts0818:20211103142535p:plain

 

        name         | default_version | installed_version |                                comment
---------------------+-----------------+-------------------+------------------------------------------------------------------------
 adminpack           | 2.1             | 2.1               | administrative functions for PostgreSQL
 amcheck             | 1.3             |                   | functions for verifying relation integrity
 autoinc             | 1.0             |                   | functions for autoincrementing fields
 bloom               | 1.0             |                   | bloom access method - signature file based index
 bool_plperl         | 1.0             |                   | transform between bool and plperl
 bool_plperlu        | 1.0             |                   | transform between bool and plperlu
 btree_gin           | 1.3             |                   | support for indexing common datatypes in GIN
 btree_gist          | 1.6             |                   | support for indexing common datatypes in GiST
 citext              | 1.6             |                   | data type for case-insensitive character strings
 cube                | 1.5             |                   | data type for multidimensional cubes
 dblink              | 1.2             |                   | connect to other PostgreSQL databases from within a database
 dict_int            | 1.0             |                   | text search dictionary template for integers
 dict_xsyn           | 1.0             |                   | text search dictionary template for extended synonym processing
 dummy_index_am      | 1.0             |                   | dummy_index_am - index access method template
 earthdistance       | 1.1             |                   | calculate great-circle distances on the surface of the Earth
 file_fdw            | 1.0             |                   | foreign-data wrapper for flat file access
 fuzzystrmatch       | 1.1             |                   | determine similarities and distance between strings
 hstore              | 1.8             |                   | data type for storing sets of (key, value) pairs
 hstore_plperl       | 1.0             |                   | transform between hstore and plperl
 hstore_plperlu      | 1.0             |                   | transform between hstore and plperlu
 hstore_plpython2u   | 1.0             |                   | transform between hstore and plpython2u
 hstore_plpython3u   | 1.0             |                   | transform between hstore and plpython3u
 hstore_plpythonu    | 1.0             |                   | transform between hstore and plpythonu
 insert_username     | 1.0             |                   | functions for tracking who changed a table
 intagg              | 1.1             |                   | integer aggregator and enumerator (obsolete)
 intarray            | 1.5             |                   | functions, operators, and index support for 1-D arrays of integers
 isn                 | 1.2             |                   | data types for international product numbering standards
 jsonb_plperl        | 1.0             |                   | transform between jsonb and plperl
 jsonb_plperlu       | 1.0             |                   | transform between jsonb and plperlu
 jsonb_plpython2u    | 1.0             |                   | transform between jsonb and plpython2u
 jsonb_plpython3u    | 1.0             |                   | transform between jsonb and plpython3u
 jsonb_plpythonu     | 1.0             |                   | transform between jsonb and plpythonu
 lo                  | 1.1             |                   | Large Object maintenance
 ltree               | 1.2             |                   | data type for hierarchical tree-like structures
 ltree_plpython2u    | 1.0             |                   | transform between ltree and plpython2u
 ltree_plpython3u    | 1.0             |                   | transform between ltree and plpython3u
 ltree_plpythonu     | 1.0             |                   | transform between ltree and plpythonu
 moddatetime         | 1.0             |                   | functions for tracking last modification time
 old_snapshot        | 1.0             |                   | utilities in support of old_snapshot_threshold
 pageinspect         | 1.9             |                   | inspect the contents of database pages at a low level
 pgcrypto            | 1.3             |                   | cryptographic functions
 pgrowlocks          | 1.2             |                   | show row-level locking information
 pgstattuple         | 1.5             |                   | show tuple-level statistics
 pg_buffercache      | 1.3             |                   | examine the shared buffer cache
 pg_freespacemap     | 1.2             |                   | examine the free space map (FSM)
 pg_prewarm          | 1.2             |                   | prewarm relation data
 pg_stat_statements  | 1.9             |                   | track planning and execution statistics of all SQL statements executed
 pg_surgery          | 1.0             |                   | extension to perform surgery on a damaged relation
 pg_trgm             | 1.6             |                   | text similarity measurement and index searching based on trigrams
 pg_visibility       | 1.2             |                   | examine the visibility map (VM) and page-level visibility info
 pldbgapi            | 1.1             |                   | server-side support for debugging PL/pgSQL functions
 plperl              | 1.0             |                   | PL/Perl procedural language
 plperlu             | 1.0             |                   | PL/PerlU untrusted procedural language
 plpgsql             | 1.0             | 1.0               | PL/pgSQL procedural language
 plpython2u          | 1.0             |                   | PL/Python2U untrusted procedural language
 plpython3u          | 1.0             |                   | PL/Python3U untrusted procedural language
 plpythonu           | 1.0             |                   | PL/PythonU untrusted procedural language
 plsample            | 1.0             |                   | PL/Sample
 pltcl               | 1.0             |                   | PL/Tcl procedural language
 pltclu              | 1.0             |                   | PL/TclU untrusted procedural language
 postgres_fdw        | 1.1             |                   | foreign-data wrapper for remote PostgreSQL servers
 refint              | 1.0             |                   | functions for implementing referential integrity (obsolete)
 seg                 | 1.4             |                   | data type for representing line segments or floating-point intervals
 spgist_name_ops     | 1.0             |                   | Test opclass for SP-GiST
 sslinfo             | 1.2             |                   | information about SSL certificates
 system_stats        | 1.0             |                   | EnterpriseDB system statistics for PostgreSQL
 tablefunc           | 1.0             |                   | functions that manipulate whole tables, including crosstab
 tcn                 | 1.0             |                   | Triggered change notifications
 test_bloomfilter    | 1.0             |                   | Test code for Bloom filter library
 test_ext1           | 1.0             |                   | Test extension 1
 test_ext2           | 1.0             |                   | Test extension 2
 test_ext3           | 1.0             |                   | Test extension 3
 test_ext4           | 1.0             |                   | Test extension 4
 test_ext5           | 1.0             |                   | Test extension 5
 test_ext6           | 1.0             |                   | test_ext6
 test_ext7           | 1.0             |                   | Test extension 7
 test_ext8           | 1.0             |                   | Test extension 8
 test_ext_cyclic1    | 1.0             |                   | Test extension cyclic 1
 test_ext_cyclic2    | 1.0             |                   | Test extension cyclic 2
 test_ext_evttrig    | 1.0             |                   | Test extension - event trigger
 test_ginpostinglist | 1.0             |                   | Test code for ginpostinglist.c
 test_integerset     | 1.0             |                   | Test code for integerset
 test_pg_dump        | 1.0             |                   | Test pg_dump with an extension
 test_predtest       | 1.0             |                   | Test code for optimizer/util/predtest.c
 test_rbtree         | 1.0             |                   | Test code for red-black tree library
 test_regex          | 1.0             |                   | Test code for backend/regex/
 tsm_system_rows     | 1.0             |                   | TABLESAMPLE method which accepts number of rows as a limit
 tsm_system_time     | 1.0             |                   | TABLESAMPLE method which accepts time in milliseconds as a limit
 unaccent            | 1.1             |                   | text search dictionary that removes accents
 uuid-ossp           | 1.1             |                   | generate universally unique identifiers (UUIDs)
 xml2                | 1.1             |                   | XPath querying and XSLT
(91 行)

⇧「installed_version」に「バージョン」が入って無いものは有効になってないってことなんかな?

とりあえず、「CREATE EXTENTION」コマンドを試してみる。

 
CREATE EXTENTION pgcrypt;

f:id:ts0818:20211103143451p:plain

        name         | default_version | installed_version |                                comment
---------------------+-----------------+-------------------+------------------------------------------------------------------------
 adminpack           | 2.1             | 2.1               | administrative functions for PostgreSQL
 amcheck             | 1.3             |                   | functions for verifying relation integrity
 autoinc             | 1.0             |                   | functions for autoincrementing fields
 bloom               | 1.0             |                   | bloom access method - signature file based index
 bool_plperl         | 1.0             |                   | transform between bool and plperl
 bool_plperlu        | 1.0             |                   | transform between bool and plperlu
 btree_gin           | 1.3             |                   | support for indexing common datatypes in GIN
 btree_gist          | 1.6             |                   | support for indexing common datatypes in GiST
 citext              | 1.6             |                   | data type for case-insensitive character strings
 cube                | 1.5             |                   | data type for multidimensional cubes
 dblink              | 1.2             |                   | connect to other PostgreSQL databases from within a database
 dict_int            | 1.0             |                   | text search dictionary template for integers
 dict_xsyn           | 1.0             |                   | text search dictionary template for extended synonym processing
 dummy_index_am      | 1.0             |                   | dummy_index_am - index access method template
 earthdistance       | 1.1             |                   | calculate great-circle distances on the surface of the Earth
 file_fdw            | 1.0             |                   | foreign-data wrapper for flat file access
 fuzzystrmatch       | 1.1             |                   | determine similarities and distance between strings
 hstore              | 1.8             |                   | data type for storing sets of (key, value) pairs
 hstore_plperl       | 1.0             |                   | transform between hstore and plperl
 hstore_plperlu      | 1.0             |                   | transform between hstore and plperlu
 hstore_plpython2u   | 1.0             |                   | transform between hstore and plpython2u
 hstore_plpython3u   | 1.0             |                   | transform between hstore and plpython3u
 hstore_plpythonu    | 1.0             |                   | transform between hstore and plpythonu
 insert_username     | 1.0             |                   | functions for tracking who changed a table
 intagg              | 1.1             |                   | integer aggregator and enumerator (obsolete)
 intarray            | 1.5             |                   | functions, operators, and index support for 1-D arrays of integers
 isn                 | 1.2             |                   | data types for international product numbering standards
 jsonb_plperl        | 1.0             |                   | transform between jsonb and plperl
 jsonb_plperlu       | 1.0             |                   | transform between jsonb and plperlu
 jsonb_plpython2u    | 1.0             |                   | transform between jsonb and plpython2u
 jsonb_plpython3u    | 1.0             |                   | transform between jsonb and plpython3u
 jsonb_plpythonu     | 1.0             |                   | transform between jsonb and plpythonu
 lo                  | 1.1             |                   | Large Object maintenance
 ltree               | 1.2             |                   | data type for hierarchical tree-like structures
 ltree_plpython2u    | 1.0             |                   | transform between ltree and plpython2u
 ltree_plpython3u    | 1.0             |                   | transform between ltree and plpython3u
 ltree_plpythonu     | 1.0             |                   | transform between ltree and plpythonu
 moddatetime         | 1.0             |                   | functions for tracking last modification time
 old_snapshot        | 1.0             |                   | utilities in support of old_snapshot_threshold
 pageinspect         | 1.9             |                   | inspect the contents of database pages at a low level
 pgcrypto            | 1.3             | 1.3               | cryptographic functions
 pgrowlocks          | 1.2             |                   | show row-level locking information
 pgstattuple         | 1.5             |                   | show tuple-level statistics
 pg_buffercache      | 1.3             |                   | examine the shared buffer cache
 pg_freespacemap     | 1.2             |                   | examine the free space map (FSM)
 pg_prewarm          | 1.2             |                   | prewarm relation data
 pg_stat_statements  | 1.9             |                   | track planning and execution statistics of all SQL statements executed
 pg_surgery          | 1.0             |                   | extension to perform surgery on a damaged relation
 pg_trgm             | 1.6             |                   | text similarity measurement and index searching based on trigrams
 pg_visibility       | 1.2             |                   | examine the visibility map (VM) and page-level visibility info
 pldbgapi            | 1.1             |                   | server-side support for debugging PL/pgSQL functions
 plperl              | 1.0             |                   | PL/Perl procedural language
 plperlu             | 1.0             |                   | PL/PerlU untrusted procedural language
 plpgsql             | 1.0             | 1.0               | PL/pgSQL procedural language
 plpython2u          | 1.0             |                   | PL/Python2U untrusted procedural language
 plpython3u          | 1.0             |                   | PL/Python3U untrusted procedural language
 plpythonu           | 1.0             |                   | PL/PythonU untrusted procedural language
 plsample            | 1.0             |                   | PL/Sample
 pltcl               | 1.0             |                   | PL/Tcl procedural language
 pltclu              | 1.0             |                   | PL/TclU untrusted procedural language
 postgres_fdw        | 1.1             |                   | foreign-data wrapper for remote PostgreSQL servers
 refint              | 1.0             |                   | functions for implementing referential integrity (obsolete)
 seg                 | 1.4             |                   | data type for representing line segments or floating-point intervals
 spgist_name_ops     | 1.0             |                   | Test opclass for SP-GiST
 sslinfo             | 1.2             |                   | information about SSL certificates
 system_stats        | 1.0             |                   | EnterpriseDB system statistics for PostgreSQL
 tablefunc           | 1.0             |                   | functions that manipulate whole tables, including crosstab
 tcn                 | 1.0             |                   | Triggered change notifications
 test_bloomfilter    | 1.0             |                   | Test code for Bloom filter library
 test_ext1           | 1.0             |                   | Test extension 1
 test_ext2           | 1.0             |                   | Test extension 2
 test_ext3           | 1.0             |                   | Test extension 3
 test_ext4           | 1.0             |                   | Test extension 4
 test_ext5           | 1.0             |                   | Test extension 5
 test_ext6           | 1.0             |                   | test_ext6
 test_ext7           | 1.0             |                   | Test extension 7
 test_ext8           | 1.0             |                   | Test extension 8
 test_ext_cyclic1    | 1.0             |                   | Test extension cyclic 1
 test_ext_cyclic2    | 1.0             |                   | Test extension cyclic 2
 test_ext_evttrig    | 1.0             |                   | Test extension - event trigger
 test_ginpostinglist | 1.0             |                   | Test code for ginpostinglist.c
 test_integerset     | 1.0             |                   | Test code for integerset
 test_pg_dump        | 1.0             |                   | Test pg_dump with an extension
 test_predtest       | 1.0             |                   | Test code for optimizer/util/predtest.c
 test_rbtree         | 1.0             |                   | Test code for red-black tree library
 test_regex          | 1.0             |                   | Test code for backend/regex/
 tsm_system_rows     | 1.0             |                   | TABLESAMPLE method which accepts number of rows as a limit
 tsm_system_time     | 1.0             |                   | TABLESAMPLE method which accepts time in milliseconds as a limit
 unaccent            | 1.1             |                   | text search dictionary that removes accents
 uuid-ossp           | 1.1             |                   | generate universally unique identifiers (UUIDs)
 xml2                | 1.1             |                   | XPath querying and XSLT
(91 行)

⇧ すると、まぁ、何と言うことでしょう!

「pgcrypt」モジュールの「installed_version」に「バージョン」が追加されたではありませんか!

ただ、

⇧「関数」によっては、「bytea」じゃない「データ型」が返されるらしい...

とりあえず、「digest」って関数を使ってみた。

INSERT INTO public.user (user_id, user_name, user_real_name, user_password, user_newpassword, user_email, user_options, user_touched, user_token, user_email_authenticated, user_email_token, user_email_token_expires, user_registration, user_email_token_created) VALUES
 (1, 'sato', '佐藤', digest('password0000001', 'sha256'),'' , 'sato@gmail.com', '', '', '6WJ+sNg3R2JZzQP3F7d5B0ke6AQBNqre', '', 'EovQAEnJubjXxy5ZBp+qoaHm9H1nOdnN', '7200000', '', '2021-07-01 09:00:00'),
 (2, 'suzuki', '鈴木', digest('password0000002', 'sha256'),'' , 'suzuki@gmail.com', '', '', 'oLu44GMjdNAa7js8xLS4yJGrRtsp89ad', '', '1IFrgAsQtkrM+2F0oGSUO7ReXkxvJVmU', '7200000', '', '2021-04-01 09:00:00'),
 (3, 'takahashi', '高橋', digest('password0000003', 'sha256'),'' , 'takahashi@gmail.com', '', '', 'IffbaNHYoQAsNm77bc7rMYygua5O62ch', '', '+HPfhm96+KLXm2Zz9LjwdgUHybCpdH1E', '7200000', '', '2021-10-01 09:00:00'),
 (4, 'tanaka', '田中', digest('password0000004', 'sha256'),'' , 'tanaka@gmail.com', '', '', '9NlNXNr2Kqi0b8k4ZXnvuyU6nHCTlGDn', '', 'e3lbYMHLxcJZcisNxBwtkYNuw19fEsot', '7200000', '', '2021-11-01 09:00:00');

f:id:ts0818:20211103161737p:plain

とりあえず、データは入った模様。(データはめっちゃ適当です...)

f:id:ts0818:20211103145641p:plain

あと、気を付けたいのが、「PostgreSQL」以外の「データベース」でも同じだと思うけど「charcter(char)」型の「カラム」は桁数固定なんで、桁数に満たない場合は、「空白スペース」で埋められるので、「空白スペース」が不要の場合はプログラム側で「空白スペース」を「trim」したりの対応が必要になりますかね。

で、何か「Quartz」で「データベース」を利用したい場合は、

qiita.com

github.com

⇧「quartz」用のテーブルを作る必要があるらしい。今回は、「PostgreSQL」を使っているので、「tables_postgres.sql」を実行すれば、良いらしい、たぶん...。

curl -O https://raw.githubusercontent.com/quartz-scheduler/quartz/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql

f:id:ts0818:20211103232030p:plain

で、ダウンロードしたsqlファイルを「PostgreSQL」にログインしてる状態で実行。

\i 'C:\\Eclipse_2019-09\\pleiades-2019-09-java-win-64bit-jre_20191007\\pleiades\\workspace\\work_00\\QuartzProject\\src\\main\\resources\\sql\\tables_postgres.sql'

f:id:ts0818:20211103232349p:plain

f:id:ts0818:20211103232455p:plain

f:id:ts0818:20211103232606p:plain

⇧ 「quartz」用の「テーブル」ができたっぽい。

ようやっと、Javaのほうに取りかかります。

teratail.com

民法 第一編 第六章 期間の計算(第138条―第143条)に期間計算についての記述があります。
この条文を忠実に守るのも1つの仕様、luckyclockさんオリジナルの期間計算法を定めるのも1つの仕様です。
仕様があいまいだと、実装や検査でつらい思いをしますので、まずは仕様を固めることをお勧めします。

Excelの期間計算関数DATEDIFに深刻なバグがあることをご存じでしょうか?
このバグ回避のための、かつ民法に沿った方法がネット上にいろいろ公開されていますので、それを参考にするのも1つの手です。

https://teratail.com/questions/28238

⇧ というように、「期間計算」はいろいろ厄介な感じですかね...

さらに、

2038年問題(にせんさんじゅうはちねんもんだい)は、2038年1月19日3時14分8秒(UTC、以下同様)を過ぎると、コンピュータが誤動作する可能性があるとされる年問題

2038年問題 - Wikipedia

対策としては、time_t型を符号付き64ビット整数型(一般にはlong long int)にするという方法がある。符号付き64ビット整数型の場合、上限は9,223,372,036,854,775,807 (263 − 1) である。これを年数に変換するとおよそ西暦3000億年まで使用できるので、事実上問題が発生することはない。

2038年問題 - Wikipedia

⇧ ってな問題もあるらしい。

Javaの「プリミティブ型」の1つである「long型」はと言うと、

docs.oracle.com

Field Summary

static final long
A constant holding the maximum valuelong can have, 263-1.

https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Long.html

⇧「Long」クラスの「MAX_VALUE」ってフィールドの説明で、「long型」が、「 2^{63} -1」まで表現できるとあるので、Wikipediaの「2038年問題」の対応としては問題ないことになるみたいね。

ただ、

github.com

⇧ 上記サイト様で「Quartz」については、「2038年問題」に対応してないんじゃないか疑惑があるっぽい、誰も回答しておらんので真偽は不明だけども。

ちなみに、

www.kahaku.go.jp

 太陽でおきている核融合は、水素1gから7000億kWhものエネルギーを取り出す非常に効率のよい反応です。しかし燃料に限りがある以上、この反応にもいつかは終わりがやってきます。理論計算によると、太陽は約100億年の寿命があります。太陽系が生まれたのは46億年前ですから、太陽はあと50億年は今と同じように輝き続けることができます。

国立科学博物館-宇宙の質問箱-太陽編

⇧ 上記サイト様によりますと「太陽」の「寿命」があと「50億年」ぐらいって想定らしいので、「3000億年」を賄える「long型」があれば十分ということになりますかね。

なんか、同じ人類で対立してる暇があったら、未来を生きることになる後進の人たちのために研究とかに時間を費やしていった方が良い気がするんですけどね...

それにしても、「50億年」ぐらいで「太陽」が無くなるかもしれないってことは、「太陽系」以外で人類が生きていく場所を探す宇宙時代が来るってことですかね。

そう考えると、「宇宙」に対する研究に力を入れていくのも人類のためになるということですかね。

話が脱線しましたが、Eclipseの「パッケージ・エクスプローラー」上のファイルは以下のような感じに。

f:id:ts0818:20211106162734p:plain

⇧ 何だかんだで、ファイル数がかなり多くなってしまった...

しかも、ちゃんと処理について考えてないので、「アンチパターン」っぽくなってしまっています、SQLで、全件取得じゃなくて、候補を絞れば良かったのもあるし...

teratail.com

⇧ 上記サイト様で仰っているように、複数のリストを同時にループさせるのは、宜しくなさそうなのですが、Pythonとかだと、

docs.python.org

⇧ 標準で用意されてる「zip」って関数で、同時に複数のリストをループさせる仕組みはある模様。

そして、そもそも、外部システムから連携される想定の「JSONJavaScript Object Notation)」の「プロパティ」がすべて「配列」になってる作りが宜しくないですね(実際、そんな構造になってる「JSONJavaScript Object Notation)」は無いと思う...自分で勝手に作っておいてなんだけど...

あるとしたら、「オブジェクト」の「配列」みたいな作りになる感じかしらね...

脱線しましたが、ソースコードは以下のような感じになりました。

■/QuartzProject/src/main/resources/application.properties

# PsogreSQLを利用するための設定
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5434/postgres
spring.datasource.username=postgres
spring.datasource.password=postgres

# SQL文とかのデバッグログの表示とか
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
#hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
#hibernate.globally_quoted_identifiers_skip_column_definitions=true

## QuartzProperties
spring.quartz.job-store-type = jdbc
spring.quartz.properties.org.quartz.threadPool.threadCount = 5

#Quartz Log level
logging.level.org.springframework.scheduling.quartz=DEBUG
logging.level.org.quartz=DEBUG

■/QuartzProject/src/main/resources/quartz.properties

#============================================================================
# Configure Main Scheduler Properties
#============================================================================

org.quartz.scheduler.instanceName: TestScheduler
org.quartz.scheduler.instanceId: instance_one

org.quartz.scheduler.skipUpdateCheck: true

#============================================================================
# Configure ThreadPool
#============================================================================

org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 5
org.quartz.threadPool.threadPriority: 5

#============================================================================
# Configure JobStore
#============================================================================

org.quartz.jobStore.misfireThreshold: 60000

org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.dataSource=myDS
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true

#============================================================================
# Other Example Delegates
#============================================================================
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.DB2v6Delegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.DB2v7Delegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.DriverDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.HSQLDBDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.MSSQLDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PointbaseDelegate
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.WebLogicDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
#org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.oracle.WebLogicOracleDelegate

#============================================================================
# Configure Datasources
#============================================================================

org.quartz.dataSource.myDS.driver: org.postgresql.Driver
org.quartz.dataSource.myDS.URL: jdbc:postgresql://localhost:5434/postgres
org.quartz.dataSource.myDS.user: postgres
org.quartz.dataSource.myDS.password: postgres
org.quartz.dataSource.myDS.maxConnections: 5
org.quartz.dataSource.myDS.validationQuery: select 0

#============================================================================
# Configure Plugins
#============================================================================

#org.quartz.plugin.shutdownHook.class: org.quartz.plugins.management.ShutdownHookPlugin
#org.quartz.plugin.shutdownHook.cleanShutdown: true


#org.quartz.plugin.triggHistory.class: org.quartz.plugins.history.LoggingJobHistoryPlugin    

■/QuartzProject/src/main/java/QuartzProject/config/QuartzConfig.java

package QuartzProject.config;

import java.io.IOException;
import java.util.Properties;

import org.quartz.spi.JobFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@Configuration
public class QuartzConfig {

    @Autowired
    ApplicationContext applicationContext;

    @Bean
    public JobFactory jobFactory() {
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {

        SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
        schedulerFactory.setQuartzProperties(quartzProperties());
        schedulerFactory.setWaitForJobsToCompleteOnShutdown(true);
        schedulerFactory.setAutoStartup(true);
        schedulerFactory.setJobFactory(jobFactory());
        return schedulerFactory;
    }

    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }
}    

■/QuartzProject/src/main/java/QuartzProject/config/AutowiringSpringBeanJobFactory.java

package QuartzProject.config;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory
implements ApplicationContextAware{

    AutowireCapableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(final ApplicationContext applicationContext) {
        beanFactory = applicationContext.getAutowireCapableBeanFactory();
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
    }
}

■/QuartzProject/src/main/java/QuartzProject/entity/UserPrimaryKey.java

package QuartzProject.entity;

import java.io.Serializable;

import javax.persistence.Column;

/**
 * PostgreSQLのuserテーブルの主キー(複合主キー)用クラス
 * @author Toshinobu
 *
 */
public class UserPrimaryKey implements Serializable {

	/**
	 *  生成シリアル・バージョンID
	 */
	private static final long serialVersionUID = -6337698001336117167L;

	@Column(name="user_id")
	private int userId;

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

	public int getUserId() {
		return userId;
	}

	public void setUserId(int userId) {
		this.userId = userId;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}
}

■/QuartzProject/src/main/java/QuartzProject/entity/UserEntity.java

package QuartzProject.entity;

import java.io.Serializable;
import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;

/**
 * PostgreSQLのuserテーブルに紐づくEntityクラス
 * @author Toshinobu
 *
 */
@Entity
@Table(name="user", schema = "public")
@IdClass(value=UserPrimaryKey.class)
public class UserEntity implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1548577324049557983L;

	@Id
	@Column(name="user_id")
	private int userId;

	@Id
	@Column(name="user_name")
	private String userName;

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

	@Column(name="user_password")
	private byte[] userPassword;

	@Column(name="user_newpassword")
	private byte[] userNewPassword;

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

	@Column(name="user_options")
	private byte[] userOptions;

	@Column(name="user_touched", columnDefinition = "bpchar")
	private String userTouched;

	@Column(name="user_token", columnDefinition = "bpchar")
	private String userToken;

	@Column(name="user_email_authenticated", columnDefinition = "bpchar")
	private String userEmailAuthenticated;

	@Column(name="user_email_token", columnDefinition = "bpchar")
	private String userEmailToken;

	@Column(name="user_email_token_expires", columnDefinition = "bpchar")
	private String userEmailTokenExpires;

	@Column(name="user_registration", columnDefinition = "bpchar")
	private String userRegistration;

	@Column(name="user_email_token_created")
	private Timestamp userEmailTokenCreated;

	public int getUserId() {
		return userId;
	}

	public void setUserId(int userId) {
		this.userId = userId;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getUserRealName() {
		return userRealName;
	}

	public void setUserRealName(String userRealName) {
		this.userRealName = userRealName;
	}

	public byte[] getUserPassword() {
		return userPassword;
	}

	public void setUserPassword(byte[] userPassword) {
		this.userPassword = userPassword;
	}

	public byte[] getUserNewPassword() {
		return userNewPassword;
	}

	public void setUserNewPassword(byte[] userNewPassword) {
		this.userNewPassword = userNewPassword;
	}

	public String getUserEmail() {
		return userEmail;
	}

	public void setUserEmail(String userEmail) {
		this.userEmail = userEmail;
	}

	public byte[] getUserOptions() {
		return userOptions;
	}

	public void setUserOptions(byte[] userOptions) {
		this.userOptions = userOptions;
	}

	public String getUserTouched() {
		return userTouched;
	}

	public void setUserTouched(String userTouched) {
		this.userTouched = userTouched;
	}

	public String getUserToken() {
		return userToken;
	}

	public void setUserToken(String userToken) {
		this.userToken = userToken;
	}

	public String getUserEmailAuthenticated() {
		return userEmailAuthenticated;
	}

	public void setUserEmailAuthenticated(String userEmailAuthenticated) {
		this.userEmailAuthenticated = userEmailAuthenticated;
	}

	public String getUserEmailToken() {
		return userEmailToken;
	}

	public void setUserEmailToken(String userEmailToken) {
		this.userEmailToken = userEmailToken;
	}

	public String getUserEmailTokenExpires() {
		return userEmailTokenExpires;
	}

	public void setUserEmailTokenExpires(String userTokenExpires) {
		this.userEmailTokenExpires = userTokenExpires;
	}

	public String getUserRegistration() {
		return userRegistration;
	}

	public void setUserRegistration(String userRegistration) {
		this.userRegistration = userRegistration;
	}

	public Timestamp getUserEmailTokenCreated() {
		return userEmailTokenCreated;
	}

	public void setUserEmailTokenCreated(Timestamp userEmailTokenCreated) {
		this.userEmailTokenCreated = userEmailTokenCreated;
	}
}

■/QuartzProject/src/main/java/QuartzProject/http/response/dto/UserEmailTokenResponseDto.java

package QuartzProject.http.response.dto;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
 * JSONに紐づくDTOクラス
 * @author Toshinobu
 *
 */
public class UserEmailTokenResponseDto {

	@JsonProperty("userId")
	public List<String> userId;

	@JsonProperty("userName")
	public List<String> userName;

	@JsonProperty("userEmailToken")
	public List<String> userEmailToken;

	@JsonProperty("userEmailTokenExpires")
	public List<String> userEmailTokenExpiers;

	@JsonProperty("userEmailTokenCreated")
	public List<String> userEmailTokenCreated;

    @JsonIgnore
    private Map<String, Object> additionalProperties = new HashMap<String, Object>();

    @JsonAnyGetter
    public Map<String, Object> getAdditionalProperties() {
        return this.additionalProperties;
    }
    @JsonAnySetter
    public void setAdditionalProperty(String name, Object value) {
        this.additionalProperties.put(name, value);
    }
}

■/QuartzProject/src/main/java/QuartzProject/component/http/UserHttpComponent.java

package QuartzProject.component.http;

import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import QuartzProject.http.response.dto.UserEmailTokenResponseDto;

@Component
public class UserHttpComponent {

	/**
	 * JSONからJavaオブジェクトに変換
	 * @param json
	 * @return
	 * @throws JsonMappingException
	 * @throws JsonProcessingException
	 */
	public UserEmailTokenResponseDto convertJavaFromJson(String json) throws JsonMappingException, JsonProcessingException {
		ObjectMapper mapper = new ObjectMapper();
		return mapper.readValue(json, UserEmailTokenResponseDto.class);
	}
}

■/QuartzProject/src/main/java/QuartzProject/repository/UserRepository.java

package QuartzProject.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import QuartzProject.entity.UserEntity;
import QuartzProject.entity.UserPrimaryKey;

public interface UserRepository extends JpaRepository<UserEntity, UserPrimaryKey> {

}    

■/QuartzProject/src/main/java/QuartzProject/service/UserServiceImpl.java

package QuartzProject.service;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonProcessingException;

import QuartzProject.component.http.UserHttpComponent;
import QuartzProject.entity.UserEntity;
import QuartzProject.http.response.dto.UserEmailTokenResponseDto;
import QuartzProject.repository.UserRepository;

@Service
public class UserServiceImpl {

	@Autowired
	private UserRepository userRepository;

	@Autowired
	private UserHttpComponent userHttpComponent;

	/**
	 * userテーブル全件取得
	 * @return
	 */
	public List<UserEntity> searchAll() {
		return userRepository.findAll();
	}

	/**
	 * user_mail_tokenの更新
	 * @param userEntityList
	 */
	@Transactional
	public void updateUserMailToken(List<UserEntity> userEntityList) {
		final long ONE_DAY_MINUTES = 24 * 60 * 60; // 1日(秒)
		final long UPDATE_DAY_MINUTES = 60 * ONE_DAY_MINUTES; // 60日(秒)
		Timestamp todayTimeStamp = new Timestamp(System.currentTimeMillis()); // 現在日時
		// 更新対象の絞り込み
		List<UserEntity> updateUserEmailCreatedList = userEntityList
				.stream()
				.filter(userEntity -> Objects.nonNull(userEntity)
						&& Objects.nonNull(userEntity.getUserEmailTokenExpires())
						&& Objects.nonNull(userEntity.getUserEmailTokenCreated()))
				// 絶対値(「現在日時(秒)」 - 「作成日時(秒)」) >= 「60日(秒)」のものに絞り込み
				.filter(userEntity -> Math.abs(
						TimeUnit.MILLISECONDS.toSeconds(todayTimeStamp.getTime()) - TimeUnit.MILLISECONDS
								.toSeconds(userEntity.getUserEmailTokenCreated().getTime())) >= UPDATE_DAY_MINUTES)
				.collect(Collectors.toList());

		// TODO:外部システムに対してtokenの再発行を行う

		// 外部システムから取得した体で「token」「tokenの有効期限」「tokenの作成日時」で「user」テーブルを更新する
		String json = "{" +
				"        \"userId\": [\"1\", \"2\"], " +
				"        \"userName\": [\"sato\", \"suzuki\"], " +
				"        \"userEmailToken\": [\"UovQAEnJubjrxy5ZBp+qoaHm9H1nOdnN\", \"8IFrgAsQtkrM+2k0oGSUO7ReXkxvJVmU\"], "+
				"        \"userEmailTokenExpires\": [\"720000\", \"720000\"], " +
				"        \"userEmailTokenCreated\": [\"2021-11-06 00:00:00\", \"2021-11-06 00:00:00\"]" +
				"    }" +
				"}";

		UserEmailTokenResponseDto userEmailTokenResponseDto = null;
		try {
			userEmailTokenResponseDto = userHttpComponent.convertJavaFromJson(json);
		} catch (JsonProcessingException e) {
			// TODO 自動生成された catch ブロック
			e.printStackTrace();
		}

		// 更新対象のエンティティを作成し、Listに格納
		List<UserEntity> updateUserEntityList = new ArrayList<>();
		int indexUpdateUserList = 0, indexUserId = 0, indexUserName = 0, indexUserEmailToken = 0,
				indexUserEmailTokenExpire = 0, indexUserEmailTokenCreated = 0;

		while (updateUserEmailCreatedList.size() > indexUpdateUserList
				|| userEmailTokenResponseDto.userId.size() > indexUserId
				|| userEmailTokenResponseDto.userName.size() > indexUserName
				|| userEmailTokenResponseDto.userEmailToken.size() > indexUserEmailToken
				|| userEmailTokenResponseDto.userEmailTokenExpiers.size() > indexUserEmailTokenExpire
				|| userEmailTokenResponseDto.userEmailTokenCreated.size() > indexUserEmailTokenCreated) {
			// UserEntity userEntity = new UserEntity();
			// UserPrimaryKey userPrimaryKey = new UserPrimaryKey();
			if (Objects.isNull(updateUserEmailCreatedList) || updateUserEmailCreatedList.isEmpty()) {
				break;
			}
			if (Objects.isNull(updateUserEmailCreatedList.get(indexUpdateUserList))) {
				indexUpdateUserList++;
				indexUserId++;
				indexUserName++;
				indexUserEmailToken++;
				indexUserEmailTokenExpire++;
				indexUserEmailTokenCreated++;
				continue;
			}
			if (Objects.isNull(updateUserEmailCreatedList.get(indexUpdateUserList).getUserId())
					|| Objects.isNull(
							updateUserEmailCreatedList.get(indexUpdateUserList).getUserName())) {
				continue;
			}
			if (updateUserEmailCreatedList.get(indexUpdateUserList).getUserId() == Integer
					.valueOf(userEmailTokenResponseDto.userId.get(indexUserId))
					&& updateUserEmailCreatedList.get(indexUpdateUserList).getUserName()
							.equals(userEmailTokenResponseDto.userName.get(indexUserName))) {

				// 更新する項目
				updateUserEmailCreatedList.get(indexUpdateUserList).setUserEmailToken(userEmailTokenResponseDto.userEmailToken.get(indexUserEmailToken));
				updateUserEmailCreatedList.get(indexUpdateUserList).setUserEmailTokenExpires(
						userEmailTokenResponseDto.userEmailTokenExpiers.get(indexUserEmailTokenExpire));
				updateUserEmailCreatedList.get(indexUpdateUserList).setUserEmailTokenCreated(Timestamp
						.valueOf(userEmailTokenResponseDto.userEmailTokenCreated.get(indexUserEmailTokenCreated)));

				updateUserEntityList.add(updateUserEmailCreatedList.get(indexUpdateUserList));
				indexUpdateUserList++;
				indexUserId++;
				indexUserName++;
				indexUserEmailToken++;
				indexUserEmailTokenExpire++;
				indexUserEmailTokenCreated++;
			}
		}
		;
		if (indexUpdateUserList != 0 && indexUserId != 0 && indexUserName != 0 && indexUserEmailToken != 0
				&& indexUserEmailTokenExpire != 0 && indexUserEmailTokenCreated != 0) {
			// 更新
			userRepository.saveAll(updateUserEntityList);
		}
	}
}

■/QuartzProject/src/main/java/QuartzProject/schedule/QuartzScheduler.java

package QuartzProject.schedule;

import static org.quartz.JobBuilder.*;

import java.io.IOException;
import java.util.Properties;

import javax.annotation.PostConstruct;

import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

import QuartzProject.config.AutowiringSpringBeanJobFactory;

@Configuration
@ConditionalOnExpression("'${using.spring.schedulerFactory}'=='false'")
public class QuartzScheduler {

	Logger logger = LoggerFactory.getLogger(getClass());

	@Autowired
	private ApplicationContext applicationContext;

	@PostConstruct
	public void init() {
		logger.info("Hello world from Quartz...");
	}

	@Bean
	public SpringBeanJobFactory springBeanJobFactory() {
		AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
		logger.debug("Configuring Job factory");

		jobFactory.setApplicationContext(applicationContext);
		return jobFactory;
	}

	@Bean
	public Scheduler scheduler(Trigger trigger, JobDetail job, SchedulerFactoryBean factory) throws SchedulerException {
		logger.debug("Getting a handle to the Scheduler");
		Scheduler scheduler = factory.getScheduler();
		scheduler.scheduleJob(job, trigger);

		logger.debug("Starting Scheduler threads");
		scheduler.start();
		return scheduler;
	}

	@Bean
	public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
		SchedulerFactoryBean factory = new SchedulerFactoryBean();
		factory.setJobFactory(springBeanJobFactory());
		factory.setQuartzProperties(quartzProperties());
		return factory;
	}

	public Properties quartzProperties() throws IOException {
		PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
		propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
		propertiesFactoryBean.afterPropertiesSet();
		return propertiesFactoryBean.getObject();
	}

	@Bean
	public JobDetail jobDetail() {

		return newJob().ofType(OneDayJob.class).storeDurably().withIdentity(JobKey.jobKey("Qrtz_Job_Detail"))
				.withDescription("Invoke Sample Job service...").build();
	}

	@Bean
	public Trigger trigger(JobDetail job) {

		int frequencyInSec = 10;
		logger.info("Configuring trigger to fire every {} seconds", frequencyInSec);

		//		return newTrigger().forJob(job).withIdentity(TriggerKey.triggerKey("Qrtz_Trigger"))
		//				.withDescription("Sample trigger")
		//				.withSchedule(simpleSchedule().withIntervalInSeconds(frequencyInSec).repeatForever()).build();

		return TriggerBuilder.newTrigger()
				.forJob(job)
				.withIdentity("Qrtz_Trigger", "group1")
				//		.startAt(DateBuilder.tomorrowAt(0, 0, 0))  // 0:00:00 tomorrow
				//			    .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
				//			            .withIntervalInMonths(2)) // interval is set in calendar 2 months
				.startNow()
				.withSchedule(SimpleScheduleBuilder.simpleSchedule()
						.withIntervalInSeconds(1000)
						.repeatForever())
				.build();
	}
}

■/QuartzProject/src/main/java/QuartzProject/schedule/OneDayJob.java

package QuartzProject.schedule;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import QuartzProject.service.UserServiceImpl;

@Component
public class OneDayJob implements Job  {

	@Autowired
	private UserServiceImpl userServiceImpl;

	@Override
	public void execute(JobExecutionContext context) throws JobExecutionException {
		//
		userServiceImpl.updateUserMailToken(userServiceImpl.searchAll());

	}
}    

■/QuartzProject/src/main/java/QuartzProject/schedule/SpringQuartzScheduler.java

package QuartzProject.schedule;

import javax.annotation.PostConstruct;
import javax.sql.DataSource;

import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

import QuartzProject.config.AutowiringSpringBeanJobFactory;

@Configuration
@EnableAutoConfiguration
@ConditionalOnExpression("'${using.spring.schedulerFactory}'=='true'")
public class SpringQuartzScheduler {

    Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void init() {
        logger.info("Hello world from Spring...");
    }

    @Bean
    public SpringBeanJobFactory springBeanJobFactory() {
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        logger.debug("Configuring Job factory");

        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    @Bean
    public SchedulerFactoryBean scheduler(Trigger trigger, JobDetail job, DataSource quartzDataSource) {

        SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
        schedulerFactory.setConfigLocation(new ClassPathResource("/quartz.properties"));

        logger.debug("Setting the Scheduler up");
        schedulerFactory.setJobFactory(springBeanJobFactory());
        schedulerFactory.setJobDetails(job);
        schedulerFactory.setTriggers(trigger);

        // Comment the following line to use the default Quartz job store.
        schedulerFactory.setDataSource(quartzDataSource);

        return schedulerFactory;
    }

    @Bean
    public JobDetailFactoryBean jobDetail() {

        JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean();
        jobDetailFactory.setJobClass(OneDayJob.class);
        jobDetailFactory.setName("Qrtz_Job_Detail");
        jobDetailFactory.setDescription("Invoke Sample Job service...");
        jobDetailFactory.setDurability(true);
        return jobDetailFactory;
    }

    @Bean
    public SimpleTriggerFactoryBean trigger(JobDetail job) {

        SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean();
        trigger.setJobDetail(job);

        int frequencyInSec = 10;
        logger.info("Configuring trigger to fire every {} seconds", frequencyInSec);

        trigger.setRepeatInterval(frequencyInSec * 1000);
        trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
        trigger.setName("Qrtz_Trigger");
        return trigger;
    }

    @Bean
    @QuartzDataSource
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource quartzDataSource() {
        return DataSourceBuilder.create().build();
    }
}    

■/QuartzProject/src/main/java/QuartzProject/Library.java

/*
 * This Java source file was generated by the Gradle 'init' task.
 */
package QuartzProject;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class Library {
    public boolean someLibraryMethod() {
        return true;
    }

    public static void main(String[] args) {
    	//new SpringApplicationBuilder(Library.class).bannerMode(Mode.OFF).run(args);
    	SpringApplication.run(Library.class, args);

//    	JobDetail job = JobBuilder.newJob(OneDayJob.class)
//    			.withIdentity("job1", "group1")
//    			.build();
//
//    	Trigger trigger = TriggerBuilder.newTrigger()
//    			.withIdentity("trigger1", "group1")
////    			.startAt(DateBuilder.tomorrowAt(0, 0, 0))  // 0:00:00 tomorrow
////    				    .withSchedule(CalendarIntervalScheduleBuilder.calendarIntervalSchedule()
////    				            .withIntervalInMonths(2)) // interval is set in calendar 2 months
//    		    .startNow()
//    		    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
//    				    .withIntervalInSeconds(1000)
//    				    .repeatForever())
//    			.build();
//
//    	Scheduler scheduler = null;
//    	try {
//    		scheduler = StdSchedulerFactory.getDefaultScheduler();
//			scheduler.start();
//	    	if (scheduler.checkExists(job.getKey())){
//	    	    scheduler.deleteJob(job.getKey());
//	    	}
//	    	scheduler.scheduleJob(job, trigger);
//		} catch (SchedulerException e) {
//			// TODO 自動生成された catch ブロック
//			e.printStackTrace();
//		}
//
//    	try {
//			Thread.sleep(1000);
//		} catch (InterruptedException e) {
//			// TODO 自動生成された catch ブロック
//			e.printStackTrace();
//		}
//
//    	try {
//			scheduler.shutdown();
//		} catch (SchedulerException e) {
//			// TODO 自動生成された catch ブロック
//			throw new RuntimeException(e);
//		}
//    	System.out.println("Library#main end");

    }
}

⇧ ファイルが編集できたら、保存し、「実行(R)」>「Spring Boot アプリケーション」を選択でプログラムを実行。

f:id:ts0818:20211106173357p:plain

バッチ処理」で、永久に処理が走り続けるみたいな感じになってるので、対象の処理が実行されたか「コンソール」で確認し、実施されてる場合は、「停止」ボタンで「Spring Boot アプリケーション」を手動で停止します。

f:id:ts0818:20211106162404p:plain

テーブルの内容を見てみると、

f:id:ts0818:20211106174528p:plain

⇧ 期待したように更新されてるようです。

ただ、いまいまの作りだと、複数の「バッチ処理」が処理できるようになってないので、要調査ですかね。

それにしても、「Spring Framework」って本当に、必要なライブラリや、「依存性の注入(DI:Dependency Injection)」とかのために自分で何か追加しないといけないのかそうでないのかが分かり辛い...

「spring-boot-starter-〇〇」って「依存関係」がどこまでしてくれるのかハッキリ説明してくれてれば、こんな迷走しなくても良くなる気がするんですけどね...

毎回、モヤモヤ感が半端ない...

今回はこのへんで。

 

NG集

「@Autowired」アノテーションを付与したフィールドが「依存性の注入(DI:Dependency Injection)」されなくて、「NullPointerException」が起こるという事象が起きて四苦八苦しました。

まぁ、結局、原因がよく分からんかったのだけど

なんか、「MVN Repository」で、

f:id:ts0818:20211104214520p:plain

⇧ みたいな説明があったんで、「spring-boot-starter-〇〇」シリーズについては、「spring-boot-starter」の「spring-core」が連携されて「依存性の注入(DI:Dependency injection)」とかできるんかな、と思い込んでいたので、「spring-boot-starter-quartz」って依存関係を利用すると「quartz」とかを「Spring Framework」の世界の「依存性の注入(DI:Dependency injection)」に適応してくれるのかな?、と思ってたですよね。

で、公式じゃない情報だけど、「@Autowired」については、

www.geeksforgeeks.org

The @Autowired annotation marks a Constructor, Setter method, Properties and Config() method as to be autowired that is ‘injecting beans'(Objects) at runtime by Spring Dependency Injection mechanism which is clearly depicted from he image below as shown:

f:id:ts0818:20211104215212p:plain

https://www.geeksforgeeks.org/spring-autowired-annotation/

⇧ みたいな感じで、「Spring Dependency Injection mechanism」によって実行時に「Bean」が利用できる状態になっている(インスタンス化されてる)ってことを実現してくれるものだという認識なんだけど、「Spring Dependency Injection mechanism」は「spring-core」が担ってると思うので、「spring-boot-starter-〇〇」シリーズであるはずの「spring-boot-starter-quartz」についても良しなに「@Autowired」とか機能するような仕組みにしてくれてるんだろうな、って期待するじゃないですか?

なので、てっきり「@Autowired」とか機能するものと思ってたのですが、

docs.spring.io

⇧ どうも、「Spring Boot」のドキュメントとかを見ても、「Quartz」関連のクラスとか継承してる中では、そもそも「@Autowired」ができないのか、「@Autowired」を使ってる実装例が見当たらない...

嫌な予感しかしないのだが...

で、さらに、stackoverflowとかで同様の問題を上げてる人がおり、

stackoverflow.com

stackoverflow.com

stackoverflow.com

⇧ 見た感じ、

  • extends SpringBeanJobFactory
  • implements ApplicationContextAware

の「クラス」の継承と「インターフェイス」の実装をした「クラス」を用意しておかないと、「Spring Framework」の世界のルールが適応されないんじゃなか説があるらしい...。

まぁ、他にも、いろいろ追加したんだけどね...

結局、自分で「依存性の注入(DI:Dependency injection)」のためにいろいろ追加する以外の方法が分からんかったけど...

スッキリせんわ...

2022年2月13日(日)追記:↓ ここから

「spring-boot-autoconfigure」の依存関係を追加すれば、「SchedulerFactoryBean」クラスを継承したクラスを作成しなくてもQuartzの機能を使えました。

ts0818.hatenablog.com

⇧ 手前味噌で恐縮ですが、上記の記事でまとめております。

それにしても、「spring-boot-autoconfigure」を依存関係に追加してQuartzを使ってる情報がネットで皆無だったのが気になりますな...

2022年2月13日(日)追記:↑ ここまで