Objektno-relacijsko mapiranje z Entity Framework
Avtorja: Denis Balant, Enej Hudobreznik
Kljub hitremu razvoju tehnologije pa je glavni jezik za upravljanje z relacijskimi podatkovnimi bazami še vedno SQL oz. strukturirani poizvedbeni jezik, ki ima z mnogimi prilagojenimi izpeljankami (PostgreSQL, MySQL …) korenine že v 70. letih prejšnjega stoletja.
Pisanje SQL-poizvedb se pogosto izkaže za dokaj zamudno delo, še posebej pri kompleksnejši strukturi podatkovnih baz. Namesto ročnega pisanja poizvedb in pretvorb v objekte, značilne za objektno orientirane programske jezike, je veliko naravnejše in pogosto enostavnejše kar preslikati razrede iz izvorne kode v podatkovno shemo in ne obratno. S tem prihranimo na razvojnem času, hkrati pa je odnose med entitetami veliko lažje razpoznati. Ta metoda se imenuje objektno-relacijsko mapiranje (ORM).
Za okolje .NET Microsoft za ta namen ponuja odprtokodno ogrodje Entity Framework Core (na kratko EF Core), ki omogoča podatkovno usmerjen razvoj (t. i. pristop Code First), kjer med seboj povezane podatke preprosto zberemo v razrede, ki predstavljajo tabele, ogrodje pa na njihovi podlagi razbere relacije in oblikuje shemo podatkovne baze v lastnem formatu. Največja prednost takšnega pristopa je možnost namestitve podatkovne sheme na željeno podatkovno bazo brez poznavanja SQL.
EF Core s podatkovnimi bazami komunicira preko vtičnih knjižnic, imenovanih ponudniki podatkovnih baz (angl. database providers), zato je prehod na drugo podatkovno bazo preprost. Te lahko uporabnik namesti preko upravljavca paketov (NuGet za C#). Uradne knjižnice so na voljo le za Microsoftovi rešitvi SQL Server in Azure Cosmos DB ter projekt SQLite, zaradi močne odprtokodne skupnosti pa ogrodje podpira praktično vse večje relacijske podatkovne baze (MySQL, PostgreSQL …).
Kot primer modeliranja podatkov si oglejmo tabelo “Študent”, ki ima atribute ime (niz največ dvajsetih znakov), priimek (niz največ dvajsetih znakov) in enolični identifikator (celo število).
V PostgreSQL to tabelo definiramo na naslednji način:
Poleg definicij osnovnih tipov za ime in priimek zahtevamo še, da nista nikoli prazna (NOT NULL). Tip SERIAL predstavlja enolično celo število, ki v našem primeru služi kot primarni ključ Id. Za vsako dodano entiteto se avtomatsko določi na podlagi ID predhodno dodane identitete, ki se preprosto inkrementira.
Veliko lažje pa je tabelo definirati kar kot razred v izvorni kodi aplikacijske logike z uporabo ORM, ki sam ustvari objekt in ustrezne zahteve za podatkovno bazo. Oznaka Key nad atributom razreda označuje primarni ključ, Required pa označuje, da atributa ne smeta biti prazna.
Po končanem razvoju nam ogrodje EF Core omogoča avtomatsko generiranje sheme podatkov iz kode in njeno uvedbo na izbrano podatkovno bazo preko orodij ukazne vrstice, ki so vključena v ogrodje. Postopne spremembe sheme podatkovne baze upravljamo preko t. i. migracij, ki zagotavljajo da podatkovna baza ostane sinhronizirana s podatkovnim modelom aplikacije. Nove migracije, ki se pri EF Core shranjujejo v obliki posebnih razredov, so namreč ustvarjene s primerjavo trenutnega modela podatkov s trenutno shemo podatkovne baze (stanjem zadnje migracije).
Povezava s podatkovno bazo je abstrahirana z razredom, ki deduje razred DbContext. Njegovi atributi so zbirke entitet tipa DbSet, ki se preslikajo v tabele.
Omenjeno ogrodje za pisanje poizvedb uporablja sintakso LINQ, ki predstavlja enoten način za pridobivanje in obdelovanje podatkov iz različnih virov. Poizvedba se nato prevede v SQL, rezultat pa se sam prevede nazaj v objekt, njegov atribut ali tabelo objektov.
Spodnji primer prikazuje poizvedbo po študentu z znano vpisno številko. Dovolj je samo en klic metode Find z vrednostjo primarnega ključa (ID).
Če želimo doseči enak rezultat brez uporabe ORM orodja, to zahteva občutno več kode. Prikazan je primer s knjižnico NpgSql. Najprej moramo ustvariti objekta, ki predstavljata SQL-poizvedbo in branje podatkovne baze, pri tem pa moramo biti pazljivi na pravilno vključitev parametra za izogibanje morebitnim ranljivostim, kot je npr. vrivanje SQL (angl. SQL injection). Objekt študenta moramo tokrat ustvariti sami, pri tem pa moramo biti pazljivi na vrstni red atributov v poizvedbi in na možnost napake pri poizvedbi.
Objektno relacijsko mapiranje nam tako ponuja plast abstrakcije, ki razvoj programske opreme občutno pospeši zaradi svoje preslikave iz objektno usmerjenega načrta razvoja, ker SQL kode ni treba pisati ločeno od izvorne (pogosto objektno usmerjene) kode, dobro napisan ORM podpira dobre razvojne vzorce in prakse za aplikacijski dizajn, hkrati pa omogoča razvijalcem, ki se ne spoznajo na SQL, preprostejšo vključitev relacijske podatkovne baze v njihovo aplikacijo.
Pomembno pa je poudariti, da ORM ni brezhibna rešitev. Slabost se skriva ravno v abstrakciji, ki nam jo ponuja. Generira občutno več SQL-kode, kot bi jo napisal razvijalec, kar lahko močno vpliva na hitrost aplikacije, zakrije lahko slabe prakse dostopa do podatkovne baze, problem pa je tudi kompatibilnost za nazaj. Čeprav je generirana koda v večini primerov pravilna, jo je vseeno priporočljivo ročno preveriti in testirati.
Objektno relacijsko mapiranje je funkcionalnost, ki ni lastna okolju .NET. Funkcionalnost ponuja večina ogrodij in knjižnic za različne jezike. Primeri so Django za Python, Gorm za Go, Spring za Javo, Prisma za JavaScript (oz. Node.js) …