Conversion de devises (FX déclaratif via <CurrencyConversion>)
Mondrian-4 prend en charge un élément de schema <CurrencyConversion> qui produit une mesure convertie comme SUM(base_measure × exchange_rate), en choisissant automatiquement le bon taux à la date propre de la ligne de fait via une table de taux avec des intervalles de validité non chevauchants. Vous déclarez ce que vous voulez ; le moteur compile la jointure par bande d’intervalle et l’agrégation pour vous.
Pourquoi la conversion de devises déclarative ?
Sans <CurrencyConversion>, convertir une mesure vers une autre devise nécessite un membre calculé écrit à la main qui doit ré-implémenter la recherche à date effective chaque fois que la structure de la table de taux change :
<CalculatedMember name="Revenue (USD)" dimension="Measures"> <Formula> Sum( Existing [Calendar].[Month].Members, [Measures].[Revenue] * LookupCube("[fx_rate]", ...) ) </Formula></CalculatedMember>Avec <CurrencyConversion>, la même métrique est :
<CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/>currency_conversions:- name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"Le chargeur valide que la mesure de base nommée et toutes les colonnes référencées existent, génère la jointure par bande au moment du chargement, et lève une erreur claire plutôt que de produire un résultat silencieusement faux.
Placement dans le schema
Les éléments <CurrencyConversion> sont enveloppés dans un bloc <CurrencyConversions> à l’intérieur d’un <MeasureGroup>, comme frère de <Measures> et <DimensionLinks> :
<Cube name="Monthly Revenue">
<MeasureGroups> <MeasureGroup name="Revenue" table="mm_monthly">
<Measures> <Measure name="Revenue" column="revenue" aggregator="sum"/> </Measures>
<DimensionLinks> <ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/> </DimensionLinks>
<CurrencyConversions> <CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/> </CurrencyConversions>
</MeasureGroup> </MeasureGroups>
</Cube>measure_groups:- name: "Revenue" table: "mm_monthly" measures: - name: "Revenue" column: "revenue" aggregator: "sum" dimension_links: - type: "foreign_key" dimension: "Calendar" foreign_key_column: "month_key" currency_conversions: - name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"Référence des attributs
| Attribut | clé XML | clé YAML | Requis | Description |
|---|---|---|---|---|
name | name | name | oui | Le nom de la mesure convertie générée. Apparaît dans [Measures] comme toute autre mesure. |
measure | measure | measure | oui | Le nom d’une <Measure> existante dans le même groupe de mesures (la valeur de base à multiplier). |
rateTable | rateTable | rate_table | oui | Nom de la table physique qui contient les taux de change. Doit exister dans le schema physique. |
rateColumn | rateColumn | rate_column | oui | Colonne dans rateTable qui contient le taux de change numérique. |
rateType | rateType | rate_type | oui | Valeur littérale utilisée pour filtrer la table de taux à une source de taux (par ex. "ECB", "BLOOMBERG"). |
rateTypeColumn | rateTypeColumn | rate_type_column | oui | Colonne dans rateTable qui contient le discriminateur de type de taux. |
factCurrencyColumn | factCurrencyColumn | fact_currency_column | oui | Colonne dans la table de faits qui identifie la devise source de chaque ligne. |
rateCurrencyColumn | rateCurrencyColumn | rate_currency_column | oui | Colonne dans rateTable qui identifie la devise depuis laquelle le taux convertit. |
factDateColumn | factDateColumn | fact_date_column | oui | Colonne dans la table de faits utilisée pour déterminer quel intervalle de taux s’applique. |
rateValidFromColumn | rateValidFromColumn | rate_valid_from_column | oui | Colonne dans rateTable pour le début de l’intervalle (inclusif). |
rateValidToColumn | rateValidToColumn | rate_valid_to_column | oui | Colonne dans rateTable pour la fin de l’intervalle (exclusif). |
formatString | formatString | format_string | non | Chaîne de format MDX pour la mesure convertie, par ex. "#,##0.00" ou "$#,##0". |
Contrat de données d’intervalle à date effective
La table de taux contient une ligne par combinaison (currency, rate_type) par période de validité. Le moteur effectue une jointure par bande qui sélectionne exactement le taux dont l’intervalle contient la date de la ligne de fait :
fact.factDateColumn >= rate.rateValidFromColumnAND fact.factDateColumn < rate.rateValidToColumnAND fact.factCurrencyColumn = rate.rateCurrencyColumnAND rate.rateTypeColumn = '<rateType literal>'La jointure est intrinsèquement 1-à-1 — une ligne de fait correspond à au plus une ligne de taux — pourvu que les intervalles de la table de taux soient non chevauchants. Cela signifie que l’agrégation de la mesure convertie est simplement SUM(fact.measure × rate.rate), et toute mesure non convertie interrogée à côté n’est pas affectée du tout.
Structure de table de taux recommandée
CREATE TABLE fx_rate ( currency_id VARCHAR(3) NOT NULL, -- e.g. 'EUR' rate_type VARCHAR(16) NOT NULL, -- e.g. 'ECB' rate DECIMAL(18,6) NOT NULL, valid_from DATE NOT NULL, -- inclusive valid_to DATE NOT NULL, -- exclusive PRIMARY KEY (currency_id, rate_type, valid_from));Utilisez valid_to = '9999-12-31' (ou le valid_from de la période suivante) comme sentinelle pour l’intervalle ouvert courant afin que le prédicat < valid_to fonctionne toujours proprement.
Exigence de backend et validation fail-closed
La validation est fail-closed : le chargement du schema est interrompu avec un message d’erreur clair pour toutes les conditions suivantes.
| Condition | Erreur |
|---|---|
measure ne nomme pas une mesure existante dans le groupe de mesures | CurrencyConversion "X": measure "Y" not found in measure group |
rateTable n’existe pas dans le schema physique | CurrencyConversion "X": rateTable "Y" not found in physical schema |
| Toute colonne nommée n’existe pas dans la table référencée | CurrencyConversion "X": column "Y" not found in table "Z" |
| Le backend Calcite n’est pas actif | CurrencyConversion "X": requires Calcite backend |
Il n’y a pas de résultat silencieusement faux — chaque mauvaise configuration est attrapée avant que la première requête ne s’exécute.
Exemple commenté : chiffre d’affaires mensuel de la démo Bank
La démo Bank livre un cube Monthly Revenue dont les lignes de fait mm_monthly sont libellées en EUR. Une table fx_rate contient deux taux ECB annuels :
| currency_id | rate_type | rate | valid_from | valid_to |
|---|---|---|---|---|
| EUR | ECB | 1.10 | 2024-01-01 | 2025-01-01 |
| EUR | ECB | 1.20 | 2025-01-01 | 2026-01-01 |
Les données de fait brutes :
| Year | Revenue (EUR) |
|---|---|
| 2024 | 600 |
| 2025 | 750 |
| Total | 1350 |
Déclaration de schema
<MeasureGroup name="Revenue" table="mm_monthly">
<Measures> <Measure name="Revenue" column="revenue" aggregator="sum"/> </Measures>
<DimensionLinks> <ForeignKeyLink dimension="Calendar" foreignKeyColumn="month_key"/> </DimensionLinks>
<CurrencyConversions> <CurrencyConversion name="Revenue (USD)" measure="Revenue" rateTable="fx_rate" rateColumn="rate" rateType="ECB" rateTypeColumn="rate_type" factCurrencyColumn="currency_id" rateCurrencyColumn="currency_id" factDateColumn="month_key" rateValidFromColumn="valid_from" rateValidToColumn="valid_to" formatString="#,##0.00"/> </CurrencyConversions>
</MeasureGroup>- name: "Revenue" table: "mm_monthly" measures: - name: "Revenue" column: "revenue" aggregator: "sum" dimension_links: - type: "foreign_key" dimension: "Calendar" foreign_key_column: "month_key" currency_conversions: - name: "Revenue (USD)" measure: "Revenue" rate_table: "fx_rate" rate_column: "rate" rate_type: "ECB" rate_type_column: "rate_type" fact_currency_column: "currency_id" rate_currency_column: "currency_id" fact_date_column: "month_key" rate_valid_from_column: "valid_from" rate_valid_to_column: "valid_to" format_string: "#,##0.00"Résultats de référence
| Ligne | Revenue (EUR) | Revenue (USD) | Calcul |
|---|---|---|---|
| 2024 | 600 | 660 | 600 × 1.10 (taux ECB 2024) |
| 2025 | 750 | 900 | 750 × 1.20 (taux ECB 2025) |
| Grand total | 1350 | 1560 | 660 + 900 |
Les lignes 2024 et 2025 utilisent des taux différents car la jointure par bande choisit le taux dont l’intervalle [valid_from, valid_to) contient le month_key de chaque ligne de fait. Le Revenue (EUR) de base n’est pas affecté et reste 1350.
Exemple de requête MDX
SELECT { [Measures].[Revenue], [Measures].[Revenue (USD)] } ON COLUMNS, [Calendar].[Year].Members ON ROWSFROM [Monthly Revenue]Résultat :
| Year | Revenue (EUR) | Revenue (USD) |
|---|---|---|
| 2024 | 600 | 660.00 |
| 2025 | 750 | 900.00 |
| All Time | 1,350 | 1,560.00 |
Relation avec <CalculatedMembers>
<CurrencyConversion> compile vers une agrégation SQL native (SUM(fact.revenue × rate.rate)) plutôt que vers un <CalculatedMember> MDX. Cela signifie :
- La conversion est évaluée dans la base de données, pas dans le moteur MDX — elle est aussi rapide que n’importe quelle mesure d’agrégat ordinaire.
- La mesure convertie apparaît dans les énumérations de membres XMLA aux côtés des mesures régulières.
- Elle peut être référencée par des formules
<CalculatedMember>(par ex. pour calculer une marge dans la devise cible).
Si vous avez besoin d’une formule que <CurrencyConversion> ne peut pas exprimer — par exemple, convertir seulement une tranche d’une mesure, ou mélanger des taux de plusieurs sources — utilisez un <CalculatedMember> simple avec une recherche scriptée séparée. Les deux approches peuvent coexister dans le même cube.
Voir Avancé — Membres calculés pour la référence complète <CalculatedMember>.