Spring Batch – Dynamically map table from database to another


I needed to copy data from a table to a table in another database with the same fields and the datatype layout. Below is the code reference that I used for the same with Spring Boot and Spring Batch.

The input query file ,the target table are the input parameters here.

import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.JdbcCursorItemReader;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;

import javax.sql.DataSource;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

@Bean
@StepScope
public ItemReader < Map < String, Object >> reader(DataSource dataSource1, ApplicationArguments args) throws IOException {
JdbcCursorItemReader < Map < String, Object >> reader = new JdbcCursorItemReader < > ();
reader.setDataSource(dataSource1);

// Read the input SQL query from the specified text file
String queryTextFile = args.getOptionValues("sqlFile").get(0);
String sqlQuery = new String(Files.readAllBytes(Paths.get(queryTextFile)));

reader.setSql(sqlQuery);
reader.setRowMapper(new ColumnMapRowMapper());
return reader;
}

@Bean
public ItemProcessor < Map < String, Object > , Map < String, Object >> processor(ApplicationArguments args) {
String targetTableName = args.getOptionValues("outputTable").get(0);
return new MyDataItemProcessor(targetTableName);
}

@Bean
public ItemWriter < Map < String, Object >> writer(DataSource dataSource2) {
return new MyDataItemWriter(dataSource2);
}

@Bean
public Step step1(
StepBuilderFactory stepBuilderFactory,
ItemReader < Map < String, Object >> reader,
ItemProcessor < Map < String, Object > , Map < String, Object >> processor,
ItemWriter < Map < String, Object >> writer) {
return stepBuilderFactory.get("step1")
. < Map < String, Object > , Map < String, Object >> chunk(10)
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}

@Bean
public Job importJob(JobBuilderFactory jobBuilderFactory, Step step1) {
return jobBuilderFactory.get("importJob")
.incrementer(new RunIdIncrementer())
.flow(step1)
.end()
.build();
}

public static void main(String[] args) {
SpringApplication.run(BatchConfiguration.class, args);
}
}

class ColumnMapRowMapper implements RowMapper < Map < String, Object >> {
@Override
public Map < String,
Object > mapRow(ResultSet resultSet, int rowNum) throws SQLException {
Map < String, Object > row = new HashMap < > ();
row.put("source_column1", resultSet.getObject("column1"));
row.put("source_column2", resultSet.getObject("column2"));
row.put("source_column3", resultSet.getObject("column3"));
// Add more columns as needed
return row;
}
}

class MyDataItemProcessor implements ItemProcessor < Map < String, Object > , Map < String, Object >> {

private final String targetTableName;

public MyDataItemProcessor(String targetTableName) {
this.targetTableName = targetTableName;
}

@Override
public Map < String,
Object > process(Map < String, Object > item) throws Exception {
Map < String, Object > mappedItem = new HashMap < > ();

// Implement your dynamic column mapping logic here
// Example: mappedItem.put("target_column1", item.get("source_column1"));
// ... Map other columns as needed

return mappedItem;
}
}

class MyDataItemWriter implements ItemWriter < Map < String, Object >> {

private final NamedParameterJdbcTemplate jdbcTemplate;
private final String targetTableName;

public MyDataItemWriter(DataSource dataSource2) {
this.jdbcTemplate = new NamedParameterJdbcTemplate(dataSource2);
// Initialize the target table name here if needed
this.targetTableName = "target_table_name";
}

@Override
public void write(List < ? extends Map < String, Object >> items) throws Exception {
// Implement your custom write logic here
// Example: for (Map<String, Object> item : items) {
// String sql = "INSERT INTO " + targetTableName + " (...) VALUES (...)";
// jdbcTemplate.update(sql, item);
// }
}
}

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

In this code:We’ve added a main method in the Application class to run the Spring Boot application.We use ApplicationArguments to retrieve command line arguments for the input SQL query text file (–sqlFile) and the output table name (–outputTable).The input SQL query is read from the specified text file and passed to the item reader.The output table name is passed to the item processor to determine the target table for the data.You can run this Spring Boot application by providing the required command line arguments when executing the JAR file. For example:

java -jar your-app.jar --sqlFile=/path/to/sql/query.sql --outputTable=target_table_name

Replace your-app.jar, /path/to/sql/query.sql, and target_table_name with your actual JAR file name, SQL query file path, and target table name.

Implement Pagination with Native Query in Spring Data JPA


Below is the reference for this issue –

