Saturday, 1 February 2020

Spring jpa: Many To Many: Update entity, mapping class only


In Many to Many relationship, records in the first table are mapped to multiple rows in the second table and similarly rows in the second table are mapped to multiple rows in the first table.

For example, Take two tables, employees and projects. An employee can work in one or more projects and similarly a project contain one or more employees.

We can model this many to many relationship using a mapping table.

employees Table
emp_id
first_name
last_name










projects Table
pjt_id
project_name
Team_size










employee_project_mapping table
emp_id
pjt_id





We can model above relationship using ManyToMany annotation like below.
@Entity
@Table(name = "employees")
public class Employee {
 @Id
 @GeneratedValue
 @Column(name = "emp_id")
 private int id;
 private String firstName;
 private String lastName;

 @ManyToMany
 @JoinTable(name = "employee_project_mapping", joinColumns = @JoinColumn(name = "employee_id", referencedColumnName = "emp_id"), inverseJoinColumns = @JoinColumn(name = "project_id", referencedColumnName = "pjt_id"))
 @JsonManagedReference
 Set<Project> projects = new HashSet<Project>();

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

@Entity
@Table(name = "projects")
public class Project {
 @Id
 @GeneratedValue
 @Column(name = "pjt_id")
 private int projectId;

 private String projectName;

 private int teamSize;

 @ManyToMany(mappedBy = "projects")
 @JsonBackReference
 private Set<Employee> emp = new HashSet<Employee>();

 .....
 .....
}

Here the question is, assume all the projects are already onboarded in ‘projects’ table. Whenever new employee is onboarded to your organization, you should create employee record and assign the projects that he is going to work on. You want to update only employees, employee_project_mapping tables, not the project table.

How can you solve above scenario?
We can solve this problem by not passing entire project object, just pass the proxy project object that has the project id in it. You can get proxy object using EntityManager.getReference(projectId) or JpaRepository.getOne(projectId).

Above methods will create a proxy object with the appropriate id, rather than loading the entire Project entity from the data store.


Example
@Transactional
public void createEmployee(Employee employee, Long projectId) {
    employee.setProjects(Arrays.asList(projectRepository.getOne(projectId));
    employeeRepository.save(employee);
}

Find the below working application.

Employee.java
package com.sample.app.entity;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonManagedReference;

@Entity
@Table(name = "employees")
public class Employee {
 @Id
 @GeneratedValue
 @Column(name = "emp_id")
 private int id;
 private String firstName;
 private String lastName;

 @ManyToMany
 @JoinTable(name = "employee_project_mapping", joinColumns = @JoinColumn(name = "employee_id", referencedColumnName = "emp_id"), inverseJoinColumns = @JoinColumn(name = "project_id", referencedColumnName = "pjt_id"))
 @JsonManagedReference
 Set<Project> projects = new HashSet<Project>();

 public int getId() {
  return id;
 }

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

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public Set<Project> getProjects() {
  return projects;
 }

 public void setProjects(Set<Project> projects) {
  this.projects = projects;
 }

}


Project.java
package com.sample.app.entity;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonBackReference;

@Entity
@Table(name = "projects")
public class Project {
 @Id
 @GeneratedValue
 @Column(name = "pjt_id")
 private int projectId;

 private String projectName;

 private int teamSize;

 @ManyToMany(mappedBy = "projects")
 @JsonBackReference
 private Set<Employee> emp = new HashSet<Employee>();

 public int getProjectId() {
  return projectId;
 }

 public void setProjectId(int projectId) {
  this.projectId = projectId;
 }

 public String getProjectName() {
  return projectName;
 }

 public void setProjectName(String projectName) {
  this.projectName = projectName;
 }

 public Set<Employee> getEmp() {
  return emp;
 }

 public void setEmp(Set<Employee> emp) {
  this.emp = emp;
 }

 public int getTeamSize() {
  return teamSize;
 }

 public void setTeamSize(int teamSize) {
  this.teamSize = teamSize;
 }

}


EmployeeRepository.java
package com.sample.app.repository;

import org.springframework.data.repository.CrudRepository;

import com.sample.app.entity.Employee;

public interface EmployeeRepository extends CrudRepository<Employee, Integer> {

}


ProjectRepository.java
package com.sample.app.repository;

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

import com.sample.app.entity.Project;

public interface ProjectRepository extends JpaRepository<Project, Integer>, CrudRepository<Project, Integer>  {

}


EmployeeService.java
package com.sample.app.service;

import com.sample.app.dto.EmployeeDto;
import com.sample.app.entity.Employee;

public interface EmployeeService {

  Employee save(EmployeeDto emp);
  
  Employee getById(int id);

  Employee update(int id, EmployeeDto empDto);
  
}


ProjectService.java
package com.sample.app.service;

import java.util.List;

import com.sample.app.dto.ProjectDto;
import com.sample.app.entity.Project;

public interface ProjectService {

 Project getOne(int pjtId);
 
 Project saveProject(ProjectDto pjtDto);
 
 List<Project> all();
 
}


EmployeeDto.java
package com.sample.app.dto;

import java.util.List;

public class EmployeeDto {
 private String firstName;
 private String lastName;
 private List<Integer> projectIds;

 public String getFirstName() {
  return firstName;
 }

 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }

 public String getLastName() {
  return lastName;
 }

 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

 public List<Integer> getProjectIds() {
  return projectIds;
 }

 public void setProjectIds(List<Integer> projectIds) {
  this.projectIds = projectIds;
 }

}


ProjectDto.java
package com.sample.app.dto;

public class ProjectDto {

 private String projectName;

 private int teamSize;

 public String getProjectName() {
  return projectName;
 }

 public void setProjectName(String projectName) {
  this.projectName = projectName;
 }

