2026年7月アーカイブ

trust 認証のままというのもよくないだろうから、postgres ユーザのパスワードを変更して scram-sha-256 認証に戻すことにする。

まず、postgres ユーザのパスワードを 'grespost' に変更(SQL Shell (psql) で接続し作業)

postgres=# ALTER ROLE postgres WITH PASSWORD 'grespost';
ALTER ROLE

次に、設定ファイルを編集。
(例)C:\Program Files\PostgreSQL\13\data\pg_hba.conf

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     scram-sha-256
# IPv4 local connections:
host    all             all             127.0.0.1/32            scram-sha-256
# IPv6 local connections:
host    all             all             ::1/128                 scram-sha-256
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     all                                     scram-sha-256
host    replication     all             127.0.0.1/32            scram-sha-256
host    replication     all             ::1/128                 scram-sha-256

## TYPE  DATABASE        USER            ADDRESS                 METHOD
#
## "local" is for Unix domain socket connections only
#local   all             all                                     trust
## IPv4 local connections:
#host    all             all             127.0.0.1/32            trust
## IPv6 local connections:
#host    all             all             ::1/128                 trust
## Allow replication connections from localhost, by a user with the
## replication privilege.
#local   replication     all                                     trust
#host    replication     all             127.0.0.1/32            trust
#host    replication     all             ::1/128                 trust


PostgreSQL を再起動する。(タスクマネージャーより)

20260717_postgres5.jpg

これで、pgAdmin でパスワード無しで postgres ユーザで DB に接続しようとすると、「fe_sendauth: no
password supplied」と怒られるようになる。

20260717_postgres6.jpg

ここで変更後の 'grespost' パスワードを入力すれば接続できる。もう面倒くさいんで Save Password にもチェックしちゃう(笑)

無事接続完了。
作業用に割り振られた PC で、いつ誰がセットアップしたのかわからない PostgreSQL 13 が動いているんだけど、当然のようにパスワードはわからない。

