Friday, 22 October 2021

How to update one-to-many mapping entities with soft delete?

In this post, I am going to explain how to update one-to-many mapping entities using spring jpa.

 

Table’s structure

Projects

ID

DESCRIPTION

NAME

 

 

 

 

 

 

 

Roles

ID

ACTIVE

ROLE_NAME

PROJECT_ID

 

 

 

 

 

 

 

 

 

Project entity is modeled like below

@Entity
@Table(name = "PROJECTS")
public class Project {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "ID")
	private Integer id;

	@Column(name = "NAME")
	private String name;

	@Column(name = "DESCRIPTION")
	private String description;

	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "PROJECT_ID")
	@JsonManagedReference
	private Set<Role> roles;

	.......
	.......
}

 

Role entity class is modeled like below.

@Entity
@Table(name = "ROLES")
public class Role {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "ID")
	private Integer id;

	@Column(name = "ROLE_NAME")
	private String roleName;

	@Column(name = "ACTIVE")
	private Integer active;

	@JsonBackReference
	@ManyToOne
	@JoinColumn(name = "PROJECT_ID")
	private Project project;

	......
	......
}

 

How to create a project along with role names?

 

Request payload

 

{
  "description": "pjt1",
  "name": "pjt1",
  "roles": [
    "ADMIN", "EDITOR"
  ]
}


tables will be updated like below.

 

PROJECTS table


ROLES table

As you see above snippet, ADMIN and EDITOR role names are associated with the project id 1.

 

How to update the project along with rolenames?

Request payload

{
  "roles": [
    "ADMIN", "SERVICE_USER"
  ]
}

 

We need to remove the EDITOR role for the project id 1 and add new role SERVICE_USER.

 

 

After we update the project, it should deactivate the role EDITOR (ACTIVE indicator is set to value 0).

 

Steps to be taken while updating the One-to-Many entity

Step 1: Get the persisted project and roles.

Optional<Project> projectOpt = projectRepository.findById(projectId);

if (!projectOpt.isPresent()) {
	throw new AppException("Project not found", HttpStatus.NOT_FOUND);
}

Project project = projectOpt.get();
Set<Role> roles = project.getRoles();

Step 2: Make all the persisted roles to INACTIVE and populate the mapping table, where key is the role name and value is the persisted Role.

Map<String, Role> rolesMap = new HashMap<>();

for (Role role : roles) {
	role.setActive(AppConstants.INACTIVE);
	rolesMap.put(role.getRoleName(), role);
}
Set<String> existingRoleNames = rolesMap.keySet();

Step 3: Iterate over new roles

a.   if the existing role do not contain this new role, create new Role instance, and add it to the existing roles.

b.   If the existing role is still valid, set it back to ACTIVE

 

List<String> roleNames = projectCreateRequestDto.getRoles();
for (String newRoleName : roleNames) {
	if (!existingRoleNames.contains(newRoleName)) {
		Role newRoleToCreate = new Role();
		newRoleToCreate.setProject(project);
		newRoleToCreate.setActive(AppConstants.ACTIVE);
		newRoleToCreate.setRoleName(newRoleName);
		roles.add(newRoleToCreate);
	} else {
		Role persistedRole = rolesMap.get(newRoleName);
		persistedRole.setActive(AppConstants.ACTIVE);
	}
}

Save the updated project entity.

project = projectRepository.save(project);

that’s it, you are done….:)

 

Find the below working application.

Step 1: Create new maven project ‘one-to-many-update-demo’.

 

Step 2: Update pom.xml with maven dependencies.

 

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.sample.app</groupId>
	<artifactId>one-to-many-update-demo</artifactId>
	<version>1</version>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.6.RELEASE</version>
	</parent>

	<dependencies>
		<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>

		<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.9.2</version>
		</dependency>


		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>2.9.2</version>
		</dependency>

	</dependencies>

</project>

Step 3: Create application.properties file under src/main/resources folder.

 

application.properties

# Setting log level to DEBUG
logging.level.org.springframework.web=DEBUG

## H2 specific properties
spring.h2.console.enabled=true
spring.h2.console.path=/h2

spring.datasource.url=jdbc:h2:file:~/db/myOrg.db;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1;

spring.datasource.username=krishna
spring.datasource.password=password123

spring.datasource.driverClassName=org.h2.Driver

## JPA specific properties
spring.jpa.hibernate.ddl-auto=create

spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

## Database connection pooling properties
# Number of ms to wait before throwing an exception if no connection is available.
spring.datasource.max-wait=10000

# Maximum number of active connections that can be allocated from this pool at the same time.
spring.datasource.tomcat.max-active=10
spring.datasource.tomcat.max-idle=5
spring.datasource.tomcat.min-idle=3

Step 4: Define entity classes.

 

Project.java

package com.sample.app.entity;

import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonManagedReference;

@Entity
@Table(name = "PROJECTS")
public class Project {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "ID")
	private Integer id;

	@Column(name = "NAME")
	private String name;

	@Column(name = "DESCRIPTION")
	private String description;

	@OneToMany(cascade = CascadeType.ALL)
	@JoinColumn(name = "PROJECT_ID")
	@JsonManagedReference
	private Set<Role> roles;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public Set<Role> getRoles() {
		return roles;
	}

	public void setRoles(Set<Role> roles) {
		this.roles = roles;
	}

}

