diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/AbstractJdbcBatchMetadataDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/AbstractJdbcBatchMetadataDao.java index b755651fb5..ddf9ecb30c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/AbstractJdbcBatchMetadataDao.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/AbstractJdbcBatchMetadataDao.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2023 the original author or authors. + * Copyright 2006-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -29,6 +30,7 @@ * * @author Robert Kasanicky * @author Mahmoud Ben Hassine + * @author Yanming Zhou */ public abstract class AbstractJdbcBatchMetadataDao implements InitializingBean { @@ -47,6 +49,8 @@ public abstract class AbstractJdbcBatchMetadataDao implements InitializingBean { private JdbcOperations jdbcTemplate; + private JdbcClient jdbcClient; + protected String getQuery(String base) { return StringUtils.replace(base, "%PREFIX%", tablePrefix); } @@ -66,12 +70,17 @@ public void setTablePrefix(String tablePrefix) { public void setJdbcTemplate(JdbcOperations jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; + this.jdbcClient = JdbcClient.create(jdbcTemplate); } protected JdbcOperations getJdbcTemplate() { return jdbcTemplate; } + protected JdbcClient getJdbcClient() { + return jdbcClient; + } + public int getClobTypeToUse() { return clobTypeToUse; } @@ -83,6 +92,7 @@ public void setClobTypeToUse(int clobTypeToUse) { @Override public void afterPropertiesSet() throws Exception { Assert.state(jdbcTemplate != null, "JdbcOperations is required"); + Assert.state(jdbcClient != null, "JdbcClient is required"); } } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcExecutionContextDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcExecutionContextDao.java index e585661a80..2747979140 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcExecutionContextDao.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcExecutionContextDao.java @@ -24,6 +24,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -43,6 +44,7 @@ import org.springframework.core.serializer.Serializer; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.lang.NonNull; import org.springframework.util.Assert; @@ -65,7 +67,7 @@ public class JdbcExecutionContextDao extends AbstractJdbcBatchMetadataDao implem private static final String FIND_JOB_EXECUTION_CONTEXT = """ SELECT SHORT_CONTEXT, SERIALIZED_CONTEXT FROM %PREFIX%JOB_EXECUTION_CONTEXT - WHERE JOB_EXECUTION_ID = ? + WHERE JOB_EXECUTION_ID = :executionId """; private static final String INSERT_JOB_EXECUTION_CONTEXT = """ @@ -82,7 +84,7 @@ public class JdbcExecutionContextDao extends AbstractJdbcBatchMetadataDao implem private static final String FIND_STEP_EXECUTION_CONTEXT = """ SELECT SHORT_CONTEXT, SERIALIZED_CONTEXT FROM %PREFIX%STEP_EXECUTION_CONTEXT - WHERE STEP_EXECUTION_ID = ? + WHERE STEP_EXECUTION_ID = :executionId """; private static final String INSERT_STEP_EXECUTION_CONTEXT = """ @@ -98,12 +100,12 @@ public class JdbcExecutionContextDao extends AbstractJdbcBatchMetadataDao implem private static final String DELETE_STEP_EXECUTION_CONTEXT = """ DELETE FROM %PREFIX%STEP_EXECUTION_CONTEXT - WHERE STEP_EXECUTION_ID = ? + WHERE STEP_EXECUTION_ID = :executionId """; private static final String DELETE_JOB_EXECUTION_CONTEXT = """ DELETE FROM %PREFIX%JOB_EXECUTION_CONTEXT - WHERE JOB_EXECUTION_ID = ? + WHERE JOB_EXECUTION_ID = :executionId """; private Charset charset = StandardCharsets.UTF_8; @@ -154,8 +156,10 @@ public ExecutionContext getExecutionContext(JobExecution jobExecution) { Long executionId = jobExecution.getId(); Assert.notNull(executionId, "ExecutionId must not be null."); - try (Stream stream = getJdbcTemplate().queryForStream(getQuery(FIND_JOB_EXECUTION_CONTEXT), - new ExecutionContextRowMapper(), executionId)) { + try (Stream stream = getJdbcClient().sql(getQuery(FIND_JOB_EXECUTION_CONTEXT)) + .param("executionId", executionId) + .query(new ExecutionContextRowMapper()) + .stream()) { return stream.findFirst().orElseGet(ExecutionContext::new); } } @@ -165,8 +169,10 @@ public ExecutionContext getExecutionContext(StepExecution stepExecution) { Long executionId = stepExecution.getId(); Assert.notNull(executionId, "ExecutionId must not be null."); - try (Stream stream = getJdbcTemplate().queryForStream(getQuery(FIND_STEP_EXECUTION_CONTEXT), - new ExecutionContextRowMapper(), executionId)) { + try (Stream stream = getJdbcClient().sql(getQuery(FIND_STEP_EXECUTION_CONTEXT)) + .param("executionId", executionId) + .query(new ExecutionContextRowMapper()) + .stream()) { return stream.findFirst().orElseGet(ExecutionContext::new); } } @@ -248,7 +254,7 @@ public void saveExecutionContexts(Collection stepExecutions) { */ @Override public void deleteExecutionContext(JobExecution jobExecution) { - getJdbcTemplate().update(getQuery(DELETE_JOB_EXECUTION_CONTEXT), jobExecution.getId()); + getJdbcClient().sql(getQuery(DELETE_JOB_EXECUTION_CONTEXT)).param("executionId", jobExecution.getId()).update(); } /** @@ -257,7 +263,9 @@ public void deleteExecutionContext(JobExecution jobExecution) { */ @Override public void deleteExecutionContext(StepExecution stepExecution) { - getJdbcTemplate().update(getQuery(DELETE_STEP_EXECUTION_CONTEXT), stepExecution.getId()); + getJdbcClient().sql(getQuery(DELETE_STEP_EXECUTION_CONTEXT)) + .param("executionId", stepExecution.getId()) + .update(); } @Override @@ -286,16 +294,13 @@ private void persistSerializedContext(final Long executionId, String serializedC longContext = null; } - getJdbcTemplate().update(getQuery(sql), ps -> { - ps.setString(1, shortContext); - if (longContext != null) { - ps.setString(2, longContext); - } - else { - ps.setNull(2, getClobTypeToUse()); - } - ps.setLong(3, executionId); - }); + getJdbcClient().sql(getQuery(sql)) + // @formatter:off + .param(1, shortContext, Types.VARCHAR) + .param(2, longContext, getClobTypeToUse()) + .param(3, executionId, Types.BIGINT) + // @formatter:on + .update(); } /** diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobExecutionDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobExecutionDao.java index 012f42982f..14b7b3fa11 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobExecutionDao.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobExecutionDao.java @@ -92,13 +92,13 @@ public class JdbcJobExecutionDao extends AbstractJdbcBatchMetadataDao implements private static final String CHECK_JOB_EXECUTION_EXISTS = """ SELECT COUNT(*) FROM %PREFIX%JOB_EXECUTION - WHERE JOB_EXECUTION_ID = ? + WHERE JOB_EXECUTION_ID = :executionId """; private static final String GET_STATUS = """ SELECT STATUS FROM %PREFIX%JOB_EXECUTION - WHERE JOB_EXECUTION_ID = ? + WHERE JOB_EXECUTION_ID = :executionId """; private static final String UPDATE_JOB_EXECUTION = """ @@ -113,29 +113,29 @@ SELECT COUNT(*) """; private static final String FIND_JOB_EXECUTIONS = GET_JOB_EXECUTIONS - + " WHERE JOB_INSTANCE_ID = ? ORDER BY JOB_EXECUTION_ID DESC"; + + " WHERE JOB_INSTANCE_ID = :jobInstanceId ORDER BY JOB_EXECUTION_ID DESC"; private static final String GET_LAST_EXECUTION = GET_JOB_EXECUTIONS - + " WHERE JOB_INSTANCE_ID = ? AND JOB_EXECUTION_ID IN (SELECT MAX(JOB_EXECUTION_ID) FROM %PREFIX%JOB_EXECUTION E2 WHERE E2.JOB_INSTANCE_ID = ?)"; + + " WHERE JOB_INSTANCE_ID = :jobInstanceId AND JOB_EXECUTION_ID IN (SELECT MAX(JOB_EXECUTION_ID) FROM %PREFIX%JOB_EXECUTION E2 WHERE E2.JOB_INSTANCE_ID = :jobInstanceId)"; - private static final String GET_EXECUTION_BY_ID = GET_JOB_EXECUTIONS + " WHERE JOB_EXECUTION_ID = ?"; + private static final String GET_EXECUTION_BY_ID = GET_JOB_EXECUTIONS + " WHERE JOB_EXECUTION_ID = :jobExecutionId"; private static final String GET_RUNNING_EXECUTIONS = """ SELECT E.JOB_EXECUTION_ID, E.START_TIME, E.END_TIME, E.STATUS, E.EXIT_CODE, E.EXIT_MESSAGE, E.CREATE_TIME, E.LAST_UPDATED, E.VERSION, E.JOB_INSTANCE_ID FROM %PREFIX%JOB_EXECUTION E, %PREFIX%JOB_INSTANCE I - WHERE E.JOB_INSTANCE_ID=I.JOB_INSTANCE_ID AND I.JOB_NAME=? AND E.STATUS IN ('STARTING', 'STARTED', 'STOPPING') + WHERE E.JOB_INSTANCE_ID=I.JOB_INSTANCE_ID AND I.JOB_NAME=:jobName AND E.STATUS IN ('STARTING', 'STARTED', 'STOPPING') """; private static final String CURRENT_VERSION_JOB_EXECUTION = """ SELECT VERSION FROM %PREFIX%JOB_EXECUTION - WHERE JOB_EXECUTION_ID=? + WHERE JOB_EXECUTION_ID= :executionId """; private static final String FIND_PARAMS_FROM_ID = """ SELECT JOB_EXECUTION_ID, PARAMETER_NAME, PARAMETER_TYPE, PARAMETER_VALUE, IDENTIFYING FROM %PREFIX%JOB_EXECUTION_PARAMS - WHERE JOB_EXECUTION_ID = ? + WHERE JOB_EXECUTION_ID = :executionId """; private static final String CREATE_JOB_PARAMETERS = """ @@ -145,12 +145,12 @@ SELECT COUNT(*) private static final String DELETE_JOB_EXECUTION = """ DELETE FROM %PREFIX%JOB_EXECUTION - WHERE JOB_EXECUTION_ID = ? AND VERSION = ? + WHERE JOB_EXECUTION_ID = :executionId AND VERSION = :version """; private static final String DELETE_JOB_EXECUTION_PARAMETERS = """ DELETE FROM %PREFIX%JOB_EXECUTION_PARAMS - WHERE JOB_EXECUTION_ID = ? + WHERE JOB_EXECUTION_ID = :executionId """; private int exitMessageLength = DEFAULT_EXIT_MESSAGE_LENGTH; @@ -213,7 +213,10 @@ public List findJobExecutions(final JobInstance job) { Assert.notNull(job, "Job cannot be null."); Assert.notNull(job.getId(), "Job Id cannot be null."); - return getJdbcTemplate().query(getQuery(FIND_JOB_EXECUTIONS), new JobExecutionRowMapper(job), job.getId()); + return getJdbcClient().sql(getQuery(FIND_JOB_EXECUTIONS)) + .param("jobInstanceId", job.getId()) + .query(new JobExecutionRowMapper(job)) + .list(); } /** @@ -240,12 +243,21 @@ public void saveJobExecution(JobExecution jobExecution) { : Timestamp.valueOf(jobExecution.getCreateTime()); Timestamp lastUpdated = jobExecution.getLastUpdated() == null ? null : Timestamp.valueOf(jobExecution.getLastUpdated()); - Object[] parameters = new Object[] { jobExecution.getId(), jobExecution.getJobId(), startTime, endTime, - jobExecution.getStatus().toString(), jobExecution.getExitStatus().getExitCode(), - jobExecution.getExitStatus().getExitDescription(), jobExecution.getVersion(), createTime, lastUpdated }; - getJdbcTemplate().update(getQuery(SAVE_JOB_EXECUTION), parameters, - new int[] { Types.BIGINT, Types.BIGINT, Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.VARCHAR, - Types.VARCHAR, Types.INTEGER, Types.TIMESTAMP, Types.TIMESTAMP }); + + getJdbcClient().sql(getQuery(SAVE_JOB_EXECUTION)) + // @formatter:off + .param(1, jobExecution.getId(), Types.BIGINT) + .param(2, jobExecution.getJobId(), Types.BIGINT) + .param(3, startTime, Types.TIMESTAMP) + .param(4, endTime, Types.TIMESTAMP) + .param(5, jobExecution.getStatus().toString(), Types.VARCHAR) + .param(6, jobExecution.getExitStatus().getExitCode(), Types.VARCHAR) + .param(7, jobExecution.getExitStatus().getExitDescription(), Types.VARCHAR) + .param(8, jobExecution.getVersion(), Types.INTEGER) + .param(9, createTime, Types.TIMESTAMP) + .param(10, lastUpdated, Types.TIMESTAMP) + // @formatter:on + .update(); insertJobParameters(jobExecution.getId(), jobExecution.getJobParameters()); } @@ -298,27 +310,38 @@ public void updateJobExecution(JobExecution jobExecution) { : Timestamp.valueOf(jobExecution.getCreateTime()); Timestamp lastUpdated = jobExecution.getLastUpdated() == null ? null : Timestamp.valueOf(jobExecution.getLastUpdated()); - Object[] parameters = new Object[] { startTime, endTime, jobExecution.getStatus().toString(), - jobExecution.getExitStatus().getExitCode(), exitDescription, createTime, lastUpdated, - jobExecution.getId(), jobExecution.getVersion() }; // Check if given JobExecution's Id already exists, if none is found // it // is invalid and // an exception should be thrown. - if (getJdbcTemplate().queryForObject(getQuery(CHECK_JOB_EXECUTION_EXISTS), Integer.class, - new Object[] { jobExecution.getId() }) != 1) { + if (getJdbcClient().sql(getQuery(CHECK_JOB_EXECUTION_EXISTS)) + .param("executionId", jobExecution.getId()) + .query(Integer.class) + .single() != 1) { throw new NoSuchObjectException("Invalid JobExecution, ID " + jobExecution.getId() + " not found."); } - int count = getJdbcTemplate().update(getQuery(UPDATE_JOB_EXECUTION), parameters, - new int[] { Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR, - Types.TIMESTAMP, Types.TIMESTAMP, Types.BIGINT, Types.INTEGER }); + int count = getJdbcClient().sql(getQuery(UPDATE_JOB_EXECUTION)) + // @formatter:off + .param(1, startTime, Types.TIMESTAMP) + .param(2, endTime, Types.TIMESTAMP) + .param(3, jobExecution.getStatus().toString(), Types.VARCHAR) + .param(4, jobExecution.getExitStatus().getExitCode(), Types.VARCHAR) + .param(5, exitDescription, Types.VARCHAR) + .param(6, createTime, Types.TIMESTAMP) + .param(7, lastUpdated, Types.TIMESTAMP) + .param(8, jobExecution.getId(), Types.BIGINT) + .param(9, jobExecution.getVersion(), Types.INTEGER) + // @formatter:on + .update(); // Avoid concurrent modifications... if (count == 0) { - int currentVersion = getJdbcTemplate().queryForObject(getQuery(CURRENT_VERSION_JOB_EXECUTION), - Integer.class, new Object[] { jobExecution.getId() }); + int currentVersion = getJdbcClient().sql(getQuery(CURRENT_VERSION_JOB_EXECUTION)) + .param("executionId", jobExecution.getId()) + .query(Integer.class) + .single(); throw new OptimisticLockingFailureException( "Attempt to update job execution id=" + jobExecution.getId() + " with wrong version (" + jobExecution.getVersion() + "), where current version is " + currentVersion); @@ -337,8 +360,10 @@ public JobExecution getLastJobExecution(JobInstance jobInstance) { Long id = jobInstance.getId(); - try (Stream stream = getJdbcTemplate().queryForStream(getQuery(GET_LAST_EXECUTION), - new JobExecutionRowMapper(jobInstance), id, id)) { + try (Stream stream = getJdbcClient().sql(getQuery(GET_LAST_EXECUTION)) + .param("jobInstanceId", id) + .query(new JobExecutionRowMapper(jobInstance)) + .stream()) { return stream.findFirst().orElse(null); } } @@ -346,13 +371,11 @@ public JobExecution getLastJobExecution(JobInstance jobInstance) { @Override @Nullable public JobExecution getJobExecution(Long executionId) { - try { - return getJdbcTemplate().queryForObject(getQuery(GET_EXECUTION_BY_ID), new JobExecutionRowMapper(), - executionId); - } - catch (EmptyResultDataAccessException e) { - return null; - } + return getJdbcClient().sql(getQuery(GET_EXECUTION_BY_ID)) + .param("jobExecutionId", executionId) + .query(new JobExecutionRowMapper()) + .optional() + .orElse(null); } @Override @@ -363,18 +386,23 @@ public Set findRunningJobExecutions(String jobName) { JobExecutionRowMapper mapper = new JobExecutionRowMapper(); result.add(mapper.mapRow(rs, 0)); }; - getJdbcTemplate().query(getQuery(GET_RUNNING_EXECUTIONS), handler, jobName); + getJdbcClient().sql(getQuery(GET_RUNNING_EXECUTIONS)).param("jobName", jobName).query(handler); return result; } @Override public void synchronizeStatus(JobExecution jobExecution) { - int currentVersion = getJdbcTemplate().queryForObject(getQuery(CURRENT_VERSION_JOB_EXECUTION), Integer.class, - jobExecution.getId()); + int currentVersion = getJdbcClient().sql(getQuery(CURRENT_VERSION_JOB_EXECUTION)) + .param("executionId", jobExecution.getId()) + .query(Integer.class) + .single(); if (currentVersion != jobExecution.getVersion()) { - String status = getJdbcTemplate().queryForObject(getQuery(GET_STATUS), String.class, jobExecution.getId()); + String status = getJdbcClient().sql(getQuery(GET_STATUS)) + .param("executionId", jobExecution.getId()) + .query(String.class) + .single(); jobExecution.upgradeStatus(BatchStatus.valueOf(status)); jobExecution.setVersion(currentVersion); } @@ -386,8 +414,10 @@ public void synchronizeStatus(JobExecution jobExecution) { */ @Override public void deleteJobExecution(JobExecution jobExecution) { - int count = getJdbcTemplate().update(getQuery(DELETE_JOB_EXECUTION), jobExecution.getId(), - jobExecution.getVersion()); + int count = getJdbcClient().sql(getQuery(DELETE_JOB_EXECUTION)) + .param("executionId", jobExecution.getId()) + .param("version", jobExecution.getVersion()) + .update(); if (count == 0) { throw new OptimisticLockingFailureException("Attempt to delete job execution id=" + jobExecution.getId() @@ -401,7 +431,9 @@ public void deleteJobExecution(JobExecution jobExecution) { */ @Override public void deleteJobExecutionParameters(JobExecution jobExecution) { - getJdbcTemplate().update(getQuery(DELETE_JOB_EXECUTION_PARAMETERS), jobExecution.getId()); + getJdbcClient().sql(getQuery(DELETE_JOB_EXECUTION_PARAMETERS)) + .param("executionId", jobExecution.getId()) + .update(); } /** @@ -468,7 +500,7 @@ protected JobParameters getJobParameters(Long executionId) { map.put(parameterName, jobParameter); }; - getJdbcTemplate().query(getQuery(FIND_PARAMS_FROM_ID), handler, executionId); + getJdbcClient().sql(getQuery(FIND_PARAMS_FROM_ID)).param("executionId", executionId).query(handler); return new JobParameters(map); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobInstanceDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobInstanceDao.java index 73cb1cfa99..a8f153f2c6 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobInstanceDao.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobInstanceDao.java @@ -75,30 +75,30 @@ public class JdbcJobInstanceDao extends AbstractJdbcBatchMetadataDao implements private static final String FIND_JOBS_WITH_NAME = """ SELECT JOB_INSTANCE_ID, JOB_NAME FROM %PREFIX%JOB_INSTANCE - WHERE JOB_NAME = ? + WHERE JOB_NAME = :jobName """; - private static final String FIND_JOBS_WITH_KEY = FIND_JOBS_WITH_NAME + " AND JOB_KEY = ?"; + private static final String FIND_JOBS_WITH_KEY = FIND_JOBS_WITH_NAME + " AND JOB_KEY = :jobKey"; private static final String COUNT_JOBS_WITH_NAME = """ SELECT COUNT(*) FROM %PREFIX%JOB_INSTANCE - WHERE JOB_NAME = ? + WHERE JOB_NAME = :jobName """; private static final String FIND_JOBS_WITH_EMPTY_KEY = FIND_JOBS_WITH_NAME - + " AND (JOB_KEY = ? OR JOB_KEY IS NULL)"; + + " AND (JOB_KEY = :jobKey OR JOB_KEY IS NULL)"; private static final String GET_JOB_FROM_ID = """ SELECT JOB_INSTANCE_ID, JOB_NAME, JOB_KEY, VERSION FROM %PREFIX%JOB_INSTANCE - WHERE JOB_INSTANCE_ID = ? + WHERE JOB_INSTANCE_ID = :jobInstanceId """; private static final String GET_JOB_FROM_EXECUTION_ID = """ SELECT JI.JOB_INSTANCE_ID, JOB_NAME, JOB_KEY, JI.VERSION FROM %PREFIX%JOB_INSTANCE JI, %PREFIX%JOB_EXECUTION JE - WHERE JOB_EXECUTION_ID = ? AND JI.JOB_INSTANCE_ID = JE.JOB_INSTANCE_ID + WHERE JOB_EXECUTION_ID = :jobExecutionId AND JI.JOB_INSTANCE_ID = JE.JOB_INSTANCE_ID """; private static final String FIND_JOB_NAMES = """ @@ -110,19 +110,19 @@ SELECT COUNT(*) private static final String FIND_LAST_JOBS_BY_NAME = """ SELECT JOB_INSTANCE_ID, JOB_NAME FROM %PREFIX%JOB_INSTANCE - WHERE JOB_NAME LIKE ? + WHERE JOB_NAME LIKE :jobName ORDER BY JOB_INSTANCE_ID DESC """; private static final String FIND_LAST_JOB_INSTANCE_BY_JOB_NAME = """ SELECT JOB_INSTANCE_ID, JOB_NAME FROM %PREFIX%JOB_INSTANCE I1 - WHERE I1.JOB_NAME = ? AND I1.JOB_INSTANCE_ID = (SELECT MAX(I2.JOB_INSTANCE_ID) FROM %PREFIX%JOB_INSTANCE I2 WHERE I2.JOB_NAME = ?) + WHERE I1.JOB_NAME = :jobName AND I1.JOB_INSTANCE_ID = (SELECT MAX(I2.JOB_INSTANCE_ID) FROM %PREFIX%JOB_INSTANCE I2 WHERE I2.JOB_NAME = :jobName) """; private static final String DELETE_JOB_INSTANCE = """ DELETE FROM %PREFIX%JOB_INSTANCE - WHERE JOB_INSTANCE_ID = ? AND VERSION = ? + WHERE JOB_INSTANCE_ID = :jobInstanceId AND VERSION = :version """; private DataFieldMaxValueIncrementer jobInstanceIncrementer; @@ -150,10 +150,14 @@ public JobInstance createJobInstance(String jobName, JobParameters jobParameters JobInstance jobInstance = new JobInstance(jobInstanceId, jobName); jobInstance.incrementVersion(); - Object[] parameters = new Object[] { jobInstanceId, jobName, jobKeyGenerator.generateKey(jobParameters), - jobInstance.getVersion() }; - getJdbcTemplate().update(getQuery(CREATE_JOB_INSTANCE), parameters, - new int[] { Types.BIGINT, Types.VARCHAR, Types.VARCHAR, Types.INTEGER }); + getJdbcClient().sql(getQuery(CREATE_JOB_INSTANCE)) + // @formatter:off + .param(1, jobInstanceId, Types.BIGINT) + .param(2, jobName, Types.VARCHAR) + .param(3, jobKeyGenerator.generateKey(jobParameters), Types.VARCHAR) + .param(4, jobInstance.getVersion(), Types.INTEGER) + // @formatter:on + .update(); return jobInstance; } @@ -176,9 +180,12 @@ public JobInstance getJobInstance(final String jobName, final JobParameters jobP RowMapper rowMapper = new JobInstanceRowMapper(); - try (Stream stream = getJdbcTemplate().queryForStream( - getQuery(StringUtils.hasLength(jobKey) ? FIND_JOBS_WITH_KEY : FIND_JOBS_WITH_EMPTY_KEY), rowMapper, - jobName, jobKey)) { + try (Stream stream = getJdbcClient() + .sql(getQuery(StringUtils.hasLength(jobKey) ? FIND_JOBS_WITH_KEY : FIND_JOBS_WITH_EMPTY_KEY)) + .param("jobName", jobName) + .param("jobKey", jobKey) + .query(rowMapper) + .stream()) { return stream.findFirst().orElse(null); } @@ -187,19 +194,16 @@ public JobInstance getJobInstance(final String jobName, final JobParameters jobP @Override @Nullable public JobInstance getJobInstance(@Nullable Long instanceId) { - - try { - return getJdbcTemplate().queryForObject(getQuery(GET_JOB_FROM_ID), new JobInstanceRowMapper(), instanceId); - } - catch (EmptyResultDataAccessException e) { - return null; - } - + return getJdbcClient().sql(getQuery(GET_JOB_FROM_ID)) + .param("jobInstanceId", instanceId) + .query(new JobInstanceRowMapper()) + .optional() + .orElse(null); } @Override public List getJobNames() { - return getJdbcTemplate().query(getQuery(FIND_JOB_NAMES), (rs, rowNum) -> rs.getString(1)); + return getJdbcClient().sql(getQuery(FIND_JOB_NAMES)).query(String.class).list(); } @Override @@ -229,43 +233,32 @@ public List extractData(ResultSet rs) throws SQLException, DataAcce jobName = jobName.replaceAll("\\" + STAR_WILDCARD, SQL_WILDCARD); } - return getJdbcTemplate().query(getQuery(FIND_LAST_JOBS_BY_NAME), extractor, jobName); + return getJdbcClient().sql(getQuery(FIND_LAST_JOBS_BY_NAME)).param("jobName", jobName).query(extractor); } @Override @Nullable public JobInstance getLastJobInstance(String jobName) { - try { - return getJdbcTemplate().queryForObject(getQuery(FIND_LAST_JOB_INSTANCE_BY_JOB_NAME), - new JobInstanceRowMapper(), jobName, jobName); - } - catch (EmptyResultDataAccessException e) { - return null; - } + return getJdbcClient().sql(getQuery(FIND_LAST_JOB_INSTANCE_BY_JOB_NAME)) + .param("jobName", jobName) + .query(new JobInstanceRowMapper()) + .optional() + .orElse(null); } @Override @Nullable public JobInstance getJobInstance(JobExecution jobExecution) { - - try { - return getJdbcTemplate().queryForObject(getQuery(GET_JOB_FROM_EXECUTION_ID), new JobInstanceRowMapper(), - jobExecution.getId()); - } - catch (EmptyResultDataAccessException e) { - return null; - } + return getJdbcClient().sql(getQuery(GET_JOB_FROM_EXECUTION_ID)) + .param("jobExecutionId", jobExecution.getId()) + .query(new JobInstanceRowMapper()) + .optional() + .orElse(null); } @Override public long getJobInstanceCount(@Nullable String jobName) throws NoSuchJobException { - - try { - return getJdbcTemplate().queryForObject(getQuery(COUNT_JOBS_WITH_NAME), Long.class, jobName); - } - catch (EmptyResultDataAccessException e) { - throw new NoSuchJobException("No job instances were found for job name " + jobName); - } + return getJdbcClient().sql(getQuery(COUNT_JOBS_WITH_NAME)).param("jobName", jobName).query(Long.class).single(); } /** @@ -274,8 +267,10 @@ public long getJobInstanceCount(@Nullable String jobName) throws NoSuchJobExcept */ @Override public void deleteJobInstance(JobInstance jobInstance) { - int count = getJdbcTemplate().update(getQuery(DELETE_JOB_INSTANCE), jobInstance.getId(), - jobInstance.getVersion()); + int count = getJdbcClient().sql(getQuery(DELETE_JOB_INSTANCE)) + .param("jobInstanceId", jobInstance.getId()) + .param("version", jobInstance.getVersion()) + .update(); if (count == 0) { throw new OptimisticLockingFailureException("Attempt to delete job instance id=" + jobInstance.getId() diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcStepExecutionDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcStepExecutionDao.java index a9c910eb09..37fc35e5f2 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcStepExecutionDao.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/jdbc/JdbcStepExecutionDao.java @@ -45,6 +45,7 @@ import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.jdbc.core.BatchPreparedStatementSetter; import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -93,32 +94,33 @@ public class JdbcStepExecutionDao extends AbstractJdbcBatchMetadataDao implement """; private static final String GET_STEP_EXECUTIONS = GET_RAW_STEP_EXECUTIONS - + " WHERE JOB_EXECUTION_ID = ? ORDER BY STEP_EXECUTION_ID"; + + " WHERE JOB_EXECUTION_ID = :jobExecutionId ORDER BY STEP_EXECUTION_ID"; - private static final String GET_STEP_EXECUTION = GET_RAW_STEP_EXECUTIONS + " WHERE STEP_EXECUTION_ID = ?"; + private static final String GET_STEP_EXECUTION = GET_RAW_STEP_EXECUTIONS + + " WHERE STEP_EXECUTION_ID = :stepExecutionId"; private static final String GET_LAST_STEP_EXECUTION = """ SELECT SE.STEP_EXECUTION_ID, SE.STEP_NAME, SE.START_TIME, SE.END_TIME, SE.STATUS, SE.COMMIT_COUNT, SE.READ_COUNT, SE.FILTER_COUNT, SE.WRITE_COUNT, SE.EXIT_CODE, SE.EXIT_MESSAGE, SE.READ_SKIP_COUNT, SE.WRITE_SKIP_COUNT, SE.PROCESS_SKIP_COUNT, SE.ROLLBACK_COUNT, SE.LAST_UPDATED, SE.VERSION, SE.CREATE_TIME, JE.JOB_EXECUTION_ID, JE.START_TIME, JE.END_TIME, JE.STATUS, JE.EXIT_CODE, JE.EXIT_MESSAGE, JE.CREATE_TIME, JE.LAST_UPDATED, JE.VERSION FROM %PREFIX%JOB_EXECUTION JE JOIN %PREFIX%STEP_EXECUTION SE ON SE.JOB_EXECUTION_ID = JE.JOB_EXECUTION_ID - WHERE JE.JOB_INSTANCE_ID = ? AND SE.STEP_NAME = ? + WHERE JE.JOB_INSTANCE_ID = :jobInstanceId AND SE.STEP_NAME = :stepName """; private static final String CURRENT_VERSION_STEP_EXECUTION = """ SELECT VERSION FROM %PREFIX%STEP_EXECUTION - WHERE STEP_EXECUTION_ID=? + WHERE STEP_EXECUTION_ID = :stepExecutionId """; private static final String COUNT_STEP_EXECUTIONS = """ SELECT COUNT(*) FROM %PREFIX%JOB_EXECUTION JE JOIN %PREFIX%STEP_EXECUTION SE ON SE.JOB_EXECUTION_ID = JE.JOB_EXECUTION_ID - WHERE JE.JOB_INSTANCE_ID = ? AND SE.STEP_NAME = ? + WHERE JE.JOB_INSTANCE_ID = :jobInstanceId AND SE.STEP_NAME = :stepName """; private static final String DELETE_STEP_EXECUTION = """ DELETE FROM %PREFIX%STEP_EXECUTION - WHERE STEP_EXECUTION_ID = ? and VERSION = ? + WHERE STEP_EXECUTION_ID = :stepExecutionId and VERSION = :version """; private static final Comparator BY_CREATE_TIME_DESC_ID_DESC = Comparator @@ -168,7 +170,11 @@ public void saveStepExecution(StepExecution stepExecution) { parameterTypes[i] = (Integer) parameters.get(1)[i]; } - getJdbcTemplate().update(getQuery(SAVE_STEP_EXECUTION), parameterValues, parameterTypes); + JdbcClient.StatementSpec statement = getJdbcClient().sql(getQuery(SAVE_STEP_EXECUTION)); + for (int i = 0; i < parameterTypes.length; i++) { + statement.param(i + 1, parameterValues[i], parameterTypes[i]); + } + statement.update(); } /** @@ -277,21 +283,34 @@ public void updateStepExecution(StepExecution stepExecution) { : Timestamp.valueOf(stepExecution.getEndTime()); Timestamp lastUpdated = stepExecution.getLastUpdated() == null ? null : Timestamp.valueOf(stepExecution.getLastUpdated()); - Object[] parameters = new Object[] { startTime, endTime, stepExecution.getStatus().toString(), - stepExecution.getCommitCount(), stepExecution.getReadCount(), stepExecution.getFilterCount(), - stepExecution.getWriteCount(), stepExecution.getExitStatus().getExitCode(), exitDescription, - stepExecution.getReadSkipCount(), stepExecution.getProcessSkipCount(), - stepExecution.getWriteSkipCount(), stepExecution.getRollbackCount(), lastUpdated, - stepExecution.getId(), stepExecution.getVersion() }; - int count = getJdbcTemplate().update(getQuery(UPDATE_STEP_EXECUTION), parameters, - new int[] { Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.BIGINT, Types.BIGINT, - Types.BIGINT, Types.BIGINT, Types.VARCHAR, Types.VARCHAR, Types.BIGINT, Types.BIGINT, - Types.BIGINT, Types.BIGINT, Types.TIMESTAMP, Types.BIGINT, Types.INTEGER }); + + int count = getJdbcClient().sql(getQuery(UPDATE_STEP_EXECUTION)) + // @formatter:off + .param(1, startTime, Types.TIMESTAMP) + .param(2, endTime, Types.TIMESTAMP) + .param(3, stepExecution.getStatus().toString(), Types.VARCHAR) + .param(4, stepExecution.getCommitCount(), Types.BIGINT) + .param(5, stepExecution.getReadCount(), Types.BIGINT) + .param(6, stepExecution.getFilterCount(), Types.BIGINT) + .param(7, stepExecution.getWriteCount(), Types.BIGINT) + .param(8, stepExecution.getExitStatus().getExitCode(), Types.VARCHAR) + .param(9, exitDescription, Types.VARCHAR) + .param(10, stepExecution.getReadSkipCount(), Types.BIGINT) + .param(11, stepExecution.getProcessSkipCount(), Types.BIGINT) + .param(12, stepExecution.getWriteSkipCount(), Types.BIGINT) + .param(13, stepExecution.getRollbackCount(), Types.BIGINT) + .param(14, lastUpdated, Types.TIMESTAMP) + .param(15, stepExecution.getId(), Types.BIGINT) + .param(16, stepExecution.getVersion(), Types.INTEGER) + // @formatter:on + .update(); // Avoid concurrent modifications... if (count == 0) { - int currentVersion = getJdbcTemplate().queryForObject(getQuery(CURRENT_VERSION_STEP_EXECUTION), - Integer.class, stepExecution.getId()); + int currentVersion = getJdbcClient().sql(getQuery(CURRENT_VERSION_STEP_EXECUTION)) + .param("stepExecutionId", stepExecution.getId()) + .query(Integer.class) + .single(); throw new OptimisticLockingFailureException( "Attempt to update step execution id=" + stepExecution.getId() + " with wrong version (" + stepExecution.getVersion() + "), where current version is " + currentVersion); @@ -327,26 +346,32 @@ private String truncateExitDescription(String description) { @Override @Nullable public StepExecution getStepExecution(JobExecution jobExecution, Long stepExecutionId) { - try (Stream stream = getJdbcTemplate().queryForStream(getQuery(GET_STEP_EXECUTION), - new StepExecutionRowMapper(jobExecution), stepExecutionId)) { + try (Stream stream = getJdbcClient().sql(getQuery(GET_STEP_EXECUTION)) + .param("stepExecutionId", stepExecutionId) + .query(new StepExecutionRowMapper(jobExecution)) + .stream()) { return stream.findFirst().orElse(null); } } @Override public StepExecution getLastStepExecution(JobInstance jobInstance, String stepName) { - List executions = getJdbcTemplate().query(getQuery(GET_LAST_STEP_EXECUTION), (rs, rowNum) -> { - Long jobExecutionId = rs.getLong(19); - JobExecution jobExecution = new JobExecution(jobExecutionId); - jobExecution.setStartTime(rs.getTimestamp(20) == null ? null : rs.getTimestamp(20).toLocalDateTime()); - jobExecution.setEndTime(rs.getTimestamp(21) == null ? null : rs.getTimestamp(21).toLocalDateTime()); - jobExecution.setStatus(BatchStatus.valueOf(rs.getString(22))); - jobExecution.setExitStatus(new ExitStatus(rs.getString(23), rs.getString(24))); - jobExecution.setCreateTime(rs.getTimestamp(25) == null ? null : rs.getTimestamp(25).toLocalDateTime()); - jobExecution.setLastUpdated(rs.getTimestamp(26) == null ? null : rs.getTimestamp(26).toLocalDateTime()); - jobExecution.setVersion(rs.getInt(27)); - return new StepExecutionRowMapper(jobExecution).mapRow(rs, rowNum); - }, jobInstance.getInstanceId(), stepName); + List executions = getJdbcClient().sql(getQuery(GET_LAST_STEP_EXECUTION)) + .param("jobInstanceId", jobInstance.getInstanceId()) + .param("stepName", stepName) + .query((rs, rowNum) -> { + Long jobExecutionId = rs.getLong(19); + JobExecution jobExecution = new JobExecution(jobExecutionId); + jobExecution.setStartTime(rs.getTimestamp(20) == null ? null : rs.getTimestamp(20).toLocalDateTime()); + jobExecution.setEndTime(rs.getTimestamp(21) == null ? null : rs.getTimestamp(21).toLocalDateTime()); + jobExecution.setStatus(BatchStatus.valueOf(rs.getString(22))); + jobExecution.setExitStatus(new ExitStatus(rs.getString(23), rs.getString(24))); + jobExecution.setCreateTime(rs.getTimestamp(25) == null ? null : rs.getTimestamp(25).toLocalDateTime()); + jobExecution.setLastUpdated(rs.getTimestamp(26) == null ? null : rs.getTimestamp(26).toLocalDateTime()); + jobExecution.setVersion(rs.getInt(27)); + return new StepExecutionRowMapper(jobExecution).mapRow(rs, rowNum); + }) + .list(); executions.sort(BY_CREATE_TIME_DESC_ID_DESC); if (executions.isEmpty()) { return null; @@ -358,14 +383,19 @@ public StepExecution getLastStepExecution(JobInstance jobInstance, String stepNa @Override public void addStepExecutions(JobExecution jobExecution) { - getJdbcTemplate().query(getQuery(GET_STEP_EXECUTIONS), new StepExecutionRowMapper(jobExecution), - jobExecution.getId()); + getJdbcClient().sql(getQuery(GET_STEP_EXECUTIONS)) + .param("jobExecutionId", jobExecution.getId()) + .query(new StepExecutionRowMapper(jobExecution)) + .list(); } @Override public long countStepExecutions(JobInstance jobInstance, String stepName) { - return getJdbcTemplate().queryForObject(getQuery(COUNT_STEP_EXECUTIONS), Long.class, - jobInstance.getInstanceId(), stepName); + return getJdbcClient().sql(getQuery(COUNT_STEP_EXECUTIONS)) + .param("jobInstanceId", jobInstance.getInstanceId()) + .param("stepName", stepName) + .query(Long.class) + .single(); } /** @@ -374,8 +404,10 @@ public long countStepExecutions(JobInstance jobInstance, String stepName) { */ @Override public void deleteStepExecution(StepExecution stepExecution) { - int count = getJdbcTemplate().update(getQuery(DELETE_STEP_EXECUTION), stepExecution.getId(), - stepExecution.getVersion()); + int count = getJdbcClient().sql(getQuery(DELETE_STEP_EXECUTION)) + .param("stepExecutionId", stepExecution.getId()) + .param("version", stepExecution.getVersion()) + .update(); if (count == 0) { throw new OptimisticLockingFailureException("Attempt to delete step execution id=" + stepExecution.getId() diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobDaoQueryTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobDaoQueryTests.java index cd09c036e9..68e91a4893 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobDaoQueryTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/jdbc/JdbcJobDaoQueryTests.java @@ -15,11 +15,12 @@ */ package org.springframework.batch.core.repository.dao.jdbc; -import java.util.ArrayList; -import java.util.List; +import java.sql.Connection; +import java.sql.PreparedStatement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.JobInstance; import org.springframework.batch.core.job.parameters.JobParameters; @@ -27,7 +28,12 @@ import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; -import static org.junit.jupiter.api.Assertions.assertEquals; +import javax.sql.DataSource; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; import static org.springframework.test.util.AssertionErrors.assertTrue; /** @@ -36,12 +42,19 @@ */ class JdbcJobDaoQueryTests { - JdbcJobExecutionDao jobExecutionDao; + Connection connection = mock(); + + DataSource dataSource = mock(); - List list = new ArrayList<>(); + PreparedStatement preparedStatement = mock(); + + JdbcJobExecutionDao jobExecutionDao; @BeforeEach - void setUp() { + void setUp() throws Exception { + + given(dataSource.getConnection()).willReturn(connection); + given(connection.prepareStatement(anyString())).willReturn(preparedStatement); jobExecutionDao = new JdbcJobExecutionDao(); jobExecutionDao.setJobExecutionIncrementer(new DataFieldMaxValueIncrementer() { @@ -65,20 +78,16 @@ public String nextStringValue() throws DataAccessException { } @Test - void testTablePrefix() { + void testTablePrefix() throws Exception { jobExecutionDao.setTablePrefix("FOO_"); - jobExecutionDao.setJdbcTemplate(new JdbcTemplate() { - @Override - public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException { - list.add(sql); - return 1; - } - }); + jobExecutionDao.setJdbcTemplate(new JdbcTemplate(dataSource)); JobExecution jobExecution = new JobExecution(new JobInstance(11L, "testJob"), new JobParameters()); jobExecutionDao.saveJobExecution(jobExecution); - assertEquals(1, list.size()); - String query = list.get(0); + + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + then(connection).should().prepareStatement(sqlCaptor.capture()); + String query = sqlCaptor.getValue(); assertTrue("Query did not contain FOO_:" + query, query.contains("FOO_")); } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/JobRepositoryFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/JobRepositoryFactoryBeanTests.java index 645fb863e9..a10f9db85f 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/JobRepositoryFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/JobRepositoryFactoryBeanTests.java @@ -52,6 +52,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -274,6 +275,7 @@ void testTransactionAttributesForCreateMethod() throws Exception { when(transactionManager.getTransaction(transactionDefinition)).thenReturn(null); Connection conn = mock(); when(dataSource.getConnection()).thenReturn(conn); + when(conn.prepareStatement(any())).thenThrow(new IllegalArgumentException("No Statement specified")); Exception exception = assertThrows(IllegalArgumentException.class, () -> repository.createJobExecution("foo", new JobParameters())); assertEquals("No Statement specified", exception.getMessage()); @@ -292,6 +294,7 @@ void testSetTransactionAttributesForCreateMethod() throws Exception { when(transactionManager.getTransaction(transactionDefinition)).thenReturn(null); Connection conn = mock(); when(dataSource.getConnection()).thenReturn(conn); + when(conn.prepareStatement(any())).thenThrow(new IllegalArgumentException("No Statement specified")); Exception exception = assertThrows(IllegalArgumentException.class, () -> repository.createJobExecution("foo", new JobParameters())); assertEquals("No Statement specified", exception.getMessage());