需求背景
最近有一个开发需求:需要通过接口对接别的系统批量推送的数据,根据唯一键判断是插入还是更新数据到数据库中。由于是批量推送数据,如果使用遍历更新的方式会因为重复与数据库进行连接、断开,导致性能较差,因此考虑采用批量插入、更新的方式。
方案
项目用的mybatis-plus,提供了saveBatch()、updateBatchById()、saveOrUpdateBatch()批量操作的方法,但是updateBatchById()、saveOrUpdateBatch()在进行更新操作时是根据主键ID进行比较的,无法直接使用。
根据上述背景,有以下三种方案
- 先批量删除再批量插入
- 按唯一键查询得到全部已有数据
- 通过主键id删除原有的数据
- 再全量插入数据
- 删除、插入操作控制在同一个事务中,防止数据不一致
- 分别批量更新、插入数据
- 按唯一键查询得到全部已有数据
- 先将1中得到的主键id和新接入数据匹配,再通过主键id批量更新数据
- 最后全量插入余下数据
- 更新、插入操作控制在同一个事务中,防止数据不一致
- 重写mybatis-plus的saveOrUpdateBatch()方法,根据非主键唯一键进行更新
- 详细见下方代码实现
代码实现
个人比较推荐方案三,感觉代码更优雅一点(手动狗头),这里也只实现方案三的代码,重写saveOrUpdateBatch()方法,通过构建updateWrapper来自定义更新数据的相关字段和更新条件,同时通过@Transactional管理事务,保证操作前后的数据一致性。
例如:我们需要批量插入或更新员工信息,员工编号code是唯一键,通过code实现员工数据的新增和更新
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveOrUpdateBatchByCondition(List<Employee> employees, int batchSize) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(this.entityClass);
Assert.notNull(tableInfo, "error: can not execute. because can not find cache of TableInfo for entity!");
// 根据员工编码更新数据
String updateProperty = "code";
return this.executeBatch(employees, batchSize, ((sqlSession, employee) -> {
Object codeVal = ReflectionKit.getMethodValue(this.entityClass, employee, updateProperty);
LambdaUpdateWrapper<Employee> queryWrapper = new LambdaUpdateWrapper<>();
queryWrapper.eq(Employee::getCode, employee.getCode());
if (!StringUtils.checkValNull(codeVal) && !Objects.isNull(this.getOne(queryWrapper))) {
MapperMethod.ParamMap<Object> param = new MapperMethod.ParamMap<>();
param.put(Constants.WRAPPER, getUpdateWrapper(employee));
param.put(Constants.ENTITY, employee);
sqlSession.update(tableInfo.getSqlStatement(SqlMethod.UPDATE.getMethod()), param);
} else {
sqlSession.insert(tableInfo.getSqlStatement(SqlMethod.INSERT_ONE.getMethod()), employee);
}
}));
}
private LambdaUpdateWrapper<Employee> getUpdateWrapper(Employee employee) {
final LambdaUpdateWrapper<Employee> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(Employee::getAge, employee.getAge())
.set(Employee::getSubCompany, employee.getSubCompany())
.set(Employee::getJob, employee.getJob())
.set(Employee::getDepartment, employee.getDepartment())
.set(Employee::getSalary, employee.getSalary())
.eq(Employee::getCode, employee.getCode());
return updateWrapper;
}
推送过来的数据:
[
{
"name": "大壮",
"code": "DEV001",
"age": 28,
"subCompany": "上海公司",
"job": "研发",
"department": "研发一部",
"salary": 6000
},
{
"name": "铁柱",
"code": "DEV003",
"age": 34,
"subCompany": "上海公司",
"job": "研发",
"department": "研发二部",
"salary": 6000
},
{
"name": "小明",
"code": "DEV005",
"age": 25,
"subCompany": "南京公司",
"job": "研发",
"department": "研发二部",
"salary": 5000
}
]
原始数据如下:
数据更新结果:
同理,如果只需要批量更新数据,将上述带中的insert逻辑删除即可。