Role.java

package com.sample.app.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonBackReference;

@Entity
@Table(name = "ROLES")
public class Role {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "ID")
	private Integer id;

	@Column(name = "ROLE_NAME")
	private String roleName;

	@Column(name = "ACTIVE")
	private Integer active;

	@JsonBackReference
	@ManyToOne
	@JoinColumn(name = "PROJECT_ID")
	private Project project;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getRoleName() {
		return roleName;
	}

	public void setRoleName(String roleName) {
		this.roleName = roleName;
	}

	public Project getProject() {
		return project;
	}

	public void setProject(Project project) {
		this.project = project;
	}

	public Integer getActive() {
		return active;
	}

	public void setActive(Integer active) {
		this.active = active;
	}

}

Step 5: Define repository interfaces.

 

ProjectRepository.java

package com.sample.app.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.sample.app.entity.Project;

public interface ProjectRepository extends JpaRepository<Project, Integer>{

}

RoleRepository.java

package com.sample.app.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.sample.app.entity.Role;

public interface RoleRepository extends JpaRepository<Role, Integer>{

}


Step 6: Define service interfaces.

 

ProjectService.java

package com.sample.app.service;

import java.util.List;

import com.sample.app.dto.ProjectRequestDto;
import com.sample.app.dto.ProjectResponseDto;
import com.sample.app.exceptions.AppException;

public interface ProjectService {
	
	ProjectResponseDto createProject(ProjectRequestDto projectCreateRequestDto);
	
	ProjectResponseDto updateProject(Integer projectId, ProjectRequestDto projectCreateRequestDto) throws AppException;
	
	List<ProjectResponseDto> all();

}


Step 7: Define Service implementation class.

 

ProjectServiceImpl.java

package com.sample.app.service.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import com.sample.app.constants.AppConstants;
import com.sample.app.dto.ProjectRequestDto;
import com.sample.app.dto.ProjectResponseDto;
import com.sample.app.entity.Project;
import com.sample.app.entity.Role;
import com.sample.app.exceptions.AppException;
import com.sample.app.repository.ProjectRepository;
import com.sample.app.service.ProjectService;

@Service
public class ProjectServiceImpl implements ProjectService {

	@Autowired
	private ProjectRepository projectRepository;

	@Override
	public ProjectResponseDto createProject(ProjectRequestDto projectCreateRequestDto) {

		Project project = new Project();
		project.setName(projectCreateRequestDto.getName());
		project.setDescription(projectCreateRequestDto.getDescription());

		List<Role> roles = new ArrayList<>();
		if (projectCreateRequestDto.getRoles() != null) {

			for (String role : projectCreateRequestDto.getRoles()) {
				Role roleToCreate = new Role();
				roleToCreate.setRoleName(role);
				roleToCreate.setProject(project);
				roleToCreate.setActive(AppConstants.ACTIVE);
				roles.add(roleToCreate);
			}
		}

		project.setRoles(new HashSet(roles));

		project = projectRepository.save(project);
		return convert(project);

	}

	@Override
	public ProjectResponseDto updateProject(Integer projectId, ProjectRequestDto projectCreateRequestDto)
			throws AppException {

		Optional<Project> projectOpt = projectRepository.findById(projectId);

		if (!projectOpt.isPresent()) {
			throw new AppException("Project not found", HttpStatus.NOT_FOUND);
		}

		Project project = projectOpt.get();
		Set<Role> roles = project.getRoles();

		Map<String, Role> rolesMap = new HashMap<>();

		for (Role role : roles) {
			role.setActive(AppConstants.INACTIVE);
			rolesMap.put(role.getRoleName(), role);
		}

		Set<String> existingRoleNames = rolesMap.keySet();

		List<String> roleNames = projectCreateRequestDto.getRoles();
		for (String newRoleName : roleNames) {
			if (!existingRoleNames.contains(newRoleName)) {
				Role newRoleToCreate = new Role();
				newRoleToCreate.setProject(project);
				newRoleToCreate.setActive(AppConstants.ACTIVE);
				newRoleToCreate.setRoleName(newRoleName);
				roles.add(newRoleToCreate);
			} else {
				Role persistedRole = rolesMap.get(newRoleName);
				persistedRole.setActive(AppConstants.ACTIVE);
			}
		}

		project = projectRepository.save(project);
		return convert(project);
	}

	@Override
	public List<ProjectResponseDto> all() {
		List<Project> projects = projectRepository.findAll();
		return convert(projects);
	}

	private ProjectResponseDto convert(Project project) {
		ProjectResponseDto projectResponseDto = new ProjectResponseDto();
		projectResponseDto.setId(project.getId());
		projectResponseDto.setName(project.getName());
		projectResponseDto.setDescription(project.getDescription());
		projectResponseDto.setRoles(project.getRoles());

		return projectResponseDto;
	}

