※当サイトの記事には、広告・プロモーションが含まれます。

Apache Commons DbUtilsで更新系が面倒だった件

nazology.net

⇧ amazing...

Apache Commons DbUtilsで更新系が面倒だった件

JPAJava Persistence API)のようなORM(Object-Relational Mapping)と同じ感覚で、Entityを用意して更新系のSQLを実行しようと思ったら、

stackoverflow.com

stackoverflow.com

stackoverflow.com

⇧ まさかのEntityを直接設定できないという...

どうしたもんかと考えていたのだけど、

stackoverflow.com

salumarine.com

qiita.com

⇧ EntityをMapに変換後、Mapのkeyをスネークケースに変換して、SQL文を作るぐらいしか思いつかんかったのよね...

いずれにせよ、本番のソースコードでリフレクションはあまり使いたくないので、EntityをMapに変換するライブラリを利用する感じにはなるのだけど、

  • Jackson
  • Apache Commons BeanUtils
  • Gson
  • org.springframework.cglib.beans.BeanMap

あたりが、候補になってくるんだろうか。

Spring Frameworkを使ってるところは割と多いと思うので、BeanMapを試してみました。

Apache Commons DBUtilsでINSERTする際に、

qiita.com

⇧ 上記サイト様によりますと、自動採番を設定してるようなカラムの値を自動的に設定してくれる機能があるようなのですが、そのカラムについてはDBUtilsSQLで設定しない必要があり、今回実装を試みようとしているケースでは利用できないので、

forums.oracle.com

⇧ Sequenceの値を取得することにしました。

Oracle Databaseを利用しているので、テーブルの自動採番させたいカラムに紐づくSequenceオブジェクトを作成しておきます。

⇧ 上図のキャプチャ画像のようにSEQUENCEを作成しておきます。

SEQUENCEを作成して無い場合は、作成前のDROPは不要です。「TS0818」はご自身の環境の「スキーマ」に変更してください。

スキーマ」はと言うと、

docs.oracle.com

スキーマはデータベース・オブジェクトの集合です。スキーマはデータベース・ユーザーによって所有され、ユーザー名と同じ名前を共有します。スキーマ・オブジェクトは、ユーザーによって作成される論理構造です。

スキーマ・オブジェクトの管理

データベースのすべてのオブジェクトは1つのスキーマに属し、スキーマ内に一意の名前を持っています。異なるスキーマにある場合、複数のデータベース・オブジェクトは同じ名前を共有できます。スキーマ名を使用して、確実にオブジェクトを参照できます。

スキーマ・オブジェクトの管理

⇧ らしいですと。

stackoverflowの情報によりますと、

stackoverflow.com

stackoverflow.com

⇧ DBA_USERS.usernameとDBA_OBJECTS.ownerが一致するかどうかを見てるのだけど、

docs.oracle.com

docs.oracle.com

⇧ 特にドキュメントだと、DBA_USERS.usernameとDBA_OBJECTS.ownerの関係について説明が無いので、確かなことが分からないのだけど、「スキーマ」を確認できるらしいと信じて、以下のSQL文を実行。

SELECT do.* , du.* FROM dba_users du
INNER JOIN dba_objects do
ON du.USERNAME = do.OWNER
AND du.DEFAULT_TABLESPACE NOT IN
 ('SYSTEM','SYSAUX') 
AND du.ACCOUNT_STATUS = 'OPEN'
ORDER BY do.OBJECT_TYPE;

で、結果が表示されるのだけど、

⇧ ownerの値か、usernameの値が「スキーマ」ってことで良いかと、まぁ、Oracleの「スキーマ」の説明によると

スキーマはデータベース・ユーザーによって所有され、ユーザー名と同じ名前を共有します。

ってことらしいので、DBA_USERS.usernameの値を「スキーマ」と捉えれば良いんかな?

話が脱線しましたが、SEQUENCEを作成。

DROP SEQUENCE TS0818.USERS_SEQ;
CREATE SEQUENCE TS0818.USERS_SEQ MINVALUE 1 MAXVALUE 9999999999999 INCREMENT BY 1 START WITH 1 CACHE 10 NOORDER NOCYCLE;
DROP SEQUENCE TS0818.USER_DETAIL_SEQ;
CREATE SEQUENCE TS0818.USER_DETAIL_SEQ MINVALUE 1 MAXVALUE 9999999999999 INCREMENT BY 1 START WITH 1 CACHE 10 NOORDER NOCYCLE;

TRIGGERについては、手動でINSERTとかしないのであれば、不要だとは思うのだけど、一応、載っけときます。

