I'm a beginner in Spring Boot and can't manage to solve a problem. I have an entity class (Customer) and a REST repository (CustomerRepository). The class contains some sensitive fields that I don't want to be exposed by the REST repository. So, I annotated these fields with the @JsonIgnore annotation as follows:
package br.univali.sapi.entities;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Customer
{
@Id
@GeneratedValue
private Long id = null;
private String login;
private String name;
@JsonIgnore
private String password;
@JsonIgnore
private String email;
public Customer()
{
}
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public String getLogin()
{
return login;
}
public void setLogin(String login)
{
this.login = login;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
public String getEmail()
{
return email;
}
public void setEmail(String email)
{
this.email = email;
}
}
Everything worked fine and my REST API returned the desired results. However, when I do a POST request to the API in order to insert a new entity I receive database errors: "column password can't be null", "column email can't be null"
.
The password and email are being sent to the server in the POST request along with the other parameters, however it seems to be ignored. If I remove the @JsonIgnore annotation the entity is persisted normally.
I know I could use a projection to hide these fields. But the projection is an optional parameter in the URL. This way an experienced user would be able to remove the parameter from the request URL and see these fields anyway.
If I could implicitly enforce a projection, that would solve the problem, but this seems to be impossible to do using only the Spring framework. I could achieve this using an Apache URL rewrite but the maintenance would suck.
Anyone has an idea on how I can solve this? Thanks!
EDIT 1:
I believe I found a solution using DTOs/projections (Data Transfer Objects). You have to create two DTOs, one for displaying the entity and another for updating the entity, as follows:
public interface CustomerViewDTO
{
public Long getId();
public String getLogin();
public String getName();
}
public class CustomerUpdateDTO
{
private String login;
private String name;
private String password;
private String email;
// Getters and Setters ommited for breviety
}
Now, on the repository you use the DTO and Spring will do it's magic:
@Transactional(readOnly = true)
public interface CustomerRepository extends JPARepository<Customer, Long>
{
// Using derived query
public CustomerViewDTO findByIdAsDTO(Long id);
// Using @Query annotation
@Query("SELECT C FROM Customer C WHERE C.id = :customerId")
public CustomerViewDTO findByIdAsDTO(@Param("customerId") Long id);
}
And for updating, you receive the DTO on your controller and map it to the entity on your service, like this:
@RestController
public class CustomerController
{
@Autowired
private CustomerService customerService;
@RequestMapping(method = "PATCH", path = "/customers/{customerId}")
public ResponseEntity<?> updateCustomer(@PathVariable Long customerId, @RequestBody CustomerUpdateDTO customerDTO)
{
CustomerViewDTO updatedCustomer = customerService.updateCustomer(customerId, customerDTO);
return ResponseEntity.ok(updatedCustomer);
}
@RequestMapping(method = GET, path = "/customers/{customerId}")
public ResponseEntity<?> findCustomerById(@PathVariable Long customerId)
{
return ResponseEntity.ok(customerService.findByIdAsDTO(Long));
}
}
@Service
public class CustomerService
{
@Autowired
private CustomerRepository customerRepository;
// Follow this tutorial:
// https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application
@Autowired
private ModelMapper modelMapper;
@Transactional(readOnly = false)
public CustomerViewDTO updateCustomer(Long customerId, CustomerUpdateDTO customerDTO)
{
Customer customerEntity = customerRepository.findById(customerId);
// This copies all values from the DTO to the entity
modelMapper.map(customerDTO, customerEntity);
// Now we have two aproaches:
// 1. save the entity and convert back to DTO manually using the model mapper
// 2. save the entity and call the repository method which will convert to the DTO automatically
// The second approach is the one I use for several reasons that
// I won't explain here
// Here we use save and flush to force JPA to execute the update query. This way, when we do the select the entity will come with the fields updated
customerEntity = customerRepository.saveAndFlush(customerEntity);
// First aproach
return modelMapper.map(customerEntity, CustomerViewDTO.class);
// Second approach
return customerRepository.findByIdAsDTO(customerId);
}
@Transactional(readOnly = true)
public CustomerViewDTO findByIdAsDTO(Long customerId)
{
return customerRepository.findByIdAsDTO(customerId);
}
}