Adatbázisrendszerek I. Fájlszintű adattárolás C-ben 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ájlban (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ájlban 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 vagy egy sztringbe összefűzve írhatjuk a fájlba, illetve olvashatjuk ki onnan; vagy több függvényhívással egyenként tároljuk és olvassuk vissza. 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ól olvasása: 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. a) Bináris fájlból 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. printf(...); 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); b) Szövegfájlból A fájl megnyitása után (megnyitási mód: rt) az elejéről indulva minden ciklusiterációban kiolvasunk egy sort a fájlból. A tárolt sorok számának meghatározása: int number_of_lines = 0; while((ch = fgetc(fp))!= EOF) { if(ch == '\n') number_of_lines++; Ezt követően újra a fájl elejére lépünk (rewind) és soronként kiíratjuk a fájl tartalmát. rewind(fp); for (i=0; i<number_of_lines; i++) { if (fscanf(fp, "%s %s %s %d %lf", car.rendszam, car.tipus, car.szin, &car.gyart_ev, &car.ar)==0) { printf("hiba"); break; printf("\nrendszám: %s, Tipus: %s, Szin: %s, Gyártási év: %d, Ár: %.0f", car.rendszam, car.tipus, car.szin, car.gyart_ev, car.ar); 3.2 Új adat felvitele Adatfelvitelhez a fájlt hozzáfűzés módban (a) nyitjuk meg. 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. a) Felvitel bináris fájlba 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); Felvitel előtt a rendszám ellenőrzése: if (strcmp(car.rendszam, rsz) == 0) return 1; /* már létezik */ else return 0; /* nem létezik */ a) Felvitel szövegfájlba Egymás után beolvassuk a struktúra adattagjait, majd az fprintf függvénnyel az egész struktúrát egy sorban eltároljuk.
fprintf(fp, "%s %s %s %d %f\n", car.rendszam, car.tipus, car.szin, car.gyart_ev, car.ar); Felvitel előtt a rendszám ellenőrzése: while (!feof(fp)) { fscanf(fp, "%s %s %s %d %lf", car.rendszam, car.tipus, car.szin, &car.gyart_ev, &car.ar); if (strcmp(car.rendszam,rsz) == 0) return 1; 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 (w) 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: w+. 1. lépés: segédfájlba átírás (bináris fájlkezelés) if (strcmp(car.rendszam, rendszam)!= 0) /* ha nem a törlendő */ fwrite(&car, sizeof(car), 1, fp_tmp); 2. lépés: visszaírás az eredeti fájlba (bináris fájlkezelés) for (i=0; i<filesize-1; i++) { fseek(fp_tmp,sizeof(car)*i,seek_set); fread(&car,sizeof(car),1,fp_tmp); fwrite(&car, sizeof(car), 1, fp); 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. A típus alapján történő keresés bináris fájl esetén: if (strcmp(car.tipus,tipus) == 0) return 1; A feladat megoldását a C_bin_fajlkezeles ill. a C_txt_fajlkezeles CodeBlocks projektek tartalmazzák.
Önállóan megoldandó feladatok: 1. Valósítsa meg az adattörlés és keresés funkciókat szövegfájl esetén is. 2. Adatfelvitel előtt végezzen szemantikai ellenőrzést a gyártási évre: 1997 <= gyártási év <= 2017. 3. Számítsa ki a fájlban eltárolt autók átlagárát. 4. Kérdezze le az eltárolt piros autók darabszámát. 5. Keresse meg a legdrágább autót a fájlban. 6. Csökkentse az összes autó árát 10%-al. Adatmódosítás: 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+. Bináris fájl esetén fájlba írás (fwrite) előtt vissza kell lépni egy fájlpozíciót: fseek(fp,sizeof(car)*-1,seek_cur); Házi feladat: 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 minden sorszámot csak egyszer osztunk 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 { Car; char rendszam[7]; char tipus[20]; char szin[10]; int gyart_ev; double ar; int tulaj_id; //új adattag: autó tulajdonosa Feladatok: 1. Vezessen 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égezzen 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ítsen ö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.