diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js index eb10033e8c..9b150f36a4 100644 --- a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js +++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js @@ -1,13 +1,22 @@ // to execute in MongoShell after changing the database name `db.` as needed -db.createCollection("BATCH_JOB_INSTANCE"); -db.createCollection("BATCH_JOB_EXECUTION"); -db.createCollection("BATCH_STEP_EXECUTION"); +// This script is idempotent and can be safely reapplied. // SEQUENCES -db.createCollection("BATCH_SEQUENCES"); -db.getCollection("BATCH_SEQUENCES").insertOne({_id: "BATCH_JOB_INSTANCE_SEQ", count: Long(0)}); -db.getCollection("BATCH_SEQUENCES").insertOne({_id: "BATCH_JOB_EXECUTION_SEQ", count: Long(0)}); -db.getCollection("BATCH_SEQUENCES").insertOne({_id: "BATCH_STEP_EXECUTION_SEQ", count: Long(0)}); +db.getCollection("BATCH_SEQUENCES").updateOne( + {_id: "BATCH_JOB_INSTANCE_SEQ"}, + {$setOnInsert: {count: NumberLong(0)}}, + {upsert: true} +); +db.getCollection("BATCH_SEQUENCES").updateOne( + {_id: "BATCH_JOB_EXECUTION_SEQ"}, + {$setOnInsert: {count: NumberLong(0)}}, + {upsert: true} +); +db.getCollection("BATCH_SEQUENCES").updateOne( + {_id: "BATCH_STEP_EXECUTION_SEQ"}, + {$setOnInsert: {count: NumberLong(0)}}, + {upsert: true} +); // INDICES db.getCollection("BATCH_JOB_INSTANCE").createIndex( {"jobName": 1}, {"name": "job_name_idx"}); diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.jsonl b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.jsonl index 66f85ab7d6..e7c5cdd6cd 100644 --- a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.jsonl +++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.jsonl @@ -1,7 +1,9 @@ -{create:'BATCH_JOB_INSTANCE'} -{create:'BATCH_JOB_EXECUTION'} -{create:'BATCH_STEP_EXECUTION'} -{create:'BATCH_SEQUENCES'} -{insert: "BATCH_SEQUENCES", documents: [ { _id: 'BATCH_JOB_INSTANCE_SEQ', count: NumberLong(0) } ]} -{insert: "BATCH_SEQUENCES", documents: [ { _id: 'BATCH_JOB_EXECUTION_SEQ', count: NumberLong(0) } ]} -{insert: "BATCH_SEQUENCES", documents: [ { _id: 'BATCH_STEP_EXECUTION_SEQ', count: NumberLong(0) } ]} +{update: "BATCH_SEQUENCES", updates: [ { q: { _id: "BATCH_JOB_INSTANCE_SEQ" }, u: { $setOnInsert: { count: NumberLong(0) } }, upsert: true } ]} +{update: "BATCH_SEQUENCES", updates: [ { q: { _id: "BATCH_JOB_EXECUTION_SEQ" }, u: { $setOnInsert: { count: NumberLong(0) } }, upsert: true } ]} +{update: "BATCH_SEQUENCES", updates: [ { q: { _id: "BATCH_STEP_EXECUTION_SEQ" }, u: { $setOnInsert: { count: NumberLong(0) } }, upsert: true } ]} +{createIndexes: "BATCH_JOB_INSTANCE", indexes: [ { key: { jobName: 1 }, name: "job_name_idx" } ]} +{createIndexes: "BATCH_JOB_INSTANCE", indexes: [ { key: { jobName: 1, jobKey: 1 }, name: "job_name_key_idx" } ]} +{createIndexes: "BATCH_JOB_INSTANCE", indexes: [ { key: { jobInstanceId: -1 }, name: "job_instance_idx" } ]} +{createIndexes: "BATCH_JOB_EXECUTION", indexes: [ { key: { jobInstanceId: 1 }, name: "job_instance_idx" } ]} +{createIndexes: "BATCH_JOB_EXECUTION", indexes: [ { key: { jobInstanceId: 1, status: 1 }, name: "job_instance_status_idx" } ]} +{createIndexes: "BATCH_STEP_EXECUTION", indexes: [ { key: { stepExecutionId: 1 }, name: "step_execution_idx" } ]} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBSchemaScriptsIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBSchemaScriptsIntegrationTests.java new file mode 100644 index 0000000000..f778620bd1 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBSchemaScriptsIntegrationTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2026-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.batch.core.repository.support; + +import java.io.IOException; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.util.List; + +import com.mongodb.client.MongoCollection; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.batch.core.job.Job; +import org.springframework.batch.core.job.JobExecution; +import org.springframework.batch.core.job.parameters.JobParameters; +import org.springframework.batch.core.job.parameters.JobParametersBuilder; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DirtiesContext +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig(MongoDBIntegrationTestConfiguration.class) +class MongoDBSchemaScriptsIntegrationTests { + + private static final String CREATE_SCRIPT = "src/main/resources/org/springframework/batch/core/schema-mongodb.jsonl"; + + private static final String DROP_SCRIPT = "src/main/resources/org/springframework/batch/core/schema-drop-mongodb.jsonl"; + + @Autowired + private MongoTemplate mongoTemplate; + + @BeforeEach + void setUp() throws IOException { + executeScript(DROP_SCRIPT); + } + + @Test + void createScriptShouldBeIdempotent(@Autowired JobOperator jobOperator, @Autowired Job job) throws Exception { + executeScript(CREATE_SCRIPT); + + JobExecution firstExecution = jobOperator.start(job, jobParameters("first")); + assertEquals(1L, firstExecution.getJobInstanceId()); + assertEquals(1L, firstExecution.getId()); + assertEquals(List.of(1L, 2L), + firstExecution.getStepExecutions() + .stream() + .map(stepExecution -> stepExecution.getId()) + .sorted() + .toList()); + + executeScript(CREATE_SCRIPT); + + JobExecution secondExecution = jobOperator.start(job, jobParameters("second")); + assertEquals(2L, secondExecution.getJobInstanceId()); + assertEquals(2L, secondExecution.getId()); + assertEquals(List.of(3L, 4L), + secondExecution.getStepExecutions() + .stream() + .map(stepExecution -> stepExecution.getId()) + .sorted() + .toList()); + + assertSequenceValue("BATCH_JOB_INSTANCE_SEQ", 2L); + assertSequenceValue("BATCH_JOB_EXECUTION_SEQ", 2L); + assertSequenceValue("BATCH_STEP_EXECUTION_SEQ", 4L); + } + + private JobParameters jobParameters(String name) { + return new JobParametersBuilder().addString("name", name) + .addLocalDateTime("runtime", LocalDateTime.now()) + .toJobParameters(); + } + + private void executeScript(String path) throws IOException { + Files.lines(new FileSystemResource(path).getFilePath()).forEach(this.mongoTemplate::executeCommand); + } + + private void assertSequenceValue(String sequenceName, long expectedValue) { + MongoCollection sequences = this.mongoTemplate.getCollection("BATCH_SEQUENCES"); + Document sequence = sequences.find(new Document("_id", sequenceName)).first(); + assertNotNull(sequence); + assertEquals(expectedValue, sequence.getLong("count")); + } + +} diff --git a/spring-batch-docs/modules/ROOT/pages/job/configuring-repository.adoc b/spring-batch-docs/modules/ROOT/pages/job/configuring-repository.adoc index 881e27b163..4ebb1f0249 100644 --- a/spring-batch-docs/modules/ROOT/pages/job/configuring-repository.adoc +++ b/spring-batch-docs/modules/ROOT/pages/job/configuring-repository.adoc @@ -92,7 +92,8 @@ The `max-varchar-length` defaults to `2500`, which is the length of the long Similar to the JDBC-based `JobRepository`, the MongoDB-based `JobRepository` requires some collections to store the batch metadata. These collections are defined in the `org/springframework/batch/core/schema-mongodb.jsonl` of the `spring-batch-core` jar. As with the JDBC-based `JobRepository`, you need to create these collections -in your MongoDB database before running any job. +in your MongoDB database before running any job. The provided MongoDB schema scripts are idempotent and +can be safely reapplied without resetting sequence values. Moreover, since it is https://www.mongodb.com/docs/manual/core/dot-dollar-considerations/[not recommended] to use `.` in field names in MongoDB documents, you need to customize the `MongoTemplate` used by the `MongoJobRepositoryFactoryBean` @@ -319,4 +320,3 @@ If even that does not work or if you are not using an RDBMS, the only option may be to implement the various `Dao` interfaces that the `SimpleJobRepository` depends on and wire one up manually in the normal Spring way. -