Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Conceptes de persistència

Localització de les dades

Una aplicació conté codi i dades. El codi interactua amb les dades que hi ha a la memòria principal, que és volàtil, el tipus de memòria més ràpida que existeix i la més senzilla de llegir i escriure. A POO, les dades són als objectes.

Un problema de la memòria principal és que perdem el seu contingut si l’aplicació finalitza, ja sigui de forma correcta o per un problema. El context necessari per a l’execució d’una aplicació és el seu estat, i cal recuperar-lo cada cop que les executem. Això es fa utilitzant memòria no volàtil basada en un o diversos arxius físics.

Quan parlem de recuperar el context d’una aplicació per a la seva execució, no sempre és possible fer-ho amb totes les dades que es gestionen. Pot ser perquè la mida sigui superior a la de la memòria principal, però també perquè no calgui i sigui millor ser mandrós en la seva càrrega per raó de rendiment.

Model de dades

El disseny del model de dades ha de tenir en compte el millor rendiment possible quan s’utilitza memòria no volàtil, molt més lenta, i facilitar la futura escalabilitat de les nostres dades.

Un mecanisme per a minimitzar temps és la memòria cache: mantenim un nombre limitat d’objectes a la memòria principal per evitar haver-los de carregar des de la memòria no volàtil cada cop, i els invalidem si es modifiquen.

També els SGBD poden proporcionar-nos mecanismes per a processar les dades sense necessitat de carregar-les en memòria, gràcies a llenguatges declaratius com SQL: demanem quines dades necessitem, no com processar-les.

Si parlem de disseny, és convenient crear una capa de persistència que encapsuli el procés. Això ens permetrà substituir-la sense afectar la resta de l’aplicació. Si parlem d’usabilitat, la persistència hauria de ser transparent per a l’usuari.

Què, quan i com persistir

Tenim dos mètodes de persistència: d’estat, el més habitual, i basat en esdeveniments.

Persistència d’estat

Parlant de POO, ens podríem fer la pregunta de si allò que volem persistir és un objecte o una estructura de dades. Els objectes tenen comportament i encapsulen les dades. Les estructures de dades exposen les dades i no tenen comportament. Aquesta distinció ens ajudarà a decidir la nostra estratègia de persistència.

Per definir la persistència, cal repassar els camps de l’objecte i decidir quins necessitarem per tal de tornar a instanciar l’objecte en memòria principal. Alguns camps innecessaris poden ser calculats o tenir una funció temporal. Podem ajudar-nos de l’estudi dels constructors i setters de l’objecte.

Quan els camps són d’una persona, aquest repàs no el decidim només nosaltres: cadascun arrossega una finalitat, una base legal i un termini de conservació, i n’hi ha que no s’han de guardar mai. Això condiciona l’esquema, l’esborrat i fins i tot quines consultes podrem escriure. Ho teniu a dades personals.

A més, ens caldrà fer referències entre els objectes. Quan dissenyem mecanismes ad-hoc, podem utilitzar un identificador d’objecte en el moment de la persistència. En el cas dels SGBD, els objectes es relacionen amb claus foranes.

Tenim diversos àmbits de persistència:

  • Context general de l’aplicació: configuracions generals, per usuari, etc.
  • Documents, basats en serialització. Persistim en base a un document que pot intercanviar-se.
  • Registres, habitualment relacionats i associats a SGBD. No se sol restaurar completament al començament, només quan cal.

Persistència d’esdeveniments

La persistència per esdeveniments (Event Sourcing) és un mecanisme que permet restaurar l’estat d’una aplicació a partir de tots els esdeveniments que s’han produït. Es tracta d’una mena de log on es guarden els esdeveniments per ordre d’ocurrència, i que permet reconstruir l’estat en qualsevol moment del temps.

Quan l’aplicació s’inicia, reconstrueix l’estat actual aplicant tots els esdeveniments en l’ordre en què es van produir. Com que podria haver molts d’esdeveniments, aquest mecanisme se sol combinar amb altres, com per exemple emmagatzemar un estat intermedi (snapshot) i aplicar els esdeveniments a partir d’aquell instant.

Quan i com

Hem de persistir sempre que hi hagi un canvi en alguna dada? La resposta general és que sí. Així evitem perdre dades si el nostre programa falla abans d’haver persistit. Però de vegades no seria òptim, parlant de rendiment, haver de persistir tot l’estat d’una aplicació. Alternativament, es podria endarrerir el moment, o fer-ho periòdicament de forma automàtica. També podem deixar aquesta responsabilitat en mans de l’usuari (File > Save).

