snap-user/docs/USER_MODIFICATION_BEST_PRAC...

277 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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实体变更对系统的影响。关键是要保持各层之间的松耦合并建立清晰的变更流程。