首页 > 解决方案 > 更新父对象时删除特定子对象

问题描述

我正在研究 Spring Boot 和 Spring jparepository。

我让 Object 说“部门”并且有一个员工列表。我的部门创建工作正常,我在更新部门对象时面临的问题。

从 Rest Api 我得到 Department 对象,其中包含需要在数据库中更新/删除/添加的员工列表,我的员工实体有一个瞬态属性说操作,基于我正在过滤出需要执行的操作关于员工更新/删除/添加。

部门实体-

@Entity
@Table(name="department")
public class  Department{

    @Id @Column(name="dept_id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    public Integer id;

    @Column(name="dept_name")
    public String name;

    @Column(name="dept_code")
    public String code;

    @OneToMany(cascade = {CascadeType.REMOVE,CascadeType.REFRESH},orphanRemoval = true, 
    fetch = FetchType.LAZY,mappedBy="department")
    public List<Employee> employees;
}

员工实体 -

@Entity
@Table(name="employee")
public class Employee {

    @Id     @Column(name="emp_id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    public Integer id;

    @Column(name="emp_name")
    public String name;

    @ManyToOne
    @JoinColumn(name = "dept_id")
    public Department  department;

    @Transient
    public String operation;
}

部门服务层 -

@Transactional
@Service
public class DepartmentService {

    public Department createDepartment(Department department) {

        Department dept = departmentRepository.save(department);
        for (Employee emp : department.getEmployees()) {
            emp.setDepartment(department);
            employeeRepository.save(emp);
        }
        return dept;
    }

    public Department updateDepartment(Department department) {

        Department dept = departmentRepository.save(department);
        if (!dept.getEmployees().isEmpty()) {
            for (Employee emp : department.getEmployees()) {
                if (emp.getOperation().equalsIgnoreCase("delete")) 
                    employeeRepository.deleteById(emp.getId());
                 else 
                    employeeRepository.save(emp);
            }
        }
        return dept;
    }

    public Department getDepartment(int id) {
        return departmentRepository.getOne(id);
    }
}

调试后我得到的是 -

  1. 当我触发获取部门的 Api 时,我的服务层使用 @Transactional 进行了注释。它正在返回部门的代理对象,并且在服务层中没有获取员工列表。当模型映射器当时将 dept 对象转换为 deptBean 时,它正在获取员工列表。为什么我能够从事务外部的代理对象中获取员工对象列表。

  2. 在服务层的更新功能中,我也返回代理对象,但也无法获取服务层中的员工列表。此外,我尝试在部门更新控制器后调用服务层的获取功能,即使这也无法获取员工列表。

控制器

@RestController
public class DepartmentController {

    @Autowired
    DepartmentService departmentService;

     private ModelMapper modelMapper = new ModelMapper();