 public int getTeamSize() {
  return teamSize;
 }

 public void setTeamSize(int teamSize) {
  this.teamSize = teamSize;
 }

}


EmployeeServiceImpl.java
package com.sample.app.service.impl;

import javax.transaction.Transactional;

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

import com.sample.app.dto.EmployeeDto;
import com.sample.app.entity.Employee;
import com.sample.app.entity.Project;
import com.sample.app.repository.EmployeeRepository;
import com.sample.app.repository.ProjectRepository;
import com.sample.app.service.EmployeeService;
import java.util.*;

@Service
public class EmployeeServiceImpl implements EmployeeService {

 @Autowired
 private EmployeeRepository empRepo;

 @Autowired
 private ProjectRepository pjtRepo;

 @Override
 @Transactional
 public Employee save(EmployeeDto empDto) {

  String firstName = empDto.getFirstName();

  String lastName = empDto.getLastName();

  Employee emp = new Employee();
  emp.setFirstName(firstName);
  emp.setLastName(lastName);

  List<Project> pjtsProxies = new ArrayList<>();

  for (int pjtId : empDto.getProjectIds()) {
   Project tempPjt = pjtRepo.getOne(pjtId);
   pjtsProxies.add(tempPjt);
  }

  emp.setProjects(new HashSet<>(pjtsProxies));

  return empRepo.save(emp);
 }

 @Override
 public Employee getById(int id) {
  return empRepo.findById(id).get();
 }

 @Override
 public Employee update(int id, EmployeeDto empDto) {
  String firstName = empDto.getFirstName();

  String lastName = empDto.getLastName();

  Employee emp = new Employee();
  emp.setFirstName(firstName);
  emp.setLastName(lastName);
  emp.setId(id);

  List<Project> pjtsProxies = new ArrayList<>();

  for (int pjtId : empDto.getProjectIds()) {
   Project tempPjt = pjtRepo.getOne(pjtId);
   pjtsProxies.add(tempPjt);
  }

  emp.setProjects(new HashSet<> (pjtsProxies));

  return empRepo.save(emp);
 }

}


ProjectServiceImpl.java
package com.sample.app.service.impl;

import java.util.List;

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

import com.sample.app.dto.ProjectDto;
import com.sample.app.entity.Project;
import com.sample.app.repository.ProjectRepository;
import com.sample.app.service.ProjectService;

@Service
public class ProjectServiceImpl implements ProjectService {

 @Autowired
 private ProjectRepository pjtRepo;

 @Override
 public Project getOne(int pjtId) {

  return pjtRepo.getOne(pjtId);

 }

 @Override
 public Project saveProject(ProjectDto pjtDto) {
  Project pjt = new Project();
  pjt.setProjectName(pjtDto.getProjectName());
  pjt.setTeamSize(pjtDto.getTeamSize());
  return pjtRepo.save(pjt);
 }

 @Override
 public List<Project> all() {
  return pjtRepo.findAll();
 }

}


EmployeeController.java
package com.sample.app.controller;

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.RestController;

import com.sample.app.service.EmployeeService;

import io.swagger.annotations.ApiOperation;

import com.sample.app.dto.EmployeeDto;
import com.sample.app.entity.*;

@RestController
public class EmployeeController {

 @Autowired
 private EmployeeService empService;

 @ApiOperation(value = "Create new Employee", notes = "Create new Employee")
 @PostMapping(value = "/employees", produces = MediaType.APPLICATION_JSON_VALUE)
 public ResponseEntity<Employee> saveEmployee(@RequestBody EmployeeDto empDto) {

  Employee persistedEmp = empService.save(empDto);

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

 @ApiOperation(value = "Get Employee", notes = "Get employee by id")
 @GetMapping(value = "/employees/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
 public ResponseEntity<Employee> getEmployees(@PathVariable final Integer id) {

  Employee persistedEmp = empService.getById(id);

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

 @ApiOperation(value = "Update Employee", notes = "Update Existing Employee")
 @PutMapping(value = "/employees/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
 public ResponseEntity<Employee> updateEmployee(@PathVariable("id") int id, 
   @RequestBody EmployeeDto empDto) {

  Employee persistedEmp = empService.update(id, empDto);

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

}


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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.sample.app.dto.ProjectDto;
import com.sample.app.entity.Project;
import com.sample.app.service.ProjectService;

import io.swagger.annotations.ApiOperation;

@RestController
public class ProjectController {

 @Autowired
 private ProjectService pjtService;

 @ApiOperation(value = "Save Project", notes = "Create new Project")
 @PostMapping(value = "/projects", produces = MediaType.APPLICATION_JSON_VALUE)
 public ResponseEntity<Project> saveProject(@RequestBody ProjectDto pjtDto) {

  Project persistedPjt = pjtService.saveProject(pjtDto);

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

 @ApiOperation(value = "Get Projects", notes = "Get All Projects")
 @GetMapping(value = "/projects", produces = MediaType.APPLICATION_JSON_VALUE)
 public ResponseEntity<List<Project>> getProjects() {

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

}


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();
 }
}


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);
 }

}


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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>com.sample.app</groupId>
 <artifactId>manyToMany</artifactId>
 <version>1</version>

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

 <properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 </properties>


 <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>

  <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-envers -->
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-envers</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>

Create application.properties file under src/main/resources folder.


application.properties
logging.level.root=WARN
logging.level.org.hibernate=ERROR

## 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
# Creates the schema, destroying previous data.
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

spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false


Total project structure looks like below.

Run App.java.

Open the url ‘http://localhost:8080/swagger-ui.html’ in browser to get swagger user interface.

Open the url ‘http://localhost:8080/h2/login.do’ to get H2 console.


You can download complete working application from this link.

Previous                                                    Next                                                    Home

No comments:

Post a Comment