	private List<ProjectResponseDto> convert(List<Project> projects) {
		List<ProjectResponseDto> finalList = new ArrayList<>();

		for (Project project : projects) {
			finalList.add(convert(project));
		}

		return finalList;
	}

}


Step 8: Define exception handling classes.

 

AppException.java

package com.sample.app.exceptions;

import org.springframework.http.HttpStatus;

public class AppException extends Exception {

	private String message;
	private HttpStatus errorCode;

	public AppException(String message, HttpStatus errorCode) {
		super();
		this.message = message;
		this.errorCode = errorCode;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	public HttpStatus getErrorCode() {
		return errorCode;
	}

	public void setErrorCode(HttpStatus errorCode) {
		this.errorCode = errorCode;
	}

}


AppExceptionHandler.java

package com.sample.app.exceptions;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;

import com.sample.app.controller.ProjectController;

@ControllerAdvice(basePackageClasses = { ProjectController.class })
@ResponseBody
public class AppExceptionHandler {
	@org.springframework.web.bind.annotation.ExceptionHandler(AppException.class)
	public ResponseEntity<ExceptionResult> handleThrowable(AppException ex) {
		return new ResponseEntity<>(new ExceptionResult(ex), ex.getErrorCode());

	}
}


ExceptionResult.java

package com.sample.app.exceptions;

public class ExceptionResult {
	private String message;

	public ExceptionResult(AppException ex) {
		this.message = ex.getMessage();
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

}


Step 9: Define request and response dto classes.

 

ProjectRequestDto.java

package com.sample.app.dto;

import java.util.List;

public class ProjectRequestDto {

	private String name;
	private String description;
	private List<String> roles;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public List<String> getRoles() {
		return roles;
	}

	public void setRoles(List<String> roles) {
		this.roles = roles;
	}

}


ProjectResponseDto.java

package com.sample.app.dto;

import java.util.Set;

import com.sample.app.entity.Role;

public class ProjectResponseDto {

	private Integer id;

	private String name;

	private String description;

	private Set<Role> roles;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public Set<Role> getRoles() {
		return roles;
	}

	public void setRoles(Set<Role> roles) {
		this.roles = roles;
	}
	
	
}


Step 10: Define application constants class.

 

AppConstants.java

package com.sample.app.constants;

public class AppConstants {
	
	public static final int ACTIVE = 1;
	public static final int INACTIVE = 0;

}


Step 11: Define swagger configuration class.

 

SwaggerConfig.java

package com.sample.app.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Component
@EnableAutoConfiguration
@EnableSwagger2
public class SwaggerConfig {

	@Bean
	public Docket userApi() {

		return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().paths(PathSelectors.any())
				.apis(RequestHandlerSelectors.basePackage("com.sample.app.controller")).build();
	}

	private ApiInfo apiInfo() {

		return new ApiInfoBuilder().title("Query builder").description("Query builder using spring specification")
				.version("2.0").build();
	}
}


Step 12: Define project controller class.

 

ProjectController.java

package com.sample.app.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sample.app.dto.ProjectRequestDto;
import com.sample.app.dto.ProjectResponseDto;
import com.sample.app.exceptions.AppException;
import com.sample.app.service.ProjectService;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;

@RestController
@RequestMapping(value = "/v1/projects")
@Api(tags = "Projects", description = "This section contains APIs for Projects")
public class ProjectController {

	@Autowired
	private ProjectService projectService;

	@ApiOperation(value = "Get all Projects")
	@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<List<ProjectResponseDto>> getProjects() {
		List<ProjectResponseDto> projects = projectService.all();

		return new ResponseEntity<>(projects, HttpStatus.OK);
	}

	@ApiOperation(value = "Create Project")
	@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<ProjectResponseDto> createProject(
			@ApiParam(name = "ProjectDetails", value = "Project Details Object", required = true) @RequestBody ProjectRequestDto projectRequestDto) {
		ProjectResponseDto projectResponseDto = projectService.createProject(projectRequestDto);

		return new ResponseEntity<>(projectResponseDto, HttpStatus.CREATED);
	}

	@ApiOperation(value = "Update Project")
	@PutMapping(value = "/{projectId}", produces = MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<ProjectResponseDto> updateProject(
			@ApiParam(name = "projectId", value = "Numerical denoting "
					+ "pro id, for ex. 1.", required = true) @PathVariable(name = "projectId") Integer projectId,
			@ApiParam(name = "ProjectDetails", value = "Project Details Object", required = true) @RequestBody ProjectRequestDto projectRequestDto) throws AppException {
		ProjectResponseDto projectResponseDto = projectService.updateProject(projectId, projectRequestDto);

		return new ResponseEntity<>(projectResponseDto, HttpStatus.CREATED);
	}
}


Step 13: Define main application class.

 

App.java

package com.sample.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

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

}


Run App.java class file.

 

Open the url ‘http://localhost:8080/swagger-ui.html#/’ to see the swagger user interface.

 

Open the url ‘http://localhost:8080/h2’ to see the H2 console.

 

Total project structure looks like below. 

 

 

You can download complete working application from below link.

https://github.com/harikrishna553/springboot/tree/master/jpa/one-to-many-update-demo


 

Previous                                                    Next                                                    Home

No comments:

Post a Comment