🗃️ FHIR2 3.0.0 Data Access

So, I recently made a post talking about the future of the FHIR2 module, including introducing the concept of the FHIR2 3.0.0. The overall architecture of the FHIR2 module remains exactly as articulated here and as much as possible the pubic API for things remains the same. However, we have had to introduce changes to the internal API that will invariably be breaking for anyone who has used our tooling.

The intention behind our DAO layer remains to the same, to try to make support for new data access classes as simple and uniform as possible. Basically, for most DAO implementations, the intention was that you just had to extend BaseFhirDaoImpl and override hopefully no more than two methods:

class FhirExampleDaoImpl extends BaseFhirDao<OpenmrsObject> implements FhirOpenmrsObjectDao {
    @Override
    protected void setupSearchParams(Criteria criteria, SearchParameterMap theParams) {
       // use the criteria object to build a Hibernate Criteria query that matches the `SearchParameterMap`
    }

   @Override
   protected String paramToProp(String param) {
      // take the name of the parameter (passed as the _sort param in the REST API)
      // return the corresponding name of the Hibernate column to use in the ORDER BY
   }
}

With FHIR2 3.x our big change is that we’re no longer supporting the Hibernate Criteria API and this is because Hibernate itself drops support for this API in Hibernate 6 onwards. Instead this is replaced with the more standardized JPA Criteria API. Unfortunately, doing so means that the signatures above need to be changed, as do some of the implementation code. The above model instead looks something like this:

class FhirExampleDaoImpl extends BaseFhirDao<OpenmrsObject> implements FhirOpenmrsObjectDao {
    @Override
    protected <U> void setupSearchParams(OpenmrsFhirCriteriaContext<OpenmrsObject, U> criteriaContext, SearchParameterMap theParams) {
       // use the criteria object to build a JPA Criteria query that matches the `SearchParameterMap`
    }

   @Override
   protected <U> Path<?> paramToProp(@Nonnull OpenmrsFhirCriteriaContext<OpenmrsObject, U> criteriaContext, @Nonnull String param) {
      // take the name of the parameter (passed as the _sort param in the REST API)
      // return the corresponding `Path<?>` in the ORDER BY
   }

The big change is that instead of passing around a Hibernate Criteria object we’re now passing around a custom object called a OpenmrsFhirCriteriaContext. This is because emulating the behaviour of the Hibernate Criteria object requires passing around a few different objects in the JPA Criteria API.

Specifically, the OpenmrsFhirCriteriaContext gives you access to:

  1. The current EntityManager
  2. The Root object for the query, i.e., the class in the from clause
  3. The CriteriaQuery object representing the current query being built
  4. The CriteriaBuilder to build restrictions and expressions
  5. A map of Join objects for any joins used in the query
  6. A list of Predicate objects for any restrictions in the query
  7. A list of Order objects for the ORDER BY part of the query

The idea is that by passing the CriteriaContext object around, we can build queries in a similar way to how they were done with the Hibernate Criteria API.

Finally, a number of helpers have been refactored to be removed from the BaseDao and placed in a class called FhirSearchQueryHelper. The main goal here is just to try to keep BaseDao (and BaseFhirDao) a more manageable size as they were both a little difficult to understand.

Concretely, and using this to solve a marginally complex case, in FHIR2 2.x, we had this function.

private void handleIngredientCode(Criteria criteria, TokenAndListParam ingredientCode) {
    if (ingredientCode != null) {
        criteria.createAlias("ingredients", "i");
        DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Concept.class, "ic");
        handleCodeableConcept(criteria, ingredientCode, "ic", "icm", "icrt").ifPresent(detachedCriteria::add);
        detachedCriteria.setProjection(Projections.property("conceptId"));
        criteria.add(Subqueries.propertyIn("i.ingredient", detachedCriteria));
    }
}

This takes the Criteria representing the current query and a TokenAndListParam the represents the value(s) passed to the API via the ingredient-code search parameter. This would usually be a list of system-code pairs representing the concepts to search for.

This function aims to result in a query that’s something like this SQL:

SELECT * 
FROM drug d
INNER JOIN drug_ingredient i
    ON d.drug_id = i.drug_id
INNER JOIN concept ic
    ON i.ingredient_id = c.concept_id
WHERE (
    ic.concept_id in (
        SELECT c.concept_id
        FROM concept c
        WHERE c.uuid IN (?)
    )
)

To generate this same query in 3.x, we use this code:

private <U> void handleIngredientCode(OpenmrsFhirCriteriaContext<Drug, U> criteriaContext,
        TokenAndListParam ingredientCode) {
    if (ingredientCode != null) {
        Join<?, ?> ingredientsJoin = criteriaContext.addJoin("ingredients", "i");
        Join<?, ?> conceptJoin = criteriaContext.addJoin(ingredientsJoin, "ingredient", "ic");
        
        OpenmrsFhirCriteriaSubquery<Concept, Integer> conceptSubquery = criteriaContext.addSubquery(Concept.class,
            Integer.class);
        getSearchQueryHelper().handleCodeableConcept(criteriaContext, ingredientCode, conceptJoin, "icm", "icrt")
                .ifPresent(conceptSubquery::addPredicate);
        conceptSubquery.setProjection(conceptSubquery.getRoot().get("conceptId"));
        
        criteriaContext.addPredicate(criteriaContext.getCriteriaBuilder().in(conceptJoin.get("conceptId"))
                .value(conceptSubquery.finalizeQuery()));
    }
}

Things here are a bit more explicit, as we had to call the criteriaContext.addJoin() (which is sort of similar to criteria.alias(), though with some caveats noted in the Javadocs). We see the use of getSearchQueryHelper() to get the FhirSearchQueryHelper instance and that we can’t just specify properties by strings, but need to specify which objects they come from (mostly context.getRoot().get("<property_name>").

Currently on the master branch of the FHIR2 module, we have all existing queries rewritten into this format, so there are lots of other examples to trawl through and as part of the 3.x upgrade, I’ve been trying to ensure we have substantially better documentation coverage with examples, etc.

Happy hacking!

1 Like