Skip to content

Commit 17cbf89

Browse files
alex-odysseusoleg-odysseuschrisknoll
authored
Non-Standard to Standard Concepts mapping API (#2407)
* Batch operation for fetching related standard concepts with reverse mappings * Optimized SQL for fetching related standard concepts, eliminated vendor-specific string aggregating function * Renaming the migration script * Update git actions to use cache@v4. --------- Co-authored-by: oleg-odysseus <[email protected]> Co-authored-by: Chris Knoll <[email protected]>
1 parent df5175c commit 17cbf89

File tree

7 files changed

+173
-5
lines changed

7 files changed

+173
-5
lines changed

.github/workflows/ci.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
java-version: 8
3131

3232
- name: Maven cache
33-
uses: actions/cache@v2
33+
uses: actions/cache@v4
3434
with:
3535
# Cache gradle directories
3636
path: ~/.m2
@@ -57,7 +57,7 @@ jobs:
5757
- uses: actions/checkout@v2
5858

5959
- name: Cache Docker layers
60-
uses: actions/cache@v2
60+
uses: actions/cache@v4
6161
with:
6262
path: /tmp/.buildx-cache
6363
key: ${{ runner.os }}-buildx-${{ github.sha }}

.github/workflows/release.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
java-version: 8
2727

2828
- name: Maven cache
29-
uses: actions/cache@v2
29+
uses: actions/cache@v4
3030
with:
3131
# Cache gradle directories
3232
path: ~/.m2

src/main/java/org/ohdsi/webapi/service/VocabularyService.java

+76-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import static org.ohdsi.webapi.service.cscompare.ConceptSetCompareService.CONCEPT_SET_COMPARISON_ROW_MAPPER;
44
import static org.ohdsi.webapi.util.SecurityUtils.whitelist;
55

6+
import com.fasterxml.jackson.core.JsonProcessingException;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
68
import com.google.common.collect.Lists;
79
import com.google.common.collect.Maps;
810

@@ -78,6 +80,7 @@
7880
import org.springframework.jdbc.core.RowCallbackHandler;
7981
import org.springframework.jdbc.core.RowMapper;
8082
import org.springframework.stereotype.Component;
83+
import org.ohdsi.webapi.vocabulary.MappedRelatedConcept;
8184

8285
/**
8386
* Provides REST services for working with
@@ -138,7 +141,10 @@ public void customize(CacheManager cacheManager) {
138141

139142
@Autowired
140143
private ConceptSetCompareService conceptSetCompareService;
141-
144+
145+
@Autowired
146+
private ObjectMapper objectMapper;
147+
142148
@Value("${datasource.driverClassName}")
143149
private String driver;
144150

@@ -827,7 +833,75 @@ public Collection<RelatedConcept> getRelatedConcepts(@PathParam("sourceKey") Str
827833
return concepts.values();
828834
}
829835

830-
/**
836+
@POST
837+
@Path("{sourceKey}/related-standard")
838+
@Produces(MediaType.APPLICATION_JSON)
839+
public Collection<MappedRelatedConcept> getRelatedStandardMappedConcepts(@PathParam("sourceKey") String sourceKey, List<Long> allConceptIds) {
840+
Source source = getSourceRepository().findBySourceKey(sourceKey);
841+
String relatedConceptsSQLPath = "/resources/vocabulary/sql/getRelatedStandardMappedConcepts.sql";
842+
String relatedMappedFromIdsSQLPath = "/resources/vocabulary/sql/getRelatedStandardMappedConcepts_getMappedFromIds.sql";
843+
String tableQualifier = source.getTableQualifier(SourceDaimon.DaimonType.Vocabulary);
844+
845+
String[] searchStrings = {"CDM_schema"};
846+
String[] replacementStrings = {tableQualifier};
847+
848+
String[] varNames = {"conceptIdList"};
849+
850+
final Map<Long, MappedRelatedConcept> resultCombinedMappedConcepts = new HashMap<>();
851+
final Map<Long, RelatedConcept> relatedStandardConcepts = new HashMap<>();
852+
for(final List<Long> conceptIdsBatch: Lists.partition(allConceptIds, PreparedSqlRender.getParameterLimit(source))) {
853+
Object[] varValues = {conceptIdsBatch.toArray()};
854+
PreparedStatementRenderer relatedConceptsRenderer = new PreparedStatementRenderer(source, relatedConceptsSQLPath, searchStrings, replacementStrings, varNames, varValues);
855+
getSourceJdbcTemplate(source).query(relatedConceptsRenderer.getSql(), relatedConceptsRenderer.getSetter(), (RowMapper<Void>) (resultSet, arg1) -> {
856+
addRelationships(relatedStandardConcepts, resultSet);
857+
return null;
858+
});
859+
860+
final Map<Long, Set<Long>> relatedNonStandardConceptIdsByStandardId = new HashMap<>();
861+
862+
PreparedStatementRenderer mappedFromConceptsRenderer = new PreparedStatementRenderer(source, relatedMappedFromIdsSQLPath, searchStrings, replacementStrings, varNames, varValues);
863+
getSourceJdbcTemplate(source).query(mappedFromConceptsRenderer.getSql(), mappedFromConceptsRenderer.getSetter(), (RowMapper<Void>) (resultSet, arg1) -> {
864+
populateRelatedConceptIds(relatedNonStandardConceptIdsByStandardId, resultSet);
865+
return null;
866+
});
867+
868+
enrichResultCombinedMappedConcepts(resultCombinedMappedConcepts, relatedStandardConcepts, relatedNonStandardConceptIdsByStandardId);
869+
}
870+
return resultCombinedMappedConcepts.values();
871+
}
872+
873+
private void populateRelatedConceptIds(final Map<Long, Set<Long>> mappedConceptsIds, final ResultSet resultSet) throws SQLException {
874+
final Long concept_id = resultSet.getLong("CONCEPT_ID");
875+
if (!mappedConceptsIds.containsKey(concept_id)) {
876+
Set<Long> mappedIds = new HashSet<>();
877+
mappedIds.add(resultSet.getLong("MAPPED_FROM_ID"));
878+
mappedConceptsIds.put(concept_id,mappedIds);
879+
} else {
880+
mappedConceptsIds.get(concept_id).add(resultSet.getLong("MAPPED_FROM_ID"));
881+
}
882+
}
883+
884+
void enrichResultCombinedMappedConcepts(Map<Long, MappedRelatedConcept> resultCombinedMappedConcepts,
885+
Map<Long, RelatedConcept> relatedStandardConcepts,
886+
Map<Long, Set<Long>> relatedNonStandardConceptIdsByStandardId) {
887+
relatedNonStandardConceptIdsByStandardId.forEach((standardConceptId, mappedFromIds)->{
888+
if(resultCombinedMappedConcepts.containsKey(standardConceptId)){
889+
resultCombinedMappedConcepts.get(standardConceptId).mappedFromIds.addAll(mappedFromIds);
890+
} else {
891+
MappedRelatedConcept mappedRelatedConcept;
892+
try {
893+
mappedRelatedConcept = objectMapper.readValue(objectMapper.writeValueAsString(relatedStandardConcepts.get(standardConceptId)), MappedRelatedConcept.class);
894+
mappedRelatedConcept.mappedFromIds=mappedFromIds;
895+
resultCombinedMappedConcepts.put(standardConceptId,mappedRelatedConcept);
896+
} catch (JsonProcessingException e) {
897+
log.error("Could not convert RelatedConcept to MappedRelatedConcept", e);
898+
throw new WebApplicationException(e);
899+
}
900+
}
901+
});
902+
}
903+
904+
/**
831905
* Get ancestor and descendant concepts for the selected concept identifier
832906
* from a source.
833907
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.ohdsi.webapi.vocabulary;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
import java.util.Set;
7+
8+
@JsonInclude(JsonInclude.Include.NON_NULL)
9+
public class MappedRelatedConcept extends RelatedConcept {
10+
@JsonProperty("mapped_from")
11+
public Set<Long> mappedFromIds;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
INSERT INTO ${ohdsiSchema}.sec_permission (id, value, description)
2+
SELECT nextval('${ohdsiSchema}.sec_permission_id_seq'), 'vocabulary:*:related-standard:post', 'Access related mapped standard concepts resource'
3+
WHERE NOT EXISTS (
4+
SELECT NULL FROM ${ohdsiSchema}.sec_permission
5+
WHERE value = 'vocabulary:*:related-standard:post'
6+
);
7+
8+
INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id)
9+
SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id
10+
FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr
11+
WHERE sp.value IN (
12+
'vocabulary:*:related-standard:post'
13+
) AND sr.name IN ('Atlas users')
14+
AND NOT EXISTS (
15+
SELECT NULL FROM ${ohdsiSchema}.sec_role_permission
16+
WHERE permission_id = sp.id and role_id = sr.id);
17+
18+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
SELECT
2+
c.CONCEPT_ID,
3+
c.CONCEPT_NAME,
4+
COALESCE(c.STANDARD_CONCEPT, 'N') as STANDARD_CONCEPT,
5+
COALESCE(c.INVALID_REASON, 'V') as INVALID_REASON,
6+
c.CONCEPT_CODE,
7+
c.CONCEPT_CLASS_ID,
8+
c.DOMAIN_ID,
9+
c.VOCABULARY_ID,
10+
c.VALID_START_DATE,
11+
c.VALID_END_DATE,
12+
c.RELATIONSHIP_NAME,
13+
c.RELATIONSHIP_DISTANCE
14+
FROM (
15+
SELECT
16+
c.CONCEPT_ID, CONCEPT_NAME, COALESCE(c.STANDARD_CONCEPT, 'N') as STANDARD_CONCEPT, COALESCE(c.INVALID_REASON, 'V') as INVALID_REASON,
17+
c.CONCEPT_CODE, c.CONCEPT_CLASS_ID, c.DOMAIN_ID, c.VOCABULARY_ID, c.VALID_START_DATE, c.VALID_END_DATE,
18+
r.RELATIONSHIP_NAME, 1 as RELATIONSHIP_DISTANCE
19+
FROM
20+
@CDM_schema.concept_relationship cr
21+
JOIN
22+
@CDM_schema.concept c ON cr.CONCEPT_ID_2 = c.CONCEPT_ID
23+
JOIN
24+
@CDM_schema.relationship r ON cr.RELATIONSHIP_ID = r.RELATIONSHIP_ID
25+
WHERE
26+
cr.CONCEPT_ID_1 IN (@conceptIdList)
27+
AND COALESCE(c.STANDARD_CONCEPT, 'N') IN ('S', 'C')
28+
AND cr.INVALID_REASON IS NULL
29+
) c
30+
GROUP BY
31+
c.CONCEPT_ID,
32+
c.CONCEPT_NAME,
33+
c.STANDARD_CONCEPT,
34+
c.INVALID_REASON,
35+
c.CONCEPT_CODE,
36+
c.CONCEPT_CLASS_ID,
37+
c.DOMAIN_ID,
38+
c.VOCABULARY_ID,
39+
c.VALID_START_DATE,
40+
c.VALID_END_DATE,
41+
c.RELATIONSHIP_NAME,
42+
c.RELATIONSHIP_DISTANCE
43+
ORDER BY
44+
c.RELATIONSHIP_DISTANCE ASC;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
SELECT
2+
c.CONCEPT_ID,
3+
c.MAPPED_FROM_ID
4+
FROM (
5+
SELECT DISTINCT
6+
c.CONCEPT_ID,
7+
CAST(cr.CONCEPT_ID_1 AS VARCHAR) as MAPPED_FROM_ID
8+
FROM
9+
@CDM_schema.concept_relationship cr
10+
JOIN
11+
@CDM_schema.concept c ON cr.CONCEPT_ID_2 = c.CONCEPT_ID
12+
JOIN
13+
@CDM_schema.relationship r ON cr.RELATIONSHIP_ID = r.RELATIONSHIP_ID
14+
WHERE
15+
cr.CONCEPT_ID_1 IN (@conceptIdList)
16+
AND COALESCE(c.STANDARD_CONCEPT, 'N') IN ('S', 'C')
17+
AND cr.INVALID_REASON IS NULL
18+
) c
19+
ORDER BY
20+
c.CONCEPT_ID;

0 commit comments

Comments
 (0)