CREATE OR REPLACE EDITIONABLE TRIGGER "TS0818"."USERS_TRIGGER" 
BEFORE INSERT ON ts0818.USERS
FOR EACH ROW
DECLARE
--  WHEN (new.USER_ID IS NULL)
BEGIN
  IF :new.USER_ID IS NULL THEN
    :new.USER_ID := to_char(USERS_SEQ.nextval);

   -- SELECT to_char(USERS_SEQ.nextval) INTO :new.USER_ID FROM DUAL;
  END IF;
END;
CREATE OR REPLACE EDITIONABLE TRIGGER "TS0818"."USER_DETAIL_TRIGGER" 
BEFORE INSERT ON TS0818.USER_DETAIL
FOR EACH ROW
DECLARE

BEGIN
  IF :new.USER_DETAIL_ID IS NULL THEN
    :new.USER_DETAIL_ID := to_char(USER_DETAIL_SEQ.nextval);
  END IF;
END;

Oracleでデフォルト値にシーケンスを設定したいsiguniang.wordpress.com

⇧ そもそも、Oracle Database 12c以降であれば、テーブルの作成時にカラムの設定でSEQUENCEを紐づけできるみたい。

まぁ、今の現場で使ってるのがOracle Database 11gなんだけどね...

話が脱線しましたが、プロジェクトとしては、

ts0818.hatenablog.com

⇧ 上記の記事の時のものを利用していきます。

ソースコードで、今回利用しそうなものを以下に掲載。

■/spring-mvc-jsp/pom.xml

<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>
	<groupId>com</groupId>
	<artifactId>spring-mvc-jsp</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<maven.compiler.target>${java.version}</maven.compiler.target>
		<maven.compiler.source>${java.version}</maven.compiler.source>
		<org.springframework-version>5.3.24</org.springframework-version>
	</properties>
	<dependencies>
		<!-- Spring -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-jdbc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>

		<!-- Servlet -->
		<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>javax.servlet-api</artifactId>
			<version>4.0.1</version>
			<scope>provided</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/javax.servlet.jsp-api -->
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>javax.servlet.jsp-api</artifactId>
			<version>2.3.3</version>
			<scope>provided</scope>
		</dependency>
		<!-- https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>

		<!-- Database -->
		<!-- https://mvnrepository.com/artifact/commons-dbutils/commons-dbutils -->
		<dependency>
			<groupId>commons-dbutils</groupId>
			<artifactId>commons-dbutils</artifactId>
			<version>1.7</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc11 -->
<!--
		<dependency>
			<groupId>com.oracle.database.jdbc</groupId>
			<artifactId>ojdbc11</artifactId>
			<version>21.8.0.0</version>
		</dependency>
-->
		<!-- https://mvnrepository.com/artifact/com.oracle.database.jdbc/ojdbc8 -->
		<dependency>
			<groupId>com.oracle.database.jdbc</groupId>
			<artifactId>ojdbc8</artifactId>
			<version>21.9.0.0</version>
		</dependency>
		<!-- Lombok -->
		<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.24</version>
			<scope>provided</scope>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-test</artifactId>
			<version>${org.springframework-version}</version>
			<scope>test</scope>
		</dependency>

	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<version>3.3.2</version>
				<configuration>
					<webXml>src/main/webapp/WEB-INF/web.xml</webXml>
					<warSourceDirectory>src/main/webapp</warSourceDirectory>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>   

■/spring-mvc-jsp/src/main/java/com/entity/Users.java

package com.entity;

import java.sql.Timestamp;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {

	private String userId;
	
	private String insertUser;
	
	private Timestamp insertDate;
}

■/spring-mvc-jsp/src/main/java/com/entity/UserDetail.java

package com.entity;

import java.sql.Timestamp;

import lombok.Data;

@Data
public class UserDetail {

	private String userDetailId;
	
	private String userId;
	
	private String lastName;
	
	private String firstName;
	
	private String gender;
	
	private String birthday;
	
	private String addressId;
	
	private String insertUser;
	
	private Timestamp insertDate;
	
	private String updateUser;
	
	private Timestamp updateDate;
	
	private String deleteFlg;
	
	private Timestamp deleteDate;
	
}

⇧ コンストラクタで値を設定する場合は、Usersクラスと同じ様に@AllArgsConstructorを付ける感じになるかと。今回はUserDetailクラスでコンストラクタで値を設定していなかったので@AllArgsConstructorは付けてません。そもそもlombok使ってないプロジェクトとかもあると思うけど、lombok使ってない場合は自力で引数付きコンストラクタの作成を頑張る感じで。

■/spring-mvc-jsp/src/main/java/com/dao/UsersDaoImpl.java

package com.dao;