https://stackoverflow.com/questions/22345081/spring-data-jpa-query-and-pageable

Basically we need to provide two parameters in the @Query annotation.

  • value
  • countQuery
public interface UserRepository extends JpaRepository<User, Long> {

  @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1",
    countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1",
    nativeQuery = true)
  Page<User> findByLastname(String lastname, Pageable pageable);
}

Adding the countQuery parameter in the @Query annotation will make the native query compatible with pagination output. Otherwise the query generated by Hibernate will have syntax issues and will not get executed.

Spring Data JPA – Immutable Entity


An immutable entity, in the context of JPA (Java Persistence API) and Hibernate, is an entity class whose state cannot be modified once it has been created. In other words, once you create an instance of an immutable entity and persist it in the database, you cannot change its attribute values or update it. Immutable entities are typically used when you want to represent data that should remain constant throughout its lifecycle, such as historical data or configuration settings.

The key characteristics of an immutable entity are:

  • No Setter Methods: Immutable entities do not have setter methods for their attributes. Instead, you set their values through the constructor when you create an instance of the entity.
  • Read-Only: Once an instance of an immutable entity is created and persisted in the database, its attribute values cannot be modified. Any attempt to change them will result in a new entity being created.
  • No Update Operations: You cannot perform update operations (e.g., entityManager.merge()) on immutable entities. Instead, you typically create new instances with updated values.

The purpose of using immutable entities is to ensure data consistency and to represent data that should not change over time. Immutable entities are particularly useful for audit logs, historical records, or situations where you want to maintain a historical record of changes.

Here’s how an immutable entity works:

  • Creation: You create an instance of an immutable entity by providing values for its attributes through a constructor. These values are typically set during the initial insertion of the entity.
  • Persistence: You persist the entity in the database using JPA or Hibernate as you would with any other entity.
  • Reading: You can read and retrieve the entity’s data at any time.
  • No Updates: You cannot update the entity directly. If you need to change its data, you create a new instance with the updated values. This ensures that the original data remains intact.

Here’s a simple example of an immutable entity:

@Entity public class ImmutableEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) 
private Long id;
private String name;
private int age;
// Constructor for creating instances
public ImmutableEntity(String name, int age) {
this.name = name;
this.age = age;
}
// Getters but no setters
public Long getId() {
return id;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}

In this example, the ImmutableEntity class has no setter methods, and instances are created with values through the constructor. Once an instance is created and persisted, you cannot change its name or age attributes directly. To “update” the data, you create a new instance with the desired values and persist it as a separate record in the database. This maintains the immutability of the original data.

If your use case involves scenarios where you need to update certain fields while keeping others immutable (e.g., updating some fields while preventing changes to createdBy), using a fully immutable entity as described previously may not be suitable.

In your case, where you want to control which fields are updated and still allow updates to some fields while keeping others unchanged, you can follow one of the earlier approaches, such as using a custom update method or DTO projections.

Fully immutable entities are best suited for situations where you want to ensure that no fields can be modified after initial creation, which might not align with your requirement of allowing updates to some fields while keeping others immutable.

To summarize, if you need to update specific fields while preventing changes to others, you should use approaches like DTO projections or custom update methods, as mentioned earlier, rather than making your entire entity immutable. These approaches provide the flexibility to update only the desired fields while keeping others unchanged.

CRUD App in Angular


Me :

Angular simple master details crud app

GPT :

Sure, I can help you create a simple master-detail CRUD app using Angular! Here’s a high-level outline of the steps you can follow:

  1. Setup: Make sure you have Node.js and Angular CLI installed. Create a new Angular project using the CLI: ng new master-detail-app.
  2. Create Components: Generate components for your master and detail views using ng generate component master and ng generate component detail.
  3. Create Services: Create a service to handle data operations like fetching, adding, updating, and deleting items. Use ng generate service item.
  4. Model: Define an interface or class for your data model.
  5. Routing: Set up routing to navigate between the master and detail views. Configure routes in the app-routing.module.ts.
  6. Master View: In the master component, use the service to fetch a list of items and display them. Use Angular’s ngFor directive to iterate through the list.
  7. Detail View: In the detail component, display the details of a selected item. Use routing parameters to pass the item’s ID and retrieve it using the service.
  8. CRUD Operations:
  • Add: Create a form in the master component to add new items. Use the service to add the item to the list.
  • Update: Create an edit button in the detail component. Implement an edit form and use the service to update the item.
  • Delete: Add a delete button in the detail component to remove the item from the list using the service.
  1. Forms: Use Angular’s Reactive Forms for creating and editing items. You can create a form group with form controls that match the properties of your data model.
  2. Styling: Apply CSS or a UI framework like Bootstrap to style your app.
  3. Testing: Write unit tests for your components and services using Angular testing tools.
  4. Deployment: Use Angular CLI to build your app and deploy it to a hosting platform of your choice.

