---
{
  "title": "ViewDefinition FHIR expliqué (spécification SQL on FHIR)",
  "description": "Ce qu'est une ViewDefinition FHIR, comment elle aplatit les ressources FHIR pour l'analyse SQL, et comment l'utiliser dans la spécification SQL on FHIR.",
  "date": "2024-07-26",
  "author": "Nikolai Ryzhikov",
  "reading-time": "6 min read",
  "tags": [
    "SQL on FHIR",
    "Analytics",
    "Database"
  ],
  "seo-tags": [
    "SQL on FHIR",
    "Database",
    "Analytics"
  ]
}
---
# Qu'est-ce qu'une ViewDefinition et comment fonctionne-t-elle?

Le groupe de travail « [SQL on FHIR](/blog/introducing-materialize-sql-interface-for-fhir-data) v2 » est sur le point de publier sa première version, prévue pour la fin de l'été 2024. La spécification vise à bâtir un pont entre les données FHIR et les écosystèmes modernes de bases de données et d'analytique. L'idée centrale est d'introduire une méthode normalisée pour aplatir les ressources FHIR en tables relationnelles. Nous croyons qu'une représentation aplatie des données de santé permettra aux ingénieurs de données et aux outils analytiques de travailler de façon plus efficace.

Cette transformation d'aplatissement est définie par un type de ressource spécial : ViewDefinition. Bien qu'il n'existe pas de vues aplaties universelles pour la plupart des ressources FHIR, nous croyons que de nombreuses vues utiles et propres à des cas d'utilisation précis pourraient exister. Les ViewDefinitions sont des CanonicalResources et peuvent être publiées dans le cadre de guides d'implémentation. Grâce à des requêtes SQL ANSI standard, elles peuvent constituer la base d'une analytique et d'une production de rapports interopérables sur FHIR. Cet article vous aidera à comprendre le fonctionnement de ViewDefinition.

Une ViewDefinition est un algorithme qui décrit la transformation d'aplatissement des ressources FHIR, composé de combinaisons de quelques fonctions.

- column({name:column_name,path: [fhirpath](/articles/unlocking-fhirpath-power-a-deep-dive-into-static-type-analysis-for-robust-tooling)},...) - le principal moteur de la transformation; cette fonction extrait des éléments à l'aide d'expressions FHIRPath et place le résultat dans des colonnes

- where(fhirpath) - cette fonction filtre les ressources par expression FHIRPath. Par exemple, vous pourriez vouloir transformer uniquement des profils spécifiques, comme la tension artérielle, en une table simple

- forEach(expr, transform) - cette fonction déplie les éléments d'une collection en lignes distinctes

- select(rows1, rows2) - cette fonction effectue une jointure croisée entre rows1 et rows2, et est principalement utilisée pour joindre les résultats de forEach avec les colonnes de niveau supérieur

- union(rows, rows) - cette fonction concatène des ensembles de lignes. Le cas d'utilisation principal est la combinaison de lignes provenant de différentes branches d'une ressource (par exemple, telecom et contact.telecom)

