Fejlesztés ImmuDB-vel: példák Python és Django környezetben (2. rész)
Az adatintegritás és a nyomon követhetőség biztosítása napjaink digitális rendszereiben egyre nagyobb hangsúlyt kap, különösen ott, ahol az adatok megmásíthatatlansága kulcsfontosságú. Az ImmuDB éppen erre a problémára kínál nyílt forráskódú megoldást. A cikk első részében bemutattuk, hogy mi is az az ImmuDB, mikor lehet rá szükség, és milyen alternatívák állnak rendelkezésre. A mostani folytatásban technikai szempontból mélyülünk el: egy Python alapú Django projekt példáján keresztül vizsgáljuk meg az ImmuDB gyakorlati alkalmazhatóságát, a felmerülő kihívásokat és a fejlesztés során szerzett tapasztalatokat.
Amikor online rendelünk egy terméket, gyakran előfordul, hogy a csomag több kézen megy keresztül: futárcégek, alvállalkozók, raktárak váltják egymást a szállítás során. Ilyen esetekben különösen fontos, hogy minden állomás és felelősségköri váltás visszakövethető és megmásíthatatlan módon legyen rögzítve. Egy immutable adatbázis, mint az ImmuDB, pontosan ezt teszi lehetővé. Biztosítja, hogy minden átadás megbízhatóan és véglegesen nyomon követhető legyen, ez által világosan kirajzolva az események láncolatát, és megkönnyítve az esetleges felelősségek tisztázását. Az adatbázis feladata tehát nem más, mint minden egyes átadás rögzítése: kitől, kihez, és adott esetben milyen állapotban került tovább a rendelt termék.
A Python immutable adatbázis jó választás lehet, különösen akkor, ha már magabiztosan mozgunk nem csak a nyelvben, hanem valamelyik keretrendszerben is, amiben viszonylag egyszerűen tudunk saját adatbázis konnektort írni. A Pythonon belül az egyik legérettebb keretrendszer erre a célra a Django.
Verziók:
- Python >= 3.13
- Django >= 5.1
- ImmuDB = 1.9.6
- ImmuDB-py = Latest
Kihívások az ImmuDB integrációja során
ImmuDB Python példa: lehetőségek és korlátok a gyakorlatban
Mivel az ImmuDB Go-alapú, kézenfekvő választás lenne maga a Go nyelv használata is a fejlesztéshez. Ugyanakkor éppen ezért informatívabb lehet egy másik, szintén széles körben használt nyelv, például a Python alkalmazása. A Python adatbázis kapcsolat kialakításához az ImmuDB-vel az immudb-py könyvtárat lehet használni, ami gRPC protokollal kommunikál az adatbázissal.
Ez már a nulladik lépésként is magában hordoz problémákat: jelen cikk írásának időpontjában a gRPC protokoll nem támogat néhány népszerű típust, mint például az UUID-t, ami adatbázisoknál gyakran hasznos lehet, és amelyet maga az ImmuDB is támogat.
Ez azt jelenti, hogy önmagában azzal, hogy a Python nyelvet választjuk, eleshetünk egy, amúgy az ImmuDB által támogatott funkciótól.
A gRPC szintje után minden további lehetőségünk az immudb-py könyvtár képességein és frissességén múlik. Ha a Codenotary egy új ImmuDB verziót ad ki valamilyen új feature-rel, előfordulhat, hogy annak elérésére csak később, vagy egyáltalán nem lesz lehetőségünk – amennyiben az immudb-py nem implementálja le a hozzá tartozó szükséges frissítéseket az oldalukon. Ez természetesen nem törvényszerű, de érdemes számolni ezzel a potenciális kockázattal.
SQL az ImmuDB-ben – lehetőségek és korlátok
Mindezen aggályokat félretéve örömteli tény, hogy kulcs–érték alapú párokon alapuló adatbázisunkat hagyományos relációs adatbázisként is tudjuk használni. Vagyis: többnyire. Számos kisebb-nagyobb meglepetés érhet bennünket, ha korábbi relációs adatbázis-tapasztalataink alapján, kellő utánajárás nélkül vetjük bele magunkat az ImmuDB SQL világába. Az alábbiakban – a teljesség igénye nélkül – néhány olyan szempontot emelünk ki, amelyek előzetes ismerete segíthet elkerülni a tipikus buktatókat.
Típusok
Az ImmuDB SQL jelenlegi dokumentációja csak néhány, alapvető adattípust támogat:
- Logikai érték
- 64 bites egész szám
- UTF-8 string
- BLOB (bájtsorozat),
- Időbélyeg (mikroszekundum, időzóna-független, UTC-ben értelmezett)
- IEEE-754 64-bit lebegőpontos szám
- UUID (128-bit)
- Json (RFC 8259)
Ez, összehasonlítva például a Postgres számtalan típusával, vagy a fejlesztő által definiált saját típusok (pl enum-ok) tárházával, csekélynek tűnhet. Ugyanakkor fontos figyelembe venni az ImmuDB viszonylagos újdonságát, valamint azt is, hogy a létező alternatívák (Blockchain-alapú megoldások) ennél is korlátozottabb választékot kínálnak.
Idegen kulcsok és tábla kapcsolások
Az SQL nyelvben a táblák közötti idegen kulcson keresztüli (pl egy-az-egyhez, egy-a-többhöz stb.) kapcsolatokat a JOIN kulcsszó valamilyen variációjával szokás megfogalmazni, ami különböző halmazműveleteket reprezentál. Az ImmuDB ezek közül kizárólag a metszetet, azaz az INNER JOIN-t támogatja, ami ugyanakkor az egyik legelterjedtebb is. Ez különösen lényeges, mivel az idegen kulcsok tábla-definíció szintű explicit megadása nem lehetséges, azaz minden idegen kulcsunk valójában egy mezei érték, amin nincs adatbázis-szintű védelem, vagy ellenőrzés vizsgálat az ImmuDB jelenlegi verziójában.
Python adatbázis lekérdezések
Összetett lekérdezések során a fő lekérdezésünket gyakran logikailag több allekérdezésre kell bontanunk.
- Az ImmuDB-ben az alábbi szabályok érvényesek:
- Az allekérdezéseknek azonos számú és típusú lekérdezéseket kell tartalmazniuk.
- A duplikátumokat automatikusan eltávolítja.
- Allekérdezés csak FROM-hoz tartozóan adható meg, azaz a SELECT után közvetlenül nem.
- Támogatott aggregációs függvények:
- COUNT
- MIN/MAX
- AVG
- SUM
- Támogatott SQL parancsok:
- ORDER BY
- HAVING
- GROUP BY
- LIMIT
Ezek használatakor probléma lehet, hogy az oszlop indexet nem fogadják el, tehát minden esetben konkrét oszlopnevet szükséges megadni. Kivételt képez a megszámlálás, ahol éppen az ellenkezője igaz: nem adhatunk meg konkrét oszlopnevet.
Tábla módosítása
Az adatok módosításainak nyomon követése miatt a tábla szerkezetének módosítása bizonyos technikai korlátokba ütközik.
- Új oszlop hozzáadása csak akkor lehetséges, ha az nullable, hiszen a meglévő adatok számára nem lehetne mit beállítani, azaz alapértelmezett értéket ImmuDB szinten nem tudunk megadni.
- Oszlop törléséhez előbb el kell távolítani az ahhoz tartozó indexeket.
- Oszlop adattípusának vagy megszorításainak módosítása nem támogatott, ami a fejlesztés során komoly korlátot jelenthet.
A következő fejezetben olyan megközelítéseket mutatunk be, amelyekkel ezek a korlátozások részben kezelhetők.
Django adatbázis integráció ImmuDB-vel
A Django keretrendszer ORM (Object-Relational Mapping) infrastruktúrája kiforrott, és sok segítséget és lehetőséget nyújt a gyors és könnyű fejlesztéshez. Ez a feature-rich ökoszisztéma azonban bizonyos kihívásokat vet fel, ha azt az ImmuDB jelenlegi, nem kifejezetten feature-rich SQL-implementációjával szeretnénk együtt használni.
Néhány tipikus eltérés, amelyek problémát okozhatnak:
- A Django ORM használata során a migrációk kezelése eltérhet a megszokottól az ImmuDB kapcsán: migráció szinten az oszlopok szabadon módosíthatóak a Django által.
- Nem feltétlenül csak INNER JOIN-t tartalmazó SQL utasításokat generál a Django.
- A Django több típust támogathat, mint az ImmuDB.
A Django adatbázis modell testreszabása kihívást jelenthet. A különbségek azonban nem jelentik azt, hogy a két technológia ne lenne használható együtt. A fejlesztőnek alkalmazkodnia kell: vagy leegyszerűsíti a Django-modelljeit, vagy kerülőutakat keres bizonyos megoldások kivitelezésére.
Megoldások
Az ImmuDB Django keretrendszeren keresztüli használata bizonyos Django osztályokból való leszármaztatást igényel. Bár teljeskörű dokumentáció erre vonatkozóan nem áll rendelkezésre, az ImmuDB konnektor fejlesztése során megjelenő hibaüzenetek jellemzően informatívak, és a Django nyílt forráskódjának köszönhetően a leszármaztatandó osztályok függvényei is könnyen áttekinthetők.
Az alap felépítés technikai háttere
Az adatbázisunkat érdemes egy külön Django applikációként elhelyezni a Django projekten belül. Tegyük fel, hogy a projektünk neve tracking
, és az ImmuDB implementációnk az immudb_backend
mappában található. A fő applikációnkban található settings.py
fájlban tudjuk megadni, hogy az adatbázissal melyik Django applikáció kommunikál. Ehhez a DATABASES
változónak a következő dict
értéket kell adni:
DATABASES = { "default": { "ENGINE": "immudb_backend", "USER": environ.get("DATABASE_USER", "immudb"), "PASSWORD": environ.get("DATABASE_PASSWORD", "immudb"), "NAME": environ.get("DATABASE_NAME", "defaultdb"), "HOST": environ.get("DATABASE_HOST", "immudb"), "PORT": "3322", } }
Megjegyzendő, hogy az itt megadott alapértelmezett értékek megegyeznek az ImmuDB adatbázis alapértelmezett értékeivel, így a fejlesztéshez szigorúan vett értelemben a környezeti változókat sem szükséges megadnunk.
Ezek után az ImmuDB implementációnkhoz a következő fájlokat kell létrehoznunk, és az osztályokból leszármaztatnunk:
base.py: BaseDatabaseWrapper
(+ saját kurzor)client.py: BaseDatabaseClient
features.py: BaseDatabaseFeatures
introspection.py: BaseDatabaseIntrospection
operations.py: BaseDatabaseOperations
schema.py: BaseDatabaseSchemaEditor
A teljes implementáció számos technikai finomságot tartalmaz, amelyek részletezésébe itt nem megyünk bele. A továbbiakban azonban bemutatjuk a fejlesztés során szerzett legfontosabb és legtanulságosabb tapasztalatainkat.
A BaseDatabaseWrapper testreszabása
Ez az osztály tartalmazza azokat a legmagasabb szintű függvényeket, amire szükségünk lehet. Itt kell felülírni többek között olyan függvényeket, mint az execute
, commit
, get_new_connection
, valamint itt kell megadnunk a saját, továbbiakban is felüldefiniált osztályainkat, mint például client_class
, features_class
.
Emellett itt konkretizálhatjuk az ImmuDB specifikus típus, valamint művelet megfeleltetéseinket is.
Mivel az ImmuDB a LIKE
kifejezés után alapból regex kifejezést vár, így például az operators
map egy eleme lehet a 'regex': 'LIKE %s'
is. Hasonlóképp a data_types
map-et is felüldefiniálhatjuk, ahol megadhatjuk például, hogy 'DateTimeField': 'TIMESTAMP'
, azaz a Django ORM-ben DateTimeField
-ként megadott mezőket adatbázis szinten TIMESTAMPT
-ként akarjuk kezelni. Ez azonban nem történik meg automatikusan, nekünk kell majd megadni, hogyan akarunk egy Django-ban megadott dátumot az ImmuDB számára érthető TIMESTAMP
formátumra átalakítani.
Minden ImmuDB-specifikus logikát egy adatbázis-kurzor osztályban definiálunk.
A kurzor kulcs metódusa az “execute
”, amit a DatabaseWrapper
“execute
“ metódusában is meg kell hívni. Ebben a függvényben kezelhetők például a dátumok, a specifikus lekérdezések (mint például a SELECT 1
, vagy GROUP BY 1)
, az UPDATE
kifejezés UPSERT
-re és az ImmuDB által megadott formátumra történő átalakítása, a SET
kifejezések felülírása.
További meglepetést okozhat az, hogy az ImmuDB felhasználó kezelése miatt egyes kulcsszavak foglaltak, ezért a következő leképezést is definiálni kell:
field_name_mapping = { 'password': 'user_pass', # PASSWORD is a reserved keyword in ImmuDB }
Ezen kurzorunkat ezek után a DatabaseWrapper
osztályban a következőképpen alkalmazzuk:
def create_cursor(self, name=None): return CursorWrapper(self.connection)
DatabaseSchemaEditor: egyedi logika
Ez az osztály felelős azoknak az SQL-utasításoknak a generálásáért, amelyek a Django ORM fejlesztése során a migrációkat hajtják végre a különböző migráció műveletek alapján.
Itt definiálhatjuk a táblákat létrehozó, módosító, és törlő SQL utasításokat is, mint például:
sql_create_table = ( "CREATE TABLE IF NOT EXISTS %(table)s" "(%(definition)s," "PRIMARY KEY (%(primary_key)s))" )
Az egyik legfontosabb logika, amit felül kell írnunk, a tábla módosítása. A fejlesztés során - különösen lokális fejlesztés közben -, jellemzően kevés adatot tárolunk az adatbázisban, vagy ha mégis, akkor azt importálható formában. Ez azért előnyös, mert lehetővé teszi az adatbázis gyors reset-elését adatvesztés nélkül. E filozófia mentén implementálhatunk olyan tábla-felülírási logikát, amelyet az ImmuDB önmagában nem támogat.
Tegyük fel, hogy a fejlesztés folyamán valamilyen mező nullable volt, de később kiderül, hogy célszerűbb lenne nem hagyni NULL
értékeket abban az oszlopban. Ilyen módosítást az ImmuDB nem támogat közvetlenül, ezért először létre kell hoznunk a táblának egy másolatát, ahol a módosítani kívánt mező már nem nullable, majd át kell másolnunk minden értéket az új táblába, és végül törölni az előzőt.
Ha az oszlop típusát szeretnénk módosítani, a másolás nem mindig lehetséges, és előfordulhat adatvesztés.
Felmerül továbbá a kérdés, hogy a korábban NULL
értékű adatokat hogyan másoljuk át az új táblába. Erre a Django rendelkezik beépített megoldással: a migráció során megkérdezi, hogy mi legyen ebben az esetben az alapértelmezett helyettesítő érték. Ha ezt sem szeretnénk megadni, akkor tanácsos lehet először manuálisan törölni minden adatot a táblából.
Features és egyéb fájlok
További fájlok kisebb módosításokat igényelnek. Ezek közül is az egyik fontosabb a features.py
, amelyben explicit módon adhatunk meg ORM és SQL specifikus flag-et, mint például:
supports_left_outer_join = False supports_right_outer_join = False supports_full_outer_join = False supports_native_bulk_update = False
Az operations.py
-ban - többek között - például az előbbi példában említett bulk update feature-t tudjuk felülírni és egy ciklussal definiálni, mivel az ImmuDB SQL implementációja ezt nem támogatja.
Az introspection
-höz csupán a következőt kell definiálni:
class DatabaseIntrospection(BaseDatabaseIntrospection): """Database introspection for ImmuDB.""" def get_table_list(self, cursor): """Return a list of table and view names in the current database.""" cursor.execute("SELECT * FROM TABLES()") return [TableInfo(row[0], 't') for row in cursor.fetchall()]
Amit egyszerűségben már csak a client.py
tud felülmúlni:
class DatabaseClient(BaseDatabaseClient): """Database client for ImmuDB.""" executable_name = 'immudb'
Összegzés: az ImmuDB helye az adatbázisok között
Az ImmuDB egy fontos és releváns igényt elégít ki, amely valahol a hagyományos adatbázisok és a Blockchain technológiák között helyezkedik el. Bár fejlettsége még elmarad a manapság standardnak számító relációs adatbázisokétól, és nyelvi integrációi is korlátozottak, a használhatósága nem kérdőjelezhető meg. A Django Python keretrendszerben történő alkalmazása ugyan igényel néhány napnyi munkát, és nem működik zökkenőmentesen minden implementációval, de adott esetben ez is a „használható” kategóriába esik, ha az alkalmazási terület ezt megköveteli.