客户有一个所谓多中心的需求,就是一家医院为中心,多家分院分享数据供科研用。所有分院的科研需求都需要中心医院审批,这个是行政上的需求。落实到技术上,各家医院都部署了我们开发的应用和所依赖的数据库,我们数据库的模型称为OMOP,和一般意义的 HIS 系统不同,需要实施工程师从 HIS 系统提前数据插入到 OMOP 模型的数据库里面,我们的应用才能跑起来。
各家医院都有自己的数据库和应用(从技术上看,各节点是平行的),而且不希望数据对其他分院公开,只允许你做的科研可以在我的应用上跑,我只把结果给你。
那么问题来了,用户做科研需要在页面上做一些操作,会产生多表的数据插入,很多数据的 ID 是从 SEQUENCE里面取值。由于在不同的数据库,同一张表做插入的时候,各个医院的 SEQUENCE 值是不一致的。
一开始我们打算做 ID 映射,也就是记录下各个合作医院在数据插入后,和请求医院对应上 ID,这样,请求方在发送 REST 请求合作医院应用的时候,可以根据 ID 映射,修改请求参数,这样的工作量比较大,特别是某些表插入的记录很多的情况下很难厘清对应关系。
另一个做法就是,让每个分院都有一个对应的大数字,比如 A 医院的大数字是 1 亿, B 医院 2 亿, C医院 3 亿,依次类推。如果医院的某个科研需求别的医院合作,那么就监控这个科研项目所涉及的数据库表,只有有 ID 值是通过 SEQUENCE 取值的,就加上本医院对应的大数字。这样,修改后的 ID 插入到其他任何医院的数据库,理论上都不会导致 ID 重复,也就可以不用修改 REST 请求参数,直接请求合作医院的应用接口。
ID 修改一个要修改数据库里面存的值,同时也要修改应用在插入后获得的数据库 ID 值,因为某个记录插入后产生的 ID 将会被它的 Children 作为关联的 ID 插入数据库。(本来我们的ID的类型是INTEGER的,为了容纳增加后的数值,不得不改为LONG型)比如我要插入 CohortDefinition 紧接着就要插入 CohortDetail,它们公用 CohortDefinition 的ID。
@Transactional
public void createCohortDefinition(CohortDefinitionDTO defDto) {
defDto.setCreatedDate(Calendar.getInstance().getTime());
defDto.setStatus(null);
cohortDefinitionDao.insert(defDto);
CohortDetail detail = new CohortDetail();
detail.setId(defDto.getId());// defDto 插入后MyBatis会将数据库ID值赋给defDto对象
detail.setConceptsets(defDto.getConceptsets());
detail.setExpression(defDto.getExpression());
detail.setDefinitionDom(defDto.getDefinitionDom());
cohortDetailDao.insert(detail);
}
我定义了一个 SyncSequenceAdvice 的切面,该Bean实例化后就去检索本医院对应的大数字,赋值给 enlargeValue 属性,定义若干个需要被监控的 insert/ batchInsert 方法,通过 AfterReturning 通知获取被插入数据库的对象的SEQUENCE值,然后加上本院的大数,更新数据库的ID值和对象的ID值。
@Component
@Aspect
public class SyncSequenceAdvice {
private static final Logger logger = Logger.getLogger(SyncSequenceAdvice.class);
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private ProjectMapper projectDao;
// 将被加到各实体的ID上,保证在各分院可以顺利插入而不用做ID的映射
private static Long enlargeValue = 0L;
/*
* 通过本地设置的应用URL,从数据库里面拿到对应本应用的增强ID值,
* 该值将会被加到所有需要增强的多中心需要的实体ID上,从而ID在各分院对应的表中唯一。
*/
@PostConstruct
public void onceAssigner(){
String localVinciUrl = ConfigUtils.getSysConfig("LOCAL_VINCI").trim();
StringBuffer sb = new StringBuffer();
sb.append("SELECT ENLARGE_VALUE FROM MULTI_CENTRES WHERE CONNECT_INFO = '")
.append(localVinciUrl).append("'");
enlargeValue = jdbcTemplate.queryForObject(sb.toString(), Long.class);
}
@AfterReturning(value="execution(* com.hebta.vinci.dao.CohortDefinitionMapper.insert(..))")
public void resetCohortDefinitionId(JoinPoint jp){
logger.debug("开始更新插入后的CohortDefinition的ID");
Object[] args = jp.getArgs();
CohortDefinition cohortDef = (CohortDefinition)args[0];
if (!projectDao.selectMultiFlagByCohortDefId(cohortDef.getId())){ // 检查是不是多中心的项目
return;
}
Long oldId = cohortDef.getId(); // 这里得到新的SEQUENCE值
Long newId = oldId + enlargeValue;
StringBuffer sb = new StringBuffer();
sb.append("UPDATE COHORT_DEFINITION SET ID = ").append(newId).append(" WHERE ID = ").append(oldId);
jdbcTemplate.execute(sb.toString()); // 更新数据库的ID
cohortDef.setId(newId); // 将心的ID值赋给即将要返回的对象
logger.debug("完成数据库ID更新, 并且更新CohortDefinition实例的ID");
}
// 其他AfterReturning 通知方法
}
为了降低技术上的难度,对于一个科研,只在本地完成时才会同步到其他医院,而不是每一个操作都要其他分院同步。我们定义了一个 BroadcastAdvice 切面,只监控两个方法,当这两个方法被调用的时候,就是我们认为的一个科研里面重要的步骤完成了。
@Component
@Aspect
public class BroadcastAdvice {
// 拦截Service而非Controller的关于分析的方法,是因为我们只需要拿到尽可能少的值,比如CohortDefinitionId
@Pointcut(value="execution(* com.hebta.vinci.service.CohortAnalysisService.generateCohortAnalysis(..))")
private void beforeAnalyzeCohort(){}
@Pointcut(value="execution(* com.hebta.vinci.controller.GroupCompareController.executeAnalysis(..))")
private void beforeAnalyzeMultiCohorts(){}
@Before(value="beforeAnalyzeCohort() || beforeAnalyzeMultiCohorts()")
public void beforeExecution(JoinPoint jp) {
logger.debug("BroadcastAdvice::beforeExecution 执行前拦截请求,获取参数");
String methodName = jp.getSignature().getName();
Object[] methodInputParams = jp.getArgs();
switch (methodName) {
case "generateCohortAnalysis":
logger.debug("请求方执行单队列分析,其他分院开始同步");
CohortDefinition cohortDef = null;
for (Object o : methodInputParams){
cohortDef = (CohortDefinition)o;
break;
}
Long projectIdInCohort = cohortDef.getProjectId();
// 是多中心项目,同时自己是发起方(技术上只有发起方PROJECT_CENTRE_DATA才有相关数据)
if (projectDao.selectMultiFlagByCohortDefId(cohortDef.getId()) && isInitiator(projectIdInCohort)){
// 此处将会同步所有当前队列相关的信息到其他分院,并且在各分院执行分析
syncCohortAnalysis(projectIdInCohort, cohortDef.getId());
}
break;
case "executeAnalysis":
logger.debug("其他分院开始同步高级分析");
Map<String, Map<Long, List<GroupCohortVariableDTO>>> carriers = null;
for (Object o : methodInputParams){
carriers = (Map<String, Map<Long, List<GroupCohortVariableDTO>>>)o;
break;
}
Long advAnalysisId = groupService.getAdvAnalysisIdFromParameters(carriers.get("INNER"), carriers.get("OUTER"));
Long projectId = advInfoDao.selectByPrimaryKey(advAnalysisId).getProjectId();
if (projectDao.selectMultiFlagById(projectId) && isInitiator(projectId)){
// 此处将同步所有高级分析的信息到其他分院,并执行分析
syncGroupAnalysis(projectId, carriers);
}
break;
default:
throw new RuntimeException("没有处理的方法调用:" + methodName);
}
}
// 根据PROJECT_CENTRE_DATA判断当前应用的该项目是发起人创建的
private boolean isInitiator(Long projectId){
List<ProjectCentreData> centres = centresDao.selectByProjectId(projectId);
if (centres != null && centres.size() > 0){
return true;
}
return false;
}
// 执行单队列的一切信息同步
private void syncCohortAnalysis(Long projectId, Long cohortDefId){
syncCohortService.sendCohortSyncRequest(projectId, cohortDefId);
}
// 执行高级分析的一切信息同步
private void syncGroupAnalysis(Long projectId, Map<String, Map<Long, List<GroupCohortVariableDTO>>> carriers){
}
}
SyncCohortService 里的 sendCohortSyncRequest 如下:
public void sendCohortSyncRequest(Long projectId, Long cohortDefId){
logger.debug("进入单队列请求广播逻辑实现的方法 sendCohortSyncRequest");
Project project = getterService.getProjectById(projectId);
List<ConceptSet> conceptSetList = getterService.getConceptSetsByProjectId(projectId);
List<ConceptSetItem> conceptSetItemList = getterService.getConceptSetItemsByProjectId(projectId);
CohortDefinition cohorDef = getterService.getCohortDefByCohortDefId(cohortDefId);
CohortDetail cohortDetail = getterService.getCohortDetailByCohortDefId(cohortDefId);
List<CohortVariable> cohortVarList = getterService.getCohortVariablesByCohortDefId(cohortDefId);
CohortSyncDTO dto = new CohortSyncDTO();
dto.setProject(project);
dto.setConceptSetList(conceptSetList);
dto.setConceptSetItemList(conceptSetItemList);
dto.setCohorDef(cohorDef);
dto.setCohortDetail(cohortDetail);
dto.setCohortVarList(cohortVarList);
logger.debug("完成单队列生态数据的检索,并构造了REST操作的请求参数数据结构");
List<ProjectCentreData> centresData = centreDataDao.selectByProjectId(projectId);
ExecutorService es = Executors.newFixedThreadPool(centresData.size());
for (ProjectCentreData data : centresData){
es.execute(()->{
logger.debug("REST操纵请求发往:" + data.getSiteName());
String url = data.getConnectInfo() + "/sync/cohort";
SpringRest<Integer> restTool = new SpringRest<>(new TypeToken<Integer>(){}.getType());
restTool.doPost(url, dto);
});
}
}
用多线程向各个分院发送同步请求,请求参数封装了所有需要同步的对象,这些对象将被插入到合作分院的数据库,然后才能进行科研分析。这些对象都是包含了上文提到了 ID 处理了,都是有值的,如果避开合作分院本地插入时SEQUENCE的取值,就需要看我的 MyBatis在插入自增ID列有值情况下的处理 ,我在程序里面有的是使用了修改后的 <selectKey>, 有的使用了该文末尾说的方法,即在业务层判断该项目是不是多中心合作的项目,如果是,就调用 batchInsertWithIdInitiated 方法,这个方法就是针对ID有值的情况下直接插入。