Adatbázisrendszerek I. File-szintű adattárolás C-ben 1. gyakorlat Feladat: Tervezzen meg egy fájlszintű adatnyilvántartó rendszert és implementálja C nyelven. A tárolandó adatok: autó rendszáma, típusa, színe, gyártási éve és ára. 1. Adatszerkezet megtervezése typedef struct car { char rendszam[6]; char tipus[20]; char szin[10]; int gyart_ev; double ar; } Car; Figyelem! Ha scanf függvénnyel történik az adatbeolvasás, a sztring nem tartalmazhat space-t. 2. Adattárolás fájlban C-ben az adatok tárolhatók ASCII szövegfájlban, vagy bináris fájlban. Fájl létrehozása: FILE *fopen(const char *fajl_nev, const char *megnyitas_mod);
Megnyitási módok (szövegfájlok esetén): r (reading) olvasás w writing) írás a (append) hozzáfűzés r+ olvasás a fájl elejétől + írás w+ írás+olvasás, felülírjuk a fájlt a+ írás + olvasás, hozzáfűzünk Bináris fájloknál a betűk után még egy b betűt is oda kell írni, pl.: ”rb” vagy ”wb+”. Ha a fájl megnyitása sikerült egy FILE struktúrára mutató pointert kapunk vissza, ha nem sikerült, akkor NULL pointert. FILE *fp=fopen("cars.bin","ab"); if (!fp) { printf("Error: cannot open file."); return -1; }
Fájl lezárása:
int fclose(FILE *fp);
A szöveges fájlokban (txt) olvasható formátumban, soronként vannak tárolva az adatok. Egy sor állhat egyetlen karakterből: int fputc( int c, FILE *fp ); vagy egy sztringből: int fputs( const char *s, FILE *fp ); int fprintf(FILE *fp,const char *format, …);
A szövegfájl tartalmának olvasásához az alábbi függvények használhatók. Egy karakter olvasása: int fgetc( FILE * fp );
Sztring olvasása: n-1 karakter (+a lezáró ‘\0’) olvasása. Ha újsor karaktert vagy fájl vége jelet olvas, a függvény befejezi futását : char *fgets( char *buf, int n, FILE *fp ); Sztring olvasása (space-ig!): int fscanf(FILE *fp, const char *format, …); Bináris fájlokban az adatok nem olvasható formátumban tárolódnak. Az eltárolt objektumok nem csak szekvenciálisan érhetők el, azaz az aktuális fájl pozíció a kívánt helyre mozgatható. A nagyobb adategységek (tömbök, struktúrák) írása, olvasása egyszerűbb (nem igényel sztringkezelő műveleteket). Jelen esetben a struktúra objektum egyetlen függvényhívással írható, olvasható, amelynek adattagjaira a szokásos módon hivatkozhatunk. Szemben a szövegfájllal, ahol a struktúra adattagjait egy sztringbe összefűzve írhatjuk a fájlba, illetve olvashatjuk ki onnan. Bináris fájlok írása és olvasása: size_t fwrite(const void *ptr, size_t meret, size_t darab, FILE *fp); size_t fread(void *ptr, size_t meret, size_t darab, FILE *fp);
Egy autó struktúra fájlba írása: fwrite(&car, sizeof(Car), 1, fp);
Egy autó struktúra fájlbó olvasása:
fread(&car,sizeof(Car),1,fp);
Pozicionálás fájlban: int fseek(FILE * fp, long int offset, int origin);
Az első paraméter a FILE pointer, a második a pozíció eltolásának mértéke. Ez az elem bájtokban mért nagyságának egész számú többszöröse. Az origin azaz az eltolás kezdete a következő makrókkal adható meg: SEEK_SET, SEEK_CUR és SEEK_END. Ezek jelentése: fájl elejétől, aktuális pozíciótól, illetve a fájl végétől számítjuk az offsetet, ami negatív is lehet. 3. Adatokon végrehajtandó műveletek: 1. Adatok listázása 2. Új adat felvitele 3. Adat törlése 4. Keresés 3.1 Listázás A fájlt az elejétől kezdve végig kell olvasni és minden adatot kiírni a képernyőre. Azaz a fájl megnyitása után (megnyitási mód: rb) az elejéről indulva minden ciklusiterációban eggyel előre mozgatjuk a fájl pointert (‘i’ a ciklusváltozó) és kiolvassuk az aktuális adatot. De mi lesz a ciklus kilépési feltétele? Addig dolgozom fel a tárolt adatokat, amíg el nem érek a fájl végére. for (i=0; i
A fájl méretének meghatározása úgy történik, hogy a végére pozicionálunk és az aktuális pointer pozíció értékét elosztjuk a struktúra méretével. fseek(fp, 0L, SEEK_END); int filesize = ftell(fp)/sizeof(Car);
3.2 Új adat felvitele Adatfelvitelhez a fájlt hozzáfűzés módban (ab) nyitjuk meg. Egymás után bekérjük a struktúra adattagjait, majd az fwrite függvénnyel az egész struktúrát egyben eltároljuk. fwrite(&car, sizeof(Car), 1, fp);
Ennél a feladatnál a legfontosabb feladat az adatbevitel “ellenőrzése”. A megadott adatok szintaktikai ellenőrzéséhez azt kell tudni, hogy a scanf függvény visszatérési értéke a sikeresen beolvasott értékek száma. De van-e lehetőség az adatok szemantikai ellenőrzésére? Jelen esetben a rendszám az autók egyedi azonosítója. Tehát az adatok fájlba írása előtt meg kell nézni, hogy a megadott rendszám létezik-e már. Vagyis végig kell olvasni a fájlt és minden tárolt rendszámot össze kell hasonlítani azzal, amit most szeretnénk felvinni. Ha nincs találat, a rendszám egyedi és a megadott adatok eltárolhatók. for (i=0; i
/* már létezik */ /* nem létezik */
3.3 Adat törlése Egy autó struktúra eltávolítása a fájlból nem triviális feladat. Szükségünk van egy segédfájlra: ebbe átmásoljuk a törlendő struktúrán kívül az összes többi adatot. Majd az eredeti fájlunkat újraírási módban (wb) megnyitva, visszaírjuk ide a segédfájl tartalmát. Az újraírási mód azt jelenti, hogy a nem létező fájl létrejön, a létező pedig felülíródik. Figyelem! A segédfájlt mindig újra kell írni, és mivel vissza is kell olvasni a tartalmát a megnyitási mód: “wb+”. 1. lépés: segédfájlba átírás
for (i=0; i
2. lépés: visszaírás az eredeti fájlba
for (i=0; i
3.4 Keresés A keresésnél először azt kell eldönteni, hogy mi alapján keresünk (rendszám, típus, stb.). Mivel a rendszám egyértelműen kijelöl egy struktúrát, ha rendszám szerint keresünk biztosan 1 autó lesz a keresés eredménye. Ha a többi adattag szerint akarunk szűrni, valószínűleg egynél több találatot kapunk. A rendszám alapján történő keresésnél ugyanaz a kód használható, mint a rendszám ellenőrzésnél: for (i=0; i
if (strcmp(car.rendszam, rsz) == 0) kiir(); else printf("Nem talált.");
/* talált */
}
A feladat megoldását a C_bin_fajlkezeles.c forráskód tartalmazza. Önállóan megoldandó feladatok: 1. Adatfelvitel előtt végezzünk szemantikai ellenőrzést a gyártási évre: 1996 <= gyártási év <= 2016. 2. Számítsuk ki az autók átlagárát. 3. Kérdezzük le a piros autók darabszámát. 4. Keressük meg a legdrágább autót.
Adatbázisrendszerek I. File-szintű adattárolás C-ben 2. gyakorlat Egészítsük ki az 1. gyakorlat feladatát: az autók mellett tároljuk el a tulajdonosok adatait is külön fájlban. Ehhez definiáljunk egy új struktúrát. typedef struct tulaj { int id; char nev[20]; char cim[50]; } Tulaj; A definícióból látható, hogy minden tulajdonos struktúrához hozzárendelünk egy sorszámot (int id). Ez segít az adatsorok egyértelmű beazonosításban. Hiszen sok “Szabó Tamás” él Magyarországon, akik akár ugyanazon a lakcímen is élhetnek (apa és fia). Ez a sorszám akkor használható azonosítóként, ha biztosítjuk, hogy minden sorszámot csak egyszer osszunk ki. A tulajdonosok adatainak kezelése egyébként az autó adatok kezeléséhez hasonló (listázás, felvitel, törlés, keresés). Azért, hogy a két feladatrész elkülönüljön, az adatkezelő műveleteket megvalósító függvényeket külön forrásfájlokban (modulokban) definiáljuk. Így a két nyilvántartást külön, egymástól függetlenül vezetjük. Ha azt szeretnénk, hogy az autók a tulajdonosaikkal össze legyenek kapcsolva, azaz az autókat lekérdezve a tulajdonos adatait is lássuk, illetve a tulajdonosok listáján a hozzájuk kapcsolódó autók adatai is szerepeljenek, a struktúra definíción kell változtatni. Ha a tulajdonos struktúrába felveszek egy ‘autója rendszáma’ adattagot, akkor ez azt jelenti, hogy egy emberhez csak egy autót tudok kötni. Hogyan tudom megadni, ha több autója van? 1. Vegyem fel többször a tulajdonost a nyilvántartásba? 2. Az ‘autója rendszáma’ adattag legyen tömb? Mekkora legyen a tömb mérete? Egyik sem tökéletes megoldás, mert felesleges memóriafoglalással jár és például a tulajdonos több példányban történő eltárolása a törlésnél problémát okozhat. Próbáljuk meg inkább az autó struktúrában tárolni a ‘tulajdonost’. Ha tulajdonos alatt az autó üzembentartóját értjük, akkor igaz az az állítás, hogy minden autónak csak egy üzembentartója van. Felmerül a kérdés, hogy akkor az autó struktúrán belül legyen a tulajdonos struktúra definiálva (beágyazott struktúra)? Ha abból indulunk ki, hogy minden autónak van tulajdonosa, akkor igen. De vizsgáljuk meg a kérdést a tulajdonos szemszögéből is. Minden tulajdonosnak egy autója van? Ha nem, akkor minden autó objektumnál újra fel kell venni az adatait és el kell tárolni. Tehát célszerű a két struktúrát külön definiálni és az autó struktúrában csak egyetlen adattaggal hivatkozni a tulajdonosára. Milyen típusú legyen ez az adattag? Célszerű a tulajdonost egyértelműen azonosító adatot választani (jelen esetben az id adattagot). typedef struct car { char rendszam[7]; char tipus[20]; char szin[10]; int gyart_ev; double ar; int tulaj_id; } Car;
//új adattag: autó tulajdonosa
Ily módon a két nyilvántartás összekapcsolását megoldottuk. A feladat megoldását a C_fajlkezeles_2 C projekt tartalmazza.
Önállóan megoldandó feladatok: 1. Vezessünk be ellenőrzést az autók felvitelénél: csak létező (a tulajdonos fájlban megtalálható) tulajdonos azonosítót lehessen megadni. 2. A tulajdonosok törlése előtt végezzünk ellenőrzést: ne lehessen olyan tulajdonost törölni, akire van hivatkozás az autó nyilvántartásban (azonosítója szerepel az autó tulajdonosok között). 3. Készítsünk összefésült listákat: 3.1 Az autók listázásánál a tulajdonos adatait is lássuk. 3.2 A tulajdonosok listázásánál a hozzájuk tartozó autók adatai is jelenjenek meg. 4. Adatmódosítás. Itt azt kell eldönteni, hogy csak teljes struktúra legyen módosítható, vagy az egyes adattagok külön. Mindenképpen be kell olvasni egy rendszámot és meg kell keresni az adott rendszámú autó adatait. Felkínálhatjuk, hogy a felhasználó válassza ki melyik adattagot kívánja módosítani (egyszerre csak egyet); vagy az új adat felvitelhez tartozó programkódot felhasználva beolvassuk az összes adattag új értékét. Akárhogy is döntünk, vagy az aktuális fájl pozícióra kell “ráírni” a módosított adatokat, ekkor a fájl megnyitási mód: “r+” ; vagy a törlésnél használt segédfájlos megoldást kell alkalmazni. 5. Csökkentsük az összes autó árát 10%-al.