Remember, this is just a high-level overview. You’ll need to dive deeper into each step, consult Angular’s documentation, and possibly look at tutorials to get a more detailed understanding of each part of the process. Good luck, and feel free to ask if you have any specific questions along the way!

Me :hikmant

Master has order id, order name and order date. Detail has order id, product id, quantity and unit price

GPT :

Got it! Here’s a more detailed explanation of how you can create a simple master-detail CRUD app with Angular using the provided information:

  1. Setup: Create a new Angular project using the CLI: ng new master-detail-app.
  2. Create Components: Generate components for your master and detail views: ng generate component master and ng generate component detail.
  3. Create Services: Generate a service for handling data operations: ng generate service order.
  4. Model: Define the data models for orders and order details. You can create interfaces for both in separate files, for example: order.model.ts:

export interface Order { id: number; name: string; date: Date; }

order-detail.model.ts: export interface OrderDetail { id: number; orderId: number; productId: number; quantity: number; unitPrice: number; }

  1. Routing: Configure routing in the app-routing.module.ts to navigate between the master and detail views.
  2. Master View: In the master component (master.component.ts), use the order service to fetch a list of orders and display them using ngFor.
  3. Detail View: In the detail component (detail.component.ts), use routing parameters to get the selected order ID. Fetch the order details for that order and display them.
  4. CRUD Operations:
  • Add: Create an “Add Order” form in the master component to add new orders. Use the order service to add the order to the list.
  • Update: In the detail component, create an “Edit” button. Implement an edit form to update order details using the order service.
  • Delete: In the detail component, create a “Delete” button to remove the order using the order service.
  1. Forms: Use Angular’s Reactive Forms for creating and editing orders. Create a form group with form controls that match the properties of the Order model.
  2. Styling: Apply CSS or a UI framework like Bootstrap to style your app.
  3. Testing: Write unit tests for your components and services using Angular testing tools.
  4. Deployment: Build and deploy your app using the Angular CLI to a hosting platform.

Remember to inject the OrderService into your components and use it to perform CRUD operations on both orders and order details. This outline should provide a solid starting point, and you can refer to Angular’s documentation and tutorials for more in-depth guidance on each step.

Me:

In my case I need to call submit to call springboot post service to add/update data into the database only once with data from master and list of detail passed as a json payload

GPT:

Understood! If you want to call a Spring Boot POST service to add/update data in the database using a JSON payload containing both master and detail information, you’ll need to perform the following steps:

1. **Create a Combined Data Object**: Create a TypeScript class or interface that represents the combined data of the master and its associated details.

2. **Master Component Form (master.component.html)**:
– Create a form that collects the necessary master information (order name, order date) and includes the detail information (product ID, quantity, unit price) as inputs within the form.
– Bind the form inputs using `[(ngModel)]` to properties in the master component.

3. **Detail Component Form (detail.component.html)**:
– Within the detail component form, you can use the same approach to collect the detail information (product ID, quantity, unit price).

4. **Master Component Submit Method (master.component.ts)**:
– Implement a method that is triggered when the user submits the master form.
– In this method, you can prepare the combined data object by merging the master information with the list of details.
– Then, call a service method that makes a POST request to your Spring Boot backend with the combined data object.

5. **Service for Making POST Requests**:
– Create a service that provides methods to interact with your Spring Boot backend.
– Implement a method that uses Angular’s `HttpClient` to make a POST request to your Spring Boot endpoint, passing the combined data object as the JSON payload.

6. **Spring Boot Backend**:
– Create a Spring Boot controller with an endpoint to handle the POST request.
– In the controller, parse the JSON payload into your backend data models and perform the necessary database operations (add/update) for both master and details.

7. **Routing and Navigation**:
– Set up Angular routing to navigate between the master and detail components.

By following these steps, you can create a flow where the user submits the form in the master component, the combined master and detail data is sent as a JSON payload to the Spring Boot backend, and then the backend processes the data and performs the required database operations.

