【实战总结】可扩展的数据模型设计,支持api参数动态扩展传入,解决业务模型字段的不确定性问题

一、背景

我们在实现功能时需要对属性进行抽象定义,但是只能针对明确的属性进行定义。如果后续随着业务增长需要添加其它的属性就需要进行程序上修改,非常的麻烦和繁琐同时还可能给功能带来一些未知的问题,那么我们能不能设计一个支持动态属性扩展的方案来解决以上的问题呢?答案是必须的,接下来看我们如何设计和实现解决以上的问题。

二、方案设计

  • 设计一个领域对象模型用于数据库存储映射(支持数据库扩展属性插入)
  • 设计一个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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值