2026年7月アーカイブ

職場の近くに新しいラーメン屋ができた。

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 環境のソフトを使うのは苦行でしかない(^^;;;

Qracle が提供している 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.

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