import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class UsersDaoImpl {
	@Autowired
	@Qualifier(value = "queryRunner")
	private QueryRunner queryRunner;

	public Integer generateInsertId(String sql) throws SQLException {
		Object obj = queryRunner.query(sql, new ScalarHandler<>());
		return Integer.valueOf(obj.toString());
	}

//	public Integer save(String sql, Object[] params) throws SQLException {
//		return queryRunner.update(sql, params);
//	}
	
	public int[] saveAll(String sql, Object[][] params) throws SQLException {
		return queryRunner.batch(sql, params);
	}
}

■/spring-mvc-jsp/src/main/java/com/dao/UserDetailDaoImpl.java

package com.dao;

import java.sql.SQLException;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
//@RequiredArgsConstructor
public class UserDetailDaoImpl {
	
	@Autowired
	@Qualifier(value = "queryRunner")
	private QueryRunner queryRunner;
	
//	public List<UserDetail> findAll() throws SQLException {
//		ResultSetHandler<List<UserDetail>> resultSetHandler =
//                new BeanListHandler<UserDetail>(UserDetail.class, new BasicRowProcessor(new GenerousBeanProcessor()));
//		final List<UserDetail> userDetailList = queryRunner.query("select * from user_detail", resultSetHandler);
//		return userDetailList;
//	}
//	
//	public List<UserDetail> findByIds(List<String> ids) throws SQLException {
//		ResultSetHandler<List<UserDetail>> resultSetHandler =
//                new BeanListHandler<UserDetail>(UserDetail.class, new BasicRowProcessor(new GenerousBeanProcessor()));
//		
//		List<String> notNullIds = ids.stream()
//				.filter(Objects::nonNull)
//				.collect(Collectors.toList());
//		
//		// SQL文の作成
//		StringBuilder sbSql = new StringBuilder();
//		  sbSql.append("SELECT ")
//		    .append("* ")
//		    .append("FROM ")
//		    .append("user_detail ")
//		    .append("WHERE ")
////		    .append("(user_detail_id, 1) IN (")		    
////		    .append(ids.stream().map(id -> "('"+id+"', 1)").collect(Collectors.joining(",")))
//            .append("(user_detail_id, '1') IN (%s) ")
//            .append("AND insert_user = ?");
//
//		// プレースホルダー設定(SQLインジェクション対策)
//		String placeHolder = notNullIds.stream()
//					.map(v -> "(?, ?)")
//					.collect(Collectors.joining(", "));
//		String sbSqlInPlaceFolder = String.format(sbSql.toString(), placeHolder);
//
//		 // プリペアードステートメントに設定する値
//		List<String> bindParamList = notNullIds.stream()
////				.map(id -> String.join("", "'", id, "'"))
//				.collect(Collectors.toList());
//		
//		int addIndex = 0;
//		int beforeListSize = bindParamList.size();
//		for (int index = 0; index < bindParamList.size(); index++) {
//			if (index % 2 == 0) {
//				addIndex = index;
//				if (index != (beforeListSize - 1) * 2) {
//					continue;
//				}
//			}
//			addIndex++;
//			bindParamList.add(addIndex, "1");
//			if (index == (beforeListSize - 1) * 2) {
//				break;
//			}
//		}
//		
//		bindParamList.add("admin");
//		Object[] sqlParams = bindParamList.toArray();
//		
//		// SELECT文を実行
//		final List<UserDetail> userDetailList = queryRunner.query(sbSqlInPlaceFolder.toString(), resultSetHandler, sqlParams);
//		return userDetailList;
//	}
	
	public Integer generateInsertId(String sql) throws SQLException {
		Object obj = queryRunner.query(sql, new ScalarHandler<>());
		return Integer.valueOf(obj.toString());
	}
	
//	public Integer save(String sql, Object[] params) throws SQLException {
//		return queryRunner.update(sql, params);
//	}
	
	public int[] saveAll(String sql, Object[][] params) throws SQLException {
		return queryRunner.batch(sql, params);
	}
}

■/spring-mvc-jsp/src/main/java/com/service/UserRelatedServiceImpl.java

package com.service;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import com.dao.UserDetailDaoImpl;
import com.dao.UsersDaoImpl;
import com.entity.UserDetail;
import com.entity.Users;

import lombok.Data;

@Service
public class UserRelatedServiceImpl {

	@Autowired
	private UsersDaoImpl usersDaoImpl;
	
	@Autowired
	private UserDetailDaoImpl userDetailDaoImpl;
	
//	public List<UserDetail> findAll() throws SQLException {
//		return userDetailDaoImpl.findAll();
//	}
//	
//	public List<UserDetail> findByIds(List<String> ids) throws SQLException {
//		return userDetailDaoImpl.findByIds(ids);
//	}
	
