snap-user/docs/USER_MODIFICATION_BEST_PRAC...

277 lines
6.7 KiB
Markdown
Raw Permalink Normal View History

2025-07-29 00:14:46 +08:00
# User对象变动时的修改最佳实践
## 概述
当`User`实体类需要变动时(如添加字段、删除字段、修改字段类型等),本文档提供了最小化影响的修改策略。
## 核心原则
### 1. 分层隔离原则
- **控制器层**只依赖DTO不直接使用实体
- **服务层**:处理业务逻辑,可以使用实体
- **数据层**:直接操作实体
### 2. 单一职责原则
- 每个组件只负责一个职责
- 变更影响范围最小化
## 变更场景与应对策略
### 场景1User实体添加新字段
**示例**为User添加`middleName`字段
#### 步骤1修改User实体
```java
// 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可选
```java
// UserDTO.java
public class UserDTO {
// 现有字段...
private String middleName; // 根据需要添加
// getter/setter
}
```
#### 步骤3更新UserMapper
```java
// 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更新控制器如果需要暴露新字段
```java
// UserControllerV2.java
public static class CreateUserRequest {
// 现有字段...
private String middleName; // 根据需要添加
// getter/setter
}
```
**影响范围**
- ✅ 控制器层:无需修改(除非要暴露新字段)
- ✅ 服务层:无需修改
- ⚠️ 映射层需要更新UserMapper
- ⚠️ 实体层:添加新字段
### 场景2User实体删除字段
**示例**删除User的`fax`字段
#### 步骤1标记字段为废弃渐进式删除
```java
// 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中移除字段
```java
// UserDTO.java - 不再包含fax字段
```
#### 步骤3更新UserMapper
```java
// UserMapper.java
public UserDTO toDTO(User user) {
// 移除fax相关映射
// dto.setFax(user.getFax()); // 注释或删除
return dto;
}
```
#### 步骤4数据库迁移如果需要
```sql
-- 可选:删除数据库列
ALTER TABLE users DROP COLUMN fax;
```
### 场景3User实体字段类型变更
**示例**:将`salary`从`Double`改为`BigDecimal`
#### 步骤1创建新字段保留旧字段
```java
// 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
```java
// UserMapper.java
public UserDTO toDTO(User user) {
// 使用新的getter方法
dto.setSalary(user.getSalary()); // 自动处理类型转换
return dto;
}
```
## 最佳实践总结
### 1. 使用DTO模式
```java
// ❌ 错误:控制器直接返回实体
@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进行转换
```java
// ❌ 错误:在控制器中手动转换
@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. 渐进式变更
```java
// ✅ 渐进式删除字段
public class User {
@Deprecated // 第一步:标记废弃
private String oldField;
private String newField; // 第二步:添加新字段
// 第三步:提供兼容性方法
public String getOldField() {
return newField; // 重定向到新字段
}
}
```
### 4. 版本化API
```java
// 为重大变更提供版本化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自动映射
```java
@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实体变更对系统的影响。关键是要保持各层之间的松耦合并建立清晰的变更流程。