La persistència per esdeveniments permet només guardar l’esdeveniment que es produeix en lloc de totes les dades, i també pot ser una solució per millorar el rendiment.

Quin tipus de persistència?

El format i organització dependrà de les nostres necessitats.

Criteris per decidir la forma de persistència:

  • És una aplicació monousuari o multiusuari?
  • Es comparteix informació a la xarxa amb altres clients o aplicacions?
  • Hi ha un volum molt alt de dades?
  • Hi ha un esquema estable (ben estructurat)?
  • Quins requisits qualitatius tenim: disponibilitat, escalabilitat, latència, rendiment, consistència…?

Si la resposta està a prop d’una aplicació monousuari, sense connexió amb altres clients, i poques dades, segurament podem gestionar-ho mitjançant persistència en fitxer. Caldria, en tot cas, decidir quin és el format d’aquest fitxer, ja que podem utilitzar solucions existents sense necessitar inventar-nos un format a mida. En POO, utilitzem el concepte de serialització d’objectes: generem una representació del graf d’objectes que permet restaurar-los quan l’aplicació torna a executar-se. El format podria ser a mida, utilitzant JSON, un arxiu de preferències clau-valor, etc.

Si la resposta s’assembla a aplicació multiusuari amb informació compartida per la xarxa i grans volums de dades, estem a prop de necessitar un sistema de gestió de base de dades (SGBD). Des d’una aplicació orientada a objectes, una opció habitual per accedir a un SGBD relacional és el mapatge d’objectes relacional (ORM), tot i que no és l’única (també podem utilitzar SQL directament o query builders). Un ORM interposa una capa entre la lògica i la persistència, de tal forma que podem persistir utilitzant el paradigma d’orientació a objecte en lloc del llenguatge SQL. És un procés complex i no exempte de problemes. A Java es pot implementar a mida (JDBC) o bé utilitzar llibreries de mapatge (JPA).

Els SGBD resolen problemes habituals que ens trobem en el desenvolupament d’aplicacions. Entre ells:

  • Definició, creació, manteniment i control d’accés a una base de dades.
  • Gestió de transaccions i concurrència (segons el model).
  • Facilitats per recuperar dades en cas de danys.
  • Gestió d’autoritzacions i accés remot.
  • Regles de comportament de les dades en funció de la seva estructura.

Patrons de mapatge

Els ORM segueixen un de dos patrons, i la diferència no és un detall d’implementació:

  • Active Record: el model és la fila de la taula i porta incorporades les operacions de persistència, de manera que un objecte sap desar-se i esborrar-se ell mateix. És el patró del Django ORM i d’Eloquent (Laravel). Directe i amb poca cerimònia, però lliga el model a la base de dades.
  • Data Mapper: una capa a part tradueix entre objectes i files, i el model no sap que existeix la base de dades. És el patró de JPA amb Hibernate, d’Entity Framework Core i de Doctrine. Demana més codi de mapatge a canvi d’un domini independent de la persistència.

La tria no la solem fer nosaltres, sinó el framework, i condiciona l’arquitectura de tota l’aplicació. Ho veiem a Arquitectura dels frameworks.

Quan l’abstracció es trenca

Un ORM amaga la base de dades, però no del tot, i val més saber per on filtra:

  • El problema N+1: recórrer una col·lecció i accedir a una relació de cada element llança una consulta per element, més la inicial. És l’error de rendiment més habitual i també el més invisible, perquè el codi sembla perfectament correcte.
  • Càrrega mandrosa (lazy) i anticipada (eager): decideixen si les dades relacionades es porten quan s’hi accedeix o totes de cop amb la consulta inicial. La solució al problema anterior és demanar explícitament la càrrega anticipada: select_related i prefetch_related a Django, JOIN FETCH o @EntityGraph a JPA, with() a Eloquent, Include() a Entity Framework Core.
  • Que cada ORM tingui un valor per defecte diferent és el millor argument que el problema és real i que no es pot delegar al framework: Django carrega sempre de forma mandrosa, Hibernate ho fa per a les col·leccions, i Entity Framework Core desactiva la càrrega mandrosa per defecte i obliga a demanar-la. És exactament el punt on s’aplica a un framework l’hàbit adquirit en un altre.

Per a informes, operacions massives i consultes complexes, sovint surt més a compte SQL directe o un query builder que forçar l’ORM.

Base de dades d’aplicació o d’integració

Una base de dades d’integració és una base de dades que actua com a magatzem de dades de diverses aplicacions i, per tant, integra dades d’aquestes aplicacions.

