top of page

Pro*c Job to Spring Batch Job

  • Writer: Anand Nerurkar
    Anand Nerurkar
  • 2 hours ago
  • 6 min read

Example1:

📌 Background

  • Pro*C job → Written in C with embedded SQL, often used for batch ETL-like jobs in Oracle.

  • Spring Batch job → Java-based batch framework supporting chunk-oriented processing, scheduling, retry/restart, transaction management, and scalability.

📌 Sample Use Case

Existing Pro*C Job Functionality:

  • Read loan application records from LOAN_APPLICATIONS table where STATUS = 'NEW'.

  • Validate credit score by calling an external service (via stored procedure / API).

  • Update the STATUS column to either ‘APPROVED’ or ‘REJECTED’ in the same table.

  • Generate a daily log file of processed applications.

📌 Step 1 – Analyze the Pro*C Job

Sample Pro*C Pseudocode:

EXEC SQL DECLARE app_cursor CURSOR FOR
    SELECT ID, CUSTOMER_ID, AMOUNT FROM LOAN_APPLICATIONS WHERE STATUS = 'NEW';

while (FETCH app_cursor INTO :id, :cust_id, :amt) {
   credit_score = call_credit_bureau_api(cust_id);
   if (credit_score > 700) {
      EXEC SQL UPDATE LOAN_APPLICATIONS SET STATUS = 'APPROVED' WHERE ID = :id;
   } else {
      EXEC SQL UPDATE LOAN_APPLICATIONS SET STATUS = 'REJECTED' WHERE ID = :id;
   }
   write_to_log_file(id, cust_id, amt, credit_score);
}
EXEC SQL COMMIT;

📌 Step 2 – Spring Batch Job Design

We design it in Reader → Processor → Writer style.

  • ItemReader → Reads loan applications with status NEW from DB.

  • ItemProcessor → Calls credit score API & decides APPROVED/REJECTED.

  • ItemWriter → Updates DB with new status + writes logs.

📌 Step 3 – Spring Batch Job Implementation

(a) Entity & DTO

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoanApplication {
    private Long id;
    private String customerId;
    private Double amount;
    private String status;
}

(b) ItemReader (JDBC Reader)

@Bean
public JdbcCursorItemReader<LoanApplication> loanReader(DataSource dataSource) {
    return new JdbcCursorItemReaderBuilder<LoanApplication>()
            .dataSource(dataSource)
            .name("loanReader")
            .sql("SELECT ID, CUSTOMER_ID, AMOUNT, STATUS FROM LOAN_APPLICATIONS WHERE STATUS='NEW'")
            .rowMapper((rs, rowNum) -> new LoanApplication(
                    rs.getLong("ID"),
                    rs.getString("CUSTOMER_ID"),
                    rs.getDouble("AMOUNT"),
                    rs.getString("STATUS")
            ))
            .build();
}

(c) ItemProcessor

@Component
public class LoanProcessor implements ItemProcessor<LoanApplication, LoanApplication> {

    @Override
    public LoanApplication process(LoanApplication loan) throws Exception {
        int creditScore = callCreditBureau(loan.getCustomerId()); // external API
        if (creditScore > 700) {
            loan.setStatus("APPROVED");
        } else {
            loan.setStatus("REJECTED");
        }
        return loan;
    }

    private int callCreditBureau(String customerId) {
        // Simulated API call
        return (int) (Math.random() * 900);
    }
}

(d) ItemWriter (Update DB + Log File)

@Bean
public ItemWriter<LoanApplication> loanWriter(DataSource dataSource) {
    return items -> {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        for (LoanApplication loan : items) {
            jdbcTemplate.update("UPDATE LOAN_APPLICATIONS SET STATUS=? WHERE ID=?",
                    loan.getStatus(), loan.getId());

            // log file
            Files.write(Paths.get("loan-processing.log"),
                (loan.toString() + "\n").getBytes(),
                StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        }
    };
}

(e) Job Configuration

@Bean
public Job loanJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory,
                   ItemReader<LoanApplication> reader,
                   ItemProcessor<LoanApplication, LoanApplication> processor,
                   ItemWriter<LoanApplication> writer) {

    Step step = stepBuilderFactory.get("loanStep")
            .<LoanApplication, LoanApplication>chunk(10)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();

    return jobBuilderFactory.get("loanJob")
            .incrementer(new RunIdIncrementer())
            .flow(step)
            .end()
            .build();
}

