Spring:无法持久使用@JsonIgnore 注释的实体

Posted

技术标签:

【中文标题】Spring:无法持久使用@JsonIgnore 注释的实体【英文标题】:Spring: can't persist entity annotated with @JsonIgnore 【发布时间】:2017-07-27 15:08:01 【问题描述】:

我是 Spring Boot 的初学者,无法解决问题。我有一个实体类 (Customer) 和一个 REST 存储库 (CustomerRepository)。该类包含一些我不想被 REST 存储库公开的敏感字段。所以,我用@JsonIgnore 注解对这些字段进行了如下注解:

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;
    

一切正常,我的 REST API 返回了预期的结果。但是,当我向 API 发出 POST 请求以插入新实体时,我收到数据库错误:"column password can't be null", "column email can't be null"

密码和电子邮件与其他参数一起在 POST 请求中发送到服务器,但似乎被忽略了。如果我删除 @JsonIgnore 注释,实体将正常保留。

我知道我可以使用投影来隐藏这些字段。但投影是 URL 中的可选参数。这样,有经验的用户将能够从请求 URL 中删除参数并看到这些字段。

如果我可以隐式执行投影,那将解决问题,但这似乎只使用 Spring 框架是不可能的。我可以使用 Apache URL 重写来实现这一点,但维护会很糟糕。

有人知道我该如何解决这个问题吗? 谢谢!

编辑 1:

我相信我找到了使用 DTO/投影(数据传输对象)的解决方案。您必须创建两个 DTO,一个用于显示实体,另一个用于更新实体,如下所示:

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

现在,在您使用 DTO 的存储库上,Spring 会发挥它的魔力:

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

对于更新,您会在控制器上收到 DTO,并将其映射到服务上的实体,如下所示:

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

【问题讨论】:

也许this question 可以帮助你。 cmets 也很有趣。 将@JsonSetter 注释添加到您的密码和电子邮件设置器中。即使相关字段被标记为忽​​略,它也应该允许反序列化。或者,按照here 的建议,仅将@JsonIgnore 应用于吸气剂 【参考方案1】:

使用 Jackson 注释最好在 Dto 或 Resources 类中使用它,而不是在 Entity 类中使​​用它。因此您可以轻松创建自己的请求和响应 DTO/资源。因此,您可以通过创建 DTO 或 Resources 类来随意使用 Jackson 注释。如果你只在你的实体类中使用它,你将很难执行你想要的操作。这是因为您的实体类与数据库相关,因此它们应该只与数据库对话。执行来自客户端的请求或响应是不正确的。你已经在一开始就回答了这个问题。例如,假设您在 Entity 类的任何字段上使用了 JsonIgnoreProperties 注释。例如,当您在控制器层中返回客户时,您不希望出现任何变量的值。但是您的客户实体类也将其作为 POST 请求发送,并且存在问题。因此,您不应在实体类上执行这些操作,而应将您期望的响应或您收到的请求包装在 DTO/Resources 类中。

【讨论】:

【参考方案2】:

有很多方法可以做到这一点

使用这个

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;

而不是这个

@JsonIgnore
private String password;

另一种方式

@JsonIgnore @JsonProperty

在类成员及其getter上使用@JsonIgnore,在其setter上使用@JsonProperty。

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



@JsonIgnore
public String getPassword()

        return password;


@JsonProperty
public void setPassword(String password)

        this.password = password;


@JsonIgnore
public String getEmail()

        return email;


@JsonProperty
public void setEmail(String email)

        this.email = email;
 

【讨论】:

【参考方案3】:

我想你在数据库级别上没有空约束。基本上,当您在字段上执行 @JsonIgnore 时,您只是没有传递它并且数据库无法插入该值,这很清楚。

所以我在这里看到的解决方案如下:

1) 移除@JsonIgnore 注释,以便能够执行POST 请求。

2) 使用GET 时使用投影。您可以将Spring Data REST 配置为始终 使用投影,但默认情况下Spring Data REST 始终在单个资源(在您的情况下使用电子邮件和密码)上返回完整的JSON,这对我来说很奇怪。我在another answer 中写过这个问题的解决方案,因为我也遇到了同样的问题。只需使用ProjectingProcessor 并为所有投影使用一些默认名称。不要忘记使用config.getProjectionConfiguration().addProjection 方法在您的配置中添加投影。

3) 避免步骤 2 的唯一方法是在 Spring Data REST 级别上禁止 GET 使用单个资源并使用自定义控制器。但我会采用第 2 步中的解决方案来避免使用样板控制器。

【讨论】:

您可以使用@RepositoryRestResourceexcerptProjection 参数将您的投影设为默认投影。

以上是关于Spring:无法持久使用@JsonIgnore 注释的实体的主要内容,如果未能解决你的问题,请参考以下文章

Spring Resttemplate:使用@JsonIgnore 解析导致值为空

Spring Boot使用@JsonProperty,@JsonIgnore,@JsonFormat注解

如何在 Spring Boot 中使用 JsonIgnore 来停止无限循环? [复制]

关于注解@JsonIgnore的使用及一些坑

Spring @JsonIgnore 序列化未按预期工作

Spring Boot @JsonIgnore 在实体上,有时我希望将属性作为 Json 返回