snap-user/docs/USER_MODIFICATION_BEST_PRAC...

6.7 KiB
Raw Blame History

User对象变动时的修改最佳实践

概述

User实体类需要变动时(如添加字段、删除字段、修改字段类型等),本文档提供了最小化影响的修改策略。

核心原则

1. 分层隔离原则

  • 控制器层只依赖DTO不直接使用实体
  • 服务层:处理业务逻辑,可以使用实体
  • 数据层:直接操作实体

2. 单一职责原则

  • 每个组件只负责一个职责
  • 变更影响范围最小化

变更场景与应对策略

场景1User实体添加新字段

示例为User添加middleName字段

步骤1修改User实体

// User.java
public class User {
    // 现有字段...
    private String middleName;  // 新增字段
    
    // getter/setter
    public String getMiddleName() { return middleName; }
    public void setMiddleName(String middleName) { this.middleName = middleName; }
}

步骤2更新UserDTO可选

// UserDTO.java
public class UserDTO {
    // 现有字段...
    private String middleName;  // 根据需要添加
    
    // getter/setter
}

步骤3更新UserMapper

// UserMapper.java
public UserDTO toDTO(User user) {
    // 现有映射...
    dto.setMiddleName(user.getMiddleName());  // 新增映射
    return dto;
}

public User toEntity(UserDTO dto) {
    // 现有映射...
    user.setMiddleName(dto.getMiddleName());  // 新增映射
    return user;
}

步骤4更新控制器如果需要暴露新字段

// UserControllerV2.java
public static class CreateUserRequest {
    // 现有字段...
    private String middleName;  // 根据需要添加
    
    // getter/setter
}

影响范围

  • 控制器层:无需修改(除非要暴露新字段)
  • 服务层:无需修改
  • ⚠️ 映射层需要更新UserMapper
  • ⚠️ 实体层:添加新字段

场景2User实体删除字段

示例删除User的fax字段

步骤1标记字段为废弃渐进式删除

// User.java
public class User {
    @Deprecated
    private String fax;  // 标记为废弃,暂不删除
    
    @Deprecated
    public String getFax() { return fax; }
    @Deprecated
    public void setFax(String fax) { this.fax = fax; }
}

步骤2从DTO中移除字段

// UserDTO.java - 不再包含fax字段

步骤3更新UserMapper

// UserMapper.java
public UserDTO toDTO(User user) {
    // 移除fax相关映射
    // dto.setFax(user.getFax());  // 注释或删除
    return dto;
}

步骤4数据库迁移如果需要

-- 可选:删除数据库列
ALTER TABLE users DROP COLUMN fax;

场景3User实体字段类型变更

示例:将salaryDouble改为BigDecimal

步骤1创建新字段保留旧字段

// User.java
public class User {
    @Deprecated
    private Double salary;  // 旧字段,标记废弃
    
    private BigDecimal salaryAmount;  // 新字段
    
    // 兼容性方法
    public Double getSalary() {
        return salaryAmount != null ? salaryAmount.doubleValue() : salary;
    }
    
    public void setSalary(Double salary) {
        this.salary = salary;
        this.salaryAmount = salary != null ? BigDecimal.valueOf(salary) : null;
    }
    
    public BigDecimal getSalaryAmount() { return salaryAmount; }
    public void setSalaryAmount(BigDecimal salaryAmount) { 
        this.salaryAmount = salaryAmount;
        this.salary = salaryAmount != null ? salaryAmount.doubleValue() : null;
    }
}

步骤2更新UserMapper

// UserMapper.java
public UserDTO toDTO(User user) {
    // 使用新的getter方法
    dto.setSalary(user.getSalary());  // 自动处理类型转换
    return dto;
}

最佳实践总结

1. 使用DTO模式

// ❌ 错误:控制器直接返回实体
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
    return ResponseEntity.ok(userService.getUserById(id));
}

// ✅ 正确控制器返回DTO
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
    User user = userService.getUserById(id);
    UserDTO dto = userMapper.toDTO(user);
    return ResponseEntity.ok(dto);
}

2. 使用Mapper进行转换

// ❌ 错误:在控制器中手动转换
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
    User user = userService.getUserById(id);
    UserDTO dto = new UserDTO();
    dto.setId(user.getId());
    dto.setName(user.getName());
    // ... 大量重复代码
    return ResponseEntity.ok(dto);
}

// ✅ 正确使用专门的Mapper
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
    User user = userService.getUserById(id);
    UserDTO dto = userMapper.toDTO(user);  // 集中处理转换逻辑
    return ResponseEntity.ok(dto);
}

3. 渐进式变更

// ✅ 渐进式删除字段
public class User {
    @Deprecated  // 第一步:标记废弃
    private String oldField;
    
    private String newField;  // 第二步:添加新字段
    
    // 第三步:提供兼容性方法
    public String getOldField() {
        return newField;  // 重定向到新字段
    }
}

4. 版本化API

// 为重大变更提供版本化API
@RestController
@RequestMapping("/api/v1/users")  // 旧版本
public class UserControllerV1 { ... }

@RestController
@RequestMapping("/api/v2/users")  // 新版本
public class UserControllerV2 { ... }

变更检查清单

添加字段时

  • 更新User实体
  • 考虑是否需要更新UserDTO
  • 更新UserMapper的映射方法
  • 更新相关的请求/响应DTO
  • 添加数据库迁移脚本
  • 更新单元测试

删除字段时

  • 标记字段为@Deprecated
  • 从UserDTO中移除字段
  • 更新UserMapper移除相关映射
  • 更新API文档
  • 通知API使用者
  • 计划最终删除时间

修改字段类型时

  • 创建新字段,保留旧字段
  • 提供兼容性方法
  • 更新UserMapper处理类型转换
  • 添加数据迁移逻辑
  • 测试向后兼容性
  • 计划旧字段删除时间

工具推荐

1. MapStruct自动映射

@Mapper(componentModel = "spring")
public interface UserMapper {
    UserDTO toDTO(User user);
    User toEntity(UserDTO dto);
}

2. 数据库迁移工具

  • Flyway
  • Liquibase

3. API版本管理

  • Spring Boot Actuator
  • Swagger/OpenAPI

总结

通过采用DTO模式、Mapper模式和渐进式变更策略可以最大程度地减少User实体变更对系统的影响。关键是要保持各层之间的松耦合并建立清晰的变更流程。