⇧ 可能な限り多くのユーザーが、目的とする情報に辿り着けるかが重要な気もしますが...
Spring Data JPAでテキストファイルをPostgreSQLのbytea型のカラムに出し入れしてみる
前回、
⇧ PostgreSQLのbytea型に対応するJDBC(Java DataBase Connectivity)のデータ型として、
- byte[[]]
- InputStream
の2つがあるということが分かりましたと。
ORM(Object Relational Mapper)の内、Spring Data JPA はと言うと、
⇧ GitHubで公開されているプロジェクトのpom.xmlでライブラリの依存関係が確認できるのだけど、いまいちイメージし辛い。
公式のドキュメントではないのですが、
⇧ 上図のような構成になっていると思われるので、JDBC(Java DataBase Connectivity)のデータ型をそのまま利用してくれているだろうと。
PostgreSQL→JDBC→Hibernate→JPA→Spring Data JPA って流れらしいので、データ型が途中で変えられていたらどうしようもないんだけど、そんな鬼畜な所業は行われていないと信じるしかないのだが...
とりあえず、PostgreSQLにbytea型のカラムを持つテーブルを用意しますか。
PostgreSQLについては、
⇧ 上記の記事の時に、WSL 2(Windows Subsystem for Linux 2)にインストールしていたRocky Linux 9にインストールしてるPostgreSQLを利用します。
WSL 2(Windows Subsystem for Linux 2)のRocky Linux 9を起動し、Rocky Linux 9にSSHログインし、PostgreSQL にログイン。(Tera TermでSSHログインしてます)
データベース作成などを実行。
-- ユーザー作成 CREATE ROLE test_user WITH SUPERUSER LOGIN PASSWORD 'password'; -- データベース作成 CREATE DATABASE testdb OWNER = test_user TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'C' LC_CTYPE = 'C'; -- publicスキーマ配下の全てのテーブルに対して全ての権限をユーザーtest_userに付与 GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO test_user; -- publicスキーマ配下の全てのシーケンスオブジェクトに対して全ての権限をユーザーtest_userに付与 GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO test_user;
そしたらば、PostgreSQLにテーブル作成。
CREATE TABLE t_file_data ( id serial ,file_name varchar(255) NOT NULL -- ,file_extension varchar(16) NOT NULL ,file_object bytea NOT NULL ,create_user varchar(128) NOT NULL ,create_datetime timestamp(3) NOT NULL ,update_user varchar(128) ,update_datetime timestamp(3) ,primary key(id) ); -- コメント COMMENT ON TABLE t_file_data IS 'ファイル情報'; COMMENT ON COLUMN t_file_data.id IS 'id'; COMMENT ON COLUMN t_file_data.file_name IS 'ファイル名'; --COMMENT ON COLUMN t_file_data.file_extension IS 'ファイル拡張子'; COMMENT ON COLUMN t_file_data.file_object IS 'ファイル'; COMMENT ON COLUMN t_file_data.create_user IS '登録ユーザー'; COMMENT ON COLUMN t_file_data.create_datetime IS '登録日時'; COMMENT ON COLUMN t_file_data.update_user IS '更新ユーザー'; COMMENT ON COLUMN t_file_data.update_datetime IS '更新日時';
⇧ テーブル追加できました。
EclipseでSpring Bootなプロジェクトを作成し、以下のようなパッケージ、ファイル、ソースフォルダー、フォルダーを追加。
手を加えているソースコードは以下のような感じ。
■/treat-file/build.gradle
plugins { id 'java' id 'war' id 'org.springframework.boot' version '3.3.0' id 'io.spring.dependency-management' version '1.1.5' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '17' } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
■/treat-file/src/main/resources/application.properties
spring.application.name=treat-file # DB接続情報 spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://172.24.91.141:5432/testdb spring.datasource.username=test_user spring.datasource.password=password spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
■/treat-file/src/main/java/com/example/demo/entity/BaseEntity.java
package com.example.demo.entity; import java.io.Serializable; import java.time.LocalDateTime; import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.PrePersist; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; import lombok.Data; @Data @MappedSuperclass public class BaseEntity implements Serializable { private static final long serialVersionUID = -5199951647986855131L; /** 登録ユーザー */ @Column(name="create_user") private String createUser; /** 登録日時 */ @Temporal(TemporalType.TIMESTAMP) @Column(name="create_datetime") private LocalDateTime createDateTime; /** 更新ユーザー */ @Column(name="update_user") private String updateUser; /** 更新日時 */ @Temporal(TemporalType.TIMESTAMP) @Column(name="update_datetime") private LocalDateTime updateDateTime; /** * 登録前処理 */ @PrePersist public void prePersist() { //LocalDateTime localDateTime = LocalDateTime.now(); createDateTime = LocalDateTime.now(); } /** * 更新前処理 */ public void preUpdate() { updateDateTime = LocalDateTime.now(); } }
■/treat-file/src/main/java/com/example/demo/entity/FileDataEntity.java
package com.example.demo.entity; import java.sql.Types; import org.hibernate.annotations.JdbcTypeCode; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; @Data @EqualsAndHashCode(callSuper=true) @ToString(callSuper=true) @Entity @Table(name="t_file_data") public class FileDataEntity extends BaseEntity { private static final long serialVersionUID = 2667666271206403613L; @Id @SequenceGenerator(name = "t_file_data_id_seq") @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name="id", columnDefinition = "serial") private Long id; @Column(name="file_name") private String fileName; // @Column(name="file_extentin") // private String fileExtention; //@Lob //@Type(type = "org.hibernate.type.BinaryType") // Hibernate 6 @JdbcTypeCode(Types.BINARY) @Column(name="file_object") private byte[] fileObject; }
■/treat-file/src/main/java/com/example/demo/repository/FileDataRepository.java
package com.example.demo.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.example.demo.entity.FileDataEntity; public interface FileDataRepository extends JpaRepository<FileDataEntity, Long> { }
■/treat-file/src/main/java/com/example/demo/service/transactional/TransactionalFileDataService.java
package com.example.demo.service.transactional; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.example.demo.entity.FileDataEntity; import com.example.demo.repository.FileDataRepository; import jakarta.transaction.Transactional; @Service public class TransactionalFileDataService { @Autowired FileDataRepository fileDataRepository; //@Transactional(rollbackFor=Exception.class) @Transactional(rollbackOn=Exception.class) public FileDataEntity save(FileDataEntity fileDataEntity) { FileDataEntity savedFileDataEntity = fileDataRepository.save(fileDataEntity); return savedFileDataEntity; } public FileDataEntity findById(Long id) { Optional<FileDataEntity> op = fileDataRepository.findById(id); return op.get(); } }
■/treat-file/src/main/java/com/example/demo/service/FileDataService.java
package com.example.demo.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.example.demo.entity.FileDataEntity; import com.example.demo.service.transactional.TransactionalFileDataService; @Service public class FileDataService { @Autowired TransactionalFileDataService transactionalFileDataService; public int executeSave(FileDataEntity fileDataEntity) { transactionalFileDataService.save(fileDataEntity); return 1; } public FileDataEntity executeFindById(Long id) { return transactionalFileDataService.findById(id); } }
動作確認用のインプット
■/treat-file/src/test/resources/testCsvFile.csv
【ファイル改行コード】LF
1,20240521,0001,ほげほげ株式会社 2,A000001,ほげほげアメリカンプロテイン,10000,100 2,A000002,ほげほげアジアンプロテイン,8000,100 2,A000003,ほげほげヨーロピアンプロテイン,12000,100 2,A000004,ぴよぴよアメリカンプロテイン,11000,100 2,A000004,ぴよぴよアメリカンプロテイン,11000,100 2,A000005,ぽよぽよアメリカンプロテイン,13000,100 2,A000006,ぽよぽよアフリカンプロテイン,7000,100 9,8
■/treat-file/src/test/resources/testCsvFile_shift-jis.csv
【ファイル文字コード】Shift-JIS
【改行コード】CRLF
1,20240521,0001,ほげほげ株式会社 2,A000001,ほげほげアメリカンプロテイン,10000,100 2,A000002,ほげほげアジアンプロテイン,8000,100 2,A000003,ほげほげヨーロピアンプロテイン,12000,100 2,A000004,ぴよぴよアメリカンプロテイン,11000,100 2,A000004,ぴよぴよアメリカンプロテイン,11000,100 2,A000005,ぽよぽよアメリカンプロテイン,13000,100 2,A000006,ぽよぽよアフリカンプロテイン,7000,100 9,8
動作確認用のソースコード。
■/treat-file/src/test/java/com/example/demo/service/TestFileDataService.java
package com.example.demo.service; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.net.URL; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import com.example.demo.entity.FileDataEntity; @SpringBootTest public class TestFileDataService { @Autowired FileDataService fileDataService; final static Path RESOURCE_DIRECTORY_ROOT = Paths.get("src","test","resources"); final static String UTF8 = "UTF-8"; final static String SHIFT_JIS = "Shift_JIS"; final static String WINDOWS_31J = "Windows-31J"; final static String LF = "\n"; final static String CRLF = "\r\n"; final static DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); @Test public void test01() { URL url = Thread.currentThread().getContextClassLoader().getResource("testCsvFile.csv"); File file = new File(url.getPath()); FileDataEntity fileDataEntity = new FileDataEntity(); fileDataEntity.setFileName(file.getName()); try { fileDataEntity.setFileObject(Files.readAllBytes(file.toPath())); } catch (IOException e) { // TODO 自動生成された catch ブロック e.printStackTrace(); } fileDataEntity.setCreateUser("test01"); fileDataService.executeSave(fileDataEntity); } @Test public void test02() { LocalDateTime localDateTime = LocalDateTime.now(); //String outputPath = ClassLoader.getSystemClassLoader().getResource("").getPath(); final Path outputPath = Paths.get(RESOURCE_DIRECTORY_ROOT.toString(), "output"); final String outputFileName = "testCsvFile_" + localDateTime.format(DATETIME_FORMATTER) + ".csv"; final Path outputFilePath = Paths.get(outputPath.toString(), outputFileName); File outputFile = outputFilePath.toFile(); try { if(outputFile.exists() == false) { outputFile.createNewFile(); } } catch (IOException e) { // TODO 自動生成された catch ブロック e.printStackTrace(); } FileDataEntity fileDataEntity = fileDataService.executeFindById(Long.valueOf("2")); byte[] fileByte = fileDataEntity.getFileObject(); try(BufferedReader reader = new BufferedReader(new StringReader(new String(fileByte, UTF8))); BufferedWriter bw = Files.newBufferedWriter(outputFilePath, Charset.forName(UTF8))) { String line = null; while ((line = reader.readLine()) != null) { System.out.println(">>> " + line); bw.write(line); bw.write(LF); } } catch (IOException e) { // TODO 自動生成された catch ブロック e.printStackTrace(); } } @Test public void test03() { URL url = Thread.currentThread().getContextClassLoader().getResource("testCsvFile_shift-jis.csv"); File file = new File(url.getPath()); FileDataEntity fileDataEntity = new FileDataEntity(); fileDataEntity.setFileName(file.getName()); try { fileDataEntity.setFileObject(Files.readAllBytes(file.toPath())); } catch (IOException e) { // TODO 自動生成された catch ブロック e.printStackTrace(); } fileDataEntity.setCreateUser("test01"); fileDataService.executeSave(fileDataEntity); } @Test public void test04() { LocalDateTime localDateTime = LocalDateTime.now(); //String outputPath = ClassLoader.getSystemClassLoader().getResource("").getPath(); final Path outputPath = Paths.get(RESOURCE_DIRECTORY_ROOT.toString(), "output"); final String outputFileName = "testCsvFile_shift-jis_" + localDateTime.format(DATETIME_FORMATTER) + ".csv"; final Path outputFilePath = Paths.get(outputPath.toString(), outputFileName); File outputFile = outputFilePath.toFile(); try { if(outputFile.exists() == false) { outputFile.createNewFile(); } } catch (IOException e) { // TODO 自動生成された catch ブロック e.printStackTrace(); } FileDataEntity fileDataEntity = fileDataService.executeFindById(Long.valueOf("4")); byte[] fileByte = fileDataEntity.getFileObject(); try(BufferedReader reader = new BufferedReader(new StringReader(new String(fileByte, SHIFT_JIS))); // BufferedWriter bw = Files.newBufferedWriter(outputFilePath, Charset.forName(WINDOWS_31J))) { BufferedWriter bw = Files.newBufferedWriter(outputFilePath, Charset.forName(SHIFT_JIS))) { String line = null; while ((line = reader.readLine()) != null) { System.out.println(">>> " + line); bw.write(line); bw.write(CRLF); } } catch (IOException e) { // TODO 自動生成された catch ブロック e.printStackTrace(); } } }
⇧ で、実行すると、
■testCsvFile_20240525141525676.csv
■testCsvFile_shift-jis_20240525141600720.csv
⇧ という結果になりました。
テーブルから取り出したバイト配列を読み込む際に、文字コードを指定する必要があるのと、ファイルに書き出す際にも文字コード、改行コードが必要になってくるので(改行コード混在は考えていないが...)、文字コード、改行コードについても、テーブルに一緒に保存しておく必要がありそうですかね。
とりあえず、テキストファイルについては、バイナリファイルのように、テーブルから取得してきたバイト配列をファイル化とかしなくても内容を読み込むことができるのが分かりましたと。
毎度モヤモヤ感が半端ない…
今回はこのへんで。