TSM - Suportul JSON în PostgreSQL

Raul Rene Lepsa - Java Devolper


Există o nevoie crescândă incontestabilă pentru flexibilitate și scalabilitate în ceea ce privește datele, acesta fiind și motivul pentru care mulți au apelat la baze de date NoSQL pe durata ultimilor ani. Există avantaje și dezavantaje în ceea ce privește folosirea lor, mai ales că acestea nu au fost destinate să înlocuiască bazele de date relaționale.

Dezvoltatorii și arhitecții soft au deseori dificultăți în a alege una sau alta, în special când formatul datelor ce urmează să fie folosite este necunoscut sau poate fi modificat ulterior. O soluție de compromis este folosirea atât a bazelor de date relaționale cât și a celor non-relaționale și crearea unui sistem de comunicare între ele. Însă această soluție poate ajunge să dea mai multe dureri de cap și probleme decât dacă s-ar folosi doar un sistem de baze de date.

De ce nu 2 în 1?

Companii precum IBM și Oracle au început să ofere metode prin care Sistemele de Baze de Date Relaționale (RDBMS) și cele non-relaționale să coexiste. PostgreSQL oferă o alternativă prin tipuri de date speciale, cu o structură mai liberă și flexibilă, care imită comportamentul NoSQL într-un RDBMS.

Începând cu versiunea 8.3, PostgreSQL a introdus tipul de date hstore, care este folositor pentru rânduri cu multe atribute care sunt rar examinate și date semi-structurate. Permite stocarea de perechi cheie-valoare (similar cu unele NoSQL-uri), suportă diferite operații și oferă funcții pentru manipularea lor. În versiunea 9.2 a fost introdus tipul de date JSON, căruia i s-au adus îmbunătățiri în ceea ce privește performanța în versiunea 9.3 beta. JSON (Javascript Object Notation) este un format lightweight, lizibil și independent de limbaj, pe care Postgres îl stochează sub formă de text.

De ce l-ai lua în considerare?

Aproape toți dezvoltatorii au sau au avut de-a face cu cerințe schimbătoare din partea clienților și au simțit nevoia de flexibilitate din partea sistemului de stocare a datelor în special când vine vorba de aplicații cu clienți multipli. În același timp, clienții simt deseori nevoia de câmpuri personalizate și cer flexibilitate. Dar ce e de făcut când unii clienți vor un câmp particular și alții vor patru, pentru aceeași funcționalitate? Sunt sigur că majoritatea ați văzut coloane precum customField1, customField2, customField3, anotherCustomField ș.a.m.d. Acestea pot fi evitate folosind un tablou (array), dar ce facem când avem de stocat perechi? Sau triplete? Dacă câmpul particular are atât o etichetă cât și o valoare? Sau chiar o dată asociată? Lucrurile devin mai complicate.

O altă problemă generală o reprezintă tratarea numelor. Există o listă de 40 de neadevăruri despre nume, și doar ca să vă faceți o idee, am enumerat câteva pe care dezvoltatorii în general omit să le ia în calcul:

Desigur, probabil nu avem de a face cu multe din aceste cazuri, dar nu poți fi niciodată sigur că sistemul nu va trebui pe viitor să suporte nume chinezești. Un mod tradițional de a stoca nume este o combinație de prenume, nume de familie și eventual al doilea prenume care este opțional. Însă dacă o persoană nu are prenume sau nume de familie, această implementare devine eronată.

O posibilă cale de a rezolva această problemă este prin folosirea unui JSON:

{
    "first_name": "Ronaldo",
    "mother_name": "de Assis",
    "last_name": "Moreira",
    "nicknames": [
        "Ronaldinho", 
        "Gaúcho"
    ]
}

Implementarea de mai sus poate adăuga muncă în plus prin schimbarea câmpului de full_name de fiecare dată când unul din restul câmpurilor este schimbat, dar acesta este doar un exemplu. În general, soluția este dependentă de aplicație și de clienții țintă ai acesteia, dar reprezentarea de mai sus oferă multă flexibilitate în comparație cu modul clasic de stocare a numelor din cadrul bazelor de date relaționale. Având rar nevoie de accesarea tuturor numelor, putem vedea utilitatea acestui tip de reprezentare. De ce am crea o coloană pentru numele mamei când avem rareori nevoie de el? De ce s-ar crea o coloană pentru poreclă sau nume de alint când unele entități ar putea avea mai mult de unul singur? Aceeași logică poate fi aplicată și celorlalte nume.

Funcții și Operatori

Cu toate că lista completă de funcții și operatori poate fi găsită în documentația celor de la Postgres, este important de menționat că un obiect JSON e accesat folosind operatorul -> (poate fi returnat și ca text folosind operatorul ->>). De exemplu, pentru o coloană nume, accesarea numelui de familie s-ar face folosind: names->>'last_name'. Operatorul prezentat anterior poate fi folosit și pentru accesarea unui element aflat la o anumită poziție într-un tablou: (names->'nicknames')->0 ar întoarce primul obiect din tabloul nicknames din cadrul coloanei.

Pe lângă operatori, începând cu versiunea 9.3 au fost adăugate și multiple funcții, pentru a ajuta dezvoltatorii în folosirea acestui tip de coloană, precum funcții pentru extragerea obiectelor dintr-un tablou de JSON, pentru transformarea unui rând într-un obiect JSON, pentru expandarea unui JSON într-un set de perechi cheie-valoare sau pentru conversia oricărui element într-un obiect JSON.

