I have a very common business requirement related to "code table"
Business requirement
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?
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);