Please note that this is a high-level overview, and you’ll need to implement the detailed logic for each step. Additionally, ensure that you handle error scenarios and validation properly for a robust application.

Me:

Provide sample

GPT:

I can provide you with a simplified example, but please note that actual implementations can vary based on your specific requirements and project structure. Here’s a basic outline with sample code snippets:

  1. Create a Combined Data Object:

export interface CombinedData {
order: MasterData;
details: DetailData[]; }
export interface MasterData {
name: string;
date: string; // You might want to use a date format that your backend expects
}
export interface DetailData {
productId: number;
quantity: number;
unitPrice: number;
}

  1. Master Component Form (master.component.html):

<form (ngSubmit)="onSubmit()" #masterForm="ngForm">
<label for="orderName">Order Name:</label>
<input type="text" id="orderName" name="orderName" [(ngModel)]="masterData.name" required>
<label for="orderDate">Order Date:</label>
<input type="date" id="orderDate" name="orderDate" [(ngModel)]="masterData.date" required>
<!-- Detail input fields here -->
<button type="submit" [disabled]="!masterForm.valid">Submit</button>
</form>

  1. Detail Component Form (detail.component.html):

<form (ngSubmit)="onSubmit()" #detailForm="ngForm">
<label for="productId">Product ID:</label>
<input type="number" id="productId" name="productId" [(ngModel)]="detailData.productId" required>
<label for="quantity">Quantity:</label>
<input type="number" id="quantity" name="quantity" [(ngModel)]="detailData.quantity" required>
<label for="unitPrice">Unit Price:</label> <input type="number" id="unitPrice" name="unitPrice" [(ngModel)]="detailData.unitPrice" required>
<button type="submit" [disabled]="!detailForm.valid">Add Detail</button>
</form>

  1. Master Component Submit Method (master.component.ts):

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormsModule } from '@angular/forms'
import { CombinedData } from '../../models/combined-data.model';
import { MasterData } from '../../models/combined-data.model';
import { DetailData } from '../../models/combined-data.model';
import { PostCRUDService } from '../../services/post-crud.service';
@Component(
{ selector: 'app-master'
, templateUrl: './master.component.html'
, styleUrls: ['./master.component.css']
, providers:[PostCRUDService]
}
)
export class MasterComponent {
masterData: MasterData = { name: '', date: '' };
detailData: DetailData = { productId: 0, quantity: 0, unitPrice: 0 };
constructor(private http: HttpClient, private postCRUDService:PostCRUDService) {}
onSubmit() {
const combinedData: CombinedData = { order: this.masterData, details: [this.detailData]};
this.postCRUDService.postData(combinedData);
}
}

  1. Service for Making POST Requests:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { CombinedData } from '../models/combined-data.model';
@Injectable({ providedIn: 'root' })
export class PostCRUDService {
constructor(private http: HttpClient) {}
postData(combinedData: CombinedData) {
return this.http.post('your-backend-url', combinedData);
}
}

  1. Spring Boot Backend:

@RestController
public class DataController {
@PostMapping("/submitData")
public ResponseEntity submitData(@RequestBody CombinedData combinedData) {
// Handle and process the combinedData object (master and details)
// Perform database operations
return ResponseEntity.ok("Data submitted successfully");
}
}

Please replace 'your-backend-url' with the actual URL of your Spring Boot backend endpoint. Keep in mind that error handling, validation, and security considerations should be added to ensure your application is robust and secure.

Detail Component Submit Method (detail.component.ts):

import { Component, Input } from ‘@angular/core’;
import { DetailData } from ‘../../models/combined-data.model’;

@Component({
selector: 'app-detail',
templateUrl: './detail.component.html',
styleUrls: ['./detail.component.css']
})
export class DetailComponent {
@Input() detailData: DetailData;
}

This version of the detail.component.ts assumes that you’re receiving the detailData object as an input property from the parent component (the master component) and that the responsibility of sending the combined data to the backend is handled in the master component.

Spring Batch Oracle Bulk Processing


Question : I want Oracle spring batch process 1000 records at a instead of record by record. How do I do it.

ChatGpt Answer :

Certainly, in Oracle Spring Batch, you can process records in batches instead of individually. This is more efficient for large datasets. You can configure the batch size using the ChunkSize property in your batch configuration. This specifies how many records will be processed together as a batch. For instance, if you set ChunkSize to 100, Spring Batch will process 100 records at a time.

