Search code examples
jpaspring-data-jpamapstruct

Prevent JPA from updating code table values


I have a very common business requirement related to "code table"

Business requirement

  • A static list of Departments, i.e. created once and any changes are done by a completely different business process
  • A dynamic list of Employees
  • Employee must be assigned to a single department
  • The department assignment for an Employee can change over time
  • NOTHING done to an Employee can change the values inside the department, only the assignment of an Employee to a Department

Modeling this in Spring JPA as a Unidirectional ManyToOne

Updated with DTO, Mapper, Service classes as in actual code

@Entity
public class Employee {
    @Id
    private Long id;
    @Version
    private Integer version;
    @Column
    private String name;
    @ManyToOne(optional = false)    // cascade by default is {}, i.e. NONE
    @JoinColumn(name = "dept_id", nullable = false)
    Department department;
    // standard getters, setters and proper equals/hascode
}

@Entity
public class Department {
    @Id
    private Long id;
    @Version
    private Integer version;
    @Column
    private String name;
    // standard getters, setters and proper equals/hascode
}

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}

public class EmployeeDto {
    private Long id;
    private String name;
    private Long deptId;
    private String deptName;
    // getters, setters
}

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface EmployeeMapper {
    @Mapping(source = "department.id", target = "deptId")
    @Mapping(source = "department.name", target = "deptName")
    Employee toEntity(EmployeeDto dto);

    @InheritInverseConfiguration(name = "toEntity")
    EmployeeDto toDto(Employee entity);
}

@Service
public class EmployeeService {
    private final EmployeeRepository employeeRepository;
    private final EmployeeMapper employeeMapper;

    @Transactional(readOnly = true)
    public EmployeeDto findEmpl(Long id) {
        return employeeMapper.toDto(employeeRepository.findById(id).orElseThrow());
    }

    @Transactional
    public EmployeeeDto saveEmpl(EmployeeDto dto) {
        // business logic to validate Employee
        return employeeMapper.toDto(employeeRepository.save(employeeMapper.toEntity(dto)));
    }
}

public class Test() {
    @Autowired
    EmployeeService employeeService;
    
    @Test
    void givenValidDatabase_whenUpdateIncorrectFromUI_thenCorrectResults() {

        // simulates what actually happened in a "quick and dirty" UI 

        // retrieve Employee details
        EmployeeDto emp4 = employeeService.findEmpl(4L);
        
        // WebUi allows user to manually type in a Department name without updating the id
        dto4.setDeptName("asdfasdf");
        
        // WebUi passes incorrectly modified data back to be saved
        EmployeeDto savedDto = employeeService.saveEmpl(dto4);
        
        // Department info should be unmodified
        assertThat(savedDto.getDeptDescription()).isNotEqualTo("asdfasdf");
    }
}

The test fails and results in an update statement Hibernate: update department set name=?, version=? where id=? and version=?

Granted that the UI is at fault but is there any way to have JPA configured to NEVER allow an update statement to the Department table when accessed through an Employee?

I realize the fix is probably to modify the MapStruct to prevent those data from being copied. However is there a JPA annotation / configuration to prevent it from happening?


Solution

  • Like commented, I don't know of any JPA annotation to achieve what you want. The lack of cascade in the @ManyToOne might lead you to believe that changes to the related Department won't be saved to the DB, but I believe that the way JPA works is that, as long as it loads an entity (here: the Department) in the persistence context, it will track changes made to it and persist them to the DB in the end. So, as soon as you modify the Department, JPA will update the record in the DB, unless you take explicit action not to. That is probably error prone, but doable anyway.

    So, I think you are right, the way to go is modify the mapping to prevent unwanted data from being copied. And MapStruct helps a lot to this direction. I would propose the following approach (roughly - adjust to your real needs):

    // in you mapper:
    
    // The shallow mapping from DTO to Entity deals only with the properties of the
    // entity itself, disregarding relations.
    @Mapping(...)
    @Mapping(...)
    Employee toEntityShallow(EmployeeDto dto); // I SUGGEST USING THIS TO SOLVE THE
                                               // PROBLEM OF THE QUESTION
    
    // That's how MapStruct helps: you may want to load an instance of the Employee
    // and apply the changes from the DTO directly to it; inherit the mapping to
    // keep your implementation DRY
    @InheritConfiguration(name = "toEntityShallow")
    void updateEmployeeShallow(EmployeeDto dto, @MappingTarget Employee e);
    
    // If you really need a full mapper - DTO to Employee + Department, you
    // can simply inherit and extend the configuration above:
    @InheritConfiguration(name = "toEntityShallow")
    @Mapping(source = "department.id", target = "deptId")
    @Mapping(source = "department.name", target = "deptName")
    Employee toEntityDeep(EmployeeDto dto);