一、背景
我们在实现功能时需要对属性进行抽象定义,但是只能针对明确的属性进行定义。如果后续随着业务增长需要添加其它的属性就需要进行程序上修改,非常的麻烦和繁琐同时还可能给功能带来一些未知的问题,那么我们能不能设计一个支持动态属性扩展的方案来解决以上的问题呢?答案是必须的,接下来看我们如何设计和实现解决以上的问题。
二、方案设计
- 设计一个领域对象模型用于数据库存储映射(支持数据库扩展属性插入)
- 设计一个DTO模型用于API参数模型 (支持前端扩展属性接收)
- 设计一个Factory实现DTO和领域对象模型的转换
三、代码实现
- 技术选型:Springboot、JPA、Jackson、Mysql
- 核心代码:DTO、Domain、Factory
API参数DTO字段设计:
/**
* 所有请求参数DTO的父类,子类集成即可,无需每个都要实现
*
* @author darrn.xiang
* @date 2022/8/14 15:07
*/
@Data
public class Description {
/**
* 扩展属性
*/
@JsonIgnore
private Map<String,String> additional;
/**
* 向扩展信息中添加属性值,如果key存在覆盖,key和value为空则忽略
*
* @param key 属性名称
* @param value 属性值
*/
@JsonAnySetter
public void setAdditional(String key,String value){
if(additional == null){
additional = new HashMap<>();
}
if(!StringUtils.hasLength(key) || !StringUtils.hasLength(value)){
return;
}
additional.put(key,value);
}
}
/**
* DTO参数
*
* @author darrn.xiang
* @date 2022/8/14 16:17
*/
@Data
public class UserDesc extends Description {
private String id;
private String name;
private Integer age;
}
领域模型属性抽象:
/**
* 所有领域模型数据存储基类,记录操作日志、表级隔离字段、扩展属性
*
* @author darrn.xiang
* @date 2022/8/14 15:07
*/
@Data
@MappedSuperclass
public class Domain implements Serializable {
/**
* 主键ID
*/
@Id
@Column(name = "id", unique = true, nullable = false)
private String id;
/**
* 创建人
*/
@Column(name = "created_by")
private String createdBy;
/**
* 创建日期
*/
@Column(name = "created_date", nullable = false, updatable = false)
@CreatedDate
@CreationTimestamp
private ZonedDateTime creationDate;
/**
* 最后更新人
*/
@Column(name = "last_updated_by")
private String lastUpdatedBy;
/**
* 最后更新日期
*/
@Column(name = "last_updated_date", nullable = false)
@LastModifiedDate
@UpdateTimestamp
private ZonedDateTime lastUpdatedDate;
/**
* 逻辑删除,是否被删除
*
*/
@Column(name = "deleted")
private boolean deleted;
/**
* 数据隔离编码,用于多租,多企业场景(如:某企业编码,某企业下的部门编码等)
@Column(name = "data_isolation_code1")
private String dataIsolationCode1;
@Column(name = "data_isolation_code2")
private String dataIsolationCode2;
@Column(name = "data_isolation_code3")
private String dataIsolationCode3;
*/
/**
* 扩展属性参数
*/
@ElementCollection(fetch = FetchType.EAGER)
private Map<String, String> additional;
}
/**
* 领域对象
*
* @author darrn.xiang
* @date 2022/8/14 15:08
*/
@Data
@Entity
@Table(name = "user")
@SQLDelete(sql = "UPDATE agreement SET deleted=true WHERE id=?")
@Where(clause = "deleted = false")
public class User extends Domain {
private String name;
private Integer age;
}
API参数模型DTO和领域模型转换实现:
/**
* DTO和领域模型转换处理,转换内部逻辑抽象,子类进行个性化重写
*
* @author darrn.xiang
* @date 2022/8/14 15:44
*/
public abstract class AFactory<T extends Domain,D extends Description> {
/**
* DTO转换为数据模型接口
*
* @param desc API参数
* @return 对象模型
*/
protected abstract T initEntity(D desc);
/**
* 更新逻辑内部扩展接口
*
* @param entity 对象模型
* @param desc API参数DTO
* @return 处理结果标识
*/
protected boolean updateInternal(final T entity, final D desc) {
return false;
}
public T create(final D desc) {
Assert.notNull(desc,"desc is null.");
final var entity = initEntity(desc);
entity.setId(UUID.randomUUID().toString());
update(entity, desc);
return entity;
}
public boolean update(final T entity, final D desc) {
Assert.notNull(entity,"entity is null.");
Assert.notNull(desc,"desc is null.");
final var additional = updateAdditional(entity, desc.getAdditional());
final var internal = updateInternal(entity, desc);
return additional || internal;
}
/**
* 扩展属性更新逻辑
*
* @param entity 实体对象
* @param additional 扩展属性
* @return 更新标识
*/
protected final boolean updateAdditional(final T entity, final Map<String, String> additional) {
Map<String, String> oldMap = entity.getAdditional();
final Map<String, String> newValues = additional == null ? new HashMap<>() : additional;
if (oldMap == null || !oldMap.equals(newValues)) {
Optional<Map<String, String>> newAdditional = Optional.of(newValues);
newAdditional.ifPresent(entity::setAdditional);
return newAdditional.isPresent();
}
return true;
}
}
/**
* DTO转领域模型实现
*
* @author darrn.xiang
* @date 2022/8/14 16:35
*/
@Component
public class UserFactory extends AFactory<User,UserDesc> {
@Override
protected User initEntity(UserDesc desc) {
User user = new User();
BeanUtils.copyProperties(desc,user);
return user;
}
@Override
protected boolean updateInternal(User entity, UserDesc desc) {
entity.setCreatedBy("root");
entity.setLastUpdatedBy("root");
return true;
}
}
测试API类实现:
/**
* 描述
*
* @author darrn.xiang
* @date 2022/8/14 16:21
*/
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserRepository userRepository;
@Autowired
private UserFactory userFactory;
@PostMapping
public ApiResult createUser(@RequestBody UserDesc desc){
User user = userRepository.saveAndFlush(userFactory.create(desc));
return ApiResult.success(user);
}
@PutMapping("/{id}")
public ApiResult updateUser(@PathVariable String id,@RequestBody UserDesc desc){
// 查询数据库是否不存在
Optional<User> optional = userRepository.findById(id);
if(!optional.isPresent()){
throw new ApplicationException("APP_100001");
}
User user = optional.get();
if(userFactory.update(user,desc) ){
User user1 = userRepository.saveAndFlush(user);
return ApiResult.success(user1);
}
throw new ApplicationException("APP_100002");
}
@GetMapping("/{id}")
public ApiResult findById(@PathVariable String id){
Optional<User> optional = userRepository.findById(id);
// 是否存在
if(!optional.isPresent()){
throw new ApplicationException("APP_100001");
}
return ApiResult.success(optional.get());
}
}
测试截图-新增:
测试截图-更新:
测试截图-查询详情:
数据库表结构:
四、总结
本次实战场景技术方案核心技术为Jackson注解+Map+JPA的应用,主要是考验开发者对Jackson注解的使用程度。
前端核心点:Jackson注解@JsonAnySetter + Map
后台核心点:JPA注解@ElementCollection+Map