JPA:仅更新特定字段

Posted

技术标签:

【中文标题】JPA:仅更新特定字段【英文标题】:JPA: update only specific fields 【发布时间】:2021-06-05 21:14:29 【问题描述】:

有没有办法使用 Spring Data JPA 中的 save 方法更新实体对象的仅某些字段

例如,我有一个这样的 JPA 实体:

@Entity
public class User 

  @Id
  private Long id;

  @NotNull
  private String login;

  @Id
  private String name;

  // getter / setter
  // ...

使用它的 CRUD 回购:

public interface UserRepository extends CrudRepository<User, Long>  

Spring MVC 中,我有一个控制器,它获取一个 User 对象来更新它:

@RequestMapping(value = "/rest/user", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<?> updateUser(@RequestBody User user) 

   // Assuming that user have its id and it is already stored in the database,
   // and user.login is null since I don't want to change it,
   // while user.name have the new value

   // I would update only its name while the login value should keep the value 
   // in the database
   userRepository.save(user);

   // ...

我知道我可以使用findOne 加载用户,然后更改其名称并使用save 更新它...但是如果我有 100 个字段并且我想更新其中的 50 个,这可能是非常烦人的更改每个值..

有没有办法告诉类似“保存对象时跳过所有空值”?

【问题讨论】:

不,没有。唯一正确的方法是检索对象、更新字段并存储它,这可能是动态的。如果你想要别的东西,你必须为它编写 SQL 并自己做。但是假设User只有null对于你不想保存的字段你可以做的事情是相反的,使用传入的User并更新你知道没有改变的字段,然后更新那个。 为什么不跳过更新对象的空值更好? 没有可靠的方法知道要跳过什么,null 可以是字段的有效选项,然后呢?你可能会用 ann EntityListener 或类似的东西来固定一些东西,但我强烈建议不要这样做,因为它可能会导致比它解决的问题更多的问题。 您使用的 JPA 实现可能会默认执行此操作。我使用的(DataNucleus)只更新我更改的字段。本来以为这是所有人的默认设置... @NeilStockton 也许我错了,但我会尝试做的不是更改值然后只更新它们,因为我从不检索对象,但就像我创建一个新的设置它id ...我会得到的只是在更新操作中跳过空值...您的建议似乎是 Hibernate 中的 @DynamicUpdate(value = true) (在我的情况下不起作用) 【参考方案1】:

问题与弹簧数据 jpa 无关,而是您使用的 jpa 实现库相关。 在休眠的情况下,您可以查看:

http://www.mkyong.com/hibernate/hibernate-dynamic-update-attribute-example/

【讨论】:

感谢您的参考。实际上我正在使用休眠。在那篇文章中建议更改dynamic-update 属性:我刚刚尝试使用注释来做到这一点,但JPA 的@Entity 注释不支持该属性,并且不推荐使用Hibernate 的@Entity 注释......你知道正确的方法吗设置它? 正如另一个答案中所建议的那样,更改dynamic-update 的实际方法是使用@DynamicUpdate 注释...我尝试过,但没有看到任何变化...有什么建议吗? 请理解这个问题,它不应该适用于所有请求,对于用户的每个请求都会有所不同。动态更新,您可以指定几个字段,它们将在每次更新中被跳过。【参考方案2】:

我也有同样的问题,正如 M. Deinum 指出的那样,答案是否定的,你不能使用保存。主要问题是 Spring Data 不知道如何处理空值。 null值是没有设置还是因为需要删除才设置?

现在从你的问题来看,我想你也有和我一样的想法,那就是 save 可以让我避免手动设置所有更改的值。

那么有没有可能避免所有的手动映射呢?好吧,如果您选择遵守 null 始终表示“未设置”并且您拥有原始模型 ID 的约定,那么可以。 您可以使用 Springs BeanUtils 避免自己进行任何映射。

您可以执行以下操作:

    读取现有对象 使用 BeanUtils 复制值 保存对象

现在,Spring 的 BeanUtils 实际不支持不复制 null 值,因此它将覆盖现有模型对象上未设置为 null 的任何值。幸运的是,这里有一个解决方案:

How to ignore null values using springframework BeanUtils copyProperties?

所以把它们放在一起,你最终会得到这样的结果

@RequestMapping(value = "/rest/user", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<?> updateUser(@RequestBody User user) 

   User existing = userRepository.read(user.getId());
   copyNonNullProperties(user, existing);
   userRepository.save(existing);

   // ...


public static void copyNonNullProperties(Object src, Object target) 
    BeanUtils.copyProperties(src, target, getNullPropertyNames(src));


public static String[] getNullPropertyNames (Object source) 
    final BeanWrapper src = new BeanWrapperImpl(source);
    java.beans.PropertyDescriptor[] pds = src.getPropertyDescriptors();

    Set<String> emptyNames = new HashSet<String>();
    for(java.beans.PropertyDescriptor pd : pds) 
        Object srcValue = src.getPropertyValue(pd.getName());
        if (srcValue == null) emptyNames.add(pd.getName());
    
    String[] result = new String[emptyNames.size()];
    return emptyNames.toArray(result);

【讨论】:

我相信应该是userRepository.save(existing); 而不是#save(user); 如果数据类型是整数,则在 mysql 中更新默认为零。它适用于字符串,我们如何为其他数据类型(如 int、char、double)做到这一点。 如果使用版本,此解决方案容易出现并发问题或 OptimisticLockException。请参阅@arjun-nayak 答案以获得正确的解决方案【参考方案3】:

使用 JPA,您可以这样做。

CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaUpdate<User> criteria = builder.createCriteriaUpdate(User.class);
Root<User> root = criteria.from(User.class);
criteria.set(root.get("lastSeen"), date);
criteria.where(builder.equal(root.get("id"), user.getId()));
session.createQuery(criteria).executeUpdate();

【讨论】:

为什么这不是公认的答案?其他解决方案可能存在竞争条件:如果一个线程更新列 A 和 C,而另一个线程更新 B 和 D,则此解决方案将正常工作,但其他解决方案可能导致仅 A+C 或 B+D 已更新为第二次更新会覆盖之前的效果。是的,当然您可以添加一个@Version 字段并处理优化锁异常。但是仅更新您想要的列要简单得多。 感谢您的回答。令我震惊的是,90% 的开发人员(基于答案投票)并不关心并发问题。 但是当我们需要更新更多列时,这有什么帮助,比如说 100 列中的 50 列(这个数字可以动态变化,即有时我们只需要更新 100 列中的 30 列)。它会弄乱代码。 @JigneshM.Khatri 发送一个需要更新的键值对数组。您可以为所有这些键和值调用 criteria.set()。或者您也可以迭代 JSON 对象。 非常感谢。正因为如此,我设法实现了这个部分更新【参考方案4】:

如果您将请求读取为 JSON 字符串,则可以使用 Jackson API 来完成。这是下面的代码。代码比较现有的 POJO 元素并创建具有更新字段的新元素。使用新的 POJO 来持久化。

public class TestJacksonUpdate 

class Person implements Serializable 
    private static final long serialVersionUID = -7207591780123645266L;
    public String code = "1000";
    public String firstNm = "John";
    public String lastNm;
    public Integer age;
    public String comments = "Old Comments";

    @Override
    public String toString() 
        return "Person [code=" + code + ", firstNm=" + firstNm + ", lastNm=" + lastNm + ", age=" + age
                + ", comments=" + comments + "]";
    


public static void main(String[] args) throws JsonProcessingException, IOException 
    TestJacksonUpdate o = new TestJacksonUpdate();

    String input = "\"code\":\"1000\",\"lastNm\":\"Smith\",\"comments\":\"Jackson Update WOW\"";
    Person persist = o.new Person();

    System.out.println("persist: " + persist);

    ObjectMapper mapper = new ObjectMapper();
    Person finalPerson = mapper.readerForUpdating(persist).readValue(input);

    System.out.println("Final: " + finalPerson);

最终输出将是,注意只有 lastNm 和 Comments 反映了变化。

persist: Person [code=1000, firstNm=John, lastNm=null, age=null, comments=Old Comments]
Final: Person [code=1000, firstNm=John, lastNm=Smith, age=null, comments=Jackson Update WOW]

【讨论】:

谢谢。它非常有帮助。另外,我们最好在实体类上添加@JsonIgnoreProperties(ignoreUnknown = true),以忽略json中的未知属性。【参考方案5】:

你可以写类似的东西

@Modifying
    @Query("update StudentXGroup iSxG set iSxG.deleteStatute = 1 where iSxG.groupId = ?1")
    Integer deleteStudnetsFromDeltedGroup(Integer groupId);

或者如果您只想更新已修改的字段,您可以使用注释

@DynamicUpdate

代码示例:

@Entity
@Table(name = "lesson", schema = "oma")
@Where(clause = "delete_statute = 0")
@DynamicUpdate
@SQLDelete(sql = "update oma.lesson set delete_statute = 1, "
        + "delete_date = CURRENT_TIMESTAMP, "
        + "delete_user = '@currentUser' "
        + "where lesson_id = ?")
@JsonIgnoreProperties("hibernateLazyInitializer", "handler")

【讨论】:

【参考方案6】:

对于long、int等类型; 您可以使用以下代码;

            if (srcValue == null|(src.getPropertyTypeDescriptor(pd.getName()).getType().equals(long.class) && srcValue.toString().equals("0")))
            emptyNames.add(pd.getName());

【讨论】:

【参考方案7】:

保存对象时跳过所有空值

正如其他人所指出的,JPA 中没有直接的解决方案。

但是开箱即用,您可以使用 MapStruct。

这意味着您使用正确的find() 方法从数据库获取要更新的对象,仅覆盖非空属性,然后保存对象。

您可以使用 JPA,在 Spring 中像这样使用 MapStruct 仅更新数据库中对象的非空属性:

@Mapper(componentModel = "spring")
public interface HolidayDTOMapper 

    /**
     * Null values in the fields of the DTO will not be set as null in the target. They will be ignored instead.
     *
     * @return The target Holiday object
     */
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    Holiday updateWithNullAsNoChange(HolidayDTO holidayDTO, @MappingTarget Holiday holiday);


详情请见the MapStruct docu on that。

您可以注入 HolidayDTOMapper,就像使用其他 bean(@Autowired、Lombok、...)一样。

【讨论】:

以上是关于JPA:仅更新特定字段的主要内容,如果未能解决你的问题,请参考以下文章

Mongoose FindOneandUpdate - 仅更新特定字段将其他字段保留为以前的值

仅更新弹性搜索中的特定字段值

ReactJS:仅更新嵌套状态对象中的特定字段[重复]

如何在仅更新特定字段的dynamo db中进行批量更新

如何仅更新/保存更改的字段

Hibernate/JPA - 使用@Future 更新还包含日期字段的实体字段