こういうときは定番の「強制的にログインしてしまう」方法がある。まあ、PostgreSQL 管理している人なら誰でも知ってる方法だけど、PostgreSQL の設定ファイルの認証方式を全て一旦「trust認証」に変えてしまうのである。
trust 認証は「パスワードの確認をせずに接続を許可する」認証方式(認証というのか、それ?(^^;;;)なので、これで psql でパスワードなしで PostgreSQL に接続できるようになる。

Windows だと以下の場所に設定ファイルはある。

C:\Program Files\PostgreSQL\13\data\pg_hba.conf

これの下の方に設定があるので、(本当は必要なところ(TYPE local のところ)だけ修正すればいいのだが(^^;)全部直しちゃう(笑)

# TYPE  DATABASE        USER            ADDRESS                 METHOD</div><div>#</div><div>## " local"="" is="" for="" unix="" domain="" socket="" connections="" only<="" div="">
#local   all             all                                     scram-sha-256
## IPv4 local connections:
#host    all             all             127.0.0.1/32            scram-sha-256
## IPv6 local connections:
#host    all             all             ::1/128                 scram-sha-256
## Allow replication connections from localhost, by a user with the
## replication privilege.
#local   replication     all                                     scram-sha-256
#host    replication     all             127.0.0.1/32            scram-sha-256
#host    replication     all             ::1/128                 scram-sha-256

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     trust
# IPv4 local connections:
host    all             all             127.0.0.1/32            trust
# IPv6 local connections:
host    all             all             ::1/128                 trust
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     all                                     trust
host    replication     all             127.0.0.1/32            trust
host    replication     all             ::1/128                 trust

みたいに修正。

Windows であれば、サービス管理ツールから postgresql-x64-13-PostgreSQL Server 13 を停止/開始で再起動すれば OK。

その後、SQL Shell (psql) を起動したら、パスワードなしで PostgreSQL に接続できる。

20260717_postgres1.jpg

GUI ツールの pgAdmin 4 の場合は、最初に pgAdmin 自体を使えるようにするマスターパスワードの入力を促されるが(Unlock Saved Passeords 画面)、適当に 'postgres' って入れてみたら突破できた(笑)

20260717_postgres2.jpg

次に DB に接続しようとすると、Connect to Server 画面で postgres ユーザのパスワードを聞いてくる。trust 認証にする前はここを突破できなかった。

20260717_postgres3.jpg

trust 認証にしても、このパスワード入力画面は開く。ただし、Password に何も入力せず「OK」ボタンを押下すれば pgAdmin 4 画面が開く。

20260717_postgres4.jpg

実は、Oracle を使うという話だった現在取り組んでいる案件が PostgreSQL 利用に変わった。
PostgreSQL をセットアップしなきゃなあと思ってたのでちょうどよかった。バージョンは古い(13。現在の最新は 18)けど、正式な開発環境を作るまで、お試しで色々プログラム組んでみるのは 13 で十分だろう。
横川の仕事部屋の近所に大きなパチンコ屋がある。その駐車場にいつも屋台の店を出しているたこ焼き屋が以前から気になってたんだけど、周りにぷかぷかタバコ吸ってるやつらがうろうろしてて空気が悪いので近づくことはなかった。

20260719_takoyaki1.jpeg
昨日、横を通りかかると周りには自転車に乗ったおっちゃんが一人いるだけだったので、一旦通り越したのだがわざわざ引き返してたこ焼きを買ったのである。

6個で 300円なので、お値段はお安いよなあ。スーパーで 4個入りのたこ焼きがそのくらいの値段で並んでたりするもんな。

ちょうど新しいのを焼き上げたばかりだったので、出来立て熱々のやつを手にすることができた。

いや、マジで容器も「こんな熱いのを入れて大丈夫?溶けるんじゃない?」と思うほど薄くて心もとなくて、徒歩数分の仕事部屋まで持って帰るのに手を火傷しそうだった(笑)。つい「エコ見栄」が出て袋もらわなかったので(^^;;;

ソースは「ソース&マヨネーズ」をチョイス。上に青のりとかつおぶしが乗る。これぞたこ焼きの定番!

しかし、自宅でのタコパを除けば、焼き立てのたこ焼きを食うの、マジで十数年ぶりじゃないの?昔大分のテレビ局の仕事をしてたときに、大分駅までの帰り道にあったたこ焼き屋で買ったのが最後の気がする(笑)
スーパーの総菜コーナーのたこ焼きはたまに買って、レンチンしてビールのアテにするけどね。

20260719_takoyaki2.jpeg
タコも決して大きくはないけど、ちゃんと噛み噛みできるサイズのやつが入ってて 300円ならいいんじゃないって感じ。

上にかけてもらうソースも、「ソース&マヨネーズ」の他に「塩ダレ」「スイートチリマヨ」「明太マヨ」「ピリ辛」「しょうゆマヨ」「しおゴマ油」「ネギぽん」と色々ある。
「しょうゆマヨ」「ネゴぽん」あたりは試してみたい気もするなあ・・・

屋台のおにいちゃんは人当たりの良い感じで好感を持てたが、パチンコ屋の駐車場ってろくでもないやつらのたまり場じゃん。パチ狂&ヤニ狂ってだけで人間のクズ度がかなり上がるし。なので、なかなか怖くて屋台までたどり着けないのが問題なのよ(笑)
もう 7か月前の話ですまん(笑)

20251207_docapon1.JPG
Facebook 上でのドゥカティ・オーナーズ・コミュニティのひとつである「Ducati Owners Club Nippon」(通称:ドカポン)の山口支部の忘年会が 2025/12/27(金)に周南市の「いけす 和食処 寿司やす」で開催されたので参加した。

まあ、山口支部といっても周南の人を中心とした10名程度の小規模なグループなので、この日も熊本から 1名のゲストの方を含めても 8名ほどのささやかな会であった。でも、それだけに気心の知れた人たちで和気あいあいと楽しい会になった。

この日の昼は、何人かの有志で「SL やまぐち号に乗ろう!」というイベントも開催され、その流れで熊本のTナガさんも忘年会に参加されたのである。

俺はちょっと用事があって SL 乗車イベントには参加できず、昼間はドカポンメンバーであるCさんと周南地区でラーメンを食べ、それから徳地まで行って卵ご飯を食べという飽食ツーリングをしており、せっかくの忘年会の美味い料理が食えないのではないかと心配したのだが、刺身、煮つけ、牛焼肉などコース料理はどれも美味くてペロリであった(笑)

20251207_docapon2.JPG
いや、マジでツーリングの最後に玖珂の八幡下のガソリンスタンドに寄ったとき、段差を越えた瞬間に卵かけご飯が喉まで上がってきてかなり危険だったんだが・・・デブをなめんなよ!!(笑)

俺とCさんは徳地の卵かけご飯の店で買った真っ黄色のださいTシャツをお揃いで身に着けて忘年会参加。そこそこウケたのでよかった(笑)

しかし、この後、なかなかこの黄色いTシャツを着る機会がなく、タンスの肥しになっているのである。
だって、「マジで着てる」と思われるとねえ・・・社会的地位とか色々ねえ(笑)

ドカポンのツーリングの時にでも着ていくしかないが、事情を知らない人に「そんなダサいTシャツを着たイタリア人はいないよ!!」ってドゥカティコミュニティから排斥されてもやばいしなあ・・・(^^;;;
20260717_kaisugi1.jpeg
この金曜日はお客さんのところでの作業を早めに上がらせてもらい、うちの会社の期末の作業を色々。
そのため、久しぶりに我が社の江波本社に行く必要があって嫁さんに車で送ってもらった。

無事作業も済んで、帰りに娘も連れて「武蔵坊」で汁なし担々麺でも食べていくかということになって盛り上がったのだが、「武蔵坊 横川店」の夜の部が始まるのは 18:00から。今の時刻、17時ちょっと前・・・一時間以上あるやん・・・(^^;

というわけで、さっさと岩国に帰ることにしていつもの「階杉 ゆめタウン大竹店」へ。

俺は「汁なし担々麺」を辛さ 5で。最後のフィニッシュのご飯も欲しかったので、唐揚げ 3個セット(白飯付き)を追加。

しかし、汁なし担々麺気分が盛り上がっていたのは俺だけだったようで、娘は二郎インスパイアの「杉二郎」、嫁さんはシンプルな中華そばを注文した。
ま、こうして選択肢が広いのも階杉の良いところよね。しかもそれぞれの料理のレベルも高いし。

20260717_kaisugi2.jpeg
俺が唐揚げ 3個セット(白飯付き)を注文するのは、上に書いたように「最後に残った汁にご飯を混ぜて食べる」というのの他に、「唐揚げをアテにビールをやる」という目的がある。

ここの唐揚げはそのままでも美味いが、やはりそこはデブの一手間を(笑)

唐揚げに「マヨネーズ」「胡椒」「おろしにんにく」「もどしにんにく(一度乾燥させたものを水で戻したもの。濃厚な風味)」をぶっかけ、そして上にきゅうりの漬物「カッパ漬」を載せ、一個で 2000kcal くらいありそうな「俺スペシャル」を作りあげる。

これがもうビールのつまみとして最高なのよ。

娘に「いる?」と聞くと「いらない」と(笑)。さすが俺の娘、聡い。には手を出さんか(笑)
Amazon Prime Video で契約していた「dアニメストア」を解約した。
実は docomo で「dアニメストア」は直接契約しているので、二重に契約していたということだ。

20260719_danime1.jpg

元々、娘から頼まれて docomo から「dアニメストア」を契約してたんだが、何年か前に Amazon Prime Video から何日か無料視聴できるやつを申し込んでしまい(何のアニメを見たか全然忘れたけど)、そのまま放置していた。毎年 6,600円、今年から 7,920円が無駄に引き落とされていたというわけだ。

娘の PC には docomo の ID/PW で設定しているので、Amazon Prime Video 側は今解約した(笑)

ちゅうても、今俺は「dアニメストア」で「東島丹三郎は仮面ライダーになりたい」を見ているので(今、エピソード17まで視聴済み)、8/6の期限までは視聴可能なオプションで解約した。

実は、なんかよくわからないけど引き落とされてるお金がいくつかあって、今まで(月に数百円とかなので)ほってたんだけど、Amazon Prime も今は使ってない方のアカウントでも契約してて、もう10年くらい使わないのに引き落とされていた。年間 7,200円だから、7~8万円くらいまったく使ってないのに無駄金にしてしまったというわけだ。これも先日解約した。

そうそう。これまた全然使ってなかったのにお袋の家にアイ・キャンのインターネット接続サービスを契約してて、これも毎月2,750円を使わなくなって 5年くらい払い続けてた。5年で 165,000円無駄銭払ってたってことか・・・。これも解約済。

横川に仕事部屋借りてから色々無駄銭の整理が進んでいる。少しでも部屋代の足しにしたいからな(笑)
今まで作ってきた Spring Batch プログラムをコマンドラインから実行可能な JAR ファイルにしてみる。

Maven プロジェクトとして作っているので、以下の手順で。

プロジェクトを選択し、右ボタンメニューから「実行」→「Maven ビルド...」

20260714_maven_buld1.jpg

構成ダイアログが開くので、「ゴール」のところに「clean package」と入力
「ゴール(Goals)」には実行したい Maven のフェーズやプラグインの機能(コマンド)を指定する。
「clean package」は、「過去の生成物を削除し、新しく JAR や WAR などの配布ファイルを生成する」という指定。
テストが不要なら、「clean package -DskipTests」。これで。JAR へのビルドだけが行われる。

20260714_maven_buld2.jpg

「実行」ボタン押下

これでビルドが実行される。

[INFO] Scanning for projects...
[WARNING] The requested profile "pom.xml" could not be activated because it does not exist.
[INFO]
[INFO]  [1m----------------------< [0;36mcom.example:BatchDB2CSV1 [0;1m >---------------------- [m
[INFO]  [1mBuilding BatchDB2CSV1 0.0.1-SNAPSHOT [m
[INFO]   from pom.xml
[INFO]  [1m--------------------------------[ jar ]--------------------------------- [m
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-clean-plugin/3.5.0/maven-clean-plugin-3.5.0.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-clean-plugin/3.5.0/
<略>
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/junit/platform/junit-platform-launcher/6.0.3/junit-platform-launcher-6.0.3.jar (246 kB at 2.2 MB/s)
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.netandfield.test. [1mBatchDb2Csv1ApplicationTests [m 16:26:53.111 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class
[com.netandfield.test.BatchDb2Csv1ApplicationTests]: BatchDb2Csv1ApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
<略>
※「ゴール」に -DskipTests を記述していないので、実際に起動する形でテストが行われる
   .  ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v4.1.0)

<略>
2026-07-14T16:26:58.105+09:00  INFO 19504 --- [BatchDB2CSV1] [   main] c.n.test.Listener.LogJobListener         : ★Job started: exportJob
2026-07-14T16:26:58.112+09:00  INFO 19504 --- [BatchDB2CSV1] [   main] o.s.batch.core.step.AbstractStep         : Executing step: [exportStep]
2026-07-14T16:26:58.816+09:00  INFO 19504 --- [BatchDB2CSV1] [   main] o.s.batch.core.step.AbstractStep         : Step: [exportStep] executed in 704ms
2026-07-14T16:26:58.821+09:00  INFO 19504 --- [BatchDB2CSV1] [   main] c.n.test.Listener.LogJobListener         : ★Job finished: exportJob with status: COMPLETED
2026-07-14T16:26:58.824+09:00  INFO 19504 --- [BatchDB2CSV1] [   main] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=exportJob]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 717ms
<略>
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/vafer/jdependency/2.10/jdependency-2.10.jar (416 kB at 298 kB/s)
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar (20 kB at 12 kB/s)
[INFO] Replacing main artifact C:\HogeHoge\pleiades\workspace\BatchDB2CSV1\target\BatchDB2CSV1-0.0.1-SNAPSHOT.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to C:\HogeHoge\pleiades\workspace\BatchDB2CSV1\target\BatchDB2CSV1-0.0.1-SNAPSHOT.jar.original
[INFO]  [1m------------------------------------------------------------------------ [m
[INFO]  [1;32mBUILD SUCCESS [m
[INFO]  [1m------------------------------------------------------------------------ [m
[INFO] Total time:  34.209 s
[INFO] Finished at: 2026-07-14T16:27:09+09:00
[INFO]  [1m------------------------------------------------------------------------ [m
[WARNING] The requested profile "pom.xml" could not be activated because it does not exist.

これで、

C:\HogeHoge\pleiades\workspace\BatchDB2CSV1\target\BatchDB2CSV1-0.0.1-SNAPSHOT.jar

という JAR ファイルが作られた。

しかし、ビルド時にもすげえ色々ダウンロードしてくるなあ。こりゃ、インターネット環境のない場所じゃビルドできんわ(笑)
開発環境を完全にインターネットから切り離してるところって時々あるけど、時代にあってないよなあ・・・

じゃ、実行してみる。ちょっと場所が深いので、C:\work の下に移動しよう。

C:\work\BatchDB2CSV1-0.0.1-SNAPSHOT.jar

では、実行。

・・・の前に、ディフォルトで使われる java.exe はバージョンが古い。今回のバッチは Java 21 での実行を前提にしている。

C:\Users\hogehoge>java -version
java version "1.7.0_09"
Java(TM) SE Runtime Environment (build 1.7.0_09-b05_Fujitsu_10-30-12_12:00_patch)
Java HotSpot(TM) Client VM (build 23.5.02_FUJITSU_MODIFIED-B04, mixed mode)

開発で使った Java 21 はここ。

C:\Users\hogehoge>C:\HogeHoge\pleiades\java\21\bin\java -version
openjdk version "21.0.5" 2024-10-15 LTS
OpenJDK Runtime Environment Temurin-21.0.5+11 (build 21.0.5+11-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.5+11 (build 21.0.5+11-LTS, mixed mode, sharing)

環境変数(PATH)を直しちゃうと、他のプログラムがうまく動かなくなる可能性もあるので、とりあえずフルパス指定で実行しよう。

C:\Users\hogehoge>C:\HogeHoge\pleiades\java\21\bin\java -jar C:\work\BatchDB2CSV1-0.0.1-SNAPSHOT.jar models=1 modelName=未来電話

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v4.1.0)
<略>
2026-07-14T16:47:55.110+09:00  INFO 18536 --- [BatchDB2CSV1] [   main] o.s.b.b.a.JobLauncherApplicationRunner   : Running default command line with: [models=1, modelName=未来電話]
2026-07-14T16:47:55.318+09:00  INFO 18536 --- [BatchDB2CSV1] [   main] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=exportJob]] launched with the following parameters: [{JobParameter{name='models', value=1, type=class java.lang.String, identifying=true},JobParameter{name='modelName', value=未来電話, type=class java.lang.String, identifying=true}}]
2026-07-14T16:47:55.345+09:00  INFO 18536 --- [BatchDB2CSV1] [   main] c.n.test.Listener.LogJobListener         : ★Job started: exportJob
2026-07-14T16:47:55.360+09:00  INFO 18536 --- [BatchDB2CSV1] [   main] o.s.batch.core.step.AbstractStep         : Executing step: [exportStep]
2026-07-14T16:47:56.095+09:00  INFO 18536 --- [BatchDB2CSV1] [   main] o.s.batch.core.step.AbstractStep         : Step: [exportStep] executed in 735ms
2026-07-14T16:47:56.102+09:00  INFO 18536 --- [BatchDB2CSV1] [   main] c.n.test.Listener.LogJobListener         : ★Job finished: exportJob with status: COMPLETED
2026-07-14T16:47:56.104+09:00  INFO 18536 --- [BatchDB2CSV1] [   main] o.s.b.c.l.s.TaskExecutorJobLauncher      : Job: [SimpleJob: [name=exportJob]] completed with the following parameters: [{JobParameter{name='models', value=1, type=class java.lang.String, identifying=true},JobParameter{name='modelName', value=未来電話, type=class java.lang.String, identifying=true}}] and the following status: [COMPLETED] in 757ms
2026-07-14T16:47:56.113+09:00  INFO 18536 --- [BatchDB2CSV1] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2026-07-14T16:47:56.135+09:00  INFO 18536 --- [BatchDB2CSV1] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

問題なく実行されたようだ。
Spring Batch 6 のプログラムにデータ検証機能(Validation)を実装してみる。

パラメータ(ジョブ引数)のチェックをするのだが、

  • パラメータとして渡せるのは models、modelName の二つだけ
  • 必須項目はなし
  • models が設定されたときは、数値でなければならない(異常終了)
  • modelName の中身はなんでも良い

という仕様で組み込んでみる。(元ネタは「出力項目を増やすようちょっと修正版 Spring Batch」→「Spring Batch 6 で Listener を追加してログを出す」のソース)


■Validator/OptionalValidator.java

単純な必須チェックなどではない、独自チェックを記述したクラス

package com.netandfield.test.Validator;

import org.springframework.batch.core.job.parameters.InvalidJobParametersException;
import org.springframework.batch.core.job.parameters.JobParameters;
import org.springframework.batch.core.job.parameters.JobParametersValidator;

public class OptionalValidator implements JobParametersValidator {

    @Override
    public void validate(JobParameters parameters) throws InvalidJobParametersException {

        // パラメーター取得
        String models    = parameters.getString("models");;

        // 存在チェック(必須項目ではない)
        if (models == null || models.isEmpty()) {
            return;
        }

        // 存在していれば数値でなければならない
        try {
            Integer.parseInt(models);
        }
        catch(NumberFormatException e) {
            throw new InvalidJobParametersException("Not Number: models=" + models);
        }

    }

}


■Config/BatchConfig.java

import の追加

import java.util.ArrayList;
import java.util.List;

import org.springframework.batch.core.job.parameters.CompositeJobParametersValidator;
import org.springframework.batch.core.job.parameters.DefaultJobParametersValidator;
import org.springframework.batch.core.job.parameters.JobParametersValidator;

import com.netandfield.test.Validator.OptionalValidator; // 作成した validator

バリデータの定義(使用可能なパラメータ(必須項目と任意項目))を追加

    @Bean
    public DefaultJobParametersValidator defaultValidator() {

        // 1. 必須パラメータの配列
        String[] requiredKeys = {};    // 必須項目はなし

        // 2. 任意パラメータの配列
        String[] optionalKeys = {"models", "modelName"};

        // コンストラクタに(必須, 任意)の順番で渡す
        return new DefaultJobParametersValidator(requiredKeys, optionalKeys);

    }

作成した Validator(OptionalValidator クラス)を CompositeJobParametersValidator(コンポジット・ジョブパラメータ・バリデータ)に追加

    @Bean
    public JobParametersValidator compositeValidator() {

        // Validator List 生成
        List<JobParametersValidator> validators = new ArrayList<>();
        validators.add(defaultValidator());
        validators.add(new OptionalValidator());

        // Composit に Validator を入れる
        CompositeJobParametersValidator compositValidator = new CompositeJobParametersValidator();
        compositValidator.setValidators(validators);

        return compositValidator;

    }

Spring Batch では、1つの Job で 1つの Validator しかセットできない。

具体的には、Jobクラス(exportJob)の中で .validator() で指定できるクラスが 1つだけなので、使用可能なパラメータをチェックする標準の Validator(defaultValidator)クラスと、独自チェックの Validator(OptionalValidator)クラスを compositeValidator クラスにまとめ、.validator(compositeValidator()) と指定するわけ。

まとめた Validator を JobBuilder のメソッドチェーンに追加する。

    @Bean
    public Job exportJob(Step exportStep) {
        return new JobBuilder("exportJob", jobRepository)
                .start(exportStep)
                .listener(jobListener) // JobExecutionListenerを追加
                .validator(compositeValidator()) // ValidatorをまとめたcompositeValidatorを追加
                .build();
    }

これで、完了。

実際に実行(「実行の構成」でプログラムの引数を指定)してみる。

パラメータ(引数)が「models=1 modelName=未来電話 mondamin=あり」のとき(許されていないパラメータ "mondamin" あり)

■実行結果ログ(長いので先頭を "~" で省略)

<略>
~ : Running default command line with: [models=1, modelName=未来電話, mondamin=あり]
~ : The JobParameters contains keys that are not explicitly optional or required: [mondamin]
~ : [{JobParameter{name='models', value=1, type=class java.lang.String, identifying=true},JobParameter{name='modelName', value=未来電話, type=class java.lang.String, identifying=true},JobParameter{name='mondamin', value=あり, type=class java.lang.String, identifying=true}}]
~ : The JobParameters contains keys that are not explicitly optional or required: [mondamin]
<略>

と、"mondamin" が「The JobParameters contains keys that are not explicitly optional or required」(JobParameters に、明示的なオプション(任意)または必須として定義されていないキーが含まれています。)と警告されるが、処理は最後まで通る。

「え?InvalidJobParametersException は発生しないの?」って話だが、実は今回の処理には「必須項目」がないので、defaultValidator は自動的に「緩いチェック」にモードを切り替えてしまう。「必須項目」があれば、Spring Batch は「厳格チェックモード」に入り「未定義のパラメータが渡されたら異常終了」するようになる・・・らしい(^^;;;

俺的には「らしい」としか言えないので、エロい人、コメントで詳細教えて(笑)

次に、数値でなければいけないパラメータに「あ」をセットした場合の動きを見てみる。
パラメータ(引数)を「models=あ modelName=未来電話」とする。

■実行結果ログ(長いので先頭を "~" で省略)

これは、以下のようなログを吐いて無事落ちる。

<略>
    at com.netandfield.test.BatchDb2Csv1Application.main(BatchDb2Csv1Application.java:10) ~[classes/:na]
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:52) ~[spring-boot-devtools-4.1.0.jar:4.1.0]
Caused by: org.springframework.batch.core.job.parameters.InvalidJobParametersException: Not Number: models=あ
    at com.netandfield.test.Validator.OptionalValidator.validate(OptionalValidator.java:25) ~[classes/:na]
    at org.springframework.batch.core.job.parameters.CompositeJobParametersValidator.validate(CompositeJobParametersValidator.java:45) ~[spring-batch-core-6.0.4.jar:6.0.4]
    at org.springframework.batch.core.launch.support.TaskExecutorJobLauncher.createJobExecution(TaskExecutorJobLauncher.java:191) ~[spring-batch-core-6.0.4.jar:6.0.4]

しかし、ぱっと見、「Not Number: models=あ」が見つけられなくて、別の理由で落ちてるのかと思って必死で原因を探したわ(笑)

ああ、年を取って目がしょぼしょぼするのよ(笑)
昨年末の話を書いとく(笑)。このブログは俺の備忘録なので。

20251226_bin1.jpeg
2025年12月26日(金)。仕事納めだったので、飲み仲間のMイさんと十日市の「ニュー美和」で二人だけの納会をして、翌日からキャンプの予定だったのでさっさと一人岩国に帰ったのだが、やっぱりなんか飲み足りない。

嫁さんにお迎えのお願いの電話をするのを保留して、一人中通り商店街を中心に彷徨ったのだがさすがにどこの店も人が多い(^^;;;そりゃ、どこの会社も多分この日が仕事納めだろうからなあ。「満マル」にまで蹴られた俺がやっとたどり着いたのは「中通り食堂 びん」であった。

日ごろであれば決して足を踏み入れることのない雰囲気の店だ。つーか、今まで中通りを何度も行き来したのに、全然その存在に気づかなかった(^^;;;
いわゆる、地元のおじいちゃん、おばあちゃんが通う大衆食堂である。

20251226_bin2.jpeg
なんか入口の看板は「中通り食堂 びん」と書いてあるのだが、入口に掲示されたメニューには「菊川精肉店 和幸内精肉部 食事処 びんBin」と書いてある。「びん」ではなく「びんびん(Bin)」が正しい名前なのか?しかし、そんな男の陰茎力強く勃起した様を表すような店名を付けるものだろうか?(^^;;;

あ、でも、「中通り食堂 びん」の下に赤い小さな字で「Bin」って書かれている。やっぱ「びんびん」なのか?(^^;;; 俺はどきどきしながら入店した。

・・・ワンダーランドだった(笑)

店内には雑貨や民芸品(?)が陳列され、どこにも食堂感がない。奥に進むと入口からは見えづらいところにテーブルが四つほどならんだ小さな食堂ゾーンがあるって感じ。いや、まあ、食堂があってよかった。

俺はとりあえず「酒1杯+小鉢2品」の「晩酌セット」1,000円也を注文。小鉢2品はどちらも肉の姿の全然ない菜っ葉や油揚げなどを煮込んだお年寄り向けなヘルシーなやつ。

20251226_bin3.jpeg
まあ、広島で飲んで腹もそんなに空いてなかったのでこれで十分・・・と思っていたのだが、やはりなんか物足りなくなって、「玉子焼き」と「熱燗(千福)」を追加注文(笑)

玉子焼きにはマヨネーズがついていて、デブの俺も満足(笑)。それに熱々で美味かったしな。

完全に浮いてた一見客の俺にも、店のおじちゃん、おばちゃんはちょこちょこ話しかけてくれて、いわゆる「あったか」系の店だったね。もう少し年を取って枯れきたら通っても良い店かな。

まだまだ毎朝下腹部ビンビンの俺にはまだ早い店だったなあ(笑)
「日々来」のお好み焼で腹は満たされたのだが、まだ時間も早いしもう一杯・・・と、すぐ近くにある「立ち飲み のんどる」を訪店したのである。

ほんとに、「日々来」から徒歩 3分ほど?横川駅裏、いや横川駅北口の横川から三篠方面にのびる道すがらにもたくさんの良い感じの店があって、やっぱ今広島で一番熱いのは十日市~横川~三篠のこのラインではないかと感じるねえ。

20260714_nondoru1.jpeg
「立ち飲み のんどる」は以前から訪問してみたかった店だ。

入ると意外なほど奥に広い造り。俺は初めての訪問だったのでビビッて入口近くに陣取ったが、そこからまだ10人は並んで飲めるくらいカウンターが続いており、奥にはテーブル席もある。これならふらりと立ち寄っても、入れないってことはないな。

客層は若い兄ちゃん、姉ちゃんから、近所の老婆まで幅広い。つーか、普通のサラリーマンは少ないな(笑)

結局、この日は「日々来」で腹いっぱいになっていたので、「ジムビームハイボール」と「小いわし炙り」の計 830円だけで撤収。

まあ、立ち飲み屋なので当たり前なのだが、店員と常連さんの距離が近く、ひっきりなしに常連さんが店員に話しかけているのでなかなか注文もしづらい感じもあった。

こりゃ、再訪はないかな・・・とも少し思ったんだが、帰り際にママさん(かな?)が「また来てくださいね。気軽に寄ってくださいね」と声をかけてくれ、一見客も大事にしてるんだなってのはわかったので、また行ってみるか・・・という気にはなっている(笑)

面白い日本酒も置いてるみたいだし。
6/18(木)。以前大竹のお客さんのところで一緒に働いていたのだが、その後色々あって郷里に帰っていた女子が「今度結婚することになったので最後の独身ひとり旅を楽しみます」って広島にやってきた。

翌日、その大竹のお客さんたちと岩国でその子の結婚のお祝いをすることになってたのだ。

20260618_torau1.jpeg
大竹時代の同僚 LINE に「夕方広島に着いて、広電に乗り間違えて比治山の方まで彷徨って、今、無事稲荷町まで戻って食事を終え、小町のホテルに向かってます。」ってメッセージが入ったのが
20時過ぎ(笑)。「ちょうど今広島にいるから、立ち飲み屋でもよかったら袋町で一杯やる?」と反応すると「ぜひぜひ」ということだったので
NHKビルの前で落ち合って「魚寅」へ。

もう時間も遅かったので二人ともいきなり日本酒。俺は「原田 夏純米 ナツ 無濾過生原酒」を。彼女は・・・なんだっけ?山口県の酒の何か(笑)で乾杯。

「魚寅」はご存じのように(いや、知らんだろうけど)山口県の酒が充実している。彼女も「辛い酒が好き」と言いながら、どちらかというと甘めの山口県の日本酒「おいしい」と楽しんでくれた。

アテは、「せっかくなんで瀬戸内の味を楽しんどく?」と刺身の盛り合わせと広島地物のアナゴ刺しを頼んだ。アナゴを一口食べて「おいしい!おいしい!」と感動していたのでアナゴは全て彼女へ(笑)。

20260618_torau2.jpeg
俺の二杯目は「五橋 純米生酒」。彼女も同じものを。これも「おいしい」と飲んでくれたので、結婚祝いに「五橋」を送ることを約束。岩国の酒を喜んでくれるとうれしいねえ。

お互いの近況報告をしつつ美味い酒と肴を楽しんでいたら、あっという間に時計の針も 23時をまわり、翌日の岩国での飲み会もあるのでお開きに。

彼女には本当に俺らおっさん技術者は色々助けてもらった。「なんか、お客さんがエラー出るって言ってるんだけど、何度やっても再現できないんでどうにかして」と彼女に丸投げして、「再現できましたよ(笑)」「ウソ?(驚)」って救われたことが何度もあった(^^;;;

そうしてわしらが色々迷惑をかけた分も、しっかり幸せになってもらいたいと広島の空に浮かぶ細い月に祈った夜であった(笑)
俺はお好み焼(もちろん広島のね)の麺の端っこはパリパリになっていてほしい。そういう人、多いでしょ?

20260714_hibiki1.jpeg
そんな俺に「この店のお好み焼はマジでパリッパリ」と教えてくださったのは横川・三篠方面在住のお客さんである。
「麺の端がパリパリに焼けてるのが好きなら、絶対満足できます」と。

先週その話を聞いて、早速今日その店を訪ねてみた。
今日はあまりに暑く、袋町から段原南まで真っ昼間に歩いて移動した俺は完全に正気を失っており、「俺、今日はクソ熱いお好み焼き屋の鉄板の前で、冷たい生ビールをゴクゴク飲むんだ」と職場の人たちに宣言し、実際に横川駅で電車を降りた俺はまっすぐその店に向かったのである。

それが「お好み焼き&鉄板焼き 日々来」。もちろん初訪店である。

20260714_hibiki2.jpeg
店に入ると俺が口開けの客だった。勧められるままにカウンター席の端に腰を下ろす。
ポテトサラダを肴に生ビールを飲みつつ、「そば肉玉」が焼けるのをジッと待つ。

そして、いよいよ俺の眼の前にお好み焼が・・・。生ビールはもう飲み干してしまった。「ブラックニッカハイボール」を追加注文し、そして手にもったヘラをお好み焼に差し込む。もう、それだけで手にはパリパリと固く焼けた麺が切れる、いや、折れる感覚が伝わってくる(笑)

マジで、広島一のパリパリ麺だった。
いや、他にも一番はおるかもしれんでって言う人もいるだろうが、これ以上パリパリになったら(そりゃ、そういう店もあるかもしれんけど)俺的にはもうそれは「お好み焼」とは別の料理だと思う。「日々来」のパリパリ度が「美味しく食べれる限界」だと思う(笑)
そういう意味で「広島一のパリパリ麺」と言っていいだろう。

20260714_hibiki3.jpeg
麺をパリパリに焼くためか、ちょっとサイズは小さめで、そこが熟年デブには少しさびしかったが、存分にパリパリ麺を堪能した。

俺のあとに三人連れの高齢のお客さんがやってきたが、この店のお好み焼がパリパリだということを知り、わざわざ車に乗り合わせてきたということだった。なので可哀想に運転手のおじいさんはノンアルを飲んでいた。

そうか、高齢者にも情報が届くほどパリパリ麺として有名なんだ。そしてわざわざ遠方から車でやってくるほどパリパリ麺好きは多いんだなあ(^^;;;

三人の客の一人が少し興奮気味に「うどんもパリパリなの?」と聞いていたが、店主はやや苦笑いで「うどんはパリパリじゃありません」と回答していた。

まあ、うどんほどの太さのものがパリパリだとちょっときついよね(笑)
Oracle DBを使ったシステムで、時間(ミリ秒まで)をキーにしているテーブルで「キーの重複エラー」が出てプログラムが落ちるという障害が出たので、ちょっと調べてみた。

Oracle でミリ秒まで管理する場合のデータ型は DATE ではなく TIMESTAMP となる。精度はナノ秒(秒数以下(小数秒) 9桁)まで。

テストで、TIMESTAMP型の項目をキーとしたテーブルを作成する。

CREATE TABLE t_test1 (
    id INTEGER,
    udate TIMESTAMP,
    PRIMARY KEY (id, udate)
)

id と udate の組み合わせでユニークでないといけないテーブルだ。

なので、

INSERT INTO t_test1 (id, udate) VALUES(1, '2026-07-07 11:00:00.111111');

を 2回実行しようとすると、

ORA-00001: 一意制約(TESTER.SYS_C008227)に反しています

のエラーとなる。

INSERT INTO t_test1 (id, udate) VALUES(1, '2026-07-07 11:00:00.111222');

であれば、ミリ秒以下(小数秒 4~6桁目のマイクロ秒)の値が違うので登録はできる。

ミリ秒(秒数以下 3桁のみ)までの指定での INSERTでは、マイクロ秒に 000 を補完するので、

INSERT INTO t_test1 (id, udate) VALUES(1, '2026-07-07 11:00:00.111');

これも(内部的には 11:00:00.111000 という時間になり)通る。

SELECT 時に書式を指定しなければ、TIMESTAMP の項目はミリ秒までしか表示しない。

SELECT * FROM t_test1 WHERE udate >= '2026-07-07 11:00:00.111';

20260707_timestamp1.jpg

例えばマイクロ秒まで表示したければ、小数行を 6桁表示するようにフォーマットを指定する。

SELECT id, TO_CHAR(udate, 'YYYY-MM-DD HH24:MI:SS.FF6') FROM t_test1
WHERE udate >= '2026-07-07 11:00:00.111';

20260707_timestamp2.jpg

ちなみに、CREATE TABLE 時に TIMESTAMP とだけ指定すると、マイクロ秒(小数秒 6桁)までの管理となる。
そのため、

INSERT INTO t_test1 (id, udate) VALUES(1, '2026-07-07 11:00:00.111222333');

は、実際には 111222 までしか見ないので(ナノ秒部の 333 は無視する)、

ORA-00001: 一意制約(TESTER.SYS_C008227)に反しています

となる。ナノ秒まで扱うのであれば、

CREATE TABLE t_test1 (
    id INTEGER,
    udate TIMESTAMP(9),
    PRIMARY KEY (id, udate)
)

と、TIMESTAMP の後ろに少数桁数の(9)を設定する必要がある。

ところで、最初に書いた障害は、CSVファイルに「ミリ秒まで」書かれたデータを読込み DB に登録するときに発生したもの。元のデータがミリ秒までなので、TIMESTAMP の指定がどうこうで解決する問題ではない。
結局、時間をキーにせず、自動採番した番号などをキーにするしかない。

CREATE TABLE t_test1 (
    num NUMBER GENERATED ALWAYS AS IDENTITY,
    id INTEGER,
    udate TIMESTAMP(9),
    PRIMARY KEY (num)
)

とかね(num はシーケンスを使ってユニークに自動採番される)。

いや、あれこれ TIMESTAMP を試していたのは何なの?って話なんだけど、今後無駄になる知識じゃないからね。勉強、勉強。

ちなみに TIMESTAMP は、PostgreSQL や MtSQL などで微妙に仕様が違うからね。使うときは注意ですぞ(笑)
Oracle Database からデータを取ってくるバッチ処理を Spring Batch 6 で作ろうと思って、Eclipse
で依存関係をこんな風に設定した。

20260702_batch1.jpg

DB関係は、Spring Data JDBC と Oracle Driver を選択した。

pom.xml の中はこんな感じ。以下依存関係が存在することを確認した。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc11</artifactId>
            <scope>runtime</scope>
        </dependency>

Spring Batch(Spring Boot)の勉強始めてから、ぼんやり、Spring Data JDBC(spring-boot-starter-data-jdbc)が設定する環境(最低限の変換アダプタとしての基盤)に、Oracle や PostgreSQL ドライバが各 DBMS 特有の機能を補完する形になっているのかと思ってた。つまり、Spring Data JDBC と実際に使用する DBMS を組み合わせて使うものだと。

でも、今朝、ふと「Oracle JDBC ドライバって言うくらいなんだから、ojdbc11 単体の依存関係設定だけでいいんじゃね?」と思いついて、そもそも Spring Data JDBC ってなんやん?実はいらんのんちゃう?と調べてみると・・・

いらんやん・・・(^^;;;

最低限必要なのは Spring Batch と Oracle Driver だけやん。

上に書いたような「最低限の変換アダプタとしての機能」は spring-boot-starter-jdbc(SpringのJDBC基盤)で作られるんだけど、これは Spring Batch のスターター(spring-boot-starter-batch)が内部的に読み込んでいる。なので、この基盤の上で動く各 DBMS のドライバ(今回は Oracle Driver)だけ依存関係を設定してやればええんや。

必要なのは、

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>

        <dependency>
            <groupId>com.oracle.database.jdbc</groupId>
            <artifactId>ojdbc11</artifactId>
            <scope>runtime</scope>
        </dependency>

これだけね。

ちなみに、Spring Data JDBC で設定される機能って何かというと(Qiita の @yoshikawaa(Atsushi
Yoshikawa)さんの記事によると)、

  • CRUDを実現するシンプルなAPIを提供する
  • @Queryでカスタムクエリを定義できる
  • SQL実行前後などのイベントをインターセプトする
  • Spring Data JDBCを経由してMyBatisのSQLを実行できる
  • これらをAuto-Configする

なるほど、依存関係を設定しておくと色々便利なのね。ただ、これらの機能を使用しない、ただ単にDBを読むだけなら必要ない。Oracle Driver だけあればいい・・・という話なのね?

とりあえず残しておこう。
Spring Batch 6 のバッチ処理にパラメータ(ジョブ引数)を渡す方法。
例えばファイルパスなど、毎回違う値を渡すときに推奨されるやり方でコーディングしてみる。(reader や write などの Step で指定する(受け取る)やり方。今回は processor でジョブ引数を受け取るか形)

「Spring Batch 6 で Listener を追加してログを出す」で修正したバッチプログラムに修正を加える。(修正点のみ記述するので、全体のソースが見たい人は、元ネタのページを手繰ってくださいませ)

修正点は以下のとおり。

■import の追加

import org.springframework.batch.core.configuration.annotation.StepScope; //追加
import org.springframework.beans.factory.annotation.Value; //追加


■processor の修正

    @Bean
    @StepScope // ジョブ引数を受け取るために必須
    public ItemProcessor<UserCombinedDto, UserCombinedDto> processor(
                            @Value("#{jobParameters['models']}") Integer models,
                            @Value("#{jobParameters['modelName']}") String modelName) {
        return item -> {
            String phone = item.user_phone_number(); // DtoがRecordの場合のゲッター。通常のクラスなら .getUser_phone_number()

            // 電話番号が null でなく、かつ 090 または 080 で始まる場合
            if (phone != null && (phone.startsWith("090") || phone.startsWith("080"))) {
                // 表示名
                String displayName = "スマホ";
                // ジョブ引数をチェック
                if (models == null || "1".equals(models)) { // ジョブ引数未指定、あるいは models == 1
                    displayName = "携帯";
                }
                else {
                    if ("2".equals(models) && (modelName != null && !modelName.isEmpty())) {
                        // ジョブ引数 models = 2 で、modelName も指定されている
                        displayName = modelName;
                    }
                    else {
                        // ジョブ引数 models が 1でも 2でもない。または modelName が未指定
                        displayName = "未定義";
                    }
                }

                // 文字列を追加した新しいDtoを作成して返す
                // (DtoがRecordの場合はイミュータブル(不変)なので、このように新しくインスタンスを作る)
                return new UserCombinedDto(
                        item.user_id(),
                        item.user_name(),
                        item.user_address1(),
                        item.user_address2(),
                        phone + "(" + displayName + ")",
                        item.anniversary(),
                        item.note()
                );
            }

            // 条件に当てはまらない場合は、加工せずにそのまま次に渡す
            return item;
        };
    }

<修正ポイント>
  • メソッドに @StepScope アノテーションを必ず付与(このアノテーションを付けないとジョブ引数が受け取れない)
  • メソッドの引数に、受け取りたいジョブ引数を設定(例:@Value("#{jobParameters['modelName']}") String modelName)
  • 受け取ったジョブ引数で色々と処理(ジョブ引数は編集できないので、編集したいときは他の変数にコピー)


■Stepの定義の変更

    // 4. Stepの定義 (Spring Batch 6 スタイル)
    @Bean
    public Step exportStep (
                JdbcCursorItemReader<UserCombinedDto> reader,
                ItemProcessor<UserCombinedDto, UserCombinedDto> processor,
                FlatFileItemWriter<UserCombinedDto> writer) {
        return new StepBuilder("exportStep", jobRepository)
                .<UserCombinedDto, UserCombinedDto>chunk(100) // 100件ごとにコミット/出力
                .transactionManager(transactionManager)
                .reader(reader)       // 読んで
                .processor(processor) // 処理して
                .writer(writer)       // 書き出す
                .build();
    }

<修正ポイント>
  • メソッドの引数を。各Bean(Reader、Processor、Writer)の定義を受け取る形に設定
  • 各 Step の呼び出しで引数指定は不要(例:.processor(processor()) →.processor(processor) )


■Jobの定義の変更

    // 5. Jobの定義
    @Bean
    public Job exportJob(Step exportStep) {
        return new JobBuilder("exportJob", jobRepository)
                .start(exportStep)
                .listener(jobListener) // JobExecutionListenerを追加
                .build();
    }

<修正ポイント>
  • メソッドの引数を、exportStep の定義を受け取る形に設定
  • exportStep を呼び出すときの引数を不要に(例:.start(exportStep())→.start(exportStep))

Spring Framework は「@Bean アノテーションの付いたメソッドの引数に他の Bean の型を書いておくと、Spring が自動的にそれを探して持ってきてくれる(依存性注入)」というルールがあるので、これをすると Step を呼ぶときの引数を省略できる。
「processor は引数でパラメータ(ジョブ引数)を受け取る」→「exportStep はその情報を読み込む(Spring により自動で流し込まれる)」→「exportJob は Spring により自動的に組み立てられた Step の情報を読み込む(流し込まれる)」という過程を経て、あとは Spring が引数はうまいことやってくれる・・・という仕掛けだ。なので、呼び出すときの引数を省略できる。

ま、あまり Framework を使わず、一からしこしこコードを書いてきた人間には、こういう「Framework が巧いことなってくれる」という状態がなかなかしっくり来なくて、いつまでも「どうしてこうなる?どこからこの値はきた?」ってことになるんだけど、「そういうもんだ」と納得するしかないのである(笑)

と、ここまでできたら Eclipse で実行してみる。

Eclipse で実行させるときにパラメータ(ジョブ引数)を渡したいときは、プロジェクト名の上で右ボタンメニューから「実行」→「実行の構成」を選択し、開いた窓で、まずはプロジェクト名とメインクラスを指定。

20260713_parameters01.jpg

次に、プログラムのパラメータ(引数)を設定する。これは、コマンドラインから jar を実行するときと同じように、「パラメータ名=値」という形で記述する。複数のパラメータがある場合は半角スペース区切りで入力する。

20260713_parameters02.jpg

値を設定後、「実行」ボタン押下でジョブ実行。

ログにも引数の情報が出力されている。(長いので、行の先頭は'~'に省略)

~: Started BatchDb2Csv1Application in 5.341 seconds (process running for 6.314)
~: Running default command line with: [models=2, modelName=未来電話]
~: [name=exportJob]] launched with the following parameters:
[{JobParameter{name='models', value=2, type=class java.lang.String,
identifying=true},JobParameter{name='modelName', value=未来電話,
type=class java.lang.String, identifying=true}}]
~: ★Job started: exportJob
~: Executing step: [exportStep]
~: Step: [exportStep] executed in 767ms
~: ★Job finished: exportJob with status: COMPLETED
~: [{JobParameter{name='models', value=2, type=class java.lang.String, identifying=true},JobParameter{name='modelName', value=未来電話, type=class java.lang.String, identifying=true}}] and the following status: [COMPLETED] in 785ms


出力された CSV ファイルを見ると、しっかり携帯種が「未来電話」と出ている。パラメータの設定が効いているようだ。

ID,名前,住所1,住所2,電話,記念日,内容
1,テスト太郎,広島市中区橋本町2-17,若林ビル302,082-221-7555 ,,
2,HIROSHIMATAROU,広島県廿日市市宮島町5050-11,,090-1111-2222(未来電話),19661210,誕生日
2,HIROSHIMATAROU,広島県廿日市市宮島町5050-11,,090-1111-2222(未来電話),19981003,結婚
3,中国 花子,島根県呪郡呪村1078-13,川村診療所内,0120-333-444 ,,
4,近藤まちゃ,山口県周南市銀座3丁目2-19,,080-5555-6666(未来電話),19641225,誕生日
4,近藤まちゃ,山口県周南市銀座3丁目2-19,,080-5555-6666(未来電話),20230407,死人村トレイルラン100 完走
4,近藤まちゃ,山口県周南市銀座3丁目2-19,,080-5555-6666(未来電話),20240122,死人村雪山激走フェス 2.1km地点リタイア
5,MASA SAITO,広島市中区橋本町2-17,若林ビル201,082-221-7549 ,,
6,KAORU MITARAI,山口県岩国市周東町祖生99999,,0827-85-0427 ,20011011,結婚
6,KAORU MITARAI,山口県岩国市周東町祖生99999,,0827-85-0427 ,20011210,離婚
6,KAORU MITARAI,山口県岩国市周東町祖生99999,,0827-85-0427 ,20200523,結婚
7,日本 太郎,,,009-1234-5678,,


次回は、第1パラメータの型を整数(Integer)にしているため、ここに文字列を入れられると異常終了していまう。それを回避する修正をしよう。
A5:SQL Mk-2 で Oracle Database に接続しようとしたところ、

NET: Network error (Cannot connect to server on host 'localhost':
対象のコンピューターによって拒否されたため、接続できませんでした。.
Socket Error Code: 10061($274D))
<Host = localhost:1521>

というエラーになってつながらない。

タスクマネージャーで見てみたら、確かに Oracle 関係が全然起動していない。

そこで、タスクマネージャーからサービス管理ツールを開き、「OracleOraDB21Home1TNSListener」を実行しようとすると、

ローカルコンピューターの OracleOraDB21Home1TNSListener サービスを開始できません。

エラー 1069: ログオンに失敗したため、サービスを開始できませんでした。

とエラーが出て起動できず・・・

20260703_taskmane4.jpg

Oracle の SYSTEM と SYS アカウントのパスワードを変更したのでそのせいかと思い(「Oracle Database 21c Express Edition (XE) をインストール」参照)OracleOraDB21Home1TNSListener のプロパティを開いて「ログオン」タブ選択。そこで新しいパスワード 'testpass01' をパスワードに入力したら、うまく起動するようになった。

20260703_taskmane7.jpg

・・・が、パスワードを変更したのは一昨日であり、昨日は正常に起動したぞ?

そもそも、「ログオン」タブの「アカウント」に入ってる「NT SERVICE\OracleOraDB21Home1TNSListener」って何?これ、アカウント???

Gemini君によるとこれは「仮想アカウント(Virtual Account)」と呼ばれる、Windowsがサービス専用に自動で管理してくれる特殊なアカウントで、通常 Windowsが裏側でパスワードを自動管理しているため、人間が手動でパスワードを入力する必要がなく、プロパティの画面ではパスワード欄は空にしとけばいいらしい。

なるほど。パスワードの確認欄があったのはそのせいね(^^;;; 俺は、単に NT SERVICE\OracleOraDB21Home1TNSListener アカウントの「新しいパスワード」を 'testpass01' にしただけの話か・・・(^^;;;

今度同様の状況になったら、その時にはパスワードには空値を設定しよう(笑)
ありま。「拉麺 徳ちゃん」の話、書いてなかったか。

20251130_tokuchan1.jpeg
昨年の11月30日。ネットで見つけた徳山のラーメン屋「拉麺 徳ちゃん」を初めて訪店した。

日曜日に「朝ラーメン(朝ラー)」できる店を探してたのよ。俺くらいのデブになると、休日は朝からラーメン食べたくなっちゃうからね。
土曜日は「ラーメンショップ 熊毛店」に出かけるから良いんだけど日曜日は困っちゃう。ラーショ熊毛は休みなんで・・・。

というわけであれこれ岩国・周南地区の店徳山青果市場を検索していたら、朝9:00からやっている「徳ちゃん」が引っかかったのである。

で、立地的にあまり期待はしていなかった。

「徳ちゃん」って、徳山青果市場のすぐ隣にあって、駐車場が満車のときは青果市場の駐車場を使ってくれと書いてあるくらい親密な関係らしい。てことは、市場で働いている人たちが「腹を膨らませに」やってくる店じゃないか?って思って。

20251130_tokuchan2.jpeg
そういうところのラーメンって、黙ってても客が来るんで、なんか麺がぐだぐだに煮られたありふれた味の醤油ラーメンじゃないかって気がして・・・偏見ですけど(笑)

・・・が、裏切られました。良い方に

この日注文したのは期間限定品の「担々麺」1,100円也。俺、初めての店で「まずはその店の一番ベーシックなラーメンで味を確認しよう」みたいな感覚はまったくないからね(笑)。そういうことしてる人間見ると笑っちゃうくらいで。
「辛いの」があればそれを一番に注文しちゃう(笑)
オプションで「麺硬め」「濃いめ」「背脂多め」「唐辛子有」「ネギ有」を選択。


これに、「唐揚げ(2個)」250円をつける。実はこの店、ラーメンを注文したらご飯一杯がサービスでついてくるのだ。つまりこれで「ラーメン+唐揚げ+ご飯」の「ラーメン・唐揚げ定食」の完成である。うーん、デブ満足。

そして、「担々麺」の完成度は最高水準であった。見て、この豚骨の滲み出した真っ白なスープ。辛味噌を混ぜて汚してしまうのが申し訳ないほどだ。ま、ガシガシと混ぜたけど。担々麺だからな(笑)

20251130_tokuchan3.jpeg
そして、もう、これが美味いの。語彙力が低いもんで、うまく表現できないけど(笑)

この店はすごく真面目にラーメンに取り組んでるね。若い男の人がいたがあれが店主だろうか?少なくとも「勝手に客が来るから、味の改良なんか必要ないわ。それより明日の徳山競艇のレースの方が気になるわ」というような姿勢ではないのは確か。

俺が入店したときは俺を含めて 3組ほどしか客はいなかったが(時間早かったからね)、店に革ジャンを忘れたことに気づいて途中でハイエースをUターンさせて戻ってみたら、既にほぼ満席状態になっていた。やっぱ人気店なんやね。
Spring Batch で用意されている Event Listener も色々あるが、とりあえず Job の実行前後で起動する JobListener を使ってログを吐いてみよう。

一番シンプルな Spring Batch 6 の Chunk 処理例」で作ったバッチ処理に足してみる。

■LogJobListener.java の作成

JobExecutionListener を実装した Job の前後にログを吐くクラス。beforeJob に Job の前の処理を、afterJob に後の処理を書く。

package com.netandfield.test.Listener;

import org.springframework.batch.core.job.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListener;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Component
@Slf4j
public class LogJobListener implements JobExecutionListener {

    @Override
    public void beforeJob(JobExecution jobExecution) {
        log.info("★Job started: " + jobExecution.getJobInstance().getJobName());
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        log.info("★Job finished: " + jobExecution.getJobInstance().getJobName() + " with status: " + jobExecution.getStatus());
    }

}


■BatchConfig.java

バッチ設定に Listener をセットする

import部に、

import org.springframework.batch.core.listener.JobExecutionListener;
import org.springframework.beans.factory.annotation.Autowired;

を追加。

メンバ変数に jobListener を宣言。

<略>
@Configuration
public class BatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final DataSource dataSource;

    @Autowired
    private JobExecutionListener jobListener;
<略>

Jobの定義に JobListener を追加する

    @Bean
    public Job exportJob() {
        return new JobBuilder("exportJob", jobRepository)
                .start(exportStep())
                .listener(jobListener) // JobExecutionListenerを追加
                .build();
    }


ここまでソースをを修正したら実行。

これで、以下のようなログが出力される(長いので頭の方を「~」と略している)

...
~ : Job: [SimpleJob: [name=exportJob]] launched with the following parameters: [{}]
~ : ★Job started: exportJob
~ : Executing step: [exportStep]
~ : Step: [exportStep] executed in 681ms
~ : ★Job finished: exportJob with status: COMPLETED
~ : Job: [SimpleJob: [name=exportJob]] completed with the following parameters: [{}] and the following status: [COMPLETED] in 698ms
...

ちゃんと出てるね。

ちなみにバッチ処理では「ボタンをクリックした」「表示されているオブジェクトにオンマウスした」などのイベントは無いので、用意されている Listener はほとんど「〇〇処理の前後に起動する」ものだ。

反対に、なんらかの処理には全て Listener が用意されている・・・かな。
「Stepの実行前後」「Chunkの実行前後とエラー発生時」「Readerの実行前後とエラー発生時」等など。
「ちょびっと」で一杯やったけどまだ小腹が空いていたので(「ちょびっと」の惣菜はちょびっとだから(笑))、同じ横川駅裏ガード下並びの「餃子ノ酒場ロッキー 横川」に入店。

20260707_rockey1.jpeg
なんか、1,000円ポッキリの「ちょい飲みセット」があるというのでそれを注文。酒1杯+餃子1皿+早出し1品というセットだ。

早出しっていうのは、さっと出せる簡単なツマミってことね。メニューから選べるのでポテトサラダをチョイス。

酒は日本酒を選んで「銘柄なんですか?」って聞くと「宝剣」とのこと。
出てきたのは、「宝剣 純米酒 レトロラベル」だった。高い酒ではないけど美味いよね。てか、変な上撰酒じゃなくて純米酒ってとこが店として評価できるわ。(「上撰酒」は昔の「一級酒」のことだからおかしな酒ではない・・・という不毛な議論はノーサンキューです)

ツマミもどれも美味い。

ポテトサラダにはカリカリに揚げたオニオンチップ?がかかってて食感も楽しいし、餃子は普通に美味い。最近食った餃子の中じゃ一番好きな味。
それに、このキムチが・・・え?なんか一品多くない?

20260707_rockey2.jpeg
キムチは付き出し、つまりお通しでした(笑)。それでこのキムチだけおかわり自由とか言ってたのか・・・

「ちょい飲みセット」は税込みできっちり 1,000円ぽっきりなんだけど、それ以外にお通し代 400円がかかります。
ポケットの中にしわくちゃの千円札一枚・・・という状態で飲みに行ったら 400円が払えなくて警察に突き出されることになるので注意ね(笑)

お通しのない立ち呑み屋ばかりで飲んでたので危なかった。まあ、俺の場合、しわくちゃの千円札の他に、ちゃんとジャラ銭もポケットに入ってたんで大丈夫だったけど(笑)
色々と Spring Batch 6 で試している元のソースは「一番シンプルな Spring Batch 6 の Chunk 処理例」に掲載しているものだと言っているんだが、実は変更されている。

真面目に「一番シンプルな Spring Batch 6 の Chunk 処理例」のソースを使って、その後の記事の変更点を適用しようとしたらなんかコードが違う!?と戸惑われている方がいらっしゃるかもしれないので、一応、変更後のソースも載せておこう。

一番シンプルな Spring Batch 6 の Chunk 処理例」のソースを参照・・・と書かれていたら、実際にはこのソースを使用する。

テーブルも複数参照するように変わっているが JOIN で連結しているので SQL 文は一本だ。追加したテーブルは以下のとおり。

t_user_addresses(ユーザ住所)

20260713_base_springbatch1.jpg

t_user_anniversaries(ユーザ記念日)

20260713_base_springbatch2.jpg

これを、以前作った t_users(ユーザ情報テーブル)と連結して CSV に出力するように修正した。

20260713_base_springbatch3.jpg

ただ、 SQL が変わったのと、出力する項目数が増えただけなので、基本的な構造は変わらない。

以下、修正点。


■Dto/UserCombinedDto.java(record クラス)

package com.netandfield.test.Dto;

public record UserCombinedDto(
    Long user_id,
    String user_name,
    String user_address1,
    String user_address2,
    String user_phone_number,
    String anniversary,
    String note
) {}

※以前作った TUserDto.java は廃止。


■Config/BatchConfig.java(バッチ設定クラス)

package com.netandfield.test.Config;

import javax.sql.DataSource;

import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.Step;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.infrastructure.item.ItemProcessor;
import org.springframework.batch.infrastructure.item.database.JdbcCursorItemReader;
import org.springframework.batch.infrastructure.item.database.builder.JdbcCursorItemReaderBuilder;
import org.springframework.batch.infrastructure.item.file.FlatFileItemWriter;
import org.springframework.batch.infrastructure.item.file.builder.FlatFileItemWriterBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.DataClassRowMapper;
import org.springframework.transaction.PlatformTransactionManager;

import com.netandfield.test.Dto.UserCombinedDto;

@Configuration
public class BatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final DataSource dataSource;

    // コンストラクタ注入
    public BatchConfig(JobRepository jobRepository, PlatformTransactionManager transactionManager, DataSource dataSource)
{
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
        this.dataSource = dataSource;
    }

    // 1. Reader: Oracle DBからデータを読み込む
    @Bean
    public JdbcCursorItemReader<UserCombinedDto> reader() {

        // 記念日が複数あり
        String joinSql = """
            SELECT
                u.user_id,
                u.user_name,
                a.user_address1,
                a.user_address2,
                u.user_phone_number,
                an.anniversary,
                an.note
            FROM t_users u
            LEFT JOIN t_user_addresses a ON u.user_id = a.user_id
            LEFT JOIN t_user_anniversaries an ON u.user_id = an.user_id
            ORDER BY u.user_id ASC, an.anniversary ASC
            """;
        return new JdbcCursorItemReaderBuilder<UserCombinedDto>()
                .dataSource(dataSource)
                .name("oracleItemReader")
                .sql(joinSql)
                .rowMapper(new DataClassRowMapper<>(UserCombinedDto.class)) // RowMapperでResultSet(検索結果オブジェクト)をDTOへマッピング
                .fetchSize(1000) // Oracleのパフォーマンス最適化(1,000行ずつ読込)
                .build();
    }

    // 2. Processor: データを加工する
    @Bean
    @StepScope // ジョブ引数を受け取るために必須
    public ItemProcessor<UserCombinedDto, UserCombinedDto> processor() {
        return item -> {
            String phone = item.user_phone_number(); // DtoがRecordの場合のゲッター。通常のクラスなら .getUser_phone_number()

            // 電話番号が null でなく、かつ 090 または 080 で始まる場合
            if (phone != null && (phone.startsWith("090") || phone.startsWith("080"))) {
                // 文字列を追加した新しいDtoを作成して返す
                // (DtoがRecordの場合はイミュータブル(不変)なので、このように新しくインスタンスを作る)
                return new UserCombinedDto(
                        item.user_id(),
                        item.user_name(),
                        item.user_address1(),
                        item.user_address2(),
                        phone + "(携帯)",
                        item.anniversary(),
                        item.note()
                );            }

            // 条件に当てはまらない場合は、加工せずにそのまま次に渡す
            return item;
        };
    }

    // 3. Writer: CSVファイルへ出力する
    @Bean
    public FlatFileItemWriter<UserCombinedDto> writer() {
        return new FlatFileItemWriterBuilder<UserCombinedDto>()
                .name("csvItemWriter")
                .resource(new FileSystemResource("C:\\work\\test\\t_users.csv"))
                .delimited() // 区切り文字(Delimited)を使ったフォーマットで書き出すことを指定
                .delimiter(",") // 区切り文字をカンマに設定(省略した場合もカンマ)
                .names("user_id", "user_name", "user_address1", "user_address2", "user_phone_number", "anniversary", "note") // 出力するフィールド名
                .headerCallback(writer -> writer.write("ID,名前,住所1,住所2,電話,記念日,内容")) // ヘッダー行
                .build();
    }

    // 4. Stepの定義 (Spring Batch 6 スタイル)
    @Bean
    public Step exportStep () {
        return new StepBuilder("exportStep", jobRepository)
                .<UserCombinedDto, UserCombinedDto>chunk(100) // 100件ごとにコミット/出力
                .transactionManager(transactionManager)
                .reader(reader())       // 読んで
                .processor(processor()) // 処理して
                .writer(writer())       // 書き出す
                .build();
    }

    // 5. Jobの定義
    @Bean
    public Job exportJob() {
        return new JobBuilder("exportJob", jobRepository)
                .start(exportStep())
                .listener(jobListener) // JobExecutionListenerを追加
                .build();
    }
}


これで実行すれば、

20260713_base_springbatch4.jpg

こういう CSV ファイルが出力される。
職場の近くに新しいラーメン屋ができた。

20260629_antei1.jpeg
「純とんこつラーメン あんてい -はなれ- 広島店」

このブログでも書いたことがある・・・と思ったら書いてなかった(^^;;;「ゆかのこしかけ」っていうそこそこ人気のあった居酒屋の後にできた店だ。店内のレイアウトから、居ぬき(店の設備をそのまま使う形)で入ったみたいね。

どうもこのラーメン屋の前に餃子屋に変わったんじゃないかという情報があったんだけど、その餃子屋がやってるのがこのラーメン屋のようだ。餃子屋がうまくいかなかったのでチェーン店契約したみたいな感じなんかね?

以前、飲み仲間のMイさんから長い列のできる人気店だと聞いていたのだが、この日の夕方、18時の開店までに並んでいたのは 3人。俺が 4人目。一昨日昼間に見たときは 13人並んでいたから、「あんてい」のラーメンが食べたい人は夕方がいいんじゃないじゃろうか。

20260629_antei2.jpeg
そういう風に、人気店という話は聞いていたのだが、それ以外の予備知識はまったくなし。というか今も結局どこ発祥のどういう特徴の店なのかとか全然知らん(笑)。興味もないし。

俺は「ラーメン好き」だけど「ラーメンマニア」ではないので、ただその店のラーメンが「俺的に美味いかどうか」だけが保持すべき情報なのである(笑)

で、初めて注文したラーメンは「純とんこつ」。この店の基本のラーメンだ。
メニューによると、ラーメンって言ってもいいのかな?実際、に出された濃厚な豚骨エキスで真っ白なスープである。

麺はカタメで。トッピングは何もなし。それに、無料のリゾット用の小ごはんも付ける。そう、ここでは残ったスープにご飯をぶち込み「リゾット風」にして食べることができる。
ええじゃん、ええじゃん。ちゃんとそれを店の公式レシピにしてるのはええじゃん。

20260629_antei3.jpeg
そして実食。うん、美味いとんこつスープだ。くせはないんだけど濃厚。トッピングなしだとチャーシューすらなくてさみしいし、しかもそれで 990円もするので、「ふん、こけおどしのぼったくりラーメンか」なんて最初は思ってたんだけど、美味いやん。これだけしっかり作ったとんこつスープなら 990円するかな。カタメに茹でた細麵もいいねえ。

で、最後の飯をぶちこんでリゾットに。ああ、これも美味い。それに、ラーメンだけだとちょっと物足りないデブの腹も満たしてくれるう。ええわ、これ。

というわけで満足したよ。
次回、またあまり並んでないところに出くわしたら、今度はラーメン、ラーメンも試してみたいね。
Chunk(チャンク)処理の基本通り、

  • ItemReader: Oracle Database からユーザー一覧(ID、名前、電話番号)を読み込む
  • ItemProcessor: 電話番号が携帯電話番号だったときは、番号の後ろに'(携帯)'とつける加工処理
  • ItemWriter: 読み込んだデータ(加工されたものもあり)を CSV ファイルに出力

という 3ステップ構成。
ただし、データが 1件もなかったときの処理や、CSV 出力に失敗したときの例外処理など、色々本物の業務バッチ処理としては足らない機能がたくさんあるので、それを勉強しながら少しずつ実装していくつもり。

まずは、そういう勉強の基盤になるソースということで、メモ代わりに貼っておく。
(プロジェクト名は 'BatchDb2Csv1')

とりあえず、

20260702_sample1.jpg

※「Oracle Database 21c Express Edition (XE) に A5 で接続」というエントリーで作成したデータ

こういうデータを Oracle Database から読み込んで、このエントリのバッチを走らせたら、

20260702_sample2.jpg

こういう CSV ファイルが吐かれます。


■application.properties

spring.application.name=BatchDB2CSV1

# Oracle 接続設定
spring.datasource.url=jdbc:oracle:thin:@localhost:1521/XEPDB1
spring.datasource.username=tester
spring.datasource.password=tester01
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver

# アプリ起動時にSpring Batchのジョブを自動実行する
spring.batch.job.enabled=true

# 【重要】Spring Batch等の管理テーブルを自動作成させる設定
spring.sql.init.mode=always

# Spring Boot DevToolsの自動再起動を無効化する(終了前に一時停止状態になるので)
spring.devtools.restart.enabled=false

YAML形式の方がお好みの方もいらっしゃるでしょうが、俺はこっちが好み。
どうも、半角スペースでインデント(字下げ)するのが(^^; Python 界隈の人は抵抗ないでしょうが(^^;;


■BatchDb2Csv1Application.java

package com.netandfield.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BatchDb2Csv1Application {

    public static void main(String[] args) {
        SpringApplication.run(BatchDb2Csv1Application.class, args);
    }

}


■Dto/TUserDto.java

package com.netandfield.test.Dto;

// DTOクラス・・・ではなく record(不変オブジェクト)で
public record TUserDto(Long user_id, String user_name, String user_phone_number) {}


■Config/BatchConfig.java

package com.netandfield.test.Config;

import javax.sql.DataSource;

import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.Step;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.infrastructure.item.ItemProcessor;
import org.springframework.batch.infrastructure.item.database.JdbcCursorItemReader;
import org.springframework.batch.infrastructure.item.database.builder.JdbcCursorItemReaderBuilder;
import org.springframework.batch.infrastructure.item.file.FlatFileItemWriter;
import org.springframework.batch.infrastructure.item.file.builder.FlatFileItemWriterBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.DataClassRowMapper;
import org.springframework.transaction.PlatformTransactionManager;

import com.netandfield.test.Dto.TUserDto;

@Configuration
public class BatchConfig {

    private final JobRepository jobRepository;
    private final PlatformTransactionManager transactionManager;
    private final DataSource dataSource;

    // コンストラクタ注入
    public BatchConfig(JobRepository jobRepository, PlatformTransactionManager transactionManager, DataSource dataSource)
    {
        this.jobRepository = jobRepository;
        this.transactionManager = transactionManager;
        this.dataSource = dataSource;
    }

    // 1. Reader: Oracle DBからデータを読み込む
    @Bean
    public JdbcCursorItemReader<TUserDto> reader() {
        return new JdbcCursorItemReaderBuilder<TUserDto>()
                .dataSource(dataSource)
                .name("oracleItemReader")
                .sql("SELECT user_id, user_name, user_phone_number
FROM t_users ORDER BY user_id ASC")
                .rowMapper(new DataClassRowMapper<>(TUserDto.class)) // RowMapperでResultSet(検索結果オブジェクト)をDTOへマッピング
                .fetchSize(1000) // Oracleのパフォーマンス最適化(1,000行ずつ読込)
                .build();
    }

    // 2. Processor: データを加工する
    @Bean
    public ItemProcessor<TUserDto, TUserDto> processor() {
        return item -> {
            String phone = item.user_phone_number(); // DtoがRecordの場合のゲッター。通常のクラスなら getUser_phone_number()

            // 電話番号が null でなく、かつ 090 または 080 で始まる場合
            if (phone != null && (phone.startsWith("090") || phone.startsWith("080"))) {
                // 文字列を追加した新しいDtoを作成して返す
                // (DtoがRecordの場合はイミュータブル(不変)なので、このように新しくインスタンスを作る)
                return new TUserDto(
                        item.user_id(),
                        item.user_name(),
                        phone + "(携帯)"
                );
            }

            // 条件に当てはまらない場合は、加工せずにそのまま次に渡す
            return item;
        };
    }

    // 3. Writer: CSVファイルへ出力する
    @Bean
    public FlatFileItemWriter<TUserDto> writer() {
        return new FlatFileItemWriterBuilder<TUserDto>()
                .name("csvItemWriter")
                .resource(new FileSystemResource("C:\\work\\test\\t_users.csv"))
                .delimited() // 区切り文字(Delimited)を使ったフォーマットで書き出すことを指定
                .delimiter(",") // 区切り文字をカンマに設定(省略した場合もカンマ)
                .names("user_id", "user_name", "user_phone_number") // 出力するフィールド名
                .headerCallback(writer -> writer.write("ID,名前,電話番号")) // ヘッダー行
                .build();
    }

    // 4. Stepの定義 (Spring Batch 6 スタイル)
    @Bean
    public Step exportStep() {
        return new StepBuilder("exportStep", jobRepository)
                .<TUserDto, TUserDto>chunk(100) // 100件ごとにコミット/出力
                .transactionManager(transactionManager)
                .reader(reader())       // 読んで
                .processor(processor()) // 処理して
                .writer(writer())       // 書き出す
                .build();
    }

    // 5. Jobの定義
    @Bean
    public Job exportJob() {
        return new JobBuilder("exportJob", jobRepository)
                .start(exportStep())
                .build();
    }
}

とこんな感じで。

次は、DBから 1件も取得できなかったときとか、CSVに出力するレコードが 0件とか(出力する?しない?ヘッダだけ出す?出さない?)、あと例外処理か。そういうのを追加していこう。
あと、Windowsバッチ(.BAT)の書き方で違和感があるのが、ダブルクォーテーションの扱いである。

エスケープしない限り、ダブルクォーテーションは文字としてそのまま使われることはなく、文字列の範囲指定、つまり引用符(ダブルクォーテーションで囲んだ中身が有効)として使われる・・・という認識なんだけど Windowsバッチでは違う。

例えば Perl で

my $a = "ABC";
if ("$a" eq "ABC") { print "ok!"; }

としたとき(まあ、変数をわざわざ "" で囲むことはないけど、例としてね)、比較されるのは $a変数に収められた ABC という文字列と、"" で囲まれた ABC という文字列である。決して、"ABC" と "ABC" が比較されるわけではない。
"" とすれば、それはダブルクォーテーションで囲まれた文字が何もない(空文字)ということになる。

ところが Windowsバッチで、

IF %1 == "ABC" ( ECHO 同じです ) ELSE ( ECHO 違います )

なんて書いて引数に ABC を指定して' test.bat ABC 'ってバッチを起動すると「違います」と言われる。「え?"ABC" と "ABC" なのに?」でも、実際は ABC と "ABC" の比較になってるのだ。
改めて、' test.bat "ABC" 'と実行すれば、これは「同じです」と返ってくる。ダブルクォーテーションも単なる文字として判断されているので、これなら "ABC" と "ABC" の比較となるからだ。

上記の IF 文で、引数に何も指定しなければ「"ABC" の使い方が誤っています。」というエラーになる。

この場合、%1 は「空文字」ではなく「無」な状態となり、変数展開後、

IF == "ABC" ( ECHO 同じです ) ELSE ( ECHO 違います )

と書いたのと同じことになるのだ。書式として誤った形になっている。なんか気持ち悪い(^^;;;

なので、正しい対応(あとで書くが、これは単なる慣習である)

IF "%1" == "ABC" ( ECHO 同じです ) ELSE ( ECHO 違います )

と書く。そうすると、引数が指定されなかったときは、

IF "" == "ABC" ( ECHO 同じです ) ELSE ( ECHO 違います )

と書いたのと同じになるので、書式として正しい。む~ん・・・

上の例は、まるで変数や文字列を二重引用符(ダブルクォーテーション)で囲ったように見えるが、これは実は引用符ではない。そもそも、Windowsバッチには引用符という概念がない。

これは実際には

IF X%1X == XABCX ( ECHO 同じです ) ELSE ( ECHO 違います )

のように、ダブルクオーテーションではなくても良い。%1 が「無」の状態とならなければいいのだ。だから囲ってやる必要もなく、

IF X%1 == XABC ( ECHO 同じです ) ELSE ( ECHO 違います )

でいい。%1が空だったときに、何かしらの値がそこに残ればいいのだ。上の例であれば、「IF X == XABC」となり書式としては成立している。そこを「頭に X を置くのでは見栄えも悪いし」ということで、慣習的にダブルクォーテーションで囲っているだけの話だ。

この辺が日ごろ Windowsバッチに触れてない人間には気持ちが悪いのよね。

「引用符の概念がない」のもそうだけど、そのため空文字対策で変数を何らかの文字で囲ってやらないといけないときは慣習的にダブルクォーテーションを使う(人が多い)。これが俺のようにたまにしか Windowsバッチに触れない人間には「変数を引用符で囲む必要ないんじゃないの?」とトラブルの元となってしまうのである。

もう!ほんま Microsoft の設計思想、嫌い(^^;;; なんでこんな仕様にした?
先週、横川駅まで帰ったところで妙に焼き鳥が食べたくなって、横川商店街の「串吉 1号店」へ。

20260702_kushikichi1.jpeg
多分、「〇〇グループ」とか「◯◯カンパニー」みたいな多数飲食店を経営してる会社がやってる店のうちのひとつだろうなと今まで敬遠してたんだけど、ついつい入ってしまった(^^;;

もう、メニュー見ただけで「ああ、この店の経営者、一切酒に興味がないな」ってわかっちゃうあれね(笑)。

このブログでも時々触れてる「コンサルか酒問屋の言うとおりに並べた酒メニュー」ね。スーパーで売ってるような大手の安い酒ばかり並べたやつ。純米酒や吟醸酒なんかひとつもないアレです(笑)

ま、今日は焼き鳥食いたいだけだからねと割り切って、「焼き鳥おまかせ五本盛り(塩)」691円(お、安っ)と「アサヒスーパードライ中ジョッキ」493円(まあまあ安い)、それに「枝豆」218円(これも安っ)を注文。
枝豆はよく冷えていて、実は大きいし茹で具合もなかなかで、これが218円なのはお得だなあと思っていると、お待ちかねの焼き鳥が。

これがちょっとがっかりだった。なんか味付け薄いし(なので焦げの苦みばかりが感じられる)、ちゃんと焼いてはいたけど、なんか冷めてて焼きたて感がない。691円は安いと思ったが、まあ、これならもう100円安くてもいいんじゃない?と思った。
酒のラインナップをコンサルや酒問屋任せにする店の品質だな。いや、マジで酒好き(アル中じゃないぞ(笑))の店主の店は自然と良い酒と良い肴が集まるからな。

「そんなにこだわりがあるんなら、こういう店に来んなよ」って言うやつがいるけど、反対に「こういう店ってなに?」て聞きたいわ。
確かに世の中にはアル中相手の「呑めさえすれば良い」「安かろう、悪かろう」の店はあるけど、そういう店を目指してるの?だったらごめんなさい。でも、そうじゃないでしょ?

で、最後に事件は起きた。居酒屋で独り酒の時の俺は、「勘定が2千円超えたら負け」と思ってる。特にこういうろくな酒を置いてない店では。現在、俺の注文計は1,402円である。もう一杯いっても2千円は超えんな。勝った...と思った俺は、最後に「いいちこ」のお湯割りを注文した。399円。合計で1,801円。完璧だ。

しかし、ほどなく出てきたお湯割り。

・・・水やん(--;; 常温の水で割ってるだけやん・・・
最初は「えらいぬるめのお湯割りやな」と思ったけど・・・やっぱ水やん。

20260702_kushikichi2.jpeg
多分、この店では常温の水で割ったあとレンチンして「お湯割り」、氷を入れたら「水割り」なんだろう。で、レンチン忘れて出したと・・・

こういうところなんだよ。本当にちゃんとお酒を愛する人だったらありえないミスだよ。「お湯割りです」って出したところで、「全然あったかくない。あ、温めるの忘れてた」って気づくもんよ。

まあ、ちょっとは従業員教育もしてるんだろうけど、やっぱ、「お酒のことはよくわからない」「サワーしか飲まないんで」「俺はもう安い焼酎ばっかだね」みたいな人が店員をしていたら限界があるのよ。

黙って「常温のお湯割り」を飲み干して俺は店を出た。「これ、水じゃん」みたいなやり取りも面倒くさいからな。

この店の2号店が閉店したのは、この翌週のことであった。
Windows(てか、DOSって書いた方が正解?)のバッチファイル(BAT ファイル)を久しぶりに書かないといけなくなった。

つーか、「久しぶり」ってのがウソで、正直言うと自分で一から BAT ファイルを書いたことはほとんどない。そういう仕事を受けたことがないもんで。だいたい UNIX 系が多かったからなあ。

とりあえずメモしとく。

IF 文の ELSE は、必ず ") ELSE (" と書かないといけない。")" の後ろで改行して "ELSE" を書くとエラーとなる。
ELSE を独立した命令と判断し

'ELSE' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

なんてことに・・・

まあ、「先頭に英字の文言が出てきたら命令と判断する」って単純な仕様なんだろうが、それにしてもなんだかなあ・・・

俺はそもそも if と else を同じ面で合わせたいので、

if (hoge) {
    hogehoge;
}
else {
    monyomonyo;
}

という書き方派なのよ。多分、(特に UNIX 系のプログラマは)

if (hoge) {
    hogehoge;
} else {
    monyomonyo;
}

って書きたい派が多いと思うけど。(なんとなく、ネット上のソースなんかを見ると)

でも、bash スクリプトでも

if [[ "$a" == "y" ]]; then
    echo "あなたはやましろしんご";
else
    echo "あなたはやなぎさわしんご";
fi

って書くでしょ?やっぱ ELSE の面は IF と合わせたいのよね、生理的に(笑)
前から気になっていた本川町のビアバー「ビールと絵のお店 THE CONTE(ザ・コント)」に寄った。

19時まで土橋で作業して、19:30に飲み仲間のN◎君と十日市の電停のところで待ち合わせていたので、ビール一杯やって時間潰そうと思って(笑)

店主が画業もしているということで、イラストのついた小物なども置いてあるそうだ。
待ち合わせまで 20分ほどしかなかったので慌ててビールを注文。「スコットランドの IPA」を注文。

20260708_conte1.jpeg 20260708_conte2.jpeg

BREWDOG PUNK IPA。950円也。

「以前から酒屋でよく見る銘柄やな。スコットランドの IPA なんや。パンクと言えばイギリスだからな・・・って、製造、アサヒビールやん!」

ブリュードッグ・カンパニーがレシピ開発だけしてアサヒビールで作ってもらってるって感じ?まあ、美味い IPA だったので文句はないけど。

ツマミも 300円の安いやつから色々あるみたいだけど、待ち合わせの時間がせまってたのでこの日はビールのみ。
また今度ゆっくり訪ねてみよう。
4/8。ちょうど三ヶ月前の話。「ぬか漬けスタンド ニポテ」。前の訪問から一ヶ月経っていた。

20260408_nipote1.jpeg 20260408_nipote2.jpeg 20260408_nipote3.jpeg

この日、Mイさんとカープの巨人戦を観戦。もちろん良いとこ無しで負けた新井カープの反省会で訪れたのである。
新井監督がまったく反省をしないので、代わりにファンであるわしらが反省会をするのだ。たまったもんではないが仕方ない(^^;;;

カープのユニフォームで訪れた俺たちに「勝ちましたか?」と尋ねる店のお姉さんに、「当然負けました」と苦笑いで返すしかない俺たちなのだった。
Mイさんにとっては「ニポテ」初訪店の記念すべき日だったんだけど(^^;;;

結局、何を飲んだかは覚えてないんだけど、日本酒を2杯ほどとぬか漬け。「ぬかみそ煮込み」ははずせんよね(笑)

新井監督によって俺たちに課せられた「監督のトンデモ采配でしょうもない負け方をするチームを、それでも応援する」という重い使命を背負わされた俺たちを、日本酒とぬか漬けが優しく癒やしてくれるのであった。
昨日は近所にあるお好み焼き屋で晩飯を食べようと計画していたのだが、18時すぎに行ってみるとすでに一杯。二人ほど待ってる状態。こりゃダメだと諦めて横川駅裏に出撃した。
横川駅裏(北口?)にはたくさんの飲み屋があるが、2月に横川に部屋を借りてからあまり足を向けていなかったのである。

20260707_chobitto1.jpeg 20260707_chobitto2.jpeg

最初は「のんどる」という立ち呑み屋を探したのだが(一度飲み仲間のN◎と行ってみたことがあるが閉まっていた)、一度店の前まで行ったことがあるのにこの日は見つけられず(^^;;; ガード下に並ぶ店の中から「立ち呑みとお惣菜 ちょびっと」を訪店した。おばあちゃんと若いお兄ちゃんの二人で回している店のようだ。

「1000ベロ」セットがあったので注文。ドリンク1杯とお惣菜3種セットのコース1を選択。ドリンクは生ビールで。

いやあ、帰宅直後の汗だくの身体に、冷たいビールが染み渡るわぁ。ビールの温度管理は完璧である。

お惣菜3種セットは、すべて3つに区分された長皿に乗って出てくるので、量はまさに店名どおりの「ちょびっと」である(笑)
デブにはちょっと淋しい量だけど、これが美味い。さすがにお惣菜の店である。

俺が入店したのが18時15分頃だったがすでに立ち呑み客が二人。そういう「ちょびっとだけど美味しい惣菜」の店だからか、二人とも女性客であった。まあ、俺の隣の女性はガンガンおつまみのおかわりと日本酒をやってた強者だったけど(笑)

とりあえずこの日は「1000ベロ」だけで終了。
ただ日本酒も色々置いてるみたいなんで、「料理がどれも美味いですね。また来ます。今度は日本酒飲みに来ます」と店のお兄ちゃんに声をかけて店をあとにしたのであった。
先週末。ちょうど広島に居た嫁さんと娘が、職場の近くの段原ショッピングセンターまで車で迎えに来てくれたので、そのまま6階に上がって「サイゼリア 段原SC」で晩飯を摂っていくことにした。

で、当然俺は100円のグラスワイン(赤)を二杯、三杯と・・・(笑)。仕方ないよね。税込 100円のグラスワインがそこにあれば、注文せんわけにいかんでしょう?

20260703_escargot1.jpeg
そして、アテには久しぶりの「エスカルゴ」。
最近、某SNSのドゥカティコミュニティで一緒の人がよくカタツムリの画像を上げてて、「サイゼリアへの納品用ですか?」なんてコメントつけたら、なんかこのネタがプチ盛り上がりしちゃって(笑)

そりゃ、サイゼリアに来たら注文するしかないやろう(笑)

ただ、久しぶりに食べるとバターの味ばかり感じて、ニンニクのピリっとした刺激や塩気も全然足りない感じで・・・(いや、サイゼリア以外でエスカルゴ食ったことないんで正解の味はわからないんだけど(^^;;;)。始めて食ったときは(サイゼリアのエスカルゴだろうと)けっこう感動したんだけどな(笑)

俺の舌が年齢とともに劣化したのか(それはある(^^;;)、老いた身体がバターを拒絶するのか(それもある気がする(^^;;;)、ま、俺の舌が肥えたということにしておこう(笑)
Java 案件は何度もやったことはあるが、既存システムの修正などが主体だったので、そんなにJavaの言語仕様を深く理解はしていなかったのよね。

なもんで、今回初めて Java のソースで """ とダブルクォーテーションが3つ並んでるのを見て、「なんじゃこりゃ?」って(^^;;;。

君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで

これを文字列リテラルに突っ込むんだけど、

String msg = """
君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで
""";

となってて。

VB.NETのようにダブルクォーテーションをダブルクォーテーションでエスケープしているわけじゃないよな。それだと文の前後にダブルクォーテーションが付いちゃうしな(「"君が世は~むすまで"」ってなっちゃう)・・・と悩む。

実はこれ、文字列リテラルにそのまま複数行を突っ込める「テキストブロック(Text Blocks)」という機能。
これを使わないと、

String msg = "君が代は千代に八千代に\n"
           + "さざれ石の巌となりて\n"
           + "苔のむすまで";

こんな風に書くしかないのでごちゃごちゃ読みづらい。

Java の改修履歴を追ってみると、

Java SE 13(2019年9月17日)
 Text Blocks(文字列リテラルの複数行記述可能)第1回試作
Java SE 14(2020年3月17日)
 Text Blocks(文字列リテラルの複数行記述可能)第2回試作
Java SE 15(2020年9月15日)
 Text Blocks・第3回試作 ※これ以降、試作の記述なし

と、2020年にやっと正式な仕様として取り込まれたようだ。マジ?それまで Java って改行を含む複数行を一発で文字列リテラルに突っ込む方法ってなかったん?

俺の母国語(笑)である Perl なら、もう何十年も前からヒアドキュメントってやつで複数行を文字列リテラルに突っ込める。

my $msg = <<EOS;
君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで
EOS

こういうやつね。さらに言えばヒアドキュメント使わなくても単純にクォーテーション(シングルでもダブルでも)で囲ってやるだけでいい。

my $msg = '君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで';

VB.NETでも同じように(こっちはダブルクォーテーションね)、

Dim msg As String = "君が代は千代に八千代に
さざれ石の巌となりて
苔のむすまで"

って書ける。

ただ、一行目をクォーテーションのすぐ横から開始しないといけないので(改行しちゃうと改行がそのまま一行目として入っちゃうので)一行目の開始位置はずれる。これが嫌なら Perl ならやっぱヒアドキュメントがいいか。

今知ったんだけど、VB.NETも 2015年リースの VB 14.0 から可能になったんだなあ。そういえば、その前は & でつなげて書いてたっけ?

Dim msg As String = "君が代は千代に八千代に" + vbCrLF +
                    "さざれ石の巌となりて" + vbCrLF +
                    "苔のむすまで"

みたいに?(もう記憶が・・・)

まあ、なんにせよ、Java でこういう書き方ができるようになったのが 2020年からとは本当に意外だ。
Java が母国語の人って、こういう要件ってあまりなかったのかね?
プログラムの中にあまり HTML を直書きしたりとか、そういうのが無かったのかもね。
今年の 2月に愛機 DUCATI MONSTER S2R 1000 にオイル漏れ???という疑いが発生(「S2R のフロントシリンダーの付け根あたりがオイルでテカテカ」というエントリー参照)。

すぐに高校の同級生が働いている整備工場に持ち込んだのだが、「冬はバイクに乗らんじゃろ?」という間違った判断のもと、一ヶ月くらい放置。二週続けて「もう原因わかったか?早よ乗りたいんじゃけど」と顔を出して、やっと「え?冬なのにバイク乗るん?」と調査をしてくれた(^^;;; 冬でも夏でもオートバイ乗るわっ!雪が積もる地方じゃないんじゃけ。

20260320_kannon1.jpeg 20260320_kannon2.jpeg 20260320_kannon3.jpeg

結果、「経年劣化でブリーザーバルブが割れてそこから漏れたオイルがエンジン前部まで流れて、シリンダーから漏れてるように見えたんじゃろう」とのこと。
で、状況みようとしてバルブを外そうとしたら、完全に固着してて、しかも本体が経年劣化で脆くなってて、上下にぱっくり割れたらしい(^^;;;
とりあえず耐熱ボンドで割れたところ固めてるって(笑)

「交換するんなら無理やり外すけど、どうしよう?部品代だけで1万数千円するけど」ってことだったので、「とりあえずこのままで良い」と答え、S2Rが返ってきた 3/20(祝)。確認のショートツーリングへ(笑)

行き先は美川町の「観音茶屋自販機コーナー」。自販機の肉うどん食べながらエンジンチェック。全然オイルで濡れてない!ブリーザーバルブ周りも大丈夫。耐熱ボンド、強いなあ(笑)

そういえば、オイル漏れが発覚したのが自販機うどんで有名な「欽明路自販機コーナー」で、そのオイル漏れの解消を確認したのがやはり自販機うどんで有名な「観音茶屋自販機コーナー」。どんだけ自販機うどんが好きなんやって話やね(笑)

というわけで、樹脂部品はやっぱり国産バイクに比べると弱いなあって感じはするんだけど、色々対応方法はあるからね。今日も俺のドゥカティは元気に走ってます(笑)
もう 3月下旬頃の話だけど・・・

20260319_tsubasa2.jpeg
広島のサッカースタジアム「EDION PEACE WING HIROSHIMA」の一階にある「廣島汁なし担担麺 -TSUBASA- 2nd」で汁なし担々麺を食べた(この店では「担担麺」と表記するようだ)
船越の方に「つばさ」って美味いラーメン屋あったけど、あれとは関係ないんよね?(笑)

実はピースウィング内に汁なし担々麺屋があるらしいということは知っていたのだが、通勤時に二階コンコースを歩きながら(自由に歩ける)店を探したのだが見つけることが出来てなかった。
ついつい、ズムスタのイメージがあるんで、二階コンコースに店が集中してるのかと思ってたのよね。一階でした。

さすがサッカースタジアムにあるお店。汁なし担々麺の店にしては店内も小綺麗な感じ。
時間は晩飯にはちょっと早めの夕方 5時頃だったので、俺の他に客はサラリーマン風の若い兄ちゃん(イケメン)と、あとは若い女子二人連れ。なんか、おっさんが若干浮く感じがしたんだけど、その後俺くらいの、しかもぽっちゃり小太りのおっさんが入ってきたのでホッとした(笑)

20260319_tsubasa1.jpeg
汁なし担々麺は見た目も味も上品な感じかな。

サンフレッチェをイメージしたのであろう紫キャベツ(赤キャベツ)の千切りがかかっていて、サンフレファンにはたまらん見た目やね(笑)
そして、他県からサンフレの応援に来て「なんか、広島じゃ汁なし担々麺てのが有名らしい」って食べて行くにはちょうど良い辛さと痺れ。多分、汁なし担々麺が好きになると思う。

・・・が、荒々しい辛さと痺れをもとめ、「武蔵坊」では辛さランク「death(デス)」を、「階杉」では「Yoga Inferno(ヨガインフェルノ)」を選ぶ俺にはちょっと物足りなさもあったのである(笑)
さて、無事に Oracle Database 21c Express Edition (XE) のインストールが終了

具体的にテスト用(お勉強用)の TABLE などを作っていきたいが、Windows 上で CUI 環境の sqlplus でゴリゴリやるのもなあ・・・いや、実際、UNIX 互換界隈では PostgreSQL を CUI 環境の psql とかでゴリゴリやってるんだけど、あれは Putty とか TeraTerm のようなターミナルソフトの上で zsh といったシェルを操作するから成り立っているのであって、Windows の Terminal の上で(PowerShell であろうと)CUI 環境のソフトを使うのは苦行でしかない(^^;;;

Oracle が提供している DB管理の GUI 環境「Oracle SQL Developer」は無償で使えるが、Oracle
アカウントを作る必要があり面倒くさいので(だって、この案件終わったら Oracle 案件はもう無い気がするし(笑))、みんな使ってるフリーの高機能 SQL クライアント「A5:SQL Mk-2」を使うことにした。

■ダウンロード&インストール

インストーラはないので、ダウンロードしてきたファイルを解凍して適当な場所に配置するだけ。

俺は Vector からダウンロードした。


a5m2_2.21.2_x64.zip がダウンロードされるので、それを解凍し、どこか適当なフォルダ(ディレクトリ)に置けば OK。


■接続情報の登録

A5M2.exe をダブルクリックして実行。
「データベース」→「データベースの追加と削除」で、「Oracle Database (OCI経由 or 直接接続)」を選択。

・接続方法 直接接続(Oracle Client不要)
・ホスト名 localhost
・サービス名 XEPDB1
・ユーザーID tester
・パスワード tester01

と指定して「テスト接続」を押し「接続に成功しました」となれば問題なし。「OK」押下で登録名を聞かれるのでわかりやすい名前を。ま、あとでリネームできるけど。
接続に成功しなかったら・・・何か typo しているので頑張って(笑)

20260701_A5_02.jpg

※ちなみに、このエントリーは A5:SQL Mk-2 の使い方を説明するものではないので、接続後にカレントスキーマを選んで TABLE を作成したりとかは自分で勉強してね。


■テーブルを作ってデータを登録

問題なく行えた。

20260701_A5_03.jpg

これで Spring Batch で DB 読込のテストができる。
勉強用に Oracle Database 21c Express Edition (XE) をインストールする。

■ダウンロード

 ↓

「Oracle Database 21c Express Edition for Windows (64-bit)」をクリックしてダウンロード開始。
(実際の業務では 19c を使うが、19c には Express Edition(XE) がない。あるのは 18c、21c だけ。19c スキップされている。19c の無償版のダウンロードは Oracle アカウントが必要だったり、色々面倒臭いので 21c(XE) で(笑))


■インストール

ダウンロードした OracleXE213_Win64.zip を解凍。

setup.exe と Oracle Database 21c Express Edition.msi があって悩むが、setup.exe 実行でいいよな?setup.exe ダブルクリックで実行。

けっこう時間がかかる。
ディフォルトの設定を変更することなく「次へ」「次へ」と進んでいったので、途中の画面画像は省略。

データベースパスワードは test@pass01 にする。(途中で @ マークを使っていることが後ほど面倒臭いことになるのをまだ俺は知らなかった(^^;)

20260701_oracle04.jpg

インストール完了。


■データベースへの接続

PS C:\Users\TESTER> sqlplus

SQL*Plus: Release 21.0.0.0.0 - Production on 水 7月 1 10:35:14 2026
Version 21.3.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.

ユーザー名を入力してください: system
パスワードを入力してください: test@pass01@localhost:1521/XEPDB1<実際は表示されない>
ERROR: ORA-12154: TNS: 指定された接続識別子を解決できませんでした

パスワードに @ マークを含む場合は、パスワードをダブルクォーテーションで囲ってやる必要がある。
@ なんか使わなきゃよかった。面倒くせえ(^^;

ユーザー名を入力してください: system
パスワードを入力してください: "test@pass01"@localhost:1521/XEPDB1<実際は表示されない>
最終正常ログイン時間: 水 7月  01 2026 10:16:04 +09:00


Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
に接続されました。
SQL>

無事接続されたので、test 用の表領域(テーブルスペース)を作成しよう。


■スキーマの作成

SQL> CREATE TABLESPACE test_space
  2   DATAFILE 'C:\app\data\test.dbf' SIZE 100M
  3   AUTOEXTEND ON NEXT 500K MAXSIZE 1024M;
CREATE TABLESPACE test_space
*
行1でエラーが発生しました。:
ORA-01119: データベース・ファイル'C:\app\data\test.dbf'の作成中にエラーが発生しました。
ORA-27040: ファイルの作成エラー、ファイルを作成できません
OSD-04002: ?t?@?C????[?v??????????S-Error: (OS 3)
?w?????p?X?????????????

あれ?失敗する。権限の問題?


■(ちょっと小休止)特殊な文字の入ってるパスワードは面倒なのでやめる

まだ、TABLESPACE  の作成が終わってないけど、その前に system ユーザのパスワードに @ マークが入っているので、うまく sqlplus で DB 接続できない。

Gemini に「ダブルクォーテーションでパスワードを囲まなきゃ」と言われたんだが、

sqlplus system/\"test@pass01\"@localhost:1521/XEPDB1
qlplus system/"test@pass01"@localhost:1521/XEPDB1

どちらも

ERROR: ORA-12154: TNS: 指定された接続識別子を解決できませんでした

になっちゃうなぁ(実際はダブルクォーテーションではなくシングルクォーテーションで囲まないとダメだったことが後ほどわかるんだけど(^^;;;)

ああ、面倒くせえ。@ マーク入りのパスワードは修正したほうがよさそうだ。

というわけで、system ユーザのパスワード変更。

ローカル(同一サーバー)での OS 認証で接続し、パスワード変更の QUERY を実行する。

PS C:\Users\TESTER> sqlplus / as sysdba

SQL*Plus: Release 21.0.0.0.0 - Production on 水 7月 1 11:23:38 2026
Version 21.3.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.



Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
に接続されました。
SQL> ALTER USER SYSTEM IDENTIFIED BY testpass01;

ユーザーが変更されました。

SQL> ALTER USER SYS IDENTIFIED BY testpass01;

ユーザーが変更されました。

SQL>


パスワードは問題なく変更された様子。
別の Termnal を開いて、試しに接続してみるか。

PS C:\Users\TESTER> sqlplus system/testpass01@localhost:1521/XEPDB1

SQL*Plus: Release 21.0.0.0.0 - Production on 水 7月 1 11:29:13 2026
Version 21.3.0.0.0

Copyright (c) 1982, 2021, Oracle.  All rights reserved.

最終正常ログイン時間: 水 7月  01 2026 10:40:00 +09:00


Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
に接続されました。
SQL>

行けた・・・やっぱり、パスワードに @ なんか使うもんじゃないね(笑)


■スキーマの作成(再び)

SQL> CREATE TABLESPACE test_space
  2   DATAFILE 'C:\app\data\test.dbf' SIZE 100M
  3   AUTOEXTEND ON NEXT 500K MAXSIZE 1024M;
CREATE TABLESPACE test_space
*
行1でエラーが発生しました。:
ORA-01119: データベース・ファイル'C:\app\data\test.dbf'の作成中にエラーが発生しました。
ORA-27040: ファイルの作成エラー、ファイルを作成できません
OSD-04002: ?t?@?C????[?v??????????S-Error: (OS 3)
?w?????p?X?????????????

ダメか。

あっ、もしかしてディレクトリは先に作成しておかないといけないの?C:\app\data ディレクトリを作って再び CREATE TABLESPACE を実行。

SQL> CREATE TABLESPACE test_space
  2   DATAFILE 'C:\app\data\test.dbf' SIZE 100M><\span>
  3   AUTOEXTEND ON NEXT 500K MAXSIZE 1024M;

表領域が作成されました。

20260701_oracle09.jpg

テーブルスペースはできた。(しかし、エラーメッセージの文字化け化けなんなんかね?すごいことが起きてると思って、単純にディレクトリを手動で作っておかないといけないだけってところに思考がいかなかったよ(^^;;;)

続けてユーザ作成。(データベースの作成もできる DBA 権限を付与)

SQL> CREATE USER tester
  2   IDENTIFIED BY tester01
  3   DEFAULT TABLESPACE test_space
  4   TEMPORARY TABLESPACE TEMP
  5   PROFILE DEFAULT;

ユーザーが作成されました。

SQL> GRANT DBA TO tester;

権限付与が成功しました。


ばっちり。
これで初期インストール作業は終了。
「どんどんレコード(行)が追加されていく複数のテキストデータを一定間隔(1秒)で読んで、追加されたレコードをDBに登録していく」バッチ処理(Spring Batch 6)の話の最後。

一気に、実際の実行部のソースを貼っておきます。
いや、俺も勉強中でしっかり中身が理解できているわけではないので(^^;;;
でも、とりあえず「ネットに公開されているソースをパチって実際に動かして内容を理解したい」って人もいるでしょ?(俺はもろにそっち系(笑))

■バッチ処理全体構成・設定クラス(BatchConfig.java)※Bean 定義クラス

package com.netandfield.test;

import jakarta.persistence.EntityManagerFactory;

import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.Step;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.batch.infrastructure.item.ItemProcessor;
import org.springframework.batch.infrastructure.item.database.JpaItemWriter;
import org.springframework.batch.infrastructure.item.database.builder.JpaItemWriterBuilder;
import org.springframework.batch.infrastructure.item.file.FlatFileItemReader;
import org.springframework.batch.infrastructure.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.infrastructure.item.file.transform.FixedLengthTokenizer;
import org.springframework.batch.infrastructure.item.file.transform.Range;
import org.springframework.batch.infrastructure.item.support.SynchronizedItemStreamReader;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class BatchConfig {

    @Bean
    public Job fileProcessJob(JobRepository jobRepository, Step fileProcessStep) {
        return new JobBuilder("fileProcessJob", jobRepository)
                .start(fileProcessStep)
                .build();
    }

    @Bean
    public Step fileProcessStep(JobRepository jobRepository,
                                PlatformTransactionManager transactionManager,
                                SynchronizedItemStreamReader<ProcessedData> synchronizedReader,
                                ItemProcessor<ProcessedData, ProcessedData> processor,
                                JpaItemWriter<ProcessedData> jpaWriter,
                                StepProgressUpdateListener progressListener) {
        return new StepBuilder("fileProcessStep", jobRepository)
                .<ProcessedData, ProcessedData>chunk(100)
                .transactionManager(transactionManager)
                .reader(synchronizedReader)
                .processor(processor)
                .writer(jpaWriter)
                .listener(progressListener) // リスナーをここで登録!
                .build();
    }

    @Bean
    @StepScope
    public SynchronizedItemStreamReader<ProcessedData> synchronizedReader(
            @Value("#{jobParameters['filePath']}") String filePath,
            @Value("#{jobParameters['linesToSkip']}") Long linesToSkip) {

        FixedLengthTokenizer tokenizer = new FixedLengthTokenizer();
        tokenizer.setColumns(new Range(1, 4), new Range(5, 24), new Range(25, 27));
        tokenizer.setNames("code", "name", "value");

        FlatFileItemReader<ProcessedData> delegate = new FlatFileItemReaderBuilder<ProcessedData>()
                .name("flatFileItemReader")
                .resource(new FileSystemResource(filePath))
                .linesToSkip(linesToSkip.intValue())
                .lineMapper((line, lineNumber) -> {
                    var fieldSet = tokenizer.tokenize(line);
                    ProcessedData data = new ProcessedData();
                    data.setCode(fieldSet.readString("code").trim());
                    data.setName(fieldSet.readString("name").trim());
                    data.setValue(fieldSet.readInt("value"));
                    return data;
                })
                .build();

        return new SynchronizedItemStreamReader<>(delegate);
    }

    @Bean
    public ItemProcessor<ProcessedData, ProcessedData> processor() {
        return item -> {
            item.setName(item.getName().toUpperCase()); // Name を大文字に変換する
            return item;
        };
    }

    @Bean
    public JpaItemWriter<ProcessedData> jpaWriter(EntityManagerFactory entityManagerFactory) {
        return new JpaItemWriterBuilder<ProcessedData>()
                .entityManagerFactory(entityManagerFactory)
                .build();
    }
}


■スケジューラ(FileWatchScheduler.java)※ジョブを定期的に実行する Bean

package com.netandfield.test;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.List;

import org.springframework.batch.core.job.Job;
import org.springframework.batch.core.job.parameters.JobParameters;
import org.springframework.batch.core.job.parameters.JobParametersBuilder;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class FileWatchScheduler {

    // Spring Batch 6 では、JobLauncher の役割を内包した JobOperator を使用
    private final JobOperator jobOperator;
    private final Job fileProcessJob;
    private final FileProgressRepository progressRepository;

    private final AppProperties appProperties;

    @Scheduled(fixedDelay = 1000)
    public void runPeriodicBatchJob() {
        Path listFile = Paths.get(appProperties.getFilesListPath());
        if (!Files.exists(listFile)) {
            log.warn("ファイル一覧が見つかりません: {}", appProperties.getFilesListPath());
            return;
        }

        try {
            List<String> targetFilePaths = Files.readAllLines(listFile);
            for (String filePath : targetFilePaths) {
                if (filePath.trim().isEmpty() || !Files.exists(Paths.get(filePath))) {
                    continue;
                }
                processSingleFile(filePath.trim());
            }
        } catch (IOException e) {
            log.error("ファイル一覧の読み込みに失敗しました", e);
        }
    }

    private void processSingleFile(String filePath) {
        // 1. 進捗管理DBから前回の行数を取得
        FileProgress progress = progressRepository.findById(filePath)
                .orElseGet(() -> {
                    FileProgress newProgress = new FileProgress();
                    newProgress.setFilePath(filePath);
                    newProgress.setLastReadLines(0);
                    return progressRepository.save(newProgress);
                });

        try {
            // 2. パラメータの組み立て
            JobParameters jobParameters = new JobParametersBuilder()
                    .addString("filePath", filePath)
                    .addLong("linesToSkip", (long) progress.getLastReadLines())
                    .addLocalDateTime("runTime", LocalDateTime.now()) // 毎回新規実行にするため
                    .toJobParameters();

            // 3. [Spring Batch 6 正攻法]
            // JobOperator が JobLauncher を継承し、run メソッドを直接安全に呼び出せる(警告なし)
            jobOperator.run(fileProcessJob, jobParameters);

            // ※進捗(行数)の更新処理は、バッチのライフサイクルに則り「StepProgressUpdateListener」が自動的に実行

        } catch (Exception e) {
            log.error("ファイル [{}] のバッチ処理中にエラーが発生しました", filePath, e);
        }
    }
}


■進捗(行数)更新処理クラス(StepProgressUpdateListener.java)※もちろん Bean

package com.netandfield.test;

import org.springframework.batch.core.ExitStatus;
import org.springframework.batch.core.listener.StepExecutionListener;
import org.springframework.batch.core.step.StepExecution;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@RequiredArgsConstructor
@Slf4j
public class StepProgressUpdateListener implements StepExecutionListener {

    private final FileProgressRepository progressRepository;

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        // ステップが正常終了した場合のみ、進捗を更新する
        if (stepExecution.getExitStatus().equals(ExitStatus.COMPLETED)) {
            // 起動パラメータからファイルパスを取得
            String filePath = stepExecution.getJobParameters().getString("filePath");
            long newlyRead = stepExecution.getReadCount();

            if (filePath != null && newlyRead > 0) {
                FileProgress progress = progressRepository.findById(filePath)
                        .orElseGet(() -> {
                            FileProgress newProgress = new FileProgress();
                            newProgress.setFilePath(filePath);
                            newProgress.setLastReadLines(0);
                            return newProgress;
                        });

                progress.setLastReadLines(progress.getLastReadLines() + (int) newlyRead);
                progressRepository.save(progress);
                log.info("ファイル [{}] の進捗を更新しました。新たに {} 行処理、累計 {} 行。",
                        filePath, newlyRead, progress.getLastReadLines());
            }
        }
        return stepExecution.getExitStatus();
    }
}

「Spring Batch 6 、だんだん増えていく固定長レコードを処理する」の(1)~(3)に載せているソースを使って新しいプロジェクトを作れば、Spring Batch 6 で、「だんだん増えていく固定長レコード」が処理できます(笑)

Spring Batch は ver 6 大きくライブラリ構成などが変わっちゃったり、古い機能が完全に削除されたりしてるので、2026年6月現在の最新の Eclipse Pleiades All in One版を落としてきて Spring Batch の勉強をしようとすると、ネット上に公開されている色々なサンプルソースではまず動かない(^^;;;

AI に「Spring Batch 6 で動かすにはどうすればいいの?」と聞いても、3 やら 5 やらのソースばかり返してくるのよね(^^;;; AI はネット上の情報をかき集めて自分の知識としてるんだけど、そもそもネット上に Spring Batch 6 に対応したソースなどの情報が少ないので、AI 自身その関係の知識がないのよね(笑)

でも、このエントリーのソースなら動きます(笑)
さあ、実際に動かして、俺に色々教えてくれたまえ。

周回レースの集計データの処理を想定した「どんどんレコード(行)が追加されていく複数のテキストデータを一定間隔(1秒)で読んで、追加されたレコードをDBに登録していく」バッチ処理(Spring Batch 6)の話のつづき

ちなみに(1)で「仕様」に書き忘れてたけど、データベースへのアクセスは JDBC ではなく JPA を使っています。
俺的には全然 SQL は苦手ではないし、もっと言えば複雑な SQL を書くのは好きな方ですが(笑)、一昨年から昨年にかけて C#.NET で「DB操作は LINQ」って案件やって、ああ、糞っ、直接生の SQL 書けば一発なのにと苦労した記憶があるので(^^;;;、敢えて JPA を選んでみたわけです。勉強のために。

※LINQ でも直接 SQL 書けるじゃんってツッコミは無しで願います。言語仕様の話ではなく、コーディングルールで禁止されていたということなのよ。

ま、とういうわけで、今回はプロパティファイルや、テーブルの設定などを。

■Apache Maven プロジェクト設定ファイル(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.1.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>BatchTest5</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>BatchTest5</name>
    <description/>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-h2console</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                    <execution>
                        <id>default-testCompile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>testCompile</goal>
                        </goals>
                        <configuration>
                            <annotationProcessorPaths>
                                <path>
                                    <groupId>org.projectlombok</groupId>
                                    <artifactId>lombok</artifactId>
                                </path>
                            </annotationProcessorPaths>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

他のエントリーでも書いたけど、バッチ処理だけど H2 コンソールを動かすために tomcat が必要なので、spring-boot-starter-webmvc モジュールが依存関係(dependency)として登録されていること。


■プロパティ(resource/application.properties)

spring.application.name=BatchTest5

# ファイル一覧
app.files-list-path=C:\\work\\files.txt

# H2 を PostgreSQL 互換モードで使用(テーブル名等は小文字で)
spring.datasource.url=jdbc:h2:./.data/h2/db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

# H2 Console を有効化
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA/Hibernate設定
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.batch.job.enabled=false

# Webアプリケーションとして常駐
spring.main.web-application-type=servlet

特に注意するのは
spring.batch.job.enabled=false
かな。
これは起動時に自動でバッチジョブを実行しないようにしている。

Spring Boot はバッチ用のライブラリ(spring-boot-starter-batch)を検知すると、定義されている @Bean のジョブを起動時にすべて片っ端から実行しようとしてしまう。今回はスケジューラでパラメータ(読み込むファイル名とか)を与えてジョブを起動する形にしているから、勝手にジョブを起動されちゃうと「パラメータが無い状態」の実行となりエラーが発生する。なので「自動で起動すんなよ」と抑制しているわけやね。

あと、app.files-list-path は「'app.files-list-path' is an unknown property. [PROP_UNKNOWN_PROPERTY]」という警告が出るので、プロパティを認識させるためのメタデータを作ってやる。


■メタデータファイル(resource/META-INF/additional-spring-configuration-metadata.json)

{"properties": [{
  "name": "app.files-list-path",
  "type": "java.lang.String",
  "description": "A description for 'app.files-list-path'"
}]}


■Spring Boot メインクラス(BatchTest5Application.java)

package com.netandfield.test;

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

@SpringBootApplication
@EnableScheduling
public class BatchTest5Application {
    public static void main(String[] args) {
        SpringApplication.run(BatchTest5Application.class, args);
    }
}

Eclipse で Spring Batch のバッチを実行するときは、このファイルを Java アプリケーションとして実行する。


■DBマッピングクラス(FileProgress.java)
 ※各ファイルが何行目まで読まれているかを保持するテーブル(file_progress)

package com.netandfield.test;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import lombok.Data;

@Entity
@Table(name = "file_progress")
@Data
// 各ファイルの読み込み行数を管理するエンティティ
public class FileProgress {
    @Id
    private String filePath; // ファイルのフルパス
    private int lastReadLines; // 前回までに読み込み完了した行数
}

20260630_e2_01.jpg


■DBマッピングクラス(ProcessedData.java)
 ※読み込まれたデータが保存されるテーブル(processed_data)

package com.netandfield.test;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import lombok.Data;

@Entity
@Table(name = "processed_data")
@Data
// データを保存するエンティティ
public class ProcessedData {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String code;
    private String name;
    // データベース上の列名を「data_value」に退避させる(value は予約語なので)
    @Column(name = "data_value")
    private Integer value;
}

20260630_e2_02.jpg

id は、@Id アノテーションで主キーに設定され、データベースの Auto Increment機能を利用して自動採番されている。
id が 1~5までは続いて、急に 33に飛んでいるのは、DBに採番を依頼するとき、ある程度まとめて採番してもらっているから。

今回は、一度に 32個の ID を発行し、読み込んだデータに順に割り当てている。足らなければ再度採番依頼をするが、今回は最初の処理では 5件しかデータが無かったので、5番まで使って 6~32は捨てている。
そして、ファイルに新しい行が追加されたので次の処理が実行されるが、この時に前回の続きで、「33~64までの32個」のIDを採番し先頭から新しいレコードの id に割り当てたというわけである。

せっかく採番した ID が捨てられるのはもったいないということなら、この同時採番される数を 1 にしても良いが、当然 1レコードずつ採番要求が発生するので処理スピードは落ちる。


■FileProgressクラスのインタフェース(FileProgressRepository.java)

package com.netandfield.test;

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

public interface FileProgressRepository extends
JpaRepository<FileProgress, String> {
}

org.springframework.data.jpa.repository.JpaRepository を継承しており、findById や save といったメソッドが実行可能に。


ふう。Gemini に聞きながら、何とか以下のような仕様のバッチ処理完成。

■仕様(簡単に)

・Spring Batch 6(Spring Boot 4)で実行されるバッチ
・DBはとりあえず H2で
・複数の固定長ファイルを読み込み、項目ごとに分割しDBに登録する
・複数の固定長ファイルのパスはひとつのファイル(files.txt)に記述されている
・固定長ファイルのレコード数は追加されていく
・1秒ごとにファイルを全てチェックし、レコード数が増えていれば増えたレコードを読み込む

例えばどんどん新しい行(ラップタイム)がレース終了まで追加されていく周回レースの集計データの処理などを想定している。
(ゴールだけではなく、周回するコースの途中に設置された複数の計測機器(中間計測機)からのデータ(それぞれ別のネットワークストレージ上に計測データを吐いている(ただし、バッチ処理を行うPCには共有されている))を一括でDB登録するイメージ)


■処理対象のファイル一覧ファイル(C:\work\files.txt)

C:\work\test1.txt
C:\work\test\test2.txt


■データファイル1(C:\work\test1.txt)

A001SHINODA Masanori    12
A002SHINODA Nyunyumi    18

あとで、
C023Shiranui Shinobu    123
が追加される(手動で(笑))


■データファイル2(C:\work\test\test2.txt)

B001Tanaka Ichiro       68
B122Yamamoto Ahomaru    48
B245Yamamoto Murishi    47

あとで、
D100Shinoda Gengoromaru 5
が追加される(手動で(笑))


■データの構成(固定長)

ユーザID(code)... 1~4桁目(4桁)
ユーザ名(name)... 5~24桁目(20桁)
年齢(value)... 25~27桁(3桁)
※テーブルの項目名は data_value(value は H2データベースの予約語のため)


実際にこのようなデータを準備し、作成したバッチ処理を実行すると、DBにデータが登録される。
H2 コンソールで確認すると、最初の 5件+追加の 2件の計 7件のデータが登録されているのがわかる。

20260630_e2_02.jpg

自動採番している id が途中で飛んでいることや、データは大文字小文字混在だった name が大文字だけになっている点などは、またソースの説明のときに・・・

このアーカイブについて

このページには、2026年7月に書かれたブログ記事が新しい順に公開されています。

前のアーカイブは2026年6月です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

月別 アーカイブ

電気ウナギ的○○ mobile ver.

携帯版「電気ウナギ的○○」はこちら