277 lines
6.7 KiB
Markdown
277 lines
6.7 KiB
Markdown
|
# User对象变动时的修改最佳实践
|
|||
|
|
|||
|
## 概述
|
|||
|
|
|||
|
当`User`实体类需要变动时(如添加字段、删除字段、修改字段类型等),本文档提供了最小化影响的修改策略。
|
|||
|
|
|||
|
## 核心原则
|
|||
|
|
|||
|
### 1. 分层隔离原则
|
|||
|
- **控制器层**:只依赖DTO,不直接使用实体
|
|||
|
- **服务层**:处理业务逻辑,可以使用实体
|
|||
|
- **数据层**:直接操作实体
|
|||
|
|
|||
|
### 2. 单一职责原则
|
|||
|
- 每个组件只负责一个职责
|
|||
|
- 变更影响范围最小化
|
|||
|
|
|||
|
## 变更场景与应对策略
|
|||
|
|
|||
|
### 场景1:User实体添加新字段
|
|||
|
|
|||
|
**示例**:为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
|
|||
|
- ⚠️ 实体层:添加新字段
|
|||
|
|
|||
|
### 场景2:User实体删除字段
|
|||
|
|
|||
|
**示例**:删除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;
|
|||
|
```
|
|||
|
|
|||
|
### 场景3:User实体字段类型变更
|
|||
|
|
|||
|
**示例**:将`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实体变更对系统的影响。关键是要保持各层之间的松耦合,并建立清晰的变更流程。
|