Wednesday, March 13, 2013

OpenMRS Development using Concept Dictionary Best-Practices

It's been a long time since I've blogged about an OpenMRS dev hint, so prepare to be hit by the firehose. If you're not a dev, you might want to skip this one. :-)


First, throat-clearing and background...

As you know if you've been following our Mirebalais dev work, we've been building an "EMR" module that includes opinionated high-level APIs to complement the low-level data APIs that OpenMRS provides, and while this currently lives in PIH's github space, we've designed this module to soon be contributed to the OpenMRS reference application for broader use.

The CIEL/MVP concept dictionary generally represents the best-practice of OpenMRS concept modeling. A downside that has always concerned me is that the best-practice modelin sometimes implies data representations that are not straightforward to handle in common tools like HTML Form Entry and the Reporting.

So...

I've recently been working on Clinic Note functionality for Mirebalais, to allow clinicians to record diagnoses and clinical impressions. I want us to model the concepts consistantly with the Visit Diagnoses concept set that Andy Kanter added to the CIEL dictionary years back, but also to write the code necessary to make these easy to interact with at the API level, and in standard tools.

First, I introduced a business-level representation of Diagnosis. Simplified, it looks like this:
public class Diagnosis {
    CodedOrFreeTextAnswer diagnosis;
    Order order;
    public enum Order { PRIMARY, SECONDARY }
    // TODO add an enum for CONFIRMED vs PRESUMED
}
To ensure that this code is independent of any specific concept dictionary (in fact PIH doesn't use CIEL's precise concepts for diagnoses) but only assumes the best-practice structure, I introduced a helper DiagnosisMetadata class that determines which concepts to use based on mappings you've tagged concepts with.

This ultimately lets us write code like this (from a test case):
ConsultNote cn = buildConsultNote();
cn.setPrimaryDiagnosis(new Diagnosis(new CodedOrFreeTextAnswer(malaria)));
cn.addAdditionalDiagnosis(new Diagnosis(new CodedOrFreeTextAnswer(diabetes))); 

cn.addAdditionalDiagnosis(new Diagnosis(new CodedOrFreeTextAnswer("SARS")));
cn.setComments(comments);
Encounter encounter = consultService.saveConsultNote(cn); 
This provided a nice API for collecting data with a custom UI, which ended up looking like this:
...but that's only the beginning.

I'm now working on a notifiable disease report, so I implemented a DiagnosisCohortDefinition for use with the reporting module, that lets you write code like this:
DiagnosisCohortDefinition cd = new DiagnosisCohortDefinition();
cd.setDiagnosisOrder(Diagnosis.Order.PRIMARY);
cd.setCodedDiagnoses(Arrays.asList(bacterialMeningitis));
Cohort withDiagnosis = service.evaluate(cd, new EvaluationContext());
Mario is working on a row-per-visit morbidity register, so he is writing related queries find visits that have diagnoses, and fetch them in bulk.

While I working on that cohort definition, I got to have some fun with Hibernate. And I say "fun" non-ironically. In all my time working on analysis and reporting tools in OpenMRS I've never done any complex Hibernate criteria queries, but now that I've written a grand total of two, I'm in love. (When I was writing the cohort builder I didn't trust hibernate and I hand-crafted every query in SQL. Probably a mistake, in retrospect...)

In the case of the code snippet above, I want to search for patients who have had a particular primary diagnosis, meaning they have an obs group like
Obs group (grouping concept = Visit Diagnosis)
|- Coded diagnosis = Bacterial meningitis
`- Diagnosis order = Primary
The actual code is (of course) more complex, but it boils down to this:
Criteria c = currentSession.createCriteria(Obs.class, "obsgroup");
c.setProjection(distinct(property("person.id")));

c.add(Restrictions.eq("voided", false));
c.createCriteria("person").add(eq("voided", false));

c.add(eq("concept", visitDiagnosisConceptSet));

DetachedCriteria oc = DetachedCriteria.forClass(Obs.class, "orderObs");
oc.add(eq("voided", false));
oc.add(eq("concept", diagnosisOrderConcept));
oc.add(eq("valueCoded", primaryConcept));
oc.add(eqProperty("obsGroup", "obsgroup.id"));
oc.setProjection(Projections.property("id"));
c.add(Subqueries.exists(oc));

// similar EXISTS subquery for the specific diagnosis
This query is in no way groundbreaking, and that's the point. Hibernate actually makes this pretty easy to write: compared to hand-coding and concatenating SQL (or even HQL) it's infinitely easier, more legible, and maintainable. (I haven't actually verified the performance of the generated SQL; my fingers are crossed on that one.)

At some future point we'll also introduce an HTML Form Entry tag that will produce a standard widget for capturing diagnosis obs groups with just a single tag.

1 comment:

  1. Ironically, now that I'm about to use this cohort definition, I realize I'd forgotten about a tricky requirement on the story, so I can't actually use this cohort definition here. But it will still be useful at some point...

    The tricky requirement is that we only want to count diagnoses from the most recent encounter that has diagnoses at any location in a visit. I.e. if a patient is out in Outpatient Clinic and gets a preliminary diagnosis of Cough, then after ordering an xray comes back the clinician records a final diagnosis of Tuberculosis, we should ignore the cough diagnosis.

    I don't even want to think about how to do _that_ in Hibernate.

    ReplyDelete