📌 Step 4 – Migration Steps Summary

  1. Analyze Pro*C job logic – SQLs, loops, validations, file writes.

  2. Model equivalent batch job in Spring Batch – Reader, Processor, Writer.

  3. Replace embedded SQL with JDBC/ORM (Spring Data / JPA / JdbcTemplate).

  4. Replace stored procedure calls with REST API clients or JDBC calls.

  5. Replace file writes with Spring Batch writers (FlatFileItemWriter / custom writer).

  6. Wrap in Job configuration for scheduling (Spring Scheduler / Quartz / Cron / Control-M).

  7. Test for functional equivalence + performance tuning (chunk size, commit interval).

  8. Deploy as Spring Boot microservice on cloud or on-prem batch infra.


Example 2:

📌 Scenario

  • Existing Pro*C Job:

    • Reads customer loan data from Oracle DB.

    • For each loan record, performs some business logic (e.g., calculate EMI).

    • Writes results back into another loan_summary table.

    • Scheduled to run daily at midnight.

We need to migrate this Pro*C job into Spring Batch.

🔹 Step 1: Analyze the Pro*C Job

Pro*C job typically has:

  1. C code + Embedded SQL (EXEC SQL).

  2. CURSOR definitions to fetch rows.

  3. Loops to process data.

  4. DML inserts/updates.

Example Pro*C snippet:

EXEC SQL DECLARE loan_cursor CURSOR FOR
  SELECT loan_id, principal, tenure, interest_rate FROM loan;

EXEC SQL OPEN loan_cursor;

while (sqlca.sqlcode == 0) {
   EXEC SQL FETCH loan_cursor INTO :loan_id, :principal, :tenure, :interest_rate;

   emi = (principal * interest_rate / 100) / tenure;

   EXEC SQL INSERT INTO loan_summary (loan_id, emi) VALUES (:loan_id, :emi);
}
EXEC SQL CLOSE loan_cursor;

🔹 Step 2: Define Spring Batch Job Structure

Spring Batch works on Reader → Processor → Writer.

  • Reader → Reads loan records from DB (similar to CURSOR).

  • Processor → Applies EMI calculation.

  • Writer → Inserts results into loan_summary table.

🔹 Step 3: Implement Spring Batch Components

1️⃣ Reader – JdbcCursorItemReader

@Bean
public JdbcCursorItemReader<Loan> loanItemReader(DataSource dataSource) {
    return new JdbcCursorItemReaderBuilder<Loan>()
            .dataSource(dataSource)
            .name("loanItemReader")
            .sql("SELECT loan_id, principal, tenure, interest_rate FROM loan")
            .rowMapper(new LoanRowMapper())
            .build();
}

public class LoanRowMapper implements RowMapper<Loan> {
    @Override
    public Loan mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Loan(
            rs.getLong("loan_id"),
            rs.getDouble("principal"),
            rs.getInt("tenure"),
            rs.getDouble("interest_rate")
        );
    }
}

2️⃣ Processor – EMI Calculation

@Component
public class LoanProcessor implements ItemProcessor<Loan, LoanSummary> {
    @Override
    public LoanSummary process(Loan loan) {
        double emi = (loan.getPrincipal() * loan.getInterestRate() / 100) / loan.getTenure();
        return new LoanSummary(loan.getLoanId(), emi);
    }
}

3️⃣ Writer – JdbcBatchItemWriter

@Bean
public JdbcBatchItemWriter<LoanSummary> loanItemWriter(DataSource dataSource) {
    return new JdbcBatchItemWriterBuilder<LoanSummary>()
            .dataSource(dataSource)
            .sql("INSERT INTO loan_summary (loan_id, emi) VALUES (:loanId, :emi)")
            .beanMapped()
            .build();
}

