Conversión de divisas (FX declarativa mediante <CurrencyConversion>)
Mondrian-4 admite un elemento de schema <CurrencyConversion> que produce una medida convertida como SUM(base_measure × exchange_rate), eligiendo automáticamente la tasa correcta a la fecha de la propia fila de hechos mediante una tabla de tasas con intervalos de validez no solapados. Usted declara qué quiere; el motor compila el join por bandas de intervalo y la agregación por usted.
¿Por qué conversión de divisas declarativa?
Sin <CurrencyConversion>, convertir una medida a una divisa diferente requiere un miembro calculado escrito a mano que debe reimplementar la búsqueda con fecha efectiva cada vez que cambia la estructura de la tabla de tasas:
<CalculatedMember name="Revenue (USD)" dimension="Measures"> <Formula> Sum( Existing [Calendar].[Month].Members, [Measures].[Revenue] * LookupCube("[fx_rate]", ...) ) </Formula></CalculatedMember>Con <CurrencyConversion> la misma métrica es:
<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"El loader valida que la medida base nombrada y todas las columnas referenciadas existen, genera el join por bandas en tiempo de carga, y lanza un error claro en lugar de producir un resultado silenciosamente incorrecto.
Colocación en el schema
Los elementos <CurrencyConversion> se envuelven en un bloque <CurrencyConversions> dentro de un <MeasureGroup>, como hermano de <Measures> y <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"Referencia de atributos
| Atributo | clave XML | clave YAML | Requerido | Descripción |
|---|---|---|---|---|
name | name | name | sí | El nombre de la medida convertida generada. Aparece en [Measures] como cualquier otra medida. |
measure | measure | measure | sí | El nombre de un <Measure> existente en el mismo grupo de medidas (el valor base a multiplicar). |
rateTable | rateTable | rate_table | sí | Nombre de la tabla física que contiene las tasas de cambio. Debe existir en el schema físico. |
rateColumn | rateColumn | rate_column | sí | Columna en rateTable que contiene la tasa de cambio numérica. |
rateType | rateType | rate_type | sí | Valor literal usado para filtrar la tabla de tasas a una fuente de tasa (por ejemplo, "ECB", "BLOOMBERG"). |
rateTypeColumn | rateTypeColumn | rate_type_column | sí | Columna en rateTable que contiene el discriminador de tipo de tasa. |
factCurrencyColumn | factCurrencyColumn | fact_currency_column | sí | Columna en la tabla de hechos que identifica la divisa origen de cada fila. |
rateCurrencyColumn | rateCurrencyColumn | rate_currency_column | sí | Columna en rateTable que identifica la divisa desde la que convierte la tasa. |
factDateColumn | factDateColumn | fact_date_column | sí | Columna en la tabla de hechos usada para determinar qué intervalo de tasa aplica. |
rateValidFromColumn | rateValidFromColumn | rate_valid_from_column | sí | Columna en rateTable para el inicio del intervalo (inclusivo). |
rateValidToColumn | rateValidToColumn | rate_valid_to_column | sí | Columna en rateTable para el fin del intervalo (exclusivo). |
formatString | formatString | format_string | no | Cadena de formato MDX para la medida convertida, por ejemplo "#,##0.00" o "$#,##0". |
Contrato de datos del intervalo con fecha efectiva
La tabla de tasas contiene una fila por combinación (currency, rate_type) por período de validez. El motor realiza un band join que selecciona exactamente la tasa cuyo intervalo contiene la fecha de la fila de hechos:
fact.factDateColumn >= rate.rateValidFromColumnAND fact.factDateColumn < rate.rateValidToColumnAND fact.factCurrencyColumn = rate.rateCurrencyColumnAND rate.rateTypeColumn = '<rateType literal>'El join es inherentemente 1-a-1 — una fila de hechos coincide con como mucho una fila de tasa — siempre que los intervalos de la tabla de tasas no se solapen. Esto significa que la agregación de la medida convertida es simplemente SUM(fact.measure × rate.rate), y cualquier medida no convertida consultada junto a ella no se ve afectada en absoluto.
Estructura recomendada de la tabla de tasas
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));Use valid_to = '9999-12-31' (o el valid_from del siguiente período) como centinela para el intervalo abierto actual para que el predicado < valid_to siempre funcione limpiamente.
Requisito de backend y validación fail-closed
La validación es fail-closed: la carga del schema se aborta con un mensaje de error claro por cualquiera de las siguientes condiciones.
| Condición | Error |
|---|---|
measure no nombra una medida existente en el grupo de medidas | CurrencyConversion "X": measure "Y" not found in measure group |
rateTable no existe en el schema físico | CurrencyConversion "X": rateTable "Y" not found in physical schema |
| Alguna de las columnas nombradas no existe en la tabla referenciada | CurrencyConversion "X": column "Y" not found in table "Z" |
| El backend Calcite no está activo | CurrencyConversion "X": requires Calcite backend |
No hay resultado silenciosamente incorrecto — cada mala configuración se captura antes de que se ejecute la primera consulta.
Ejemplo trabajado: ingresos mensuales de la demo Bank
La demo Bank incluye un cubo Monthly Revenue cuyas filas de hechos mm_monthly están denominadas en EUR. Una tabla fx_rate contiene dos tasas anuales del BCE:
| 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 |
Los datos de hechos crudos:
| Año | Revenue (EUR) |
|---|---|
| 2024 | 600 |
| 2025 | 750 |
| Total | 1350 |
Declaración en el 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"Resultados de referencia
| Fila | Revenue (EUR) | Revenue (USD) | Cálculo |
|---|---|---|---|
| 2024 | 600 | 660 | 600 × 1.10 (tasa BCE 2024) |
| 2025 | 750 | 900 | 750 × 1.20 (tasa BCE 2025) |
| Total general | 1350 | 1560 | 660 + 900 |
Las filas de 2024 y 2025 usan tasas diferentes porque el band join elige la tasa cuyo intervalo [valid_from, valid_to) contiene el month_key de cada fila de hechos. La Revenue base (EUR) no se ve afectada y permanece en 1350.
Consulta MDX de ejemplo
SELECT { [Measures].[Revenue], [Measures].[Revenue (USD)] } ON COLUMNS, [Calendar].[Year].Members ON ROWSFROM [Monthly Revenue]Resultado:
| Año | Revenue (EUR) | Revenue (USD) |
|---|---|---|
| 2024 | 600 | 660.00 |
| 2025 | 750 | 900.00 |
| All Time | 1,350 | 1,560.00 |
Relación con <CalculatedMembers>
<CurrencyConversion> compila a una agregación SQL nativa (SUM(fact.revenue × rate.rate)) en lugar de a un <CalculatedMember> MDX. Esto significa:
- La conversión se evalúa en la base de datos, no en el motor MDX — es tan rápida como cualquier medida agregada ordinaria.
- La medida convertida aparece en las enumeraciones de miembros XMLA junto a las medidas regulares.
- Puede ser referenciada por fórmulas
<CalculatedMember>(por ejemplo, para computar un margen en la divisa de destino).
Si necesita una fórmula que <CurrencyConversion> no pueda expresar — por ejemplo, convertir solo una porción de una medida, o mezclar tasas de varias fuentes — use un <CalculatedMember> plano junto con una búsqueda con script separada. Los dos enfoques pueden coexistir en el mismo cubo.
Consulte Avanzado — Miembros calculados para la referencia completa de <CalculatedMember>.