Avantajele tipului de coloană JSON

Primul avantaj ale acestui tip de coloană este caracterul de format "open standard", fiind independent de limbaj. În al doilea rând, facilitează tratarea cerințelor schimbătoare ale clienților prin scalabilitate și flexibilitate. Devine folositor când e nevoie de stocarea grafurilor de obiecte multi-nivel, deoarece oferă performanță ridicată și codul în sine este mai ușor de scris și de menținut decât în implementările obișnuite de grafe. Coloana JSON nu ocupă mult spațiu (e stocată ca și text) și permite stocarea de până la 1 GB de date într-o singură coloană. În plus, previne SQL injection în mod implicit, deoarece obiectele JSON sunt validate înainte de a fi persistate.

Cheile străine pot fi evitate prin denormalizarea datelor și câmpurile din cadrul interogărilor complexe pot fi accesate fără să fie nevoie de join cu alte tabele (posibil mari), oferind astfel un comportament similar cu NoSQL-uri într-un sistem de baze de date relațional.

Se poate dovedi a fi un plus în aplicațiile web, facilitând transportul și conversia datelor de pe client către controller-e, și apoi către nivelul de acces la date, stocând obiectele JSON venite dinspre client.

Aceste tipuri de date pot fi indexate, iar în plus indecși pot fi creați în cadrul obiectelor JSON (de exemplu, pentru tablouri din cadrul unui JSON). Indecșii curenți suportați atât de hstore cât și de coloanele JSON sunt aproape la fel de performanți ca și cei ai tipurilor de date standard. Dar se lucrează la noua generație de indecși GIN. Aceștia au fost deja implementați pentru hstore în versiunea în curs de dezvoltare 9.4. Comparațiile de performanță cu MongoDB arată că deși scanarea secvențială este aproape la fel la nivel de performanță, scanarea de indecși este mai rapidă decât în Mongo. Acești indecși vor fi cel mai probabil aplicați și tipului de date JSON începând cu versiunea 9.4.

Dezavantaje

Cel mai mare dezavantaj al tipului de date JSON este faptul că nu e portabil, fiind momentan suportat doar în PostgreSQL. Alte dezavantaje includ imposibilitatea de a adăuga chei străine și sintaxa ciudată, mai puțin lizibilă decât în cazul interogărilor obișnuite.

În plus, interogările care sunt simple pe tipuri de date comune devin complicate când se folosește tipul de date JSON, în special când avem de-a face cu tablouri de obiecte. Pentru a exemplifica acest aspect, considerați o tabelă de utilizatori users care stochează numerele de telefon ca și un tablou de obiecte JSON, precum este prezentat în Figura 1. Cu toate că acest tip de configurare arată mai bine decât crearea de coloane pentru fiecare tip de număr, precum home_number, work_number ș.a.m.d., interogarea tabelei pentru a aduce numerele de telefon de tip primary de exemplu este destul de complicată. Rezultatul interogării este afișat în Figura 2:

SELECT users.id, phone->>’number’ AS number 
FROM users INNER JOIN
(
  SELECT id, 
     json_array_elements(phones) 
     AS phone
     FROM users 
     WHERE id=users.id
) phones 
     ON phones.id = users.id
     WHERE phone->>’type’=’primary’

Figura 1 - Numere de telefon stocate ca tablou de obiecte JSON, fiecare având un tip și un număr.

Figura 2 - Numerele de tip ”primary” returnate de interogare

În general, nu putem fi siguri că numărul de telefon de tip primary se află pe prima poziție a tabloului, acest lucru fiind subiectiv și dependent de sistem. Cu toate acestea, impunerea unor astfel de constrângeri pot duce la interogări simplificate, având operatorul -> pentru a accesa direct elementul de pe poziția n a unui tablou. De asemenea, important de notat este că exemplul precedent prezintă un caz simplu de stocare de numere de telefon. Interogările devin cu atât mai complicate cu cât datele stocate în tablourile JSON sunt mai complexe, în special când apare nevoia de a face join acestor date din cadrul tabloului cu alte tabele.

Concluzii

A opta doar pentru un sistem de baze de date relațional sau pentru unul NoSQL nu reprezintă întotdeauna o soluție viabilă. Cu toate că se fac pași pentru a ușura integrarea acestor două tipuri de baze de date, cel puțin pentru moment acest lucru poate să se dovedească a fi o prea mare bătaie de cap.

PostgreSQL oferă o soluție de "compromis" prin suportul pentru coloane hstore care imită mediile de stocare cheie-valoare și pentru coloane de tip JSON, care permit stocarea de obiecte și tablouri JSON. În plus, oferă operatori și funcții pentru a facilita manipularea datelor, suportând în același timp și indecși atât pe date JSON cât și pe câmpuri din interiorul lor.

Dacă această soluție este sau nu optimă este o întrebare subiectivă, dependentă de sistem și de cerințele pe care acesta trebuie să le îndeplinească. Ceea ce este important de ținut minte este că dezvoltatorul are la dispoziție puterea și complexitatea sistemului de baze de date relațional Postgres, indiferent dacă alege sau nu să folosească tipul de date JSON.