4️⃣ Job & Step Configuration

@Bean
public Step loanStep(JobRepository jobRepository, 
                     PlatformTransactionManager transactionManager,
                     JdbcCursorItemReader<Loan> reader,
                     LoanProcessor processor,
                     JdbcBatchItemWriter<LoanSummary> writer) {

    return new StepBuilder("loanStep", jobRepository)
            .<Loan, LoanSummary>chunk(100, transactionManager)
            .reader(reader)
            .processor(processor)
            .writer(writer)
            .build();
}

@Bean
public Job loanJob(JobRepository jobRepository, Step loanStep) {
    return new JobBuilder("loanJob", jobRepository)
            .start(loanStep)
            .build();
}

🔹 Step 4: Scheduling (Replace Cron/Control-M/Unix Script)

  • Earlier Pro*C job was scheduled via Unix cron.

  • In Spring Boot:

    • Use Spring Scheduler (@Scheduled(cron="0 0 0 *")) OR

    • Integrate with Quartz OR

    • External job scheduler (Control-M, Autosys, Airflow).

🔹 Step 5: Testing & Validation

  1. Unit test Processor logic.

  2. Run job with sample dataset.

  3. Validate DB inserts in loan_summary.

  4. Benchmark performance against old Pro*C job.

End Result:The Pro*C job is successfully migrated to Spring Batch, improving maintainability, testability, and enabling integration with cloud-native environments.


Example 3:


Mapping Summary

  • SQLCODE == 0 → normal Spring Batch flow (reader → processor → writer).

  • SQLCODE < 0 → SQLException triggers RetryPolicy / SkipPolicy / fail step.

  • SQLCODE > 0 → mapped to LoanWarningException → logged/skipped but job continues.


🔹 Pro*C Reference

  • SQLCODE == 0 → success, continue.

  • SQLCODE < 0 → error, stop or handle exception.

  • SQLCODE > 0 → warning (like “no rows”), continue but log.


🔹 Spring Batch Equivalent

  • Success → just return processed item (equivalent to SQLCODE == 0).

  • Error (SQLCODE < 0) → handled via SkipPolicy / RetryPolicy / Job step failure.

  • Warning (SQLCODE > 0) → Spring Batch doesn’t give warnings directly, but we can simulate with SQLWarning on JDBC connection or custom validation exceptions.


proc*c

===

/* Pro*C pseudo-code */

EXEC SQL DECLARE loan_cursor CURSOR FOR

SELECT APPLICATION_ID, CREDIT_SCORE

FROM LOAN_APPLICATIONS;


EXEC SQL OPEN loan_cursor;


while (SQLCODE == 0) {

EXEC SQL FETCH loan_cursor INTO :appId, :creditScore;


if (SQLCODE != 0) break; /* exit loop */


if (creditScore > 700) {

EXEC SQL UPDATE LOAN_APPLICATIONS

SET STATUS = 'APPROVED'

WHERE APPLICATION_ID = :appId;

} else {

EXEC SQL UPDATE LOAN_APPLICATIONS

SET STATUS = 'REJECTED'

WHERE APPLICATION_ID = :appId;

}

}

EXEC SQL COMMIT;

EXEC SQL CLOSE loan_cursor;


✅ Code Example – Spring Batch Job with SQLCODE Equivalent Handling