    @RequestMapping(value = "/dept", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<DepartmentBean> updateDepartment(@RequestBody DepartmentBean deptBean) {

        Department dept = modelMapper.map(deptBean, Department.class);
        Department persistedDept = departmentService.updateDepartment(dept);
        Department d  = departmentService.getDepartment(persistedDept.getId());
        DepartmentBean userDTO = modelMapper.map(d, DepartmentBean.class);
        return  new ResponseEntity<DepartmentBean>(userDTO, HttpStatus.OK);
    }

    @RequestMapping(value = "/dept/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ResponseEntity<DepartmentBean> getDepartment(@PathVariable int id) {

        Department dept = departmentService.getDepartment(id);
        DepartmentBean userDTO = modelMapper.map(dept, DepartmentBean.class);
        return  new ResponseEntity<DepartmentBean>(userDTO, HttpStatus.OK);
    }
}

用于更新部门 Api 的 Json

{
    "id": 5,
    "name": "DEPT061",
    "code": "CODE061",
    "employees": [

    ]
}

标签: javahibernatespring-bootjpaspring-data

解决方案


当我触发获取部门的 Api 时,我的服务层使用 @Transactional 进行了注释。它正在返回部门的代理对象,并且在服务层中没有获取员工列表。当模型映射器当时将 dept 对象转换为 deptBean 时,它正在获取员工列表。为什么我能够从事务外部的代理对象中获取员工对象列表。

你有几种方法可以处理这个问题。

我强烈不建议但可用的一种方法是将集合标记为像这样急切地加载到您的映射模型中

// inside your Department entity
@OneToMany(mappedBy = "department", fetch = FetchType.EAGER)
private List<Employee> employees;

这种方法的问题在于,虽然它可以工作,但它引入了我们所说SELECT N+1的基本上持久性提供者将获取的地方Department,并将通过发出选择来填充集合来跟进。选择 1 时Department,这显然不是什么大问题。当您像这样选择多个部门时,它会成为一个主要的性能问题

SELECT * FROM Department // returns 3 rows
SELECT * FROM Employees WHERE departmentId = :row1DepartmentId   
SELECT * FROM Employees WHERE departmentId = :row2DepartmentId
SELECT * FROM Employees WHERE departmentId = :row3DepartmentId

对于大型结果集,这可能会对性能造成重大影响。

根据查询调用者的要求,在查询时耦合关联获取的最佳和建议方法。换句话说,不要使用#findOne()而是编写一个专门的查询来返回您的代码所需的内容。

@Query( "SELECT d FROM Department d JOIN FETCH employees WHERE d.id = :id" )
public List<Department> getDepartmentWithEmployees(Integer id)

这将避免延迟初始化问题,因为您明确要求提供者在您离开事务边界之前预先为您提供所需的所有信息。

在服务层的更新功能中,我也返回代理对象,但也无法获取服务层中的员工列表。此外,我尝试在部门更新控制器后调用服务层的获取功能,即使这也无法获取员工列表。

因为我们通过调用解决了延迟初始化问题,所以#getDepartment这应该不再是问题。

从 Rest Api 我得到 Department 对象,其中包含需要在数据库中更新/删除/添加的员工列表,我的员工实体有一个瞬态属性说操作,基于我正在过滤出需要执行的操作关于员工更新/删除/添加。

这里有几个昵称。

我首先会考虑将您的 JSON 对象和数据库实体对象解耦。您实际上是在使用瞬态字段污染您的数据库模型,这样您就可以将一些数据从控制器传递到您的持久层。这对我来说感觉不对。

如果您不想分离您的 json 对象和实体模型,那么至少将该瞬态数据放在您单独填充并提供给更新过程的单独上下文对象中

public class EmployeeOperationContext {
  private Integer employeeId;
  private EmployeeOperation operation;
}

public enum EmployeeOperation {
  INSERT,
  UPDATE,
  DELETE
}

public void updateDepartment(
     Department dept, 
     List<EmployeeOperationContext> contexts) { 
  ...
}

这里要指出的关键点是,在任何时候,您都可能需要重构数据库模型以更好地执行或以更规范的方式提供更好的数据库视图。这样做并不意味着您的 REST API 会改变。

相反,当 REST API 的使用者指示更改但您不希望这些更改影响您的数据库模型时,也会发生同样的情况。

您的控制器和服务层的全部意义在于弥合这些差距,而不是成为通路帮助者。因此,按预期使用它们。您可能会认为这是很多开销,但它肯定会改善您的设计并减少变化对频谱两端的影响,从而产生更大的连锁反应。

更新

Department更新的问题是您盲目地从传入的 rest 调用中获取数据并将其推送到数据库而不将其与现有数据合并。换句话说,看看这个

// Here you covert your DepartmentBean JSON object to a Department entity
Department dept = modelMapper.map( deptBean, Department.class );
// Here you overwrite the existing Department with JSON data
Department persistedDept = departmentRepository.save( dept );
// ^ this department no longer has employees because of this

有几种方法可以解决这个问题,但它们都涉及相同的前提。这里主要关注的是您必须首先从数据库中获取现有的部门对象,以便您拥有正确的状态,然后应用传入的更改。简而言之:

// Fetch department from the database
Department department = departmentRepository.get( departmentId );
// overlay the DepartmentBean data on the Department
modelMapper.map( deptBean, department, Department.class );
// save department
departmentRepository.save( department );

然后我最终要做的是修改服务方法以将其DepartmentBean作为输入并在服务中执行上述操作:

@Transactional
public void updateDepartment(DepartmentBean jsonModel) {
  // Now we can read the department & apply a read lock in the trx
  Department department = repository.getWithLock( departmentId );

  // Overlay the json data on the entity instance
  modelMapper.map( jsonModel, department, Department.class );

  // save the changes
  repository.save( department );
}

您可以在此处添加原始更新中的其他服务逻辑,也可以根据需要处理员工的删除。美妙之处在于,由于这都包含在事务绑定的服务方法中,您不再需要员工实体中的瞬态字段。您可以简单地从传入的 bean 参数中读取操作并直接调用适当的员工存储库方法。


推荐阅读