	public String createGenerateInsertIdSql (String sequenceName) {
		String sql = "SELECT "
				+ sequenceName
				+ ".nextval "
				+ "FROM dual";
		return sql;
	}
	
//	public String saveUsers(Users users) {
//		if (Objects.isNull(users)) {
//			return null;
//		}
//		QueryRunnerArgs queryRunnerArgs = createInsertSql("users", users);
//		if (Objects.isNull(queryRunnerArgs)) {
//			return null;
//		}
//		try {
//			usersDaoImpl.save(queryRunnerArgs.getSql(), queryRunnerArgs.getParams());
//		} catch (SQLException e) {
//			// TODO 自動生成された catch ブロック
//			e.printStackTrace();
//		}
//		return users.getUserId();
//	}
//	
//	public int saveUserDetail(UserDetail userDetail, String userId) {
//		if (Objects.isNull(userDetail) || Objects.isNull(userId)) {
//			return 0;
//		}
//		userDetail.setUserId(userId);
//		QueryRunnerArgs queryRunnerArgs = createInsertSql("user_detail", userDetail);
//		if (Objects.isNull(queryRunnerArgs)) {
//			return 0;
//		}
//		try {
//			userDetailDaoImpl.save(queryRunnerArgs.getSql(), queryRunnerArgs.getParams());
//		} catch (SQLException e) {
//			// TODO 自動生成された catch ブロック
//			e.printStackTrace();
//		}
//		return 1;
//	}
	
	public List<String> saveAllUsers(List<Users> usersList) {
		if (CollectionUtils.isEmpty(usersList)) {
			return Collections.emptyList();
		}
		Optional<QueryRunnerArgsForBatch> queryRunnerArgsForBatch = createBatchInsertSql("users", usersList);
		if (!queryRunnerArgsForBatch.isPresent()) {
			return Collections.emptyList();
		}
		try {
			usersDaoImpl.saveAll(queryRunnerArgsForBatch.get().getSql(), queryRunnerArgsForBatch.get().getParams());
		} catch (SQLException e) {
			// TODO 自動生成された catch ブロック
			e.printStackTrace();
		}
		List<String> userIdList = usersList.stream()
				.filter(Objects::nonNull)
				.map(users -> users.getUserId())
				.collect(Collectors.toList());
		return userIdList;
	}

	public List<String> saveAllUserDetail(List<UserDetail> userDetailList) {
		if (CollectionUtils.isEmpty(userDetailList)) {
			return Collections.emptyList();
		}
		Optional<QueryRunnerArgsForBatch> queryRunnerArgsForBatch = createBatchInsertSql("user_detail", userDetailList);
		if (!queryRunnerArgsForBatch.isPresent()) {
			return Collections.emptyList();
		}
		try {
			userDetailDaoImpl.saveAll(queryRunnerArgsForBatch.get().getSql(), queryRunnerArgsForBatch.get().getParams());
		} catch (SQLException e) {
			// TODO 自動生成された catch ブロック
			e.printStackTrace();
		}
		List<String> userDetailIdList = userDetailList.stream()
				.filter(Objects::nonNull)
				.map(userDetail -> userDetail.getUserDetailId())
				.collect(Collectors.toList());
		return userDetailIdList;
	}
	
//	/**
//	 * 
//	 * @param exampleForm
//	 * @return
//	 */
//	public Users createUsers(ExampleForm exampleForm) {
//		Users users = new Users();
//		String generateSeqSql = createGenerateInsertIdSql("users_seq");
//		try {
//			// Formの値を詰め替え
//			BeanUtils.copyProperties(exampleForm, users);
//			// INSERTするIDを採番
//			Integer userId = usersDaoImpl.generateInsertId(generateSeqSql);
//			users.setUserId(String.valueOf(userId));
//			users.setInsertUser("admin");
//			users.setInsertDate(Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC)));
//		} catch (SQLException e) {
//			// TODO 自動生成された catch ブロック
//			e.printStackTrace();
//		}
//		return users;
//	}
//	
//	public UserDetail createUserDetail(ExampleForm exampleForm) {
//		UserDetail userDetail = new UserDetail();
//		String sql = createGenerateInsertIdSql("user_detail_seq");
//
//		try {
//			// Formの値を詰め替え
//			BeanUtils.copyProperties(exampleForm, userDetail);	
//			// INSERTするIDを採番
//			Integer userDetailId = userDetailDaoImpl.generateInsertId(sql);
//			userDetail.setUserDetailId(String.valueOf(userDetailId));
//			userDetail.setInsertUser("admin");
//			userDetail.setInsertDate(Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC)));
//			userDetail.setDeleteFlg("0");
//		} catch (SQLException e) {
//			// TODO 自動生成された catch ブロック
//			e.printStackTrace();
//		}
//		return userDetail;
//	}
//
//	private QueryRunnerArgs createInsertSql(String tableName, Object bean) {
//		StringBuilder sbInsert = new StringBuilder("INSERT"
//				+ " INTO "
//				+ tableName
//				+ " (");
//		StringBuilder sbKeys = new StringBuilder();
//		StringBuilder sbPrepare = new StringBuilder();
//		BeanMap beanMap = BeanMap.create(bean);
//		int index = 0;
//		Object[] params = new Object[beanMap.size()];
//		for (Object key: beanMap.keySet()) {
//			if (index != 0) {
//				sbKeys.append(", ");
//				sbPrepare.append(", ");
//			}
//			sbKeys.append(convertLowerCamelToSnakeCase(String.valueOf(key)));
//			sbPrepare.append("?");
//			params[index] = beanMap.get(key);
//			index++;
//		}
//		sbInsert.append(sbKeys.toString())
//		  .append(") VALUES (")
//		  .append(sbPrepare.toString())
//		  .append(")");
//
//		return new QueryRunnerArgs(sbInsert.toString(), params);
//	}
	