@Configuration
@EnableBatchProcessing
public class LoanBatchJobConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public JdbcCursorItemReader<LoanApplication> reader() {
        return new JdbcCursorItemReaderBuilder<LoanApplication>()
            .dataSource(dataSource)
            .name("loanReader")
            .sql("SELECT ID, APPLICANT_NAME, CREDIT_SCORE, STATUS FROM LOAN_APPLICATIONS")
            .rowMapper((rs, rowNum) -> {
                LoanApplication app = new LoanApplication();
                app.setId(rs.getLong("ID"));
                app.setApplicantName(rs.getString("APPLICANT_NAME"));
                app.setCreditScore(rs.getInt("CREDIT_SCORE"));
                app.setStatus(rs.getString("STATUS"));

                // --- Simulating SQLCODE > 0 (Warning: e.g., NULL name)
                if (app.getApplicantName() == null) {
                    throw new LoanWarningException("Applicant name missing (SQLCODE > 0 simulated).");
                }

                return app;
            })
            .build();
    }

    @Bean
    public ItemProcessor<LoanApplication, LoanApplication> processor() {
        return app -> {
            // --- Simulating SQLCODE == 0 (Success)
            if (app.getCreditScore() > 700) {
                app.setStatus("APPROVED");
            } else {
                app.setStatus("REJECTED");
            }
            return app;
        };
    }

    @Bean
    public JdbcBatchItemWriter<LoanApplication> writer() {
        return new JdbcBatchItemWriterBuilder<LoanApplication>()
            .dataSource(dataSource)
            .sql("UPDATE LOAN_APPLICATIONS SET STATUS = :status WHERE ID = :id")
            .beanMapped()
            .build();
    }

    @Bean
    public Step loanStep(StepBuilderFactory stepBuilderFactory) {
        return stepBuilderFactory.get("loanStep")
            .<LoanApplication, LoanApplication>chunk(10)
            .reader(reader())
            .processor(processor())
            .writer(writer())
            // --- SQLCODE < 0 (Errors) → Skip or Retry
            .faultTolerant()
            .skipPolicy(new SQLCodeSkipPolicy())
            .retryPolicy(new SQLCodeRetryPolicy())
            .listener(new LoanJobListener())
            .build();
    }

    @Bean
    public Job loanJob(JobBuilderFactory jobBuilderFactory, Step loanStep) {
        return jobBuilderFactory.get("loanJob")
            .start(loanStep)
            .build();
    }
}

🔹 Custom SkipPolicy (SQLCODE < 0)

public class SQLCodeSkipPolicy implements SkipPolicy {
    @Override
    public boolean shouldSkip(Throwable t, int skipCount) {
        if (t instanceof SQLException) {
            // SQLCODE < 0 (Fatal Error) → allow skip up to 5 times
            return skipCount < 5;
        }
        if (t instanceof LoanWarningException) {
            // SQLCODE > 0 (Warning) → always skip safely
            return true;
        }
        return false;
    }
}

🔹 Custom RetryPolicy (SQLCODE < 0, transient errors)

public class SQLCodeRetryPolicy extends SimpleRetryPolicy {
    public SQLCodeRetryPolicy() {
        super(3, Collections.singletonMap(SQLException.class, true));
    }
}

🔹 Listener for Logging (like Pro*C SQLCODE trace)

public class LoanJobListener extends JobExecutionListenerSupport {
    @Override
    public void beforeJob(JobExecution jobExecution) {
        System.out.println("Starting Loan Processing Job...");
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
            System.out.println("Job finished successfully (SQLCODE == 0).");
        } else {
            System.out.println("Job failed (SQLCODE < 0).");
        }
    }
}

Mapping Summary

  • SQLCODE == 0 → normal Spring Batch flow (reader → processor → writer).

  • SQLCODE < 0 → SQLException triggers RetryPolicy / SkipPolicy / fail step.

  • SQLCODE > 0 → mapped to LoanWarningException → logged/skipped but job continues.

📌 3. Mapping SQLCODE to Spring Batch

Pro*C SQLCODE

Meaning

Spring Batch Equivalent

== 0

Success

Normal execution (no exception thrown)

> 0

Warning

SkipPolicy → skip and continue

< 0

Error

RetryPolicy (retry a few times, else fail)


 
 
 

Recent Posts

See All
why springbatch job??

Spring Batch Job Spring Batch is designed exactly for batch workloads  like Pro*C migrations. ✅ Advantages: Chunk-oriented processing...

 
 
 
Pro*c Module to Java Microservices

End-to-end example of migrating a Pro*C module to a Java microservice (target: PostgreSQL on Azure) . I’ll use a realistic use case and...

 
 
 

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
  • Facebook
  • Twitter
  • LinkedIn

©2024 by AeeroTech. Proudly created with Wix.com

bottom of page