02Bases de dades5 min

Vaig reescriure el mateix model de contractes més de deu vegades

Un model de domini complicat no surt bé a la primera. S'hi convergeix reescrivint-lo amb les dades reals al davant, i això inclou revertir decisions que semblaven correctes.

Per Salva Castells

Imatge de portada: Ernst Alban, Domini públic.
En aquest post

El primer model de contractes que vaig escriure per a una eina de BI d'una empresa gran tenia unes vuitanta línies de SQL i em va semblar bé. Un contracte, una data d'inici, una data de fi, unes condicions penjant. Es llegia net. Un any després aquell mateix model l'havia reescrit més de deu vegades, inclosa una en què li vaig canviar el nom perquè el nom mentia.

No va ser mala planificació. Va ser que el domini només es veu quan entren les dades de debò.

Per què el primer model sempre sembla correcte

Perquè l'escrius contra el domini que t'han explicat, no contra el que existeix. Et diuen "un contracte té data de fi". El modeles. Funciona amb les vint files que mires.

El primer avís va arribar amb el nom. El model es deia kpi_02_20_revenue_contracts i el vaig reanomenar a kpi_02_20_rent_contracts. No era revenue, era rent: contractes de lloguer on l'empresa paga, no on cobra. Signe contrari. El nom venia del tiquet, no de la taula. Un mes treballant amb un model que es deia el contrari del que contenia.

Els noms mal posats no són cosmètica. Algú els llegeix i assumeix.

El que només apareix quan entren les dades de debò

La data de fi va ser el pou. La taula tenia dues columnes: DT_FIRST_CONTRACT_END i DT_CONTRACT_END_NEXT. La primera versió de la data unificada era això:

case
    when c.DT_FIRST_CONTRACT_END is null then c.DT_CONTRACT_END_NEXT
    when c.DT_CONTRACT_END_NEXT is null then c.DT_FIRST_CONTRACT_END
    when c.DT_FIRST_CONTRACT_END < CURRENT_DATE() then c.DT_CONTRACT_END_NEXT
    else c.DT_FIRST_CONTRACT_END
end as dt_end_merged

Sembla exhaustiu. No ho és. Hi falta el cas que totes dues siguin nul·les, i el cas que la primera sigui al passat i la segona no existeixi. A les dades, tots dos casos volien dir el mateix: contracte indefinit. El case els enviava a un null accidental que després es comptava com a "expirat". La versió final hi afegeix aquestes dues branques explícites i un when dt_end_merged is null then True al flag is_unlimited, perquè nul allà és informació, no absència d'informació.

I aquell mateix flag ja arrossegava una altra joia del món real:

when year_end_merged in (8909, 8899, 9999) then True

Tres anys sentinella diferents per dir "sense fi". Cap documentat. Tots tres en producció.

Les tres vegades que vam revertir una decisió

La més il·lustrativa és l'any 99. Als Excel d'origen hi havia dates de fi amb l'any com a 99, i algú de l'equip deia que volia dir "indefinit". Vaig afegir el tractament:

case
    when {{ normalize_date('ew.WARUNEK_WA_NY_DO') }} like '%99%' then '2099-12-31'::date
    else {{ normalize_date('ew.WARUNEK_WA_NY_DO') }}::date
end

I el filtre corresponent, un or ... like '%99%' perquè aquelles files no caiguessin per la finestra de dates. Ho vaig treure tres dies després. El like '%99%' casa amb 1999, amb 2099 i amb qualsevol data que porti un 99 al dia o al mes. Estava ficant contractes tancats feia vint anys al càlcul de revenue actiu. La regla del negoci potser era certa; la implementació amb like sobre una cadena de data no ho era.

Dos mesos després va tornar el mateix problema per una altra porta, al model de renegociacions: anys de dos dígits. Aquesta vegada ho vaig fer sobre l'any ja extret, no sobre el text:

case
  when year(normalized_next_possible_termination_old_contract) < 100
    then year(...) + 2000
  else year(...)
end

Això tampoc no va sobreviure intacte: dues setmanes més tard va quedar comentat al fitxer amb un -- ! al davant. Tercera volta sobre la mateixa columna.

L'altre rollback gros va ser l'inner join entre contractes i condicions. Un contracte sense condició no apareixia. Canviar-ho a left join va canviar els números del dashboard, i aquesta és exactament la conversa que cal tenir amb qui demana el KPI: si un contracte sense condició compta o no compta. La primera vegada ho vaig decidir jo sol, amb un inner, sense adonar-me que ho estava decidint.

Com sé quan un model ja és estable

Pel tipus de commit. Durant el primer any els commits eren estructurals: joins que canvien, filtres que entren i surten, columnes que es reanomenen. Al final del projecte els commits eren d'una altra mena: treure un multiplicador per 100, fusionar dos case en un, escriure la documentació de columnes. Això és un model que ja no es mou per sota, només per sobre.

L'altre senyal és que el .yml de documentació passa de tenir quatre bullets vagues a llistar els criteris de selecció exactes: entitats, tipus de contracte, moneda, què s'exclou i per què. Quan pots escriure aquesta llista sense mirar el SQL, el model existeix de debò. Abans el que tens és una consulta.

Què faig avui abans del primer CREATE TABLE

Abans escrivia el model i després mirava les dades. Ara és a l'inrevés, i en aquest ordre:

  • Comptar els nuls de cada columna que hagi d'entrar en una condició. Una columna amb un 40% de nuls no és una data, són dos conceptes barrejats.
  • Buscar valors sentinella. select distinct year(...) order by 1 desc. Si en surten 9999, 8899 o 2099, hi ha regles de negoci sense documentar amagades allà.
  • Escriure el filtre en prosa abans que en SQL. Si no sé dir en una frase quins contractes hi entren, el where acabarà amb sis condicions contradictòries i dues comentades.
  • Normalitzar sobre tipus, mai sobre cadenes. Tot like sobre una data és una bomba amb temporitzador.

Un model de domini no es dissenya: es negocia amb les dades, i les dades no cedeixen. Cada vegada que vaig revertir alguna cosa va ser perquè havia decidit jo el que havia de preguntar.