	/**
	 * 実行SQLの作成
	 * @param <T> エンティティのデータ型
	 * @param tableName テーブル名
	 * @param beanList エンティティのリスト
	 * @return
	 */
	private <T> Optional<QueryRunnerArgsForBatch> createBatchInsertSql(String tableName, List<T> beanList) {
		Object[][] paramsArr = new Object[beanList.size()][];
		BeanMap beanMap = null;
		Object[] keys = null;
		List<Integer> beanSizeList = new ArrayList<>();
		// エンティティのリストの数の分だけ処理
		for (int index = 0; index < beanList.size(); index++) {
			beanMap = BeanMap.create(beanList.get(index));
			if (Objects.isNull(beanMap) || CollectionUtils.isEmpty(beanMap.values())) {
				continue;
			}
			beanSizeList.add(beanMap.size());
			paramsArr[index] = beanMap.values().toArray();
			// テーブルのカラム名を作成する用
			if (Objects.isNull(keys)) {
				keys = beanMap.keySet().toArray();
			}
		}
		if (Objects.isNull(keys)) {
			return Optional.empty();
		}
		
		Integer paramSize = Collections.max(beanSizeList);
		String placeHolder = String.join(",", IntStream.range(0, paramSize).boxed()
		  .map(n -> "?")
		  .collect(Collectors.toList())
		);
		String columns = String.join(",", Arrays.asList(keys).stream()
				.map(key -> convertLowerCamelToSnakeCase(String.valueOf(key)))
				.collect(Collectors.toList()));

		// INSERT文を作成
		StringBuilder sbInsert = new StringBuilder();
		sbInsert.append("INSERT ")
		.append("INTO ")
		.append(tableName)
		.append(" (")
		.append(columns)
		.append(") VALUES (")
		.append(placeHolder)
		.append(")");
		
		return Optional.of(new QueryRunnerArgsForBatch(sbInsert.toString(), paramsArr));
	}
	
	/**
	 * テーブルのカラム名を作成する
	 * @param lowerCamelProperties エンティティのプロパティ名
	 * @return テーブルのカラム名
	 */
	private static String convertLowerCamelToSnakeCase(final String lowerCamelProperties) {
        if (!StringUtils.hasLength(lowerCamelProperties)) {
            return lowerCamelProperties;
        }
        final StringBuilder sb = new StringBuilder(lowerCamelProperties.length() + lowerCamelProperties.length());
        for (int i = 0; i < lowerCamelProperties.length(); i++) {
            final char c = lowerCamelProperties.charAt(i);
            if (Character.isUpperCase(c)) {
                sb.append(sb.length() != 0 ? '_' : "")
                  .append(Character.toLowerCase(c));
            } else {
                sb.append(Character.toLowerCase(c));
            }
        }
        return sb.toString();
	}
	
//	/**
//	 * 通常のSQL実行用のデータ作成
//	 */
//	@Data
//	private class QueryRunnerArgs {
//		private String sql;
//		private Object[] params;
//		
//		QueryRunnerArgs(String sql, Object[] params) {
//			this.sql = sql;
//			this.params = params;
//		}
//	}
	
	/**
	 * JDBCのbatch実行用データ作成
	 */
	@Data
	private class QueryRunnerArgsForBatch {
		private String sql;
		private Object[][] params;
		