>  Utilisez notre [générateur de ViewDefinition](https://sqlonfhir.aidbox.app/) en ligne et gratuit pour convertir des données FHIR stockées en représentation JSON dans un format tabulaire et aplati, idéal pour l'analyse de données. [Accéder au générateur de ViewDefinition](https://sqlonfhir.aidbox.app/)

Une ViewDefinition est représentée sous la forme d'une ressource FHIR (document JSON) dont les éléments (mots-clés) correspondent à des fonctions :


```javascript
{
    "resourceType": "ViewDefinition",
    "resource": "Patient",
    // (0)
    "where": [{filter: "active = true"}],
    // (5)
    "select": [
        {
          // (4)
          "column": [
             {"path": "getResourceKey()", "name": "id"},
             {"path": "identifier.where(system='ssn')", "name": "ssn"},
          ]
        },
        { 
          // (3)
          "unionAll": [
            {
              // (1)
              "forEach": "telecom.where(system='phone')",
              "column": [{"path": "value", "name": "phone"}] 
            },
            { 
              // (2)
              "forEach": "contact.telecom.where(system='phone')",
              "column": [{"path": "value",  "name": "phone"}] 
            }
       ]}       
    ]
}
```


Cette vue produit une table des contacts des patients, où chaque ligne représente une entrée de télécommunication.

- « where » filtre uniquement les patients actifs

- « forEach » déplie Patient.telecom et sélectionne les numéros de téléphone

- « forEach » déplie Patient.contact.telecom et sélectionne les numéros de téléphone

- « unionAll » concatène les résultats des deux opérations « forEach »

- l'instruction « column » extrait l'identifiant et le NAS

- l'instruction « select » effectue une jointure croisée entre l'identifiant et le NAS avec les numéros de téléphone des télécommunications

Voici un exemple d'entrée et de sortie pour cette ViewDefinition.


```javascript
1[
2 {
3    "resourceType": "Patient", 
4    "id": "pt1", 
5    "identifier": [{"system": "ssn", "value": "s1"}],
6    "telecom":   [{"system": "phone", "value": "tt1"}],
7    "contact": [
8         {"telecom": [{"system": "phone", "value": "t12"}]},
9         {"telecom": [{"system": "phone", "value": "t13"}]}
10     ]
11 },
12 {
13    "resourceType": "Patient", 
14    "id": "pt2", 
15    "identifier": [{"system": "ssn", "value": "s2"}],
16    "telecom":   [{"system": "phone", "value": "t21"}],
17    "contact": [
18         {"telecom": [{"system": "phone", "value": "t22"}]},
19         {"telecom": [{"system": "phone", "value": "t23"}]}
20     ]
21 }
22]
```


### Résultat

| id | ssn | phone |
| --- | --- | --- |
| pt1 | s1 | t11 |
| pt1 | s1 | t12 |
| pt1 | s1 | t13 |
| pt2 | s1 | t21 |
| pt2 | s1 | t22 |
| pt2 | s1 | t23 |

### Sous-ensemble FHIRPath

Les ViewDefinitions utilisent un [sous-ensemble minimal de FHIRPath](https://build.fhir.org/ig/FHIR/sql-on-fhir-v2/StructureDefinition-ViewDefinition.html#supported-fhirpath-functionality) afin de rendre l'implémentation aussi simple que possible. De plus, la spécification introduit quelques fonctions spéciales :

- getResourceKey - obtient indirectement l'identifiant de la ressource. Cela peut parfois être complexe, d'où l'utilisation de cette couche d'indirection

- getReferenceKey(resourceType) - une fonction similaire qui obtient l'identifiant à partir d'une référence

### Fonctions / Mots-clés

Examinons chaque fonction en détail.

#### column

La fonction « column » extrait des éléments dans des colonnes à l'aide d'expressions FHIRPath. L'algorithme commence par recevoir une liste de paires {name, path}. Pour chaque enregistrement dans le contexte donné, il évalue l'expression de chemin afin d'extraire les éléments souhaités. Les valeurs résultantes sont ensuite ajoutées sous forme de colonnes à la ligne de sortie.


```javascript
{
    "column": [
        {"name": "id",          "path": "getResourceKey()"},
        {"name": "bod",         "path": "birthDate"},
        {"name": "first_name",  "path": "name.first().given.join(' ')"},
        {"name": "last_name",   "path": "name.first().family"},
        {"name": "ssn",         "path": "identifier.where(system='ssn').value.first()"},
        {"name": "phone",       "path": "telecom.where(system='phone').value.first()"},
    ]
}
```


Voici l'implémentation naïve en JavaScript :


```javascript
function column(cols, rows) {
    return rows.map((row)=> {
        return cols.reduce((res, col ) => {
            res[col.name] = fhirpath(col.path, row)
            return res
        }, {})
    })
}
```


#### where

La fonction « where » conserve uniquement les enregistrements pour lesquels son expression FHIRPath retourne vrai.


```javascript
{
  "resourceType": "ViewDefinition",
  "resource": "Patient",
  "where": [
      {"filter": "meta.profile.where($this = 'myprofile').exists()"},
      {"filter": "active = 'true'"}
  ]
}
```


Implémentation de base en JavaScript :


```javascript
function where(exprs, rows) {
    return rows.filter((row)=> {
        return exprs.every((expr)=>{
            return fhirpath(expr, row) == true;
        })
    })
}
```


#### forEach & forEachOrNull

La fonction forEach est destinée à l'aplatissement de collections imbriquées en appliquant une transformation à chaque élément. Elle se compose d'une expression FHIRPath pour la collection à itérer et d'une transformation à appliquer à chaque élément. Cette fonction est analogue à flatMap ou mapcat dans d'autres langages de programmation.


```javascript
{
    "resourceType": "ViewDefinition",
    "resource": "Patient",
    "select": [{
      "forEach": "name",
      "column": [
        {"path": "given.join(' ')", "name": "first_name"},
        {"path": "family", "name": "last_name"}
      ]
    }]
}
```


Il existe deux versions de cette fonction : forEach et forEachOrNull. La principale différence est que forEach supprime les enregistrements pour lesquels l'expression FHIRPath ne retourne aucun résultat, tandis que forEachOrNull conserve un enregistrement vide dans ce cas.


```javascript
function forEach(path, expr, rows) {
    return rows.flatMap((row)=> {
        return fhirpath(expr, row).map((item)=>{
            // evalKeyword will call column, select or other functions
            return evalKeyword(expr, item)
        })
    })
}
```


#### select

La fonction select est utilisée en combinaison avec forEach pour effectuer une jointure croisée entre les éléments parents (comme Patient.id) et les éléments de collection dépliés (comme Patient.name). Cette fonction fusionne les colonnes de chaque ensemble de lignes, produisant une combinaison complète des données provenant des collections d'entrée.


```javascript
{
    "resourceType": "ViewDefinition",
    "resource": "Patient",
    "select": [
     {
         "column": [
            {"path": "getResourceKey()", "name": "id"}
         ]
     },
     {
        "forEach": "name",
        "column": [
          {"path": "given.join(' ')", "name": "first_name"},
          {"path": "family", "name": "last_name"}
        ]
     }
    ]
}
```


L'implémentation naïve est :


```javascript
function select(rows1, rows2){
    return rows1.flatMap((r1)=> {
        return rows2.map((r2)=>{
            // merge r1 and r2
            return { ...r1, ...r2 }
        })
    })
}

select([{a: 1}, {a: 2}], [{b: 1}, {b: 2}])
//=>
[{a: 1, b: 1}, 
 {a: 1, b: 2},
 {a: 2, b: 1},
 {a: 2, b: 2}]
```


#### unionAll

La fonction unionAll combine des lignes provenant de différentes branches d'un arbre de ressources en concaténant plusieurs ensembles d'enregistrements. Cette fonction concatène essentiellement plusieurs collections d'enregistrements en une seule collection unifiée, en préservant toutes les lignes des ensembles d'entrée.


```javascript
{
    "resourceType": "ViewDefinition",
    "resource": "Patient",
    "select": [
        {
          "column": [
             {"path": "getResourceKey()", "name": "id"}
          ]
        },
        { 
          "unionAll": [
            {
              "forEach": "telecom.where(system='phone')",
              "column": [{"path": "value", "name": "phone"}] 
            },
            { 
              "forEach": "contact.telecom.where(system='phone')",
              "column": [{"path": "value",  "name": "phone"}] 
            }
       ]}       
    ]
}
```


L'implémentation n'est qu'une simple concaténation :


```javascript
function unionAll(rowSets){
    return rowSet.flatMap((rows)=> { return rows})
}

unionAll([1,2,3], [3,4,5])
//=>
[1,2,3,3,4,5]
```


Dans une ressource, différents mots-clés peuvent apparaître au même niveau. Par exemple, select, forEach et unionAll peuvent tous être présents dans le même nœud JSON. Pour interpréter ces nœuds, vous devez réordonner les mots-clés (fonctions) selon leur ordre de priorité, les fonctions de priorité supérieure remontant vers le haut :

- forEach(OrNull)
- select
- unionAll
- column


```javascript
{
    "forEach":   FOREACH,
    "column":    [COLUMNS], // got into select
    "unionALL":  [UNIONS],  // got into select
    "select":    [SELECTS]
}
//=>
{     
    "forEach": FOREACH
    "select": [
       {"column":   [COLUMNS]},
       {"unionAll": [UNIONS]},
       SELECTS...
    ]
}
```


Consultez l'[implémentation de référence.](https://github.com/FHIR/sql-on-fhir-v2/blob/master/sof-js/src/index.js#L144)

### Moteurs ViewDefinition

Une ViewDefinition peut être exécutée par un moteur afin de produire des vues aplaties à partir de ressources FHIR. Il existe deux catégories de moteurs :
- **Moteurs en mémoire** : Ces moteurs consomment des ressources, les aplatissent et exportent les résultats dans un flux, un fichier ou une table. On peut imaginer un pipeline ETL transformant des fichiers NDJSON issus d'une exportation Bulk FHIR en fichiers Parquet.

- **Moteurs en base de données** : Ces moteurs traduisent une ViewDefinition en une requête SQL sur une base de données native FHIR. Dans ce cas, la vue peut être une vraie vue de base de données. Les moteurs en base de données peuvent être beaucoup plus efficaces que les moteurs en mémoire en ce qui concerne la vitesse et les ressources de stockage, mais ils sont plus complexes à implémenter.

![](image-1.avif)
Une liste officielle des implémentations est disponible à l'adresse [https://fhir.github.io/sql-on-fhir-v2/#impls](https://fhir.github.io/sql-on-fhir-v2/#impls). La plupart des implémentations sont des moteurs en mémoire. Aidbox (PostgreSQL) et Pathling (Spark SQL) sont des moteurs en base de données.

### Aidbox (moteur en base de données)

Aidbox est un serveur et une base de données FHIR pour les systèmes natifs FHIR, avec une prise en charge intégrée de [SQL on FHIR](/blog/sql-on-fhir-in-postgresql). Aidbox transpile une ViewDefinition en une requête SQL PostgreSQL, qui peut être exécutée « telle quelle » ou utilisée pour créer une vue de base de données.

Par exemple, cette ViewDefinition


```javascript
{
  "resource": "Patient",
  "select": [
    {
      "column": [
        {
          "name": "id",
          "path": "getResourceKey()"
        }
      ]
    },
    {
      "forEach": "name",
      "select": [
        {
          "column": [
            {
              "name": "family",
              "path": "family"
            },
            {
              "name": "given",
              "path": "given.join(' ')"
            }
          ]
        }
      ]
    }
  ]
}
```


sera transpilée en :


```javascript
SELECT
  cast(id AS text) as "id",
  cast(
    jsonb_path_query_first(q1_1, '$  . family') #>> '{}' AS text
  ) as "family",
  coalesce(
    array_to_string(
      (
        SELECT
          array_agg(x)
        FROM     jsonb_array_elements_text(jsonb_path_query_array(q1_1, '$  . given  [*]')) as x
      ),
      ' '
    ),
    ''
  ) as "given"
FROM
  "patient" as r
  JOIN LATERAL jsonb_path_query(r.resource, '$  . name  [*]') q1_1 
  ON true
LIMIT 100
```


> Vous pouvez exécuter Aidbox localement ou dans un bac à sable infonuagique en quelques minutes - [https://www.health-samurai.io/aidbox#run](https://www.health-samurai.io/aidbox?utm_source=article&utm_medium=view%20definition&utm_campaign=from%20text#run).

Vous pouvez créer et déboguer visuellement une ViewDefinition avec la saisie semi-automatique FHIRPath grâce à notre [générateur de ViewDefinition.](https://sqlonfhir.aidbox.app/?utm_source=article&utm_medium=view%20definition&utm_campaign=from%20text)
![](image-2.avif)
ViewDefinition dans FHIR est un outil puissant qui permet une gestion flexible de la représentation des données en créant des vues personnalisables basées sur diverses conditions et paramètres. Cela est particulièrement utile lorsque vous devez adapter l'affichage de l'information pour répondre aux exigences spécifiques d'un utilisateur ou d'un système. En utilisant ViewDefinition, vous pouvez simplifier et accélérer considérablement le processus d'intégration des données tout en garantissant l'intégrité et l'accessibilité de celles-ci.

Pour explorer concrètement les capacités de ViewDefinition, vous pouvez installer la [version gratuite d'Aidbox](https://www.health-samurai.io/aidbox#run). Elle vous permet de tester toutes les fonctionnalités sans limitations, offrant un environnement idéal pour le développement et l'expérimentation.


{% embed url="https://youtu.be/Z6YvmE6opMY" %}

Découvrez la démonstration de l'implémentation ELT pour PostgreSQL avec [Aidbox](https://health-samurai.io/aidbox?utm_source=web&utm_medium=artviewdef&utm_campaign=nikolai), le [**générateur de ViewDefinition**](https://sqlonfhir.aidbox.app/?utm_source=web&utm_medium=artviewdef&utm_campaign=nikolai&__hstc=96403715.6949fa01ff24027a1f8ca2dfe7e9bd7f.1703067308257.1730969951321.1730973906861.675&__hssc=96403715.8.1730973906861&__hsfp=2929422519) à source ouverte, et Grafana.

### Rejoindre le groupe de travail

Si vous souhaitez poser des questions ou contribuer à [SQL on FHIR](/articles/sql-on-fhir-an-inside-look), rejoignez-nous dans la discussion sur [chat.fhir.org](https://chat.fhir.org/). Pour des questions personnelles, n'hésitez pas à me contacter sur [LinkedIn](https://www.linkedin.com/in/nikolai-ryzhikov-586a6913/).