使用RESTful Web实现多数据库的数据同步

客户有一个所谓多中心的需求,就是一家医院为中心,多家分院分享数据供科研用。所有分院的科研需求都需要中心医院审批,这个是行政上的需求。落实到技术上,各家医院都部署了我们开发的应用和所依赖的数据库,我们数据库的模型称为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有值的情况下直接插入。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值