Разработка динамических REST-сервисов на документо-ориентированной БД Bagri

Не так давно, просматривая ленту CNews, наткнулся на анонс конференции «ИТ в здравоохранении: в ожидании прорыва». Оказывается, «начиная с 2011 г. в России реализуется масштабный государственный проект по внедрению Единой государственной информационной системы в сфере здравоохранения (ЕГИСЗ)». Углубившись немного в материал обнаружил, что ЕГИЗС базируется на широко используемых на западе стандартах организации Health Language 7 (далее HL7). А в основе стандартов HL7 лежит XML. Появилось желание построить прототип системы, обрабатывающей документы HL7, на документной БД Bagri и, если прототип выйдет удачным, подготовить доклад о нем на конференцию.
image


Пришлось на некоторое время уйти в изучение документов HL7. Потом, кстати, и на Хабре обнаружил неплохой цикл статей об этой технологии от Wayfarer15. Попутно выяснил, что самым последним активно разрабатываемым стандартом в этой области является Fast Healthcare Interoperability Resources (далее FHIR). В основе FHIR лежит технология REST и обмен XML/JSON документами через REST ресурсы.

Как это применимо к Bagri? Оказалось, что вполне: примерно с месяц назад в Bagri добавилась поддержка REST, а также возможность динамического определения ресурсов REST в модулях XQuery с помощью аннотаций RESTXQ.Т. е. любой ресурс FHIR можно динамически создать и опубликовать, даже без рестарта серверов Bagri. Давайте попробуем?

Создаём прототип FHIR-сервера за 45 минут…


Для этого нам понадобятся:
  • последняя версия Bagri, развёрнутая на вашем компьютере, ее можно скачать здесь;
  • тестовый набор данных FHIR, доступен по данной ссылке;
  • базовые знания языка XQuery, с помощью которого мы будем разрабатывать наш прототип:

Создадим новую схему в конфигурационном файле Bagri (/config/config.xml), назовём её FHIR.
Схема FHIR в конфигурационном файле Багри

        1
        2016-11-09T23:14:40.096+03:00
        admin
        FHIR: schema for FHIR XML demo
        
            
            11000
            11100
            localhost
            16
            true
            
            ../data/fhir/xml
            File
            XML
            271
            1
            64
            true
            NEVER
            true
            0
            1
            false
            false
            1
            0
            0
            1
            true
            0
            0
            true
            60000
            25
            0
            2048
            1000000
            true
            32
            file:/../data/fhir/xml/
            2
            1
            0
            1
            1
            2
            1
            0
            http://www.w3.org/2005/xpath-functions
            http://www.w3.org/2001/XMLSchema
            1
            2
            http://www.w3.org/2005/xpath-functions/collation/codepoint
            1
        
        
        
            
                1
                2016-11-09T23:14:40.096+03:00
                admin
                /{http://hl7.org/fhir}Patient
                All patient documents
                true
            
        
        
        
        
            
                1
                2016-11-09T23:14:40.096+03:00
                admin
                /{http://hl7.org/fhir}Patient
                /{http://hl7.org/fhir}Patient/{http://hl7.org/fhir}id/@value
                xs:string
                true
                false
                true
                Patient id
                true
            
        
        
            
            
                1
                2016-11-09T23:14:40.096+03:00
                admin
                /
                common_module
                FHIR Conformance resource exposed via REST
                true
            
            
            
                1
                2016-11-09T23:14:40.096+03:00
                admin
                /Patient
                patient_module
                FHIR Patient resource exposed via REST
                true
            
        
        
    


Тестовые данные распакуем на локальный диск в директорию /data/fhir/xml. Про работу с JSON документами в Bagri я писал в предыдущей статье, так что в данном примере для экономии места я покажу только работу с данными в формате XML.

На момент написания статьи спецификация FHIR определяла 110 стандартных ресурсов, доступ к которым может предоставляться сервером. Часть из них является служебными и служит для предоставления информации о самой системе, а остальная часть — это прикладные ресурсы, которые выполняют работу с медицинскими данными. Служебный ресурс Conformance является обязательным для реализации и предоставляет сведения о доступном функционале системы. Наличие или отсутствие остальных ресурсов и их поведение определяется тем, что мы задекларируем в Conformance.

Прикладные ресурсы, согласно спецификации FHIR, могут публиковать следующие методы:

Операции на уровне ресурсов:

  • read — получение текущего состояния заданного идентификатором ресурса
  • vread — получение состояния конкретной версии заданного ресурса
  • update — обновление заданного ресурса
  • delete — удаление заданного ресурса
  • history — получение истории обновлений заданного ресурса

Операции на уровне типа ресурса:
  • create — создание нового ресурса
  • search — поиск среди ресурсов одного типа по разным критериям
  • history — получение истории обновлений по указанному типу ресурса

В показательных целях мы реализуем 2 ресурса: уже обозначенный Conformance и прикладной ресурс Patient. Conformance определит, какой функционал будет доступен клиентам ресурса Patient.

Ниже по тексту будет много смайликов. Не пугайтесь, это издержки синтаксиса XQuery:).

Реализация Conformance для нашего прототипа выглядит довольно просто: cоздадим новый модуль XQuery /data/fhir/common_module.xq. В заголовке объявим используемую версию языка, пространство имен модуля и пространства имен используемых внешних схем:

xquery version "3.1";
module namespace conf = "http://hl7.org/fhir"; 
declare namespace rest = "http://www.expath.org/restxq";

Далее идет код функции, реализующей требуемое поведение ресурса:
declare 
  %rest:GET  (: определяет метод HTTP, через который ресурс будет доступен :)
  %rest:path("/metadata")  (: определяет путь доступа к ресурсу, относительно базового URL:)
  %rest:produces("application/fhir+xml")  (: возвращает данные в формате XML :)
  %rest:query-param("_format", "{$format}")  (: принимает один необязательный параметр _format :)
function conf:get-conformance($format as xs:string?) as item() {
  if (exists($format) and not ($format = ("application/xml", "application/fhir+xml"))) then 
    "The endpoint produce response in application/fhir+xml format, but [" || $format || "] specified"
  else
  
    
    
    
    
    
    
    
    
    
        
        
            
            
            
        
    
    
    
    
    
        
        
        
    
    
        
        
    
    
    
    
    
        
        
        
            
            
                
            
             
            
                
            
            
                
            
            
                
            
            
                
            
            
                
            
            
                
            
            
            
            
            
                
                
                
                
                
                
            
            
                
                
                
                
                
            
            
                
                
                
                
                
                
            
            
                
                
                
                
                
            
            
                
                
                
                
                
            
        
    
  
};


Собственно, это единственный метод, из которого состоит ресурс Conformance. Его задача — определить другие точки доступа к системе и параметры, которыми можно пользоваться в этих взаимодействиях.

Для прикладного ресурса Patient создадим другой модуль XQuery:

/data/fhir/patient_module.xq. Так же в заголовке объявим используемые пространства имен:

module namespace fhir = "http://hl7.org/fhir/patient"; 
declare namespace http = "http://www.expath.org/http";
declare namespace rest = "http://www.expath.org/restxq";
declare namespace bgdm = "http://bagridb.com/bagri-xdm";
declare namespace p = "http://hl7.org/fhir"; 

Реализуем метод read:
declare 
  %rest:GET  (: определяет метод HTTP, через который ресурс будет доступен :)
  %rest:path("/{id}")  (: определяет путь доступа к ресурсу; id - шаблонный параметр пути :)
  %rest:produces("application/fhir+xml")  (:  устанавливает формат возвращаемых данных :)
function fhir:get-patient-by-id($id as xs:string) as element()? {
  collection("Patients")/p:Patient[p:id/@value = $id]
};

Выглядит, на мой взгляд, весьма привлекательно: реализация требуемого функционала всего в одну строку! Но, как известно, дьявол кроется в деталях. Помимо основного поведения, спецификация FHIR определяет так же многочисленные дополнительные ситуации и статусы и заголовки HTTP, которые сервис обязан возвращать в таких случаях. Попробуем переписать показанный выше метод read с учётом расширенных требований:
declare 
  %rest:GET
  %rest:path("/{id}")
  %rest:produces("application/fhir+xml")
function fhir:get-patient-by-id($id as xs:string) as element()* {
  let $itr := collection("Patients")/p:Patient[p:id/@value = $id]
  return
    if ($itr) then 
      (
         
          (: запрашиваемый ресурс имеет версию? :)
         {if ($itr/p:meta/p:versionId/@value) then (
            (: заголовок ETag должен содержать номер версии найденного ресурса Patient :)
           ,
            (: заголовок Content-Location должен содержать адрес, по которому доступна последняя версия ресурса :)
            
          ) else (
            (: иначе заголовок Content-Location должен содержать базовый адрес ресурса :)
            
          )}
            (: заголовок Last-Modified должен содержать дату/время последней модификации ресурса :)
           
                              
       , $itr)
    else 
      (: возвращаем статус 404 если пациент с заданным id не найден :)
      
        
      
};

Для указания статуса и заголовков ответа HTTP используется структура http: response, которая должна передаваться в первом элементе последовательности возвращаемых данных. Так же обратите внимание, что пришлось изменить тип возвращаемых данных с element ()? на element ()*, чтобы передать эту служебную информацию на REST сервер.

Конечно, такая полная реализация требований спецификации получается гораздо более многословной. Но не берусь сказать, с помощью какого языка/технологии можно выполнить требования FHIR компактнее. С другой стороны, сильно привлекают возможности XQuery по работе с XML и с последовательностями данных.

Ниже я уже не буду отвлекаться на обработку всех возможных дополнительных сценариев, в примере выше было показано, как возвращать на сервер дополнительные статусы и заголовки HTTP.
Базовая реализация метода vread выглядит очень похоже:

declare 
  %rest:GET
  %rest:path("/{id}/_history/{vid}")  (: кроме идентификатора здесь в качестве шаблона пути также используется номер версии :)
  %rest:produces("application/fhir+xml")
function fhir:get-patient-by-id-version($id as xs:string, $vid as xs:string) as element()? {
  collection("Patients")/p:Patient[p:id/@value = $id and p:meta/p:versionId/@value = $vid]
};

Следующий метод — search. В ресурсе Conformance мы указали, что можем выполнять поиск пациентов по 5 параметрам: name, birthday, gender, identifier и telecom. Так же мы указали как именно используется параметр поиска, через элемент modifier, который может принимать следующие значения: missing | exact | contains | not | text | in | not-in | below | above | type. Их описание и соответствующее поведение поисковой системы можно посмотреть здесь.
declare 
  %rest:GET
  %rest:produces("application/fhir+xml")
  %rest:query-param("identifier", "{$identifier}")  (: параметры поиска передаём в строке :)
  %rest:query-param("birthdate", "{$birthdate}") (: запроса http; все они не обязательные :)
  %rest:query-param("gender", "{$gender}") 
  %rest:query-param("name", "{$name}")
  %rest:query-param("telecom", "{$telecom}")
function fhir:search-patients($identifier as xs:string?, $birthdate as xs:date?, $gender as xs:string?, $name as xs:string?, $telecom as xs:string?) as element()* {
(: получим набор результатов (пациентов), удовлетворяющих условиям поиска :)
  let $itr := collection("Patients")/p:Patient[ 
	(not(exists($gender)) or p:gender/@value = $gender)
    and (not(exists($birthdate)) or p:birthDate/@value = $birthdate) 
    and (not(exists($name)) or contains(data(p:text), $name)) 
    and (not(exists($identifier)) or contains(p:identifier/p:value/@value, $identifier)) 
    and (not(exists($telecom)) or contains(string-join(p:telecom/p:value/@value, " "), $telecom))] 
(: возвращаем результаты внутри контейнера Bundle :)
  return
    
        (: сгенерируем уникальный bundle ID :)
      
        
      
      
      
      
        
        
      
      {for $ptn in $itr
       return  
         
           {$ptn}
         
      }
    
};  

сreate — создание нового ресурса Patient, либо новой версии уже имеющегося ресурса.
declare 
  %rest:POST (: создание нового ресурса осуществляется методом POST :)
  %rest:consumes("application/fhir+xml")  (: ожидаем получить полное состояние ресурса в теле запроса в формате XML :)
  %rest:produces("application/fhir+xml")  (: новое состояние ресурса вернем клиенту в том же формате :)
function fhir:create-patient($content as xs:string) as element()? {
  let $doc := parse-xml($content)  (: распарсим входную строку в документ XML, заодно и провалидируем его :)
  let $uri := xs:string($doc/p:Patient/p:id/@value) || ".xml"  (: сформируем uri нового ресурса :)
  let $uri := bgdm:store-document(xs:anyURI($uri), $content, ())  (: сохраним документ и получим в ответ его uri, хотя он не должен отличаться от сформированного нами 2мя строками выше :)
  let $content := bgdm:get-document-content($uri)  (: а вот состояние ресурса, в соответствие со спецификацией, может отличаться от полученного на вход, например система могла заполнить некоторые пропущенные поля их значениями по умолчанию :)
  let $doc := parse-xml($content)
  return $doc/p:Patient  
};

update — создание новой версии имеющегося ресурса Patient, либо создание нового ресурса, если пациент с заданным идентификатором ещё не зарегистрирован в системе.
declare 
  %rest:PUT (: изменение существующего ресурса осуществляется методом PUT :)
  %rest:path("/{id}"). (: изменяем ресурс соответствующий заданному шаблонному параметру :)
  %rest:consumes("application/fhir+xml")  
  %rest:produces("application/fhir+xml")
function fhir:update-patient($id as xs:string, $content as xs:string) as element()? {
  for $uri in fhir:get-patient-uri($id)  (: используем утилитную функцию чтобы не дублировать код :)
  let $uri := bgdm:store-document($uri, $content, ())
  let $content := bgdm:get-document-content($uri, ())
  let $doc := parse-xml($content) 
  return $doc/p:Patient
};

delete — удаление зарегистрированного в системе ресурса Patient.
declare 
  %rest:DELETE  (: удаление ресурса, естественно, с помощью DELETE :)
  %rest:path("/{id}")
function fhir:delete-patient($id as xs:string) as item()? {
  for $uri in fhir:get-patient-uri($id)
  return bgdm:remove-document($uri)  (: удалить соответствующий ресурсу документ :)
};

Вспомогательный метод, используемый из функций обновления и удаления:
declare 
  %private
function fhir:get-patient-uri($id as xs:string) as xs:anyURI? {
  (: сформируем динамический запрос XQuery :)
  let $query := 
' declare namespace p = "http://hl7.org/fhir"; 
  declare variable $id external;
  for $ptn in fn:collection("Patients")/p:Patient
  where $ptn/p:id/@value = $id
  return $ptn'


  (: выполнив его, получим в ответ uri документа, удовлетворяющего условиям запроса :)
  let $uri := bgdm:query-document-uris($query, ("id", $id), ())
  return xs:anyURI($uri)
};

Как видим, в реализации логики управления ресурсами используются функции XQuery, предоставляемые библиотеками Bagri. Вот их краткое описание:
bgdm:get-uuid() as xs:string - сгенерировать уникальный идентификатор uuid
bgdm:query-document-uris(xs:string, xs:anyType*, xs:anyAtomicType*) as xs:string* - вернуть uri документов, которые попадают в динамическую выборку XQuery
bgdm:store-document(xs:anyURI, xs:string, xs:anyAtomicType*) as xs:anyURI - зарегистрировать в системе новый документ, либо новую версию имеющегося документа
bgdm:get-document-content(xs:anyURI) as xs:string* - вернуть текстовое содержимое документа
bgdm:remove-document(xs:anyURI) as xs:anyURI - удалить документ

На этом реализация серверных модулей, выполняющихся логику управления ресурсами FHIR, закончена. Думаю, в 45 минут мы уложились :). В следующей части статьи я хотел бы показать, как запустить разработанные выше ресурсы и оттестировать их. Ну и, конечно, было бы очень интересно послушать, что многоуважаемая аудитория Хабра думает по этому поводу.

Комментарии (1)

  • 29 ноября 2016 в 15:26

    0

    Максим, спасибо за статью. Похоже ты проект с гитхаба собирал, да? Я пре-релиз с REST-сервером еще выложить то не успел :). Ну ок, сегодня тогда займусь этим.

© Habrahabr.ru