		QueryRunnerArgsForBatch(String sql, Object[][] params) {
			this.sql = sql;
			this.params = params;
		}
	}
	
	
//	private static Object[][] createParamsForBatch(List<List<Object>> paramsList) {
//		Object[][] params = paramsList.stream()
//		  .map(list -> list.stream().toArray(Object[]::new))
//		  .toArray(Object[][]::new);		
//		return params;
//	}
}

今回、JUnit経由で動作確認するので利用しないけど、Serviceクラスでコメントアウトした部分に合わせて、ControllerでServiceクラスのメソッドを利用していた部分をコメントアウトしておきます。

■/spring-mvc-jsp/src/main/java/com/controller/HomeController.java

package com.controller;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.dto.FromDto;
import com.dto.GenderDto;
import com.entity.ex.ExUserAndAddressDetail;
import com.form.ExampleForm;
import com.service.UserRelatedServiceImpl;
import com.service.ex.ExUserAndAddressDetailServiceImpl;

@Controller
@RequestMapping("homeController")
public class HomeController {
	
	@Autowired
	private UserRelatedServiceImpl userRelatedServiceImpl;

	@Autowired
	private ExUserAndAddressDetailServiceImpl exUserAndAddressDetailServiceImpl;
	
	@ModelAttribute
	public ExampleForm initForm() {
		ExampleForm exampleForm = new ExampleForm();
		return exampleForm;
	}
	
	@GetMapping("home")
	public String index(Model model) {
		List<String> list = new ArrayList<>();
		list.add("リスト1");
		list.add("リスト2");
		list.add("リスト3");
		model.addAttribute("strList", list);
		return "home/index";
	}
	
//	@GetMapping("find-all")
//	public ModelAndView findAll() throws SQLException {
//		List<UserDetail> userDetailList = userRelatedServiceImpl.findAll();
//		List<String> ids = userDetailList.stream()
//		.map(userDetail -> userDetail.getUserDetailId())
//		.collect(Collectors.toList());
//		
//		userRelatedServiceImpl.findByIds(ids);
//		
//		ModelAndView mv = new ModelAndView("home/index");
//		mv.addObject("userDetailList", userDetailList);
//		return mv;
//	}
	
	@GetMapping("find-all-ex")
	public ModelAndView findAllEx() throws SQLException {
		List<ExUserAndAddressDetail> exUserAndAddressDetailList = exUserAndAddressDetailServiceImpl.findAll();
		ModelAndView mv = new ModelAndView("home/index");
		mv.addObject("exUserAndAddressDetailList", exUserAndAddressDetailList);
		return mv;
	}
	
	@RequestMapping("init")
	public ModelAndView initialForm(ExampleForm exampleForm) {
		List<FromDto> fromDtoList = new ArrayList<>();
		fromDtoList.add(new FromDto(1, "北海道"));
		fromDtoList.add(new FromDto(2, "東北"));
		fromDtoList.add(new FromDto(3, "東京"));
		fromDtoList.add(new FromDto(4, "名古屋"));
		fromDtoList.add(new FromDto(5, "京都"));
		fromDtoList.add(new FromDto(6, "大阪"));
		fromDtoList.add(new FromDto(7, "九州"));

		List<GenderDto> genderDtoList = new ArrayList<>();
		genderDtoList.add(new GenderDto(0, "男性"));
		genderDtoList.add(new GenderDto(1, "女性"));
		
		ModelAndView mv = new ModelAndView("home/index");
		mv.addObject("fromDtoList", fromDtoList);
		mv.addObject("genderDtoList", genderDtoList);
		mv.addObject("exampleForm", exampleForm);
		return mv;
	}
	
//	@RequestMapping("send")
//	public ModelAndView send(ExampleForm exampleForm) {
//		//ModelAndView mv = new ModelAndView("forward:/homeController/search");
//		ModelAndView mv = new ModelAndView("home/index");
//		Users users = userRelatedServiceImpl.createUsers(exampleForm);
//		String userId = userRelatedServiceImpl.saveUsers(users);
//		UserDetail userDetail = userRelatedServiceImpl.createUserDetail(exampleForm);
//		userRelatedServiceImpl.saveUserDetail(userDetail, userId);
//		return mv;
//	}
	
//	@RequestMapping("saveAll")
//	public ModelAndView saveAll(List<>) {
//		ModelAndView mv = new ModelAndView("home/index");
//		
//		return mv;
//	}
	
	@RequestMapping("search")
	public ModelAndView searchRequest(ExampleForm exampleForm) {
		ModelAndView mv = new ModelAndView("forward:/homeController/reset");		
		return mv;
	}
	
	@RequestMapping("reset")
	public ModelAndView resetForm(ExampleForm exampleForm) {
		ModelAndView mv = new ModelAndView("forward:/homeController/init");
		exampleForm = new ExampleForm();
		mv.addObject("exampleForm", exampleForm);
		return mv;		
	}
}

JUnitで実行するテストクラスを作成。

■/spring-mvc-jsp/src/test/java/UserTest.java

import static org.junit.Assert.*;

import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.util.CollectionUtils;

import com.dao.UserDetailDaoImpl;
import com.dao.UsersDaoImpl;
import com.entity.UserDetail;
import com.entity.Users;
import com.service.UserRelatedServiceImpl;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/spring/spring.xml")
public class UserTest {

	@Autowired
	private UserRelatedServiceImpl userRelatedServiceImpl;
	
	@Autowired
	private UsersDaoImpl usersDaoImpl;
	
	@Autowired
	private UserDetailDaoImpl userDetailDaoImpl;
	
	@Test
	public void test01() throws SQLException {
		// テストデータ作成
		Users users01 = new Users(
				String.format("%13s",
						String.valueOf(usersDaoImpl
								.generateInsertId(userRelatedServiceImpl.createGenerateInsertIdSql("users_seq"))))
						.replace(" ",
								"0")
				, "admin"
				, Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC))
				);
		Users users02= new Users(
				String.format("%13s",
						String.valueOf(usersDaoImpl
								.generateInsertId(userRelatedServiceImpl.createGenerateInsertIdSql("users_seq"))))
						.replace(" ",
								"0")
				, "admin"
				, Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC))
				);
		Users users03 = new Users(
				String.format("%13s",
						String.valueOf(usersDaoImpl
								.generateInsertId(userRelatedServiceImpl.createGenerateInsertIdSql("users_seq"))))
						.replace(" ",
								"0")
				, "admin"
				, Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC))
				);
		Users users04 = new Users(
				String.format("%13s",
						String.valueOf(usersDaoImpl
								.generateInsertId(userRelatedServiceImpl.createGenerateInsertIdSql("users_seq"))))
						.replace(" ",
								"0")
				,"admin"
				, Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC))
				);
		
		List<Users> userList = Arrays.asList(users01, users02, users03, users04);
		
		// usersテーブルへの登録処理を実施
		List<String> userIdList = null;
		try {
			userIdList = userRelatedServiceImpl.saveAllUsers(userList);
		} catch (Exception e) {
			// テスト失敗
			fail(Arrays.toString(e.getStackTrace()));
		}
		
		// テストデータ作成
		final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE;
		final String deleteFlg = "0";
		UserDetail userDetail01 = new UserDetail();
		userDetail01.setUserDetailId(				String.format("%13s",
						String.valueOf(userDetailDaoImpl
								.generateInsertId(userRelatedServiceImpl.createGenerateInsertIdSql("user_detail_seq"))))
						.replace(" ",
								"0"));
		userDetail01.setLastName("鈴木");
		userDetail01.setFirstName("ミッシェル");
		userDetail01.setBirthday(LocalDate.of(1983, 7, 20).format(dateTimeFormatter));
		userDetail01.setGender("man");
		userDetail01.setDeleteFlg(deleteFlg);
		userDetail01.setInsertUser("admin");
		userDetail01.setInsertDate(Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC)));

		UserDetail userDetail02 = new UserDetail();
		userDetail02.setUserDetailId(				String.format("%13s",
						String.valueOf(userDetailDaoImpl
								.generateInsertId(userRelatedServiceImpl.createGenerateInsertIdSql("user_detail_seq"))))
						.replace(" ",
								"0"));
		userDetail02.setLastName("佐藤");
		userDetail02.setFirstName("マイク");
		userDetail02.setBirthday(LocalDate.of(1981, 3, 20).format(dateTimeFormatter));
		userDetail02.setGender("man");
		userDetail02.setDeleteFlg(deleteFlg);
		userDetail02.setInsertUser("admin");
		userDetail02.setInsertDate(Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC)));

		UserDetail userDetail03 = new UserDetail();
		userDetail03.setUserDetailId(				String.format("%13s",
						String.valueOf(userDetailDaoImpl
								.generateInsertId(userRelatedServiceImpl.createGenerateInsertIdSql("user_detail_seq"))))
						.replace(" ",
								"0"));
		userDetail03.setLastName("高橋");
		userDetail03.setFirstName("スティーブ");
		userDetail03.setBirthday(LocalDate.of(1980, 11, 20).format(dateTimeFormatter));
		userDetail03.setGender("man");
		userDetail03.setDeleteFlg(deleteFlg);
		userDetail03.setInsertUser("admin");
		userDetail03.setInsertDate(Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC)));

		UserDetail userDetail04 = new UserDetail();
		userDetail04.setUserDetailId(				String.format("%13s",
						String.valueOf(userDetailDaoImpl
								.generateInsertId(userRelatedServiceImpl.createGenerateInsertIdSql("user_detail_seq"))))
						.replace(" ",
								"0"));
		userDetail04.setLastName("田中");
		userDetail04.setFirstName("メイブ");
		userDetail04.setBirthday(LocalDate.of(1984, 1, 1).format(dateTimeFormatter));
		userDetail04.setGender("man");
		userDetail04.setDeleteFlg(deleteFlg);
		userDetail04.setInsertUser("admin");
		userDetail04.setInsertDate(Timestamp.valueOf(LocalDateTime.ofInstant(OffsetDateTime.now().toInstant(), ZoneOffset.UTC)));

		List<UserDetail> userDetailList = Arrays.asList(userDetail01, userDetail02, userDetail03, userDetail04);

		if(!CollectionUtils.isEmpty(userIdList)) {
			if (userIdList.size() == userDetailList.size()) {
				for (int index = 0; index < userIdList.size(); index++) {
					userDetailList.get(index).setUserId(userIdList.get(index));
				}
			}
		}

		// user_detailテーブルへの登録処理を実施
		try {
			userRelatedServiceImpl.saveAllUserDetail(userDetailList);
		} catch (Exception e) {
			// テスト失敗
			fail(Arrays.toString(e.getStackTrace()));
		}
	}
}

で、テストクラスでエラーが出たら、「JUnit 5 ライブラリーをビルド・パスに追加」を選択します。

 

そうすると、MavenプロジェクトにJUnit 5のライブラリが追加されます。

Eclipseのバージョンによっては、「Java development tools(JDT)」のプラグインとしてJUnitが同梱されるようになっているため、MavenやGradleといったビルドツールに依存関係としてJUnitを追加しなくても利用できるようです。

Eclipse以外の「統合開発環境IDE:Integrated Development Environment)」でもJUnitが同梱されてるのかは分かりません...

話が脱線しましたが、「実行(R)」>「JUnitテスト」を選択してプログラムの動作確認を実施します。

JUnit」タブが緑色になってれば、処理自体でエラーは出てないことが分かります。

テーブルを確認するとテストデータがINSERTされていることが確認できたので、期待した処理が実行されたことが分かりました。

本来であれば、登録された値とテストデータの値の検証もするべきですが、今回は目視で確認してます。

それにしても、Apache Commons DBUtils、使い辛い...

Entityを渡したらそのまま登録してくれる感じにして欲しいかな...

そして、Java 8から導入されたOptionalを使ってみたけど、

medium.com

⇧ 結局、Nullチェックしてるのと変わらんのよな...

もし仮に、使用しているライブラリとかで値が取得できなかった場合に戻り値がOptionalでラップされてない場合、nullが返ってくると思うからNullチェック必要になると思うし、なかなかnullを返すなと言い切れない難しさがあるんかな...

Optionalな値がreturnできない場合に、nullのreturnを禁止ってなった場合に、何の値を返すか決めないといけないから、とりあえずは、開発現場の方針に合わせる感じになるんかな?

まぁ、return null patternを禁止するにせよ、

owasp.org

⇧ インプットのチェックは重要そうなので、どちらにしろ、Optionalを使うにしろ値チェックすることになるのは、まぁ、コーディング的には間違ってはいない...

インプットのチェックしないといけないんだから、return nullしてもええじゃないか、という気持ちも分からんでもない...

2023年3月13日(月)追記:↓ ここから

どうやら、

stackoverflow.com

⇧ queryRunner.batch使っても処理が遅い?みたいですね...

とは言え、仮に100000行の更新処理を実施する場合に、UPDATE文を100000回実行するよりは、queryRunner.batchで1回実行するほうが早いと信じたいけど...

そもそも、queryRunner.batchで処理できるレコード数のMAXがどれぐらいなのか知りたい...

github.com

⇧ 見たところ、JDBCのexecuteBatch()を実行してるっぽいので、確認してみたけども、

docs.oracle.com

⇧ 特にレコード数の制限は記載が無いんですよね。

stackoverflowによると、

stackoverflow.com

stackoverflow.com

⇧ メモリの制限以外には制限が無いらしいというのだけど、目安が無いので、1000レコードに一回実行するなど、方針を決めてあげる必要がありそうですね...

kagamihoge.hatenablog.com

⇧ 上記サイト様によりますと、100レコードに一度のタイミングで実行するのが良い感じですかね?

2023年3月13日(月)追記:↑ ここまで

毎度モヤモヤ感が半端ない...

今回はこのへんで。