目次
概要
本記事は、クラウドで提供されるリソースをDocker内に作成しバッチ開発を行う一連について記載しております。
クラウドと連携が必要な機能についても、Dockerを使うことでローカルでの開発や動作確認等を容易にすることができます。
今回はファイルの連携先に、GoogleCloudStorageのエミュレータである、fake-gcs-serverを利用しております。
ソースコードだけ見たい方はこちらから。
※AWSをご利用の方は、LocalStackというものもあるので、そちらで代用は可能そうです。
※GoogleCloudStorageは、以降は「GCS」と略しております。
環境
- Kotlin 1.6.21
- SpringBoot 3.0.1
※ SpringBatchを使用したかったが、思いのほか大変であったため、今回はCommandLineRunnerで代用しております。 - Docker 20.10.21
- Docker-Compose 2.13.0
処理内容
今回作成したバッチの大まかな処理内容です。
バッチ処理に必要なMySQLとGCSは、Dockerで用意しております。
※取込ファイルについては、CKANで提供されている、福岡市の小学校給食の献立を借用しております。
取込ファイルの内容は、以下のようなレイアウトなっている。
故に今回取り込みを行う対象は、ヘッダー行を除いた62032行が対象となる。
ma7k5@DESKTOP-451NI4R MINGW64 ~/Projects/IdeaProjects/loader-example/config/gcp/cloud-storage/data
$ file -i 202102kondate.csv
202102kondate.csv: text/csv; charset=utf-8
ma7k5@DESKTOP-451NI4R MINGW64 ~/Projects/IdeaProjects/loader-example/config/gcp/cloud-storage/data
$ wc -l 202102kondate.csv
62033 202102kondate.csv
ma7k5@DESKTOP-451NI4R MINGW64 ~/Projects/IdeaProjects/loader-example/config/gcp/cloud-storage/data
$ head -n 3 202102kondate.csv
都道府県コード又は市区町村コード,NO,都道府県名,市区町村名,コース名,学校名,献立日,献立番号,料理名,明細番号,食品区分,食品名
401307,2102000001,福岡県,福岡市,イ,香住丘小学校,2021-02-01,1,牛乳,1,おもな材料,牛乳
401307,2102000002,福岡県,福岡市,イ,香住丘小学校,2021-02-01,2,麦ご飯,1,おもな材料,精白米
設定内容
docker-compose.yml
version: "3"
services:
mysql: # データベース
container_name: test-db
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
MYSQL_USER: test
MYSQL_PASSWORD: test
LANG: C.UTF-8
LANGUAGE: ja_JP
TZ: 'Asia/Tokyo'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
volumes:
- ./config/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./config/mysql/sql:/docker-entrypoint-initdb.d
ports:
- "3306:3306"
flyway: # テーブル作成及び初期データ投入用
container_name: test-flyway
image: boxfuse/flyway:5.1.4
depends_on:
- mysql
volumes:
- ./config/flyway/conf:/flyway/conf
- ./config/flyway/migration:/flyway/sql
entrypoint: >
/bin/sh -c "
until (/flyway/flyway clean) do echo 'waiting ...' & sleep 1; done;
/flyway/flyway migrate;
exit $$?;
"
cloud-storage: # GCS用
container_name: test-cloud-storage
image: fsouza/fake-gcs-server:1.43.0
tty: true
ports:
- "4443:4443"
volumes:
- ./config/gcp/cloud-storage/data:/data/test
- ./config/gcp/cloud-storage/storage:/storage
command: -scheme http -public-host ${URL:-localhost}:4443
volumes:
database:
driver: local
- MySQLの設定
MySQL5.6以降から、セキュリティの問題でloose-local-infile=1とallowLoadLocalInfile=trueの指定が必要になった。
指定がないとエラーになるので注意が必要です。
local.env
DATABASE_URL=jdbc:mysql://localhost:3306/test?allowLoadLocalInfile=true
DATABASE_USERNAME=test
DATABASE_PASSWORD=test
GCP_STORAGE_URL=http://0.0.0.0:4443
GCP_PROJECT_ID=test
GCP_BUCKET_NAME=test
GCP_DOWNLOAD_PATH=C:/test/download
application.yml
spring:
application:
name: Loader-Example
main:
banner-mode: off
log-startup-info: false
web-application-type: none # ここが無効化の設定
... 中略
my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
[client]
default-character-set=utf8mb4
loose-local-infile=1
- DDL
CREATE TABLE TBL_SCHOOL_LUNCH (
PUBLIC_ORGANIZATION_CD CHAR(6) NOT NULL COMMENT '全国地方公共団体コード',
ID CHAR(10) NOT NULL COMMENT 'ID',
PREFECTURE_NAME VARCHAR(3) NOT NULL COMMENT '都道府県名',
CITY_NAME VARCHAR(3) NOT NULL COMMENT '市区町村名',
COURSE_NAME CHAR(1) NOT NULL COMMENT 'コース名',
SCHOOL_NAME VARCHAR(20) NOT NULL COMMENT '学校名',
MENU_DATE DATE NOT NULL COMMENT '献立日',
MENU_NO INT NOT NULL COMMENT '献立番号',
DISH_NAME VARCHAR(100) COMMENT '料理名',
ITEM_NO INT NOT NULL COMMENT '明細番号',
FOOD_CATEGORY VARCHAR(100) NOT NULL COMMENT '食品区分',
FOOD_NAME VARCHAR(100) NOT NULL COMMENT '食品名',
CREATED_BY VARCHAR(100) NOT NULL COMMENT '作成者',
CREATED_AT DATETIME NOT NULL COMMENT '作成日時' DEFAULT CURRENT_TIMESTAMP,
UPDATED_BY VARCHAR(100) COMMENT '更新者',
UPDATED_AT DATETIME COMMENT '更新日時' DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(ID)
) ENGINE = InnoDB, COMMENT '給食献立管理テーブル';
実装内容
- JobRunner.kt
コマンドライン引数で渡されるジョブIDから、application.ymlに指定されているクラスにリフレクションしてメイン処理を実行している。
詳細を確認したい方は、GitHubのソースコードをご確認お願い致します。
JobRunner.kt
package com.example.loaderexample
import com.example.loaderexample.constant.JobProperty
import com.example.loaderexample.constant.JobStatus
import com.example.loaderexample.job.JobExecutor
import com.example.loaderexample.util.ConfigUtil
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.config.AutowireCapableBeanFactory
import org.springframework.boot.CommandLineRunner
import org.springframework.stereotype.Component
import java.io.IOException
import kotlin.system.exitProcess
@Component
class JobRunner(
private val autowiredCapableBeanFactory: AutowireCapableBeanFactory,
private val jobProperty: JobProperty,
private val configUtil: ConfigUtil
): CommandLineRunner {
companion object {
private val logger = LoggerFactory.getLogger(this::class.java)
private const val DEFAULT_JOB_ID = "BE-BATCH000"
}
override fun run(vararg args: String?) {
var jobId = ""
var jobName = ""
var status = JobStatus.FAILED
try {
// パラメータ設定
jobId = args[0]?.trim() ?: DEFAULT_JOB_ID
jobName = configUtil.getProperty("$jobId.job-name")
// 開始ログ
logger.info(configUtil.getLogMessage("BE0001", jobId, jobName))
// 対象ジョブ実行
val executor = Class.forName(configUtil.getProperty("$jobId.class-name"))
.getConstructor(JobProperty::class.java, ConfigUtil::class.java)
.newInstance(jobProperty, configUtil) as JobExecutor
executor.jobId = jobId
executor.jobName = jobName
autowiredCapableBeanFactory.autowireBean(executor)
status = executor.execute()
} catch (e: IOException) {
logger.error(configUtil.getLogMessage("BE9996"), e)
} catch (e: Exception) {
logger.error(configUtil.getLogMessage("BE9999", e.message), e)
} finally {
// 終了ログ
logger.info(configUtil.getLogMessage("BE0002", jobId, jobName, status.code.toString()))
// 終了
exitProcess(status.code)
}
}
}
- GoogleCloudStorageConfig.kt
GCSへのアクセスを行うにあたって、バケット名等の設定が必要になります。
本クラスでは、クライアントクラスへプロジェクトID等を設定してDIコンテナへ登録して使うようにしております。
GoogleCloudStorageConfig.kt
package com.example.loaderexample.config
import com.example.loaderexample.constant.GoogleCloudConstants
import com.google.cloud.NoCredentials
import com.google.cloud.storage.Storage
import com.google.cloud.storage.StorageOptions
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class GoogleCloudStorageConfig(
private val googleCloudConstants: GoogleCloudConstants
) {
@Bean
fun storage(): Storage {
return StorageOptions.newBuilder()
.setHost(googleCloudConstants.url)
.setProjectId(googleCloudConstants.projectId)
.setCredentials(NoCredentials.getInstance())
.build()
.service
?: throw Exception("GoogleCloudStorageクライアント設定エラー")
}
}
- GoogleCloudStorageUtil.kt
実際にファイルをダウンロードしているところです。
FileChannel#transferFromでファイル内容をコピーしています。
GoogleCloudStorageUtil.kt
package com.example.loaderexample.util
import com.example.loaderexample.constant.GoogleCloudConstants
import com.google.cloud.storage.BlobId
import com.google.cloud.storage.Storage
import org.springframework.stereotype.Component
import java.io.FileOutputStream
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.pathString
import kotlin.jvm.Throws
@Component
class GoogleCloudStorageUtil(
private val storage: Storage,
private val googleCloudConstants: GoogleCloudConstants
) {
@Throws(IOException::class)
fun download(fileName: String): Path {
val path = Path.of("${googleCloudConstants.downloadPath}/$fileName")
storage.reader(BlobId.of(googleCloudConstants.bucketName, fileName)).use { rc ->
FileOutputStream(path.pathString).use { fos ->
fos.channel.transferFrom(rc,0, Long.MAX_VALUE)
}
}
return path
}
}
- SchoolLunchMapper.xml
MyBatisでLOAD DATAするサンプルを散見できませんでしたので、こちらに備忘録的に記載いたします。
取り込んだファイルをそのままテーブルに挿入しています。
SchoolLunchMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.loaderexample.mapper.SchoolLunchMapper">
<insert id="loadData" parameterType="string">
LOAD DATA LOCAL INFILE #{filePath}
INTO TABLE TBL_SCHOOL_LUNCH
FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' IGNORE 1 LINES
(@1,@2,@3,@4,@5,@6,@7,@8,@9,@10,@11,@12)
SET
PUBLIC_ORGANIZATION_CD = @1,
ID = @2,
PREFECTURE_NAME = @3,
CITY_NAME = @4,
COURSE_NAME = @5,
SCHOOL_NAME = @6,
MENU_DATE = @7,
MENU_NO = @8,
DISH_NAME = @9,
ITEM_NO = @10,
FOOD_CATEGORY = @11,
FOOD_NAME = @12,
CREATED_BY = #{operatorId},
UPDATED_BY = #{operatorId};
</insert>
<delete id="deleteAll">
DELETE FROM TBL_SCHOOL_LUNCH;
</delete>
</mapper>
前準備
バッチを動かしてみたい方向けに、実行前は以下の作業が必要です。
- Git Cloneでソースをダウンロード
- 取込ファイルを用意する
ma7k5@DESKTOP-451NI4R MINGW64 ~/Projects/IdeaProjects/loader-example/config/gcp/cloud-storage/data
$ pwd
/c/Users/ma7k5/Projects/IdeaProjects/loader-example/config/gcp/cloud-storage/data
ma7k5@DESKTOP-451NI4R MINGW64 ~/Projects/IdeaProjects/loader-example/config/gcp/cloud-storage/data
$ ls -lh
total 7.3M
-rw-r--r-- 1 ma7k5 197609 7.3M Jan 8 22:27 202102kondate.csv
- docker-compose up -d でコンテナを立ち上げる。
- 後はbootRunで動かすだけになります。
実行結果
- バッチ実行前のテーブル内容
mysql> -- バッチ実行前
mysql> SELECT COUNT(*) FROM TBL_SCHOOL_LUNCH;
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (0.00 sec)
- バッチ実行時のログ
2023-03-15 21:02:43 INFO o.s.b.d.e.DevToolsPropertyDefaultsPostProcessor - Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-03-15 21:02:43 INFO o.s.b.d.e.DevToolsPropertyDefaultsPostProcessor - For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2023-03-15 21:02:46 INFO o.s.b.d.a.OptionalLiveReloadServer - LiveReload server is running on port 35729
2023-03-15 21:02:46 INFO c.e.l.JobRunner$Companion - [BE-BATCH001] 給食献立の情報更新処理を開始します。
2023-03-15 21:02:46 INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
2023-03-15 21:02:46 INFO com.zaxxer.hikari.pool.HikariPool - HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@5b02fb47
2023-03-15 21:02:46 INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
2023-03-15 21:02:46 INFO c.e.l.r.SchoolLunchRepository$Companion - 全件削除実行。対象テーブル=TBL_SCHOOL_LUNCH
2023-03-15 21:02:46 INFO c.e.l.r.SchoolLunchRepository$Companion - ファイルダウンロード実行。ファイル名=202102kondate.csv
2023-03-15 21:02:47 INFO c.e.l.r.SchoolLunchRepository$Companion - 全件取込実行。対象テーブル=TBL_SCHOOL_LUNCH,取込ファイル名=202102kondate.csv
2023-03-15 21:02:48 INFO c.e.l.JobRunner$Companion - [BE-BATCH001] 給食献立の情報更新処理を終了します。[STATUS: 1]
2023-03-15 21:02:48 INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown initiated...
2023-03-15 21:02:48 INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Shutdown completed.
- バッチ実行後のテーブル内容
mysql> -- バッチ実行後
mysql> SELECT COUNT(*) FROM TBL_SCHOOL_LUNCH;
+----------+
| COUNT(*) |
+----------+
| 62032 |
+----------+
1 row in set (0.05 sec)
mysql>
mysql> SELECT * FROM TBL_SCHOOL_LUNCH LIMIT 2\G
*************************** 1. row ***************************
PUBLIC_ORGANIZATION_CD: 401307
ID: 2102000001
PREFECTURE_NAME: 福岡県
CITY_NAME: 福岡市
COURSE_NAME: イ
SCHOOL_NAME: 香住丘小学校
MENU_DATE: 2021-02-01
MENU_NO: 1
DISH_NAME: 牛乳
ITEM_NO: 1
FOOD_CATEGORY: おもな材料
FOOD_NAME: 牛乳
CREATED_BY: BE-BATCH001
CREATED_AT: 2023-01-13 00:07:11
UPDATED_BY: BE-BATCH001
UPDATED_AT: 2023-01-13 00:07:11
*************************** 2. row ***************************
PUBLIC_ORGANIZATION_CD: 401307
ID: 2102000002
PREFECTURE_NAME: 福岡県
CITY_NAME: 福岡市
COURSE_NAME: イ
SCHOOL_NAME: 香住丘小学校
MENU_DATE: 2021-02-01
MENU_NO: 2
DISH_NAME: 麦ご飯
ITEM_NO: 1
FOOD_CATEGORY: おもな材料
FOOD_NAME: 精白米
CREATED_BY: BE-BATCH001
CREATED_AT: 2023-01-13 00:07:11
UPDATED_BY: BE-BATCH001
UPDATED_AT: 2023-01-13 00:07:11
2 rows in set (0.00 sec)
mysql>