You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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好,还支持自定义映射规则,适合复杂场景:

  1. 添加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>
  1. 定义映射接口:
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);
}
  1. 在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)模式

如果字段更新规则复杂(比如某些字段有特殊校验逻辑),可以把每个字段的更新逻辑封装成独立类,实现开闭原则:

  1. 定义更新器接口:
public interface AtleteFieldUpdater {
    void update(Atlete saved, Atlete updated);
}
  1. 实现具体字段的更新器,比如姓名更新器:
@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());
        }
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 22:25:33