By processing records in batches, you can significantly improve the performance of your batch processing job and reduce the overhead of individual record processing.

Question : I mean I need to retrieve 1000 records at a time to the DB like BULK Collect in spring batch.

Chat GPT answer:

Spring Batch doesn’t have a built-in equivalent to Oracle’s BULK COLLECT. However, you can achieve similar results by designing your own approach. You can use Spring JDBC’s NamedParameterJdbcTemplate or similar to fetch a predefined number of records (e.g., 1000) at once from the database, and then process them in chunks within your Spring Batch job. This can help reduce the database round-trips and improve efficiency.

Here’s a simplified example of how you might structure your Spring Batch job to retrieve and process records in chunks:

@Bean public Step myStep(StepBuilderFactory stepBuilderFactory, ItemReader<MyRecord> itemReader, ItemProcessor<MyRecord, ProcessedRecord> itemProcessor, ItemWriter<ProcessedRecord> itemWriter) { return stepBuilderFactory.get("myStep") .<MyRecord, ProcessedRecord>chunk(1000) .reader(itemReader) .processor(itemProcessor) .writer(itemWriter) .build(); }

In this example, the chunk(1000) configuration processes 1000 records at a time. You would need to implement the ItemReader, ItemProcessor, and ItemWriter interfaces with your own logic to retrieve, process, and write the records accordingly.

Keep in mind that Spring Batch is designed for efficient batch processing, but it might not directly mirror the BULK COLLECT functionality of Oracle PL/SQL. You’ll need to design your own solution using Spring Batch’s features and appropriate JDBC libraries.

Question : In SPRING Jdbc can I have a feature like BULK COLLECT?

Chatgpt Answer :

Spring JDBC doesn’t have a direct equivalent of Oracle’s BULK COLLECT feature, which allows you to fetch multiple rows at once into a collection variable in PL/SQL. However, you can achieve similar results by optimizing your data retrieval strategy in Spring JDBC.

Instead of fetching rows individually, you can fetch them in batches using Spring JDBC’s JdbcTemplate or NamedParameterJdbcTemplate. Here’s an example of how you might achieve this:

NamedParameterJdbcTemplate jdbcTemplate; // Inject or create an instance public List<MyRecord> fetchRecordsInBatches(int batchSize) { List<MyRecord> records = new ArrayList<>(); String sql = "SELECT * FROM your_table LIMIT :batchSize"; MapSqlParameterSource parameters = new MapSqlParameterSource(); parameters.addValue("batchSize", batchSize); jdbcTemplate.query(sql, parameters, (rs) -> { while (rs.next()) { MyRecord record = new MyRecord(); // Populate record fields from the ResultSet records.add(record); } }); return records; }

In this example, batchSize determines the number of records you want to fetch in a single batch. By adjusting this value, you can control the number of rows fetched at once from the database.

Remember that Spring JDBC focuses on efficient interaction with databases, but the implementation will not be as streamlined as the native BULK COLLECT feature in Oracle. Nonetheless, using batching strategies like the one shown above can help you optimize data retrieval in Spring JDBC.

Question : Ok, similarly how I insert records in bulk?

Chatgpt Answer:

Certainly, you can insert records in bulk using Spring JDBC by optimizing the insertion process. Instead of inserting records one by one, you can use batch updates to insert multiple records at once. Here’s how you can achieve this:

NamedParameterJdbcTemplate jdbcTemplate; // Inject or create an instance public void insertRecordsInBulk(List<MyRecord> records) { String sql = "INSERT INTO your_table (column1, column2) VALUES (:value1, :value2)"; SqlParameterSource[] batchParams = new SqlParameterSource[records.size()]; for (int i = 0; i < records.size(); i++) { MyRecord record = records.get(i); MapSqlParameterSource parameters = new MapSqlParameterSource(); parameters.addValue("value1", record.getValue1()); parameters.addValue("value2", record.getValue2()); batchParams[i] = parameters; } jdbcTemplate.batchUpdate(sql, batchParams); }

In this example, insertRecordsInBulk takes a list of records and inserts them into the database using a batch update. You construct an array of SqlParameterSource objects representing the values for each record, and then use the batchUpdate method to execute the batch insertion.

By batching the inserts, you can reduce the number of database round-trips, which improves the efficiency of the insertion process.

Keep in mind that the exact implementation may vary depending on your database and requirements, but this example should give you a good starting point for performing bulk inserts using Spring JDBC.