Una base de dades d’aplicació es controla i accedeix des d’una sola aplicació. Per a compartir dades amb altres aplicacions, l’aplicació que controla la base de dades hauria de proporcionar serveis.

La recomanació general és la d’evitar bases de dades d’integració. En general, les bases de dades d’integració comporten problemes greus perquè la base de dades esdevé un punt d’acoblament entre les aplicacions que hi accedeixen. Generalment es tracta d’un acoblament profund que augmenta significativament el risc que suposa canviar aquestes aplicacions i dificulta la seva evolució.

Evolució de l’esquema

Decidit qui és l’amo de la base de dades, queda saber com la canvia. L’esquema no és fix: al llarg de la vida de l’aplicació hi apareixen taules noves, columnes que canvien de tipus i índexs que s’afegeixen. Una migració és un d’aquests canvis guardat com un fitxer al projecte, amb un ordre i un identificador, de manera que qualsevol còpia de la base de dades es pot posar al dia aplicant les que li falten.

En desenvolupament sembla una complicació innecessària, perquè sempre pots esborrar la base de dades i tornar-la a crear. En producció no pots: allà hi ha dades reals que no es poden perdre, i l’esquema ha de canviar amb elles a dins. Les migracions són el que fa possible que el mateix canvi arribi igual a la màquina de cada persona de l’equip, a l’entorn de proves i a producció, que es pugui revisar abans d’aplicar-lo i que es pugui repetir sense sorpreses. Sense això, l’esquema de producció acaba sent el resultat d’una successió de canvis manuals que ningú no recorda i que no es poden reproduir enlloc.

Això té una conseqüència que sorprèn el primer cop: el codi es pot revertir, l’esquema no. Si el desplegament va malament tornes a la versió anterior de l’aplicació en un moment, però la migració ja s’ha aplicat i el codi antic es troba davant l’esquema nou. Desfer una migració en producció rarament és una opció real: revertir un DROP COLUMN recupera la columna però no les dades que hi havia, i entre l’aplicació i la marxa enrere els usuaris han seguit escrivint. La pràctica habitual és anar sempre endavant (roll forward): si una migració és incorrecta, se n’escriu una de nova que ho arregla. Les eines ho reflecteixen; Django i Rails generen migracions reversibles, útils sobretot en desenvolupament, mentre que Flyway no ofereix undo a la versió lliure.

Tampoc no arriben alhora el codi nou i l’esquema nou. Si el desplegament és sense aturada (rolling, blue-green), conviuen la versió antiga i la nova de l’aplicació i totes dues han de poder treballar amb la base de dades. I encara que l’aplicació s’aturi del tot, queden els treballs en segon pla, les transaccions llargues i qualsevol altre consumidor de la base de dades que no es desplega amb tu. Per això els canvis es fan en dos passos, primer afegint i després retirant: s’afegeix la columna nova, es desplega el codi que la fa servir i, en un desplegament posterior, s’esborra la vella. Canviar les coses de lloc de cop trenca l’aplicació que encara s’està aturant. Si cada canvi és additiu, en canvi, el codi antic segueix funcionant amb l’esquema nou i revertir el codi torna a ser segur.

Dues coses no canvien mai: les migracions són codi, i per tant van al repositori i es revisen com la resta, i s’apliquen al desplegament, no a mà. Qui les escriu, en canvi, depèn de l’eina, i sovint la decideix el framework: ho veiem a Migracions de l’esquema.

Model relacional vs NoSQL

Les bases de dades actuals responen majoritàriament al model relacional, que ha triomfat gràcies a l’establiment de l’SQL com a estàndard. En paral·lel han guanyat pes les bases de dades NoSQL (key-value, document, column-family i graf), afavorides pel Big Data i les aplicacions en temps real. Sovint són sistemes sense esquema, i poden conviure amb solucions relacionals en models de persistència poliglota.

Dins el model relacional també tenim extensions sense esquema (camps JSON, taules d’atributs) que permeten encabir dades no estructurades dins d’un esquema, tot i que aquestes no són tan accessibles des de consultes SQL.

A l’hora de triar entre un model i l’altre, els criteris principals són:

  • Esquema: les dades tenen una estructura estable i coneguda, o cal flexibilitat per fer-la evolucionar ràpidament?
  • Relacions: hi ha relacions entre entitats que haurem de navegar sovint? Les relacionals les modelen de forma natural; als NoSQL solen resoldre’s a l’aplicació o amb models de graf.
  • Integritat: qui garanteix que les dades siguin coherents, el SGBD o l’aplicació? (veure Integritat de les dades).
  • Escalabilitat: el volum de dades i de peticions requereix escalat horitzontal en clúster? Les NoSQL hi estan més preparades.
  • Consistència vs disponibilitat: el sistema pot tolerar lectures lleugerament desactualitzades a canvi de més disponibilitat? Les relacionals tendeixen a ACID, les NoSQL a BASE, tot i que la frontera s’ha difuminat (molts NoSQL ofereixen ACID).
  • Rendiment: les relacionals poden patir amb escriptures massives o joins grans degut a les garanties ACID, però la normalització en si sovint millora el rendiment de lectura.

