Spring Boot REST API中Atlete实体更新方法的设计模式优化
问题
我用Java Spring Boot开发REST API,刚接触设计模式,现在要优化Atlete实体的update方法,实现所有信息的校验。目前的update方法需要逐个字段判断是否更新,要是实体有50个字段的话会非常繁琐,想问问有没有合适的设计模式或者Spring工具,不用逐个校验字段就能实现实体更新?
Atlete实体代码
@Getter @Setter @Entity @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) @Table(name = "atletes", schema = "personas") public class Atlete { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id_atlete") private Long idAtlete; @Lob @Type(type = "org.hibernate.type.TextType") @Column(name = "atlete_name") private String atleteName; @Temporal(TemporalType.DATE) @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") @Column(name = "date_birth") private Date dateBirth; @Column(name = "atlete_height") private Double atleteHeight; @Column(name = "atlete_weight") private Double atleteWeight; @Column(name = "atlete_imc") private Double atleteImc; @Lob @Type(type = "org.hibernate.type.TextType") @Column(name = "atlete_bid") private String atleteBid; @Enumerated(EnumType.STRING) @Column(name = "dominant_leg") private DominantLeg dominantLeg; @Enumerated(EnumType.STRING) @Column(name = "position") private Position position; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "id_adress") private Adress adress; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "id_contact") private Contact contact; @JsonProperty("atleteClubs") @OneToMany(fetch = FetchType.LAZY, mappedBy = "atlete", cascade = CascadeType.REMOVE) private List<AtleteClub> atleteClubs; @Lob @Type(type = "org.hibernate.type.TextType") @Column(name = "atlete_deceases") private String deceases; }
AtleteController的PUT请求方法
@PutMapping(value = URL_SINGULAR, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<AtleteDTO> updateAtlete(@PathVariable Long id, @RequestBody @Valid AtleteDTO atleteDTO) { Optional<Atlete> verifyId = atleteRepository.findById(id); if (verifyId.isPresent()) { Atlete updatedAtlete = atleteService.desconvertObject(atleteDTO); Atlete atlete = atleteService.update(id, updatedAtlete, atleteRepository); return ResponseEntity.ok(new AtleteDTO(atlete)); } return ResponseEntity.notFound().build(); }
AtleteService的update方法
@Transactional public Atlete update(Long id, Atlete updated, AtleteRepository atleteRepository) { Atlete saved = atleteRepository.findByidAtlete(id); if (updated.getAtleteName() != null && !updated.getAtleteName().equals(saved.getAtleteName())) { saved.setAtleteName(updated.getAtleteName()); } if (updated.getDateBirth() != null && !updated.getDateBirth().equals(saved.getDateBirth())) { saved.setDateBirth(updated.getDateBirth()); } if (updated.getAtleteHeight() != null && !updated.getAtleteHeight().equals(saved.getAtleteHeight())) { saved.setAtleteHeight(updated.getAtleteHeight()); } if (updated.getAtleteWeight() != null && !updated.getAtleteWeight().equals(saved.getAtleteWeight())) { saved.setAtleteWeight(updated.getAtleteWeight()); } if (updated.getAtleteImc() != null && !updated.getAtleteImc().equals(saved.getAtleteImc())) { saved.setAtleteImc(updated.getAtleteImc()); } if (updated.getAtleteBid() != null && !updated.getAtleteBid().equals(saved.getAtleteBid())) { saved.setAtleteBid(updated.getAtleteBid()); } if (updated.getDominantLeg() != null && !updated.getDominantLeg().equals(saved.getDominantLeg())) { saved.setDominantLeg(updated.getDominantLeg()); } if (updated.getPosition() != null && !updated.getPosition().equals(saved.getPosition())) { saved.setPosition(updated.getPosition()); } if (updated.getContact() != null && !updated.getContact().equals(saved.getContact())) { saved.setContact(contactService.update(updated.getContact().getId(), updated.getContact(), contactRepository)); } if (updated.getAdress() != null && !updated.getAdress().equals(saved.getAdress())) { saved.setAdress(adressService.update(updated.getAdress().getId(), updated.getAdress(), adressRepository)); } if (updated.getAtleteClubs() != null && !updated.getAtleteClubs().isEmpty()) { for (AtleteClub atleteClub : updated.getAtleteClubs()) { atleteClubService.update(atleteClub.getId(), atleteClub,atleteClubRepository); } } if (updated.getDeceases() != null && !updated.getDeceases().equals(saved.getDeceases())) { saved.setDeceases(updated.getDeceases()); } return saved; }
解决方案
针对这种大量字段的更新场景,有几种实用的优化方案,不用再手写一堆if判断:
方案1:利用Spring Data JPA的save方法(全量更新)
如果是全量更新(前端传入完整的实体数据,没有缺失字段),直接用JPA的save方法即可。JPA会自动跟踪持久化实体的状态变化,执行merge操作时自动对比更新:
@Transactional public Atlete update(Long id, Atlete updated, AtleteRepository atleteRepository) { // 查询出数据库中已有的实体 Atlete saved = atleteRepository.findById(id) .orElseThrow(() -> new RuntimeException("未找到对应运动员")); // 给更新对象设置ID,确保JPA识别为已有实体 updated.setIdAtlete(id); // JPA自动执行更新,无需手动赋值 return atleteRepository.save(updated); }
注意:这种方式不适合增量更新(前端只传修改的字段),否则未传入的字段会被设为null。
方案2:使用Spring的BeanUtils.copyProperties(增量更新,忽略null值)
Spring自带的BeanUtils可以通过反射自动复制非null字段到目标实体,搭配自定义工具方法忽略null值,省去手动判断:
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import java.beans.PropertyDescriptor; import java.util.HashSet; import java.util.Set; @Transactional public Atlete update(Long id, Atlete updated, AtleteRepository atleteRepository) { Atlete saved = atleteRepository.findById(id) .orElseThrow(() -> new RuntimeException("未找到对应运动员")); // 复制更新对象中的非null字段到已有实体 BeanUtils.copyProperties(updated, saved, getNullPropertyNames(updated)); // 关联对象单独处理(比如Contact、Adress) if (updated.getContact() != null) { saved.setContact(contactService.update(updated.getContact().getId(), updated.getContact(), contactRepository)); } if (updated.getAdress() != null) { saved.setAdress(adressService.update(updated.getAdress().getId(), updated.getAdress(), adressRepository)); } if (updated.getAtleteClubs() != null && !updated.getAtleteClubs().isEmpty()) { updated.getAtleteClubs().forEach(atleteClub -> atleteClubService.update(atleteClub.getId(), atleteClub, atleteClubRepository) ); } return saved; } // 工具方法:获取对象中值为null的属性名,用于copy时忽略 private String[] getNullPropertyNames(Object source) { final BeanWrapper src = new BeanWrapperImpl(source); PropertyDescriptor[] pds = src.getPropertyDescriptors(); Set<String> emptyNames = new HashSet<>(); for (PropertyDescriptor pd : pds) { Object srcValue = src.getPropertyValue(pd.getName()); if (srcValue == null) { emptyNames.add(pd.getName()); } } return emptyNames.toArray(new String[0]); }
这个方法解决了单个字段的复制问题,关联对象因为涉及业务逻辑,还是需要单独处理。
方案3:用MapStruct实现类型安全的字段映射(推荐)
MapStruct是编译时生成映射代码的工具,性能比反射类的BeanUtils好,还支持自定义映射规则,适合复杂场景:
- 添加Maven依赖:
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.5.Final</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.5.5.Final</version> <scope>provided</scope> </dependency>
- 定义映射接口:
import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import org.mapstruct.NullValuePropertyMappingStrategy; @Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) public interface AtleteMapper { // 忽略null值,只更新非null字段 void updateAtleteFromDto(Atlete updated, @MappingTarget Atlete target); }
- 在Service中使用:
@Service public class AtleteService { private final AtleteMapper atleteMapper; // 其他依赖... // 构造注入映射器 public AtleteService(AtleteMapper atleteMapper, ...) { this.atleteMapper = atleteMapper; } @Transactional public Atlete update(Long id, Atlete updated, AtleteRepository atleteRepository) { Atlete saved = atleteRepository.findById(id) .orElseThrow(() -> new RuntimeException("未找到对应运动员")); // 自动复制非null字段到已有实体 atleteMapper.updateAtleteFromDto(updated, saved); // 关联对象处理逻辑 if (updated.getContact() != null) { saved.setContact(contactService.update(updated.getContact().getId(), updated.getContact(), contactRepository)); } // ... 其他关联对象处理 return saved; } }
MapStruct会在编译时生成具体的映射代码,没有反射开销,还能自定义字段映射规则,扩展性很强。
方案4:设计模式:更新器(Updater)模式
如果字段更新规则复杂(比如某些字段有特殊校验逻辑),可以把每个字段的更新逻辑封装成独立类,实现开闭原则:
- 定义更新器接口:
public interface AtleteFieldUpdater { void update(Atlete saved, Atlete updated); }
- 实现具体字段的更新器,比如姓名更新器:
@Component public class AtleteNameUpdater implements AtleteFieldUpdater { @Override public void update(Atlete saved, Atlete updated) { if (updated.getAtleteName() != null && !updated.getAtleteName().trim().isEmpty() && !updated.getAtleteName().equals(saved.getAtleteName())) { saved.setAtleteName(updated.getAtleteName()); } } }
- 在Service中注入所有更新器,批量执行:
@Service public class AtleteService { private final List<AtleteFieldUpdater> fieldUpdaters; // 其他依赖... // Spring会自动注入所有AtleteFieldUpdater的实现类 public AtleteService(List<AtleteFieldUpdater> fieldUpdaters, ...) { this.fieldUpdaters = fieldUpdaters; } @Transactional public Atlete update(Long id, Atlete updated, AtleteRepository atleteRepository) { Atlete saved = atleteRepository.findById(id) .orElseThrow(() -> new RuntimeException("未找到对应运动员")); // 执行所有字段的更新逻辑 fieldUpdaters.forEach(updater -> updater.update(saved, updated)); // 关联对象处理逻辑 // ... return saved; } }
新增字段时只需要添加对应的更新器类,不用修改原有代码,符合开闭原则,适合复杂业务场景。
内容的提问来源于stack exchange,提问作者F4NT0
相关产品推荐
相关产品推荐