Les dimensions tècniques es detallen a Model Relacional i Model NoSQL.

Integritat de les dades

Una de les diferències pràctiques més importants entre el model relacional i el NoSQL és qui garanteix que les dades siguin correctes.

Les BBDD relacionals ofereixen mecanismes declaratius per a expressar regles d’integritat directament a l’esquema:

  • Claus primàries i restriccions UNIQUE per evitar duplicats.
  • Claus externes amb accions referencials (CASCADE, SET NULL, RESTRICT) per mantenir la coherència entre taules.
  • NOT NULL per garantir que certs camps sempre tinguin valor.
  • CHECK per expressar regles de domini (age >= 0, status IN (...)).

El SGBD rebutja qualsevol escriptura que violi aquestes regles, independentment de qui sigui l’autor: l’aplicació principal, un script de manteniment, una migració o un nou servei afegit més tard.

A la majoria de BBDD NoSQL aquestes garanties no existeixen a nivell d’emmagatzematge. La responsabilitat d’assegurar la integritat recau a l’aplicació, i això té conseqüències:

  • És una propietat de correctesa, no de rendiment: si només l’aplicació valida les dades, qualsevol bug, script ad-hoc o servei nou pot introduir dades incoherents silenciosament.
  • Els problemes s’acumulen: les violacions d’integritat en un NoSQL solen passar desapercebudes fins que una query falla o un informe mostra dades impossibles. Llavors els danys estan distribuïts en mesos o anys d’escriptures.
  • És difícil retrofitar-ho: afegir restriccions a un SGBD relacional existent és laboriós però ben definit. Afegir-les a un NoSQL implica auditar tots els escriptors i netejar les dades històriques.
  • Es perd documentació: una restricció SQL és autodocumentada, qualsevol que miri l’esquema la veu. Les regles implementades a codi estan disperses i són invisibles per a qui no llegeix l’aplicació.
  • Sovint es reconstrueix en codi: molts equips acaben adoptant validadors (JSON Schema, Mongoose, Pydantic) que recreen una capa d’esquema sobre el NoSQL, el que en part desfà l’argument de la flexibilitat.

Això no vol dir que el NoSQL sigui una mala opció, però sí que la flexibilitat d’esquema té un cost que cal assumir conscientment: la responsabilitat de la integritat es desplaça del SGBD a l’aplicació.

Característiques ACID

En el context de bases de dades, ACID (acrònim anglès de Atomicity, Consistency, Isolation, Durability) són tot un seguit de propietats que ha de complir tot sistema de gestió de bases de dades per tal de garantir que les transaccions (unitats d’execució que agrupen una o més operacions sobre les dades) siguin fiables.

Concretament, l’acrònim ACID significa:

  • Atomicitat: Una transacció o bé finalitza correctament i confirma o bé no deixa cap rastre de la seva execució.
  • Consistència: Una transacció porta la base de dades d’un estat vàlid a un altre, respectant totes les restriccions definides (claus, checks, integritat referencial).
  • llament (o Isolament): Cada transacció del sistema s’ha d’executar com si fos l’única que s’executa en aquell moment en el sistema. Aquesta és la propietat que regula el comportament davant de la concurrència.
  • Durabilitat: Si es confirma una transacció, el resultat d’aquesta ha de ser definitiu i no es pot perdre.

Característiques BASE

Les característiques BASE estan associades a les BBDD NoSQL. Es basen en el teorema CAP (Consistency-Availability-Partition Tolerance), que afirma que en presència d’una partició de xarxa un sistema distribuït només pot garantir dues d’aquestes tres propietats. Com que les particions són inevitables a la pràctica, la decisió real és entre consistència i disponibilitat quan n’hi ha una (veure Model NoSQL).

Són l’acrònim de:

  • Basic Availability: la base de dades funciona la majoria del temps.
  • Soft-State: no cal tenir consistència a l’escriptura, ni les rèpliques han de ser consistents.
  • Eventual consistency: la consistència es pot tenir més tard en el temps (funcionament mandrós).
Last change: , commit: feff8b3