Dr. Vermes Mátyás1
2005. február
Az SQL2 könyvtár szabványosított adatbáziskezelő API-t nyújt CCC programok számára. A ,,szabványosított" jelző azt jelenti, hogy ugyanaz a program ugyanazt a működést produkálja több adatbáziskezelővel is. Jelenleg az Oracle és PostgreSQL adatbáziskezelőket támogatjuk.2 Koncepciónk szerint az Oracle-t azoknak az ügyfeleknek szánjuk, akik nem engedhetnek meg maguknak alacsony költségvetésű projekteket, a Postgrest pedig azoknak, akik bíznak a szabad szoftverekben. Természetesen ilyen típusú interfész készíthető volna más adatbáziskezelőkhöz is (DB2, Interbase, stb.).
A jelen dokumentáció programozóknak szól, számukra leggyorsabban példaprogramokkal lehet megvilágítani a lényeget. Lássuk tehát a lehetőségeket: Az alkalmazások egyidejűleg belinkelhetik az Oracle és Postgres könyvtárat. A két könyvtár párhuzamosan ugyanolyan függvényneveket definiál, ámde különböző névterekben, ezért a nevek nem ütköznek.
con1:=sql2.oracle.sqlconnectionNew() //sql2.oracle névtér con2:=sql2.postgres.sqlconnectionNew() //sql2.postgres névtér
Most van két adatbáziskapcsolatunk, egy Oracle és egy Postgres. Hogy az sqlconnectionNew() függvény pontosan milyen felhasználóként, hova jelentkezik be, az pillanatnyilag nem lényeges.
Tételezzük fel, hogy van egy bankszámlákat tartalmazó táblánk, aminek van egy számlaszám és egy egyenleg oszlopa. Lekérdezzük ezeket az sqlquery objektummal:
q1:=con1:sqlqueryNew("select * from szamla") // Oracle lekérdezés ... q1:close q2:=con2:sqlqueryNew("select * from szamla") // Postgres lekérdezés while( q2:next ) ? q2:getchar("szamlaszam"), q2:getnumber("egyenleg") end q2:close
Ugyanaz a lekérdezés tableentity objektummal:
t1:=szamla.tableEntityNew(con1) // Oracle tábla ... t2:=szamla.tableEntityNew(con2) // Postgres tábla rowset:=t2:select while( (rowentity:=rowset:next)!=NIL ) ? rowentity:szamlaszam, rowentity:egyenleg end
Új adatrekord létrehozása tableentity objektummal:
rowentity:=t2:instance rowentity:szamlaszam:="111111112222222233333333" rowentity:egyenleg:=0 rowentity:insert
Módosítás tableentity objektummal:
rowentity:=t2:find("111111112222222233333333") if( rowentity!=NIL ) rowentity:egyenleg+=100 rowentity:update end
A példában szereplő szamla.tableEntityNew() függvény adatbázisfüggetlen, azaz ilyen objektumgyártó függvényekből bináris könyvtárat lehet létrehozni, ami minden adatbáziskezelővel működik. Az objektumgyártó függvények kódját programmal generáljuk a táblák adatbázisfüggetlen XML leírásából.
Mint látjuk az SQL2 interfész két legfontosabb eleme az sqlquery és tableentity osztályok. Az sqlquery körülbelül azt tudja, mint ami a JDBC 1.0 specifikációban van. A fejlettebb specifikációkkal kapcsolatban a J2EE Útikalauz Java Programozóknak (szerk. Nyékyné Gaizler Judit, ELTE TTK Hallgatói Alapítvány, Budapest, 2002) könyv 475. oldalán olvashatjuk:
,,JDBC 2.1 alap API: Az előző verzióhoz képest már tetszőleges sorrendben lehet feldolgozni az eredménytáblákat, melyeket már nem csak olvasni, hanem módosítani is lehet ..."Ehhez képest az Oracle szilárdan kitart amellett, hogy a fetch-ek pontosan egyszer és csakis előre haladhatnak végig az eredménysorokon. A realitáshoz alkalmazkodva az SQL2 interfészben csak a next metódust implementáltuk sqlquery-kben és rowset-ekben való pozícionálásra.
Az előbbi könyv 479. oldalán a JDBC továbbfejlesztési terveiről olvashatjuk:
,,Relációs adattáblák és Java osztályok direkt megfeleltetése: Egy adattábla minden sora a táblához rendelt osztály egy objektumpéldánya lesz, a tábla oszlopainak pedig a sorobjektumok adatmezői felelnek meg. A sorobjektumokkal történő bármilyen manipuláció esetén az azt megvalósító SQL utasítások a háttérben automatikusan végrehajtódnak. Ilyen típusú API például a Java Data Objects (JDO) specifikáció."A tableentity objektumok is éppen így működnek, csak Jáva helyett CCC osztályokkal. A connection objektumból kapjuk a tableentity-ket, ezek select metódusaival a rowset-eket, ezekből a next metódussal a rowentity-ket (sorokat), amiknek minden oszlophoz van egy metódusuk, amivel kiolvasható, vagy átírható az oszlop értéke. Ezzel nagyjából pozícionálni tudjuk, hogy mit is nyújt az SQL2 interfész.
Megemlítem még, hogy a CCC korszerű rétegeivel dolgozunk: névtereket használunk, a felépítés teljesen objektumorientált.
Az SQL2-1.1.x specifikáció a CCC3 környezetre épül. Az implementáció alkalmazkodik az UTF-8/unicode kódolás követelményeihez.
Változások történtek a tds-ben. Az oszlodefiníciókban az oszlop valódi tábláját és nevét nem a t=tabid és c=colid opciókkal adjuk meg, hanem az új x=column_expression opcióval. Ez szükség esetén az x=tab_alias.real_column_name formával pótolja a korábbi tabid és colid opciókat, de sokkal használhatóbb, mivel általánosabb SQL kifejezést is tartalmazhat.
Újabb attribútumok is megadhatók az oszlopdefiníciókban:
utóbbi három a táblafüggetlen megjelenítőprogramok érdekében.
Itt arról lesz szó, hogyan vegyük birtokba az SQL2 szoftvert.
Először is kell legyen tesztelésre alkalmas adatbázisszerverünk. A függelékekben le van írva, hogyan tudunk Linuxon Oracle-t és/vagy Postgrest installálni. Aki Windowson akar dolgozni, sajnos magára van utalva. Az Oracle-lel Windowson sem lehet gond, és a Postgres 8.0 már Windowson is megy.
Akinek már eleve van adatbázisszervere, az is nézze meg a függelékeket, ott ui. az is le van írva, hogy milyen tablespace-t, user-t, schema-t kell létrehozni az adatbázisban, hogy a demó programok működhessenek. A demó programok számítanak a konto sémára, és elszállnak, ha az nem létezik.
A CCC programokat fordító/futtató gépen szükségünk lesz az adatbáziskezelők fejlesztő/kliens környezetére is.
Szükségünk lesz a legfrisebb CCC környezetre, ez letölthető a webről http://ok.comfirm.hu/ccc3/download. Mivel a Flex és Lemon nem olyan régen tartozik csak bele a CCC környezetbe, külön megemlítem, hogy a CCC-vel telepíteni kell a Flexet és a Lemont.
Le kell fordítani az egész SQL2 projektet. A fenti előkészületek után Linuxon ez nem lehet probléma, az sql2 directoryban elindítjuk
mkall.b
Ez mindenhova benéz, és elindítja a fordítást végző Build scripteket (Linuxon). Aki Windowson dolgozik, mkall.b helyett az mkall.bat scriptet indítsa el. Meggyőződünk arról, hogy a fordítás hibamentes. Csak a Postgres developer csomag installálásával lehet gond
Az sql2/test/testdata directoryban van néhány bt tábla, amik tesztadatokat tartalmaznak. A tview programmal meg tudjuk nézni ezek tartalmát.
Az sql2/test/testdata_import directoryban megtaláljuk a btimport.exe programot. Ez az előbbi bt táblákat importálja az SQL adatbázis szerverbe:
Ha már vannak tesztadataink, akkor átfáradunk az sql2/test/basicdemo directoryba. Itt azt fogjuk tapasztalni, hogy a demóprogramok már mind le vannak fordulva, méghozzá két verzióban:
Hangsúlyozom, hogy a példaprogramok a dokumentáció lényeges részét képezik. Sok munkám van abban, hogy a példákból és a közéjük írt megjegyzésekből megfelelően domborodjon a mondanivaló. A példaprogramokat tehát el kell olvasni, és ki kell próbálni, miközben a kódot összevetjük az eredményekkel. Sajnos ez időt és fáradságot igényel.
Felhívom a figyelmet az SQLDEBUG környezeti változóra. Ha ez be van állítva,
export SQLDEBUG=onakkor az SQL2 interfész listázza az adatbázisszervernek küldött SQL utasításokat. Ezek vizsgálata rendkívül sokat segít a program működésének megértésében, a hibák kijavításában.
A sql2/test/entitybrowse directoryban egy olyan demó van, ami Jáva terminálban browse-ol egy tableentity objektumot. A program nem bonyolult, de csak akkor fog lefordulni, ha a CCC környezetben van Jáva terminál támogatás, ezért kezdetben kihagyjuk ennek a projektnek a fordítását.
Egy tableentity objektum SQL select utasításokkal készített, azonos struktúrájú, de különféleképpen szűrt és rendezett eredménytábla halmazt képvisel. (Másképp: SQL select utasítások, amikben ugyanazt a joint, különféle where és order by záradékokkal kombináljuk.) Az SQL select tartalmazhat egy vagy több elemi táblát, vagy nézetet (view-t).
A tableentity objektumnak vannak select metódusai, ezek egy-egy rowset objektumot adnak. A különféle select metódusok különféle szűréseket végeznek, ennek megfelelően a rowsetek ugyanannak az (esetleg összetett) alaptáblának különféle részhalmazait tartalmazzák. A rowset objektum next metódusával lehet megkapni az eredménysorokat, a sorokat rowentity objektum formájában kapjuk.
A tableentity objektumnak van egy find metódusa, ami egy kulcsokkal maghatározott sort betölt egy rowentity objektumba. (A select speciális eseteként egyelemű rowsetet állít elő, és az egyetlen elemből rögtön rowentityt készít.)
Az adatbázisrekord, vagy select eredménytábla sorának programbeli leképezése a rowentity objektum. A rowentity objektum a kulcsmezők értékével (primary key) kapcsolódik a neki megfelelő adatbázis rekordokhoz. A tableentity minden oszlopához tartozik a rowentitynek egy azonos nevű metódusa, amivel az oszlop (mező) értéke lekérdezhető, módosítható. A rowentity nem egy rekordpointer vagy kurzor, hanem önállóan létező objektum. A program ugyananabból a táblából egyszerre több rowentityt is készíthet, azokat egymástól függetlenül módosíthatja, tárolhatja, törölheti.
A rowset objektumokat a tableentity select metódusaival kapjuk. Ugyanabból a tableentity objektumból származó rowsetekben közös, hogy ugyanazokat az elemi táblákat tartalmazzák, és a táblák ugyanúgy vannak összekapcsolva. A közös alaptáblából a különféle select metódusok különféle filterezettségű (where) és rendezettségű (order by) rowseteket adnak, amikből viszont egyforma struktúrájú rowentityket kapunk. A rowset:next metódushívással lehet megkapni a sorokat. A next a tábla elejétől a végéig haladva egyesével adogatja a sorokat (rowentityket), ha a sorok elfogytak, akkor NIL-t. Visszafelé haladni, vagy bármi egyéb módon pozícionálni nem lehet.
A tableenitynek rendelkeznie kell elsődleges kulccsal (primary key). Ez azon oszlopok felsorolásából áll, amely oszlopok értékének megadásával egyértelműen azonosítani lehet a sort, azaz a rowentity objektumot. A primary key-ben felsorolt oszlopoknak nem szabad null értéket megengedni, ezenkívül az egyediséget unique indexszel ki kell kényszeríteni.
A tableentity oszlopainak megadására columndef objektumokat használunk.
A columnref objektumokat olyan oszlopok leírására használjuk, amik nem szerepelnek a tableentity oszlopai között, viszont a tableentity valamilyen módon hivatkozik rájuk egy from, where, vagy order by záradékban.
Az indexdef objektumokkal indexek leírását közöljük a tableentityvel. A tableentity:create metódus (a tábla kreálása után) létrehozza a megadott indexeket is. Ha az indexünk unique minősítésű, akkor azzal kikényszeríthetjük sorok egyediségét, mint ahogy azt a primary key esetében is megtesszük.
Az SQL-ben az indexek léte, nemléte, nem befolyásolja a select utasítások eredményét, legfeljebb a végrehajtás hatékonyságát. Az adatbázisszerver mindig saját hatáskörben dönt egy index használatáról, mellőzéséről, sőt létrehozásáról.
A Table Definition Script (tds) bekezdésekből áll. Minden bekezdés egy sor elején álló kulcsszóval kezdődik, amit szóköz nélkül kettőspont követ. Az érvényes kulcsszavak: name, version, table, join, column, colref, primkey, index, select, include, type, comment. A bekezdések a következő bekezdésig, vagy a filé végéig tartanak. A bekezdések sorrendje nem kötött. Az elemző a comment bekezdéseket kihagyja.
Névtér, amibe a tableentityNew() objektumgyártó függvény kerül.
name: multi.level.namespace
A tds-ből generált tableentityt így tudjuk legyártani:
tableentity:=multi.level.namespace.tableentityNew(con)
A bekezdés tartalma egy tetszőleges tartalmú string (idézőjelekkel), amiből a tableentity objektum version attribútumának értéke lesz.
version: "arbitrary text"
Legalább egy table bekezdésnek kell lennie, ezek alakja:
table: real.qualified.tablename=aliasnameA script más részeiben a táblára kizárólag az alias névvel hivatkozunk. Ha nincs megadva join bekezdés, akkor a table bekezdésekben magadott táblák Descartes-féle szorzata lesz a tableentity alaptáblája. Ha csak egy table bekezdés van, akkor triviálisan az abban megadott tábla lesz az alaptábla.
Opcionális bekezdés. Ha nincs megadva, akkor a táblák Descartes-féle szorzata lesz az alaptábla. Ha meg van adva, akkor a join bekezdés tartalmából készül a tableentity által generált SQL select utasítások from záradéka (tehát lényegében egy from záradékot írunk a joinba). A from záradék SQL-beli szintaktikájától annyiban térünk el, hogy a táblákra és oszlopokra kizárólag aliasokkal hivatkozunk. Példa:
join: a full join b on id_a=id_b left join c on name_a=name_c
A legegyszerűbb példa:
column: szamlaszam C24
A kettőspont utáni első szóközökkel határolt szó egyszerre
A második szóközökkel határolt egység a típusleképezés. Ebben azt a típust kell megadni, amiben a CCC program kéri/adja az oszlop adatait (függetlenül attól, hogy mi az oszlop tényleges SQL adattípusa). Az érvényes típusok:
Cw | : w hoszúságú karakter string |
Nw | : w helyiértéken tárolt egész |
Nw.d | : w helyiértéken tárolt, d tizedesjegyet tartalmazó szám |
Nw,d | : w helyiértéken tárolt, d tizedesjegyet tartalmazó szám |
D | : dátum |
L | : logikai érték |
M | : memó |
A karakter stringeket C (unicode) típusban, a memókat X (bináris) típusban kapja a program.
Az oszlopnevet és típust kiegészítő adatok követhetik. A kiegészítő adatok opcionálisak, és a sorrendjük nem kötött.
x=column_expression
Ezzel a formával adható meg az oszlopkifejezés. Ha nincs megadva, akkor maga a név lesz a kifejezés:
column: szamlaszam C24 ;ez a default: x=szamlaszam
Amikor az oszlopnév nem egyértelmű, megadjuk a minősített nevét:
column: szamlaszam C24 x=ugyfszl.szamlaszam
Megadhatunk kifejezést:
column: EGYENLEG N17.2 (x=napinyito+napitartoz+napikovet) (l=Aktuális egyenleg)
Használhatunk SQL kifejezéseket:
column: devnem C3 x=lower(devnem)
l=label
Beállítja az oszlop label attribútumát. A labelt táblafüggetlen megjelenítőprogramok kiírhatják oszlopfejlécbe, vagy az adatot megjelenítő text mező elé.
t=tooltip
Beállítja az oszlop tooltip attribútumát (táblafüggetlen megjelenítőprogramok számára).
p=picture
Beállítja az oszlop picture attribútumát (táblafüggetlen megjelenítőprogramok számára).
d=default
Beállítja az oszlop default érték attribútumát. A defaultot nem az interfész, hanem az adatbázisszerver használja, ezért csak akkor van hatása, ha a tableentityvel kreáljuk a táblát (tehát ugyanaz a helyzet, mint a not null-nál).
nn
Jelentése not null. Az attribútum az adatbázisszervernek szól. Az interfész nem tudja kikényszeríteni a not null-t, hiszen az adatbázist mások is használhatják, nem feltétlenül a mi SQL2 interfészünkön keresztül. Ha viszont a táblát a tableenity create metódusával hozzuk létre, akkor az oszlop not null minősítéssel kreálódik, és a szerver biztosítja a not null feltétel teljesülését.
Az oszlopdefiníció szóközökkel határolt egységekből áll. Ha valamelyik paraméterérték szóközt tartalmaz, akkor azt zárójelpárral lehet védeni.
column: DEVNEM C3 l=Dev (t=A számla devizaneme) (x=upper( devnem )) (p=@! AAA) nn
A védelemre alkalmazott zárójelpáron belül lehetnek kiegyensúlyozott zárójelek. Ha nem kiegyensúlyozott zárójelet tartalmazó szöveget akarunk védeni, használjunk másik zárójelfajtát: [] vagy {}.
A primkey bekezdés kötelező. Benne vesszőkkel elválasztott listában felsoroljuk azokat az oszlopokat (column bekezdésben definiált alias neveket), amik egyértelműen azonosítják a rekordokat. Az alkalmazás felelőssége, hogy az azonosítás, és ezáltal az adatbázis rekord<->rowentity objektum megfeleltetés egyértelmű legyen.
primkey: column_alias1,column_alias2,...
Az opcionális index bekezdések formája:
index: index_name(column_alias1,column_alias2,...) [unique]Az index bekezdésekben indexeket definiálhatunk a táblára, ezzel bizonyos lekérdezéseket gyorsíthatunk, illetve a unique indexekkel kikényszeríthetjük rekordok egyediségét.
Az opcionális select bekezdések első szóközökkel határolt szavából a tableentity metódusneve lesz.
A metódusnevet követheti a where kulcsszó, majd tetszőleges SQL kifejezés, amit az SQL a where és order by kulcsszavak között elfogad. Az oszlopokra kizárólag az oszlop aliasokkal szabad hivatkozni. A kifejezés :1, :2,... alakú szimbólumokat tartalmazhat, amik helyére az SQL select parancs generálásakor paramétereket fogunk helyettesíteni.
A where feltételt követheti az order kulcsszó, ami után zárójelek között fel kell sorolni azokat az oszlopokat, amik szerint rendezni akarjuk az eredménytáblát. A felsorolásban az oszlopnevek után opcionálisan megjelenhet az asc/desc kiegészítés, ami előírja, hogy az adott oszlop szerint növekvő vagy csökkenő rendezettséget akarunk.
A where és order záradék közül legalább az egyiknek léteznie kell.
Példa:
select: select_a where col_a_alias like :1 order(col_a_alias, col_b_alias desc)
Ezután a tableentity:select_a("a%") metódushívás kiválasztja az alaptábla azon sorait, melyekben col_a első karaktere "a", és a sorokat rendezi col_a, majd csökkenő sorrendben col_b szerint.
Nézzük az alábbi példát az sql2/test/basicdemo/tds/proba.tds-ből:
name: proba version: "2,2006-07-18" table: konto.proba=p column: szamlaszam C24 nn x=szamla column: devnem C3 column: nev C30 x=megnevezes column: egyenleg N17.2 x=osszeg column: tulmenflag L column: konyvkelt D column: megjegyzes M primkey: szamlaszam,devnem index: nev(nev,szamlaszam) select: select_kk where konyvkelt<:1 or konyvkelt is null order(szamlaszam) select: select_ge where szamlaszam>=:1 order(nev desc,szamlaszam) select: select_tf where tulmenflag=:1 order(szamlaszam) select: select_bl where egyenleg<:1 order(szamlaszam)
Ebből a scriptből a tds2prg kódgenerátorral programot készítünk:
tds2prg.exe proba.tdsKapunk egy prg-t, amit a szokásos módon befordítunk a programunkba. A prg-ben van definiálva a proba.tableEntityNew() objektumgyártó függvény. Nézzünk egy-két programozási mintát, mire használható.
Az objektumgyártó egy tableentity objektumot ad, amivel a con adatbáziskapcsolaton át elérhető konto.proba táblát manipulálhatjuk. A konto.proba név a tds script table bekezdéséből jön. Valójában nem tudjuk, hogy a név mögött tábla van-e, vagy view, mert a két eset formailag nem különbözik. A tábla (view) neve lehet minősített. A jelen esetben a séma neve (ami a táblát tartalmazza) ,,konto", a tábla vagy view neve ,,proba". A tds script name bekezdésében egy névtér van megadva, esetünkben ,,proba". Az itt megadott névtérbe (ami lehetne többszintű is) kerül az objektumgyártó tableEntityNew() függvény, amit tehát így kell meghívni:
tableentity:=proba.tableEntityNew(con)
A tableEntityNew()-nak van egy második, opcionális paramétere is. Ha ezt megadjuk, akkor a konto.proba helyett a paraméterként megadott masik.tabla-hoz kapunk hozzáférést. Tételezzük fel, hogy ennek a másik táblának hasonló szerkezete van, mint a konto.proba-nak:
tableentity:=proba.tableEntityNew(con,{"masik.tabla"})A táblanév egy lista (array) elemeként van megadva. Mint később látni fogjuk egy tds script több táblára is hivatkozhat, tehát több tábla paraméternek is értelme lehet, ezért a táblákat egy arrayben felsorolva kell megadni.
Ezen a ponton nem tudjuk, hogy az adatbázisban van-e egyáltalán ilyen tábla, egyelőre csak egy programbeli objektumunk van. Ha emögött még nincs tábla, akkor most létre tudjuk hozni:
tableentity:createEz persze csak akkor értelmes, ha valóban tábláról van szó, nem pedig view-ról, ui. az interfész mindenképpen egy create table utasítást fog küldeni az adatbázisszervernek, view-król mit sem tud. Természetesen a konto.proba, vagy masik.tabla tábla fog kreálódni, ahogy a tableentity gyártásakor paraméterként megadtuk.
Itt egy kicsit érdemes elidőzni, megtárgyalni, milyen lesz az új tábla. Általában olyan nevű oszlopok lesznek benne, mint a tds scriptben az oszlopok neve, de vannak kivételek. A példában van egy ,,nev" nevű oszlop, amiből a tableentity sorobjektumainak lesz egy ,,nev" nevű metódusa. A táblában azonban az x=megnevezes előírás alapján ennek az oszlopnak az adatbázisbeli neve ,,megnevezes". Nem tudjuk pontosan, hogy mi az oszlopok SQL adattípusa, de bízhatunk benne, hogy az interfész olyan adattípust választ, ami kompatibilis a tds scriptben előírt Clipper típussal. A nev esetében sejthető a varchar(30), de pl. boolean típus egyes adatbáziskezelőkben van, másokban nincs, ezért nem egyértelmű, mi a Clipper L típus SQL megfelelője. Az nn-nel jelölt oszlopok not null minősítést kapnak. Az interfész a tábla elkészítésekor egyúttal indexeket kreál a primkey és index bekezdések szerint. A unique indexeknek szerepük lehet sorok egyediségének kikényszerítésében.
Kissé más a helyzet, ha az adatbázistáblát nem a tableentity interfésszel csináljuk, hanem készen kapjuk. A megrendelő bank alapadatait tartalmazó táblát pl. biztosan nem mi fogjuk kreálni. Ilyenkor egyáltalán nem tudjuk befolyásolni az oszlopok típusát, azonban az interfész oda-vissza elvégzi az SQL típus és a Clipper típus közötti konrverziót, ha egyáltalán lehetséges. Sajnos ilyenkor az egyediséget nem tudjuk indexekkel biztosítani, sem a not null feltételeket betartatni, ha az eleve nincs benne az adatdefiníciókban.
Ha a táblát meg akarjuk szüntetni, megtehetjük a
tableentity:dropmetódushívással. Ez egy drop table utasítást küld a szervernek, (view-ra nyilván nem működik).
Térjünk rá az adatok lekérdezésére. A tableentity objektumnak mindig van egy select metódusa, ami az alaptábla összes sorát tartalmazó rowset objektumot ad.
rowset:=tableentity:select
A rowset objektum legfontosabb metódusa a next, ezzel egyesével, előrefelé haladva lekérhetjük a sorokat:
while( (rowentity:=rowset:next)!=NIL ) ... end rowset:closeAmíg van újabb sor, addig rowset:next egy rowentity objektumot ad, ha már nincs több sor, akkor NIL-t. Ügyelni kell a rowset-ek lezárásra, máskülönben elfogynak bizonyos erőforrások (handlerek).
Most már soraink is vannak, nézzük a mezőértékeket:
r:=rowentity ? r:szamlaszam, r:devnem, r:nev, r:egyenlegPontosan mit is írtunk ki? A konto.proba.szamla, konto.proba.devnem, konto.proba.megnevezes, konto.proba.osszeg nevű adatbázismezőket. A rowentity objektumok tehát olyan attribútumokkal rendelkeznek, mint amilyen oszlopnevek vannak a tds scriptben. Ha nem intézkedünk másképp, akkor ezek az adattábla azonos nevű oszlopát jelentik, de ezt felülbírálhatjuk az x=valid_sql_expression előírással.
A CCC programból nézve a rowentity attribútumoknak olyan típusuk van, mint amit a tds scriptben előírtunk. Pl. az alábbi programrészlet végrehajtása után
rowentity:nev:="" ? valtype(rowentity:nev), strtran(rowentity:nev," ","x")ezt látjuk kiírva: C xxx...xxx (30 darab x). Az adatbázisból kaphatunk SQL null értékeket is. E tekintetben a Clipper hagyományokat követjük, és az interfésztől a nullok helyén nem NIL-t kapunk, hanem egy megfelelő típusú empty értéket. Ha mindenképpen szükség van a nullok vizsgálatára, arra is van mód, lásd a osztály referenciát és a példaprogramokat.
Tegyük fel, hogy van egy '111111112222222233333333' számlaszámú, 'HUF' devizanemű sorunk a táblában, ezt a
rowentity:=tableentity:find({'111111112222222233333333','HUF'})metódushívással tudjuk beolvasni. Ha mégsincs sor a megadott számlaszámmal, akkor tableentity:find NIL-t ad.
A tds scripttől függetlenül a tableentitynek mindig van egy általános select metódusa, ami az alaptábla összes sorát tartalmazó rowsetet ad, és egy find metódusa, ami a primary key alapján kiválaszt egyetlen sort. Lehetnek azonban más select metódusok is, ha definiálunk ilyeneket a tds-ben. A jelen példában a
rowset:=tableentity:select_kk({stod("20000101")})metódushívás kigyűjti azokat a sorokat, amikben a könyvelés dátuma régebbi, mint 2000-01-01, vagy egyáltalán nem volt rajta könyvelés, és ezért a dátum null. Az interfész a tds-ben megadott where záradékból SQL where záradékot készít úgy, hogy a select metódus array paraméterében felsorolt értékeket behelyettesíti az :1, :2,... szimbólumok helyére. A tds-ben bármit megadhatunk a where záradék helyén, amit az SQL elfogad a ,,where" és az ,,order by" kulcsszavak között. A tds-beli order záradékból SQL order by záradék lesz. Az interfész az order zárójelei közötti szöveget egyszerűen a generált SQL parancs ,,order by" kulcsszava után írja. A gyakorlatban általában a zárójelek között felsoroljuk az oszlopokat, amik szerint rendezni akarunk, esetleg az asc/desc módosítással kiegészítve.
Rátérve a módosítások tárgyalására mégegyszer megjegyezzük, hogy nem minden tábla módosítható. Ha az alaptábla egy view, vagy több táblából képzett join, akkor a módosítási kísérlet valószínűleg hibát eredményez. Rakjunk most be egy új sort a táblába:
rowentity:=tableentity:instance rowentity:szamlaszam:="XXXXXXXXYYYYYYYYZZZZZZZZ" rowentity:devnem:="EUR" rowentity:egyenleg:=100000 rowentity:insert con:sqlcommitA tableentity objektum instance metódusa gyárt nekünk egy üres rowentityt, amit feltöltünk adatokkal. A rowentity insert metódusa generál egy insert into utasítást, amivel kiírja a rekordot. A kiírást a commit véglegesíti. Tegyük fel, hogy ugyanezt a rekordot módosítani kell:
rowentity:=tableentity:find({'XXXXXXXXYYYYYYYYZZZZZZZZ','EUR'}) rowentity:egyenleg+=1000 rowentity:update con:sqlcommitMost az interfész generálni fog egy ilyen utasítást
update "KONTO"."PROBA" set "OSSZEG"=101000 where "SZAMLA"='XXXXXXXXYYYYYYYYZZZZZZZZ' and "DEVNEM"='EUR'amivel módosul a rekord. Rendkívül fontos észrevétel, hogy a rowentity objektum, a primary key oszlopok egyezése alapján találja meg a hozzá tartozó rekordot. Mi történik akkor a következők után?
rowentity:szamlaszam:="valami más érték" //ROSSZ! rowentity:update //ROSSZ!Így nem az eredeti rekord módosul, hanem a szerver minden olyan rekordot módosít, aminek a számlaszáma a "valami más érték". Lehet, hogy semmi sem módosul, lehet, hogy egy olyan rekord, amire nem számítottunk. A tanulság, hogy a primary key-t alkotó oszlopokat nem módosíthatjuk közvetlenül. Szerencsére a gyakorlatban erre ritkán van szükség. Ha mégis, akkor ezt csináljuk:
rowentity:=tableentity:find({'XXXXXXXXYYYYYYYYZZZZZZZZ','EUR'}) rowentity:delete rowentity:szamlaszam:="valami más érték" rowentity:insert con:sqlcommitMivel a két művelet egy tranzakcióban van, nem fenyeget, hogy csak az egyik hajtódik végre, a másik pedig nem. A példa alapján világos: az alkalmazásnak létfontosságú, hogy a primary key egyedisége megmaradjon, így az adatbázisrekordok és a program rowentity objektumai közötti megfeleltetés ne sérüljön.
A rowentity lényeges tulajdonsága, hogy igazi objektum, nem pedig csak holmi rekordpointer vagy kurzor. Az objektum az adatait saját memóriabufferben tárolja, ami nem szűnik meg attól, hogy végrehajtunk egy újabb rowset:next-et, rowset:close-t, vagy akár con:rollback-et. A program változóiban tárolhatunk egyidejűleg akárhány rowentity objektumot.
Ebben az alfejezetben egy olyan példát elemzünk, amiben a tableentity alaptáblája több elemi tábla összekapcsolásával keletkező join. Az ilyen tableentity objektumoknak is vannak módosító metódusai (create, drop, zap, insert, delete, update), ám ezek meghívása nagy valószínűséggel hibát okoz, erre többet nem is térünk ki. A példa az sql2/test/basicdemo/tds/probaszerencse.tds-ből való.
name: probaszerencse version: "2,2006-07-18" table: konto.proba=p table: konto.szerencse=q join: p left join q on szamlaszam=qszamlaszam column: szamlaszam C24 x=p.szamla column: devnem C3 column: nev C30 x=megnevezes column: egyenleg N17.2 x=osszeg column: tulmenflag L column: konyvkelt D column: megjegyzes M column: kiegdata C20 colref: qszamlaszam x=q.szamla primkey: szamlaszam,devnem
Előzőleg a konto.proba nevű táblával dolgoztunk, most ezt kiegészítjük a konto.szerencse táblával. A két tábla a szamla nevű oszlopon keresztül kapcsolódik. A szerencse nevű táblából a kiegdata oszlopot akarjuk hozzávenni a tableentityhez (nem túl sok, de most nem az a lényeg, hanem maga az összekapcsolás).
Először is megfigyeljük, hogy a tableentity objektum gyártó függvény most a probaszerencse névtérbe van helyezve, tehát így lehet meghívni:
tableentity:=probaszerencse.tableEntityNew(con)Látjuk, hogy a tds-ben két table bekezdés is van. Ezek megadják az alapesetben használt táblákat, és azok alias neveit (p és q). A script más részeiben a táblákra kizárólag az alias nevekkel hivatkozunk.
Tudjuk, hogy szükség esetén a tds-ben meghatározott táblákat helyettesíthetjük más táblákkal, például:
tablist:={"masik.proba","masik.szerencse"} tableentity:=probaszerencse.tableEntityNew(con,tablist)A helyettesítendő táblákat utólag is megadhatjuk:
tableentity:=probaszerencse.tableEntityNew(con) tableentity:tablist:={"masik.proba","masik.szerencse"}A két módszer ugyanarra az eredményre vezet.
A table definition script (tds) join bekezdésében határozzuk meg, hogyan legyen a két tábla összekapcsolva. Ha ugyanezt a joint közönséges SQL-ben akarnánk megcsinálni, pl. az sqlplus-ban, akkor valami ilyesmit írnánk (rövidítésekkel):
select p.szamla, devnem, ... kiegdata from konto.proba p left join konto.szerencse q on p.szamla=q.szamla order by p.szamla,devnemKoncentráljunk a from záradékra. A tds-beli join bekezdés úgy vezethető le az SQL from záradékból, hogy a redundáns információt kihagyjuk. A konto.proba p helyett a tds joinban csak annyit írunk: p, hiszen a table bekezdésből már tudjuk, hogy a p alias név a konto.proba táblát jelenti. A p.szamla helyett azt írjuk szamlaszam, a q.szamla helyett pedig qszamlaszam, mert a column és colref bekezdések meghatározzák, hogy ezek a nevek pontosan melyik oszlopot jelentik. Látható a column bekezdések kettős szerepe. Egyrészt metódusneveket jelentenek, másrészt oszlop alias neveket hoznak létre, amiket a tds scriptben használunk. A colref bekezdéseknek csak oszlop alias szerepe van. Foglaljuk össze a table és join bekezdésekkel kapcsolatos tudnivalókat:
1) A table bekezdések sorolják fel a tableentityben szereplő elemi táblákat. Minden táblához rendelünk egy alias nevet.
2) A join bekezdés opcionális (ha csak egy tábla van, akkor a joinnak nem is volna értelme). Ha nincs join, akkor a tableentity alaptáblája az elemi táblák Descartes-féle szorzata, vagyis ilyenkor az interfész olyan SQL select parancsot generál, aminek a from záradékában egyszerűen fel vannak sorolva a táblák.
3) Ha van join bekezdés, akkor oda egy SQL select from záradékot írunk azzal az eltéréssel, hogy a táblanevek helyett tábla aliast, az oszlopnevek helyett pedig mindenhol oszlop aliast írunk.
Az a fajta from záradék, amit itt használunk az SQL92 szabvánnyal került be az SQL-be. Korábban csak az ún. inner join volt ismert (a where záradékban). Az újabb szabvány kiegészült az outer joinnal, és a különféle joinok összes variációinak egységes értelmezésével, de ezeket a from záradékba kell írni. Sajnos ebben a dokumentációban nincs hely az SQL mélyebb ismertetésére.
Folytassuk az oszlopokkal. Az első column bekezdésben ezt látjuk:
column: szamlaszam C24 x=p.szamla
Szó volt már a szamlaszam név szerepéről. A tableentityből származó rowentity (sor)objektumoknak lesz egy ilyen nevű attribútuma, egyúttal bevezet egy oszlop aliast, amivel a tds-ben bárhol (join, where, order) hivatkozni tudunk az oszlopra.
A C24 típus értelmezése: Nem érdekel minket, hogy az adott oszlop SQL adattípusa pontosan micsoda. Akármi is az SQL adattípus, a mi programunk C24-re konvertálva kéri az adatot a szervertől. Mármost a tényleges típus és a szerver tudása dönti el, hogy ez az esetleges konverzió értelmes-e, lehetséges-e egyáltalán. Ha nem értelmes, vagy nem lehetséges, akkor az az alkalmazás logikai, vagy közvetlen futási hibáját fogja okozni.
A x=p.szamla kiegészítő adat azt mondja: A szamlaszam metódusnévhez és oszlop aliashoz a p alias névvel azonosított tábla szamla oszlopa tartozik. Ha ez nem volna megadva, akkor az interfész és az adatbázis szerver szamlaszam nevű oszlopot keresne, és hibához vezetne, ha az adott oszlop nem pontosan egy táblában volna megtalálható (esetünkben két táblában is van ilyen).
A column adatok sorrendjéről tudni kell, hogy a kettőspont utáni első szóközökkel határolt szó kötelezően az alias név, a második szó kötelezően a típus, a további adatok opcionálisak, és sorrendjük tetszőleges.
A colref bekezdésekkel olyan oszlopokhoz készítünk oszlop aliast, amiket nem akarunk bevenni a tableentity alaptáblájába, de hivatkoznunk kell rá a join-ban, where-ben, vagy order-ben. A colref szintaktikája lényegében ugyanaz, mint a columné, csak kimarad belőle a típusmeghatározás.
A további összetevőket, mint a primkey, különféle selectek, már ismerjük. Indexdefiníciókat helyezni egy többtáblás tds-be értelmetlen, mivel soha nem fogjuk a táblát kreálni. A lekérdezések ugyanúgy működnek, mint az egytáblás esetben, például:
tableentity:=probaszerencse.tableEntityNew(con) rowset:=tableentity:select while( (r:=rowset:next)!=NIL ) ? r:szamlaszam,r:kiegdata end rowset:close
Aszerint, hogy melyik névtérből hívjuk meg az sqlconnectionNew() objektumgyártó függvényt, Oracle vagy Postgres adatbáziskapcsolathoz jutunk:
con_ora:=sql2.oracle.sqlconnectionNew(connect_string) con_pg:=sql2.postgres.sqlconnectionNew(connect_string)A connect_string paraméter opcionális. Oracle esetében a connect string tartalma a megszokott user@dbsid/password alakú, ahogy azt pl. az sqlplus is várja. Postgres esetén a connect string tartalma pontosan az lehet, mint amit a PQconnectdb függvény (a libpq klienskönyvtárból) elfogad. Ha a connect string nincs egyáltalán megadva, akkor a program az ORACLE_CONNECT, illetve POSTGRES_CONNECT környezeti változók tartalmát használja bejelentkezéshez, például:
export ORACLE_CONNECT=scott@database/tiger export POSTGRES_CONNECT="host=HH dbname=DD user=UU password=PP"A sqlconnection objektum létrehozásakor azonnal megtörténik a bejelentkezés. Az attribútum/metódusok:
Az sqlquery objektumok a con:sqlqueryNew(select_stmt) objektumgyártó metódushívással keletkeznek. A metódusok:
A connection osztály sqlsequencenew metódusával kapunk új sqlsequence objektumot: con:sqlsequenceNew(name), ahol name az adatbázisbeli sequence objektum neve. Ne feledjük, az objektumgyártó csak programobjektumot készít, az adatbázisban van, vagy nincs hozzá tartozó sequence objektum. Az attribútum/metódusok:
Az absztrakt tableentity osztály a közös őse minden konkrét tableentitynek. Az alábbi felsorolás csak az alkalmazási programok számára érdekes attribútumokat/metódusokat tartalmaza.
t:tablist:={"tab1=alias1","tab2=alias2",...}ahol tab1,... az adattáblák valódi (minősített) neve, alias1,... pedig a táblák hivatkozási neve. A tableentity objektum többi részében, ahol az szükséges (pl. amikor meg kell jelölnünk, hogy egy oszlop melyik táblából való) mindig a tábla alias nevét használjuk. Ennek eredményeként egy tableentity osztály invariáns a benne szereplő táblákra, azaz a tablist attribútum átírásával ugyanaz az objektum és adatstruktúra más fizikai táblákra is alkalmazható.
Az alias nevek alkalmazása nem kötelező, nélkülük azonban elvész az invariancia. Ha a tableentity csak egyetlen táblát tartalmaz, akkor a tableentity definícióban sehol sincs szükség táblahivatkozásra, hiszen mindig a tablist egyetlen eleméről lehet csak szó, ilyenkor sem szükséges alias nevet használni.
Az absztrakt tableentity osztályt oszlopokkal és select metódusokkal bővítve kapjuk a konkrét tableentity osztályokat. Ezek kódját a gyakorlatban nem kézzel írjuk, hanem XML leírásból, vagy tds scriptből programmal generáljuk. A generált kód a tableentityre jellemző névtérbe helyezi az objektumgyártó függvényt, amit így hívhatunk meg:
tableentity:=multi.level.namespace.tableentityNew(con)
A __method__ alakú metódusokat az alkalmazási programok közvetlenül nem használják, csak a tanulság kedvéért szerepelnek az ismertetésben.
p left join q on szamlaszam=qszamlaszam
A find és select metódusokkal lehet a tábla kiválasztott sorait lockolni.
Megjegyzés: Postgresben nincs timeout támogatás, ezért minden lock végtelen ideig, vagy a deadlock detektálásáig vár.
szamlaszam like :1Legyen az opcionális selectünk neve select_like, akkor a metódust így hívhatjuk:
rowset:=t:select_like({'1111111122222222%'})Ez egy olyan rowsetet ad, amiben a számlaszám 1-16 jegyei a megadott mintához illeszkednek.
A select* metódusok első paramétere egy array, amiben a where záradék sorszámozott paraméterei helyére helyettesítendő (bind) értékek vannak felsorolva (az automatikus selectnél ezt a paramétert üresen kell hagyni).
A select* metódusok második paraméterével a lockolás szabályozható:
Megjegyzés: Postgresben nincs timeout támogatás, ezért minden lock végtelen ideig, vagy a deadlock detektálásáig vár.
A rowset objektumokat az alkalmazás sosem közvetlenül hozza létre, hanem a tableentityk select metódusainak értékeként kapja. Egy rowset objektum egy feldolgozás alatt álló SQL select utasítást képvisel. A select utasítással (rowsettel) mindössze két dolgot lehet csinálni:
Az adatbázisrekord, vagy az SQL select eredménytábla egy sorának programbeli képe a rowentity objektum. Háromféleképpen lehet rowentity objektumhoz jutni:
A tableentity minden oszlopához létezik a rowentity objektumnak egy azonos nevű metódusa, amivel az adott mező értéke lekérdezhető és módosítható. A rowentity objektumok mind saját adatbufferrel rendelkeznek, emiatt egyszerre több (akárhány) azonos típusú rowentity objektumunk lehet, amikkel egymástól függetlenül végezhetünk műveleteket.
Minden rowentity rendelkezik az alábbi metódusokkal:
E metódusok a tableentity azonos nevű metódusaihoz továbbítva hajtódnak végre, ezért a részletesebb leírást lásd a tableentity osztálynál.
A columnref objektumok nem kerülnek bele a tableentitybe, csak a tableentitykben levő SQL utasítások elkészítésekor kapnak segédszerepet azáltal, hogy tárolják egyes közvetlenül nem használt oszlopok adatbázisbeli azonosítóját.
Columndef objektumokkal az alkalmazási programokban ritkán találkozunk, mert az oszlopok általában csak a tableentityk belső működéséhez kellenek. A tableentity columndef objektumokkal való feltöltését sem közvetlenül végezzük, mert ezt az XML definíció alapján generált kód teszi. Ha azonban vizsgálni kell, hogy a szervertől kapott eredeti érték null, vagy nem null, mégiscsak a columndef objektumhoz kell fordulnunk. A columndef az előző columnref osztály leszármazottja, tehát rendelkezik az előbbi metódusokkal. A további metódusok:
Ha egy létező Oracle adatbázis egy oszlopát le akarjuk képezni Clipper típusra, akkor választanunk kell a Cw, Nw[.d], D, L, M típusok közül. A konverziót az adatbázisszerver fogja végezni, ha lehetséges, ha pedig nem lehetséges, akkor runtime errort kapunk. Az ,,értelmes" konverziók általában lehetségesek.
A típusleképezés szabályai Oracle esetében:
Cw --> char(w) ha w<=8 Cw --> varchar(w) ha w>8 M --> blob Nw --> number(w) Nw,d --> number(w,d) Nw.d --> number(w,d) L --> number(1) a boolean-t nem ismeri D --> date
A típusleképezés szabályai Postgres esetében:
Cw --> char(w) ha w<=8 Cw --> varchar(w) ha w>8 M --> bytea Nw --> numeric(w) Nw,d --> numeric(w,d) Nw.d --> numeric(w,d) L --> boolean D --> date
Ez egyúttal behatárolja, hogy a tableentity interfésszel milyen struktúrájú adattáblák hozhatók létre.
Index adatok tárolására használjuk az indexdef objektumokat.
Nézzük ezt a (pszeudo)programot:
function konyveles1() begin könyvelgetünk különféle számlákra if( sikertelen ) break(konyverrorNew()) end tovább könyvelgetünk különféle számlákra if( sikertelen ) break(konyverrorNew()) end //minden rendben commit recover e <konyverror> //a hibát elkapjuk rollback endMinden príma, szépen lekezeltük a lehetséges hibákat. Tegyük fel, hogy van egy hasonló stílusban megírt konyveles2() függvényünk is, és a kétféle tranzakció kombinálásával létre akarunk hozni egy harmadik, összetett (még összetettebb) tranzakciót.
function osszetett_konyveles() //HIBÁS! begin konyveles1() konyveles2() commit recover e <konyverror> rollback endRá kell jönnünk, hogy az összetett tranzakció fenti implementációja rossz, méghozzá a résztranzakciókban levő commitok miatt. Át kell szerveznünk a programot. Először megírjuk az alg_konyveles1() függvényt, ami a könyvelés algoritmusát (az üzleti logikát) tartalmazza.
function alg_konyveles1() könyvelgetünk különféle számlákra if( sikertelen ) break(konyverrorNew()) end tovább könyvelgetünk különféle számlákra if( sikertelen ) break(konyverrorNew()) end //minden rendben (mégsem commitolunk)Azután az algoritmust tranzakciós keretek közé tesszük:
function trn_konyveles1() begin alg_konyveles1() commit recover e <konyverror> rollback endAzaz szétválasztottuk az algoritmust és a tranzakciókezelést. Ha ezt a módszert alkalmazzuk az összetett könyvelésünk leprogramozására, a következő eredményhez jutunk.
function alg_osszetett_konyveles() alg_konyveles1() alg_konyveles2() function trn_osszetett_konyveles() begin alg_osszetett_konyveles() commit recover e <konyverror> rollback endTehát minden tranzakciónak külön meg kell írni az algoritmikus részét és a tranzakciókezelő keretét. A tranzakciókat végrehajtó programok nyilván a trn_* függvényeket fogják hívni. Valaki talán azt gondolja, hogy ez plusz munkával jár, valójában ennek éppen az ellenkezője igaz, így ugyanis újra felhasználható kódot kapunk.
Azt is érdemes megfontolni, hogy az előző példákban a commit/rollback helyén nem feltétlenül csak egyetlen adatbáziskapcsolatra kiadott commit/rollback parancs áll. Az alkalmazás tudja, hogy az általa aktivizált tranzakció milyen adatbázisokat használ, és minden adatbáziskapcsolatra kiadja a commit/rollback parancsot. Ezzel a technikával áttekinthető módon lehet kezelni a több adatbázisra kiterjedő elosztott tranzakciókat.
Az Oracle és a Postgres is támogatja az idézett azonosítókat. Ha Oracleben egy azonosító ilyen alakú
"KONTO"."SZAMLASZAM" "KONTO"."UPDATE"akkor azokat az Oracle nem tekinti kulcsszónak. Idézőjelek nélkül nem használhatnánk update nevű mezőt, mert az ütközik az update kulcsszóval. Ha az idézett azonosítókat csupa nagybetűvel írjuk, akkor az azonosító case insensitive abban az értelemben, hogy
"KONTO"."SZAMLASZAM" <=> Konto.Szamlaszamegyenértékű. Ha az idézett azonosítóban nagybetűn kívül más is van, akkor az azonosító case sensitive lesz. A Postgres ugyanígy nem tekinti kulcsszónak az idézett azonosítókat, az Oraclelel ellentétben azonban a csupa kisbetűs írásmód eredményez case insensitive azonosítókat, azaz
"konto"."szamlaszam" <=> Konto.SzamlaszamAz SQL2 interfész alkalmazkodik ehhez. Mindig idézett azonosítókat használ, ezáltal nincs szükség az SQL kulcsszavak kerülgetésére. Oracle esetében az idézett azonosítók nagybetűsek, Postgresnél kisbetűsek. Ez azzal az előnnyel jár, hogy az általunk létrehozott adatbázisokban case insensitive azonosítók lesznek, így az sqlplus-ban vagy psql-ben érdektelen a kis/nagybetűk használata, azaz a szokott módon dolgozhatunk. Persze egy kulcsszóval egyező azonosító esetén interaktív módban is használnunk kell az idézőjeleket. Hátrány, hogy a mások által létrehozott case sensitive azonosítókat nem lehet elérni az SQL2 interfészen keresztül.
Az Oracle automatikusan indítja a tranzakciókat. Egyáltalán nincs begin transaction (vagy hasonló) utasítás, az Oracle mindig tranzakció közben van, a tranzakció a következő commit/rollback utasításig tart, ami után automatikusan indul a következő tranzakció.
A Postgres is tudott így működni a 7.4 verzió előtt, ez volt az ún. autocommit off üzemmód. A 7.4-es verzióban azonban megszűnt az autocommit utasítás, ezért az SQL2 interfészben kénytelenek vagyunk minden commit/rollback után begin transaction utasítást küldeni a szervernek, ezzel pótolva az autocommitot.
Vannak esetek, amikor a szerver saját hatáskörben befejezettnek nyilvánít egy tranzakciót:
Az Oracle a DDL utasítások (create table, drop table, ...) előtt és után (belsőleg) automatikusan végrehajt egy commitot. Az SQL2 interfész az egységes működés érdekében a Postgres esetében is kiadja ezeket a commitokat.
A Postgres bármilyen hiba esetén azonnal és elkerülhetetlenül abortálja a tranzakciót, és végrehajt (belsőleg) egy rollbacket. Mivel ilyenkor nincs módunkban automatikusan indítani a következő tranzakciót, előfordulhat (ha nem vesszük észre a hibát), hogy a következő utasításaink tranzakción kívülre kerülnek, ezáltal mind hibásak lesznek. Tulajdonképpen ez előnyös, mert így legalább nem lehet elsiklani a hiba felett, úgy gondolom, éppen ennek érdekében szűnt meg az autocommit. Az Oracle ezzel szemben úgy működik, mintha minden SQL utasítás előtt volna egy implicit savepoint, és hiba esetén eddig a savepointig rollbackelne. A Oracle tehát visszaáll a hibás utasítást megelőző állapotra, és az alkalmazási programra bízza, hogy rollbackeli-e a tranzakció egészét. Ezt a különbséget az SQL2 interfész nem egyenlíti ki. Az alkalmazási programokat a Postgres szigorúbb feltételeihez alkalmazkodva kell megírni.
Az indexszekvenciális rekordkezelőkhöz szokott programozó hajlamos arra, hogy az SQL select-fetch utasításokat egy iterátor módján képzelje el: Az első fetch 1 millisec alatt behozza az első rekordot, a következő fetch a másodikat, stb. Ez az, amiről szó sincs.
Az SQL select még az első fetch előtt összeállítja, lerendezi a teljes (!) eredménytáblát még akkor is, ha az GB-os nagyságrendű. Emiatt lehet, hogy az első rekord megérkezéséig percek (órák) is eltelnek, míg a szerver egyre csak dolgozik az eredménytáblán. Az alkalmazóknak tehát csínján kell bánni a nagy eredménytáblához vezető lekérdezésekkel, mivel azok igencsak megviselik a szervert.
Megfigyelésem szerint az Oracle az eredménytáblát a szerveren tartja, és csak a fetchelt rekordokat küldi át a kliensre.
A Postgres két módszert is kínál a select utasításra. A közvetlen select utasítás az egész eredménytáblát előre átküldi a kliensgépre, ebből a kliens sor- és oszlopindex alapján (mintha csak egy mátrixot címezne) éri el az adatokat. Mondani sem kell, milyen nehézségekkel jár egy nagy eredménytábla kezelése. Emellett a Posgresben kurzort is definiálhatunk:
declare crs_id cursor for select ... fetch forward 1 in crs_id ... fetch forward 1 in crs_idA kurzoros select csak a ténylegesen fetchelt rekordokat küldi át a kliensre, ezért hatékonyabban kezeli a nagy eredménytáblákat, viszont funkcionalitásában korlátozott, pl. nem működik vele a lockolás (select ... for update). Az SQL2 interfész aszerint választ a közvetlen és kurzoros select között, hogy az alkalmazás akar-e lockolni. Ha lock van előírva, akkor közvetlen, egyébként kurzoros lesz a select.
Tovább bonyolódik a helyzet a tranzakcióhatárok miatt. Az Oracle képes a tranzakcióhatárokon átnyúló fetchelésre. Ez a tulajdonság használható a következő típusú programnál:
select 100 ezer darab számla while(ciklus 100 ezer darab számlára) fetch kamatszámítás egy számlán if(sikeres) commit else rollback end endA Postgres közvetlen select utasítása elvileg szintén tudná ezt, a gyakorlatban viszont nagyon megkottyan neki a 100 ezres eredménytábla kliensen történő kezelése. A kurzoros select (új fejleményként) félig képes a tranzakcióhatáron átnyúló fetchelésre, nevezetesen a commiton átmegy, a rollback viszont menthetetlenül lezárja. Nem könnyű tehát összhangba hozni az Oracle és a Postgres fetchelésre vonatkozó elképzeléseit.
Nézetem szerint azonban egyáltalán nem kell belemenni ebbe a zsákutcába. A tranzakcióhatárokon átnyúló fetchelés fogalmilag zagyva dolog, jobb elkerülni, az SQL2 interfész ezért egységesen megakadályozza. Az előző programot egyszerűen implementálhatjuk két adatbáziskapcsolattal: Az egyiket használjuk a 100 ezer darab számla felsorolásához, a másikat a kamatszámításhoz. Így a kamatszámítás commit/rollback utasításai nincsenek hatással a számlák bejárására. Ez tiszta ügy.
Az itt leírt eljárással Debian Sarge, Ubuntu 4.10, SuSE 9.0, SuSE 10.0 32-bites Linuxokra tudtam Oracle-t installálni.
Ellenőrizzük, hogy az alábbi csomagok installálva legyenek a rendszerünkön:
gcc, make, binutils, motif, lesstif, rpm
A hiányzó csomagokat installáljuk. A libmotif3 csomag Ubuntun a multiverse-ből installálható. SuSE-n csak libmotif2 van, lesstif pedig csak forrásból volna beszerezhető. Mindenesetre nem értem, hogy minek lesstif, ha van motif, és minek egyáltalán akármelyik, amikor a GUI megjelenítését Jáva végzi. SuSE 9.0 esetében a linker jelezni fogja a motif hiányát, de attól a rendszer még használható lesz. SuSE 10.0-án a telepítő hiányolni fogja a gcc 2.96-ot, de az installáció a gcc 4.x-szel is működni fog. Kezdetben (a runInstaller elindításáig) root-ként dolgozunk.
Létrehozzuk az alábbi csoportokat és felhasználókat:
id nobody # léteznie kell groupadd oinstall groupadd dba useradd -g oinstall -G dba -d /opt/oracle oracle passwd oracle #jelszót adunk az oracle usernek
A továbbiakban az installációt az oracle felhasználó nevében csináljuk.
Elhozzuk az Oracle-től a ship.db.cpio.gz csomagot és alkalmas helyen kibontjuk.
gunzip ship.db.cpio.gz cpio -idmv < ship.db.cpio
Létrehozzuk az Oracle base directoryt:
mkdir /opt/oracle/base
Beírjuk /etc/profile-ba:
export ORACLE_BASE=/opt/oracle/base export ORACLE_HOME=$ORACLE_BASE/product/10g export TNS_ADMIN=$ORACLE_HOME/network/admin export ORA_NLS33=$ORACLE_HOME/nls/data export NLS_LANG=AMERICAN_AMERICA.WE8ISO8859P1 export NLS_DATE_FORMAT="YYYY-MM-DD" export ORACLE_SID=test PATH=$PATH:$ORACLE_HOME/bin LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLE_HOME/lib export PATH export LD_LIBRARY_PATH
Beírjuk /etc/sysctl.conf-ba:
kernel.shmall = 2097152 kernel.shmmax = 2147483648 kernel.shmmni = 4096 kernel.sem = 250 32000 100 128 fs.file-max = 65536 net.ipv4.ip_local_port_range = 1024 65000
Utána futtatjuk: /sbin/sysctl -p
Beírjuk /etc/security/limits.conf-ba:
* soft nproc 2047 * hard nproc 16384 * soft nofile 1024 * hard nofile 65536
Beírjuk /etc/pam.d/login-ba:
session required /lib/security/pam_limits.so
Megcsináljuk a következő symlinkeket:
ln -s /usr/bin/awk /bin/awk ln -s /usr/bin/rpm /bin/rpm ln -s /usr/bin/basename /bin/basename ln -s /etc /etc/rc.d
Létrehozzuk a /etc/redhat-release filét a következő tartalommal:
Red Hat Linux release 2.1 (drupal)
Eddig root-ként dolgoztunk, most váltunk oracle-re, és oracle-ként elindítjuk a runInstaller-t:
cd .../Disk1 ./runInstaller
Itt hosszú ideig dolgozik, fordítgat, eközben adódhatnak hibák, ezek némelyike figyelmen kívül hagyható.
Az installer felszólít a root.sh script futtatására, megtesszük neki. Ezután vár 10 percet a cssd démon elindulására. Vagy várunk 10 percet, vagy küldünk egy HUP-ot az init-nek, amire az újraolvassa a konfigurációs filéit.
A root.sh script beír a /etc/inittab végére egy ilyen sort:
h2:35:respawn:/etc/init.d/init.cssd run >/dev/null 2>&1 </dev/null
A cssd démon az ASM (Automatic Storage Manager) modullal tart fenn valamilyen kommunikációt. Nálunk nincs ASM, tehát az inittab-ba írt sort kiszedhetjük, ha az installer túljutott rajta (többet nem kell).
Beírjuk /etc/oratab-ba:
test:/opt/oracle/base/product/10g:Y
Csinálunk egy /etc/init.d/oracle filét a következő tartalommal:
#!/bin/bash # # Run-level Startup script for the Oracle Instance and Listener ORA_HOME="/opt/oracle/base/product/10g" ORA_OWNR="oracle" # if the executables do not exist -- display error if [ ! -f $ORA_HOME/bin/dbstart -o ! -d $ORA_HOME ] then echo "Oracle startup: cannot start" exit 1 fi # depending on parameter -- startup, shutdown, restart # of the instance and listener or usage display case "$1" in start) # Oracle listener and instance startup echo -n "Starting Oracle: " /sbin/sysctl -p su - $ORA_OWNR -c "$ORA_HOME/bin/lsnrctl start" su - $ORA_OWNR -c $ORA_HOME/bin/dbstart touch /var/lock/subsys/oracle echo "OK" ;; stop) # Oracle listener and instance shutdown echo -n "Shutdown Oracle: " su - $ORA_OWNR -c "$ORA_HOME/bin/lsnrctl stop" su - $ORA_OWNR -c $ORA_HOME/bin/dbshut rm -f /var/lock/subsys/oracle echo "OK" ;; reload|restart) $0 stop $0 start ;; *) echo "Usage: $0 start|stop|restart|reload" exit 1 esac exit 0
Ezután a szokásos módon vezérelhető a szerver:
/etc/init.d/oracle start|stop|restart
A szerveren az $ORACLE_HOME/network/admin/listener.ora filében van leírva a listenerek paraméterezése. Az installáció után ez általában ,,magától" jó.
A kliens programok számára az $ORACLE_HOME/network/admin/sqlnet.ora filében fel vannak sorolva a protokollok, amikkel hálózati szolgáltatásokat tudnak keresni. Nálam ez így néz ki:
# SQLNET.ORA Network Configuration File NAMES.DEFAULT_DOMAIN = comfirm.x NAMES.DIRECTORY_PATH= (TNSNAMES, ONAMES, HOSTNAME)Tehát elsősorban a tnsnames filét nézzük, ha egy adatbázist keresünk a hálózaton. A $ORACLE_HOME/network/admin/tnsnames.ora filé egy kis adatbázist tartalmaz a hálózaton található Oracle szolgáltatásokról. Ahhoz, hogy az Oracle hálózati kliens megtalálja a ,,test" adatbázis szolgáltatást (ez volna a SID?), el kell helyezni a tnsnames.ora filébe egy ilyen bekezdést:
test.COMFIRM.X = (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = 1g.comfirm.x)(PORT = 1521)) ) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = test) ) )A domain, host és service neveket a konkrét esetnek megfelelően kell beírni.
A jelen demóban az adatbázis objektumok a konto schema-ban jönnek létre. Legjobb ezt a schema-t mindjárt a telepítés után létrehozni.
Elindítjuk (system-ként) az sqlplus-t, és végrehajtjuk benne a következőket:
create tablespace konto logging datafile '/opt/oracle/base/oradata/test/konto.dbf' size 8M reuse autoextend on next 4M maxsize unlimited extent management local autoallocate segment space management auto; create user konto identified by konto default tablespace konto quota 100000M on konto; grant connect to konto; quit;
Ezután az Oracle adatbázis alkalmas a demó programok kiszolgálására.
Ha egy gépre Oracle szervert installálunk, azon lesz Oracle kliens is. Ha azonban csak a kliensre van szükségünk, akkor nem érdemes 100-szoros munkával szervert telepíteni.
Letöltjük az Oracle-től az Oracle Instant Client csomagokat. A szoftver 4 darab zip filéből áll (egyes platformokra rpm is van). A 4 közül a CCC-hez csak két összetevőre van szükség, a basic-re és az sqlplus-ra. A zip-eket kibontjuk, és a tartalmukat betesszük a /opt/oracle/instantclient (vagy egy tetszés szerint választott) directoryba.
Megcsináljuk a /opt/oracle/instantclient/network/admin directoryt, és abban elhelyezzük az alábbi két filét:
A sqlnet.ora filé tartalma:
# SQLNET.ORA Network Configuration File NAMES.DEFAULT_DOMAIN = comfirm.x NAMES.DIRECTORY_PATH= (TNSNAMES)
A tnsnames.ora ilyen bekezdésekből áll:
test.COMFIRM.X = (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = 1g.comfirm.x)(PORT = 1521)) ) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = test) ) )Természetesen a domain, host, service neveket a saját adatainkkal helyettesítjük.
A /etc/profile-ba beírjuk:
export ORACLE_HOME=/opt/oracle/instantclient export PATH=$PATH:$ORACLE_HOME export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLE_HOME export TNS_ADMIN=$ORACLE_HOME/network/admin export NLS_LANG=AMERICAN_AMERICA.WE8ISO8859P1 #export NLS_LANG=HUNGARIAN_HUNGARY.EE8ISO8859P2 export NLS_DATE_FORMAT="YYYY-MM-DD"Ezután újonnan bejelentkezve fut az sqlplus, és futnak a CCC programok.
Ha az sqlnet.ora és tnsnames.ora filék elhelyezkedése az ORACLE_HOME-hoz képest olyan, mint a fenti példában, akkor a TNS_ADMIN változó megadása felesleges (de nem árt).
Az adatbázisok általában Latin-1 (WE8ISO8859P1) kódkészlettel jönnek létre, mert ez a default. Ha egy ilyen adatbázissal állunk kapcsolatban, de a kliensen Latin-2 (EE8ISO8859P2) kódkészlet van beállítva, akkor a szövegmezőkben elromlanak a 128 feletti byteok. (Ha kiírunk egy 128 feletti betűkből álló stringet, akkor a visszaolvasás a legtöbb helyen kérdőjelet ad.) Ezért a kliensen kötelezően ugyanazt a kódkészletet kell beállítani, mint ami a szerveren van, akkor is, ha így a szerver üzeneteit magyar helyett esetleg angolul kapjuk.
A 7.4.x változatban megszűnt a szerver oldali autocommit. Korábban a set autocommit off utasítás hatására egy commit/rollback után automatikusan indult a következő tranzakció, ami megint csak a következő commit/rollback-ig tartott. Ehelyett az interfész minden commit/rollback után küld a szervernek egy begin transaction utasítást. A 8.0.8 változat körül megjelentek a savepoint-ok. A 8.1.4-ben megjelent a kétfázisú commit (2PC) és az osztott rekordzár (select ... for share).
Adatbázis clusternek nevezzük az adatbázisok egy olyan halmazát, amik ugyanabban a PGDATA könyvtárban vannak, és ugyanaz a Postmaster folyamat kezeli őket. Csinálunk egy üres directoryt:
mkdir pgdata
Ebben végrehajtjuk:
initdb --no-locale -D `pwd`
Létrehozzuk pgdata-ban az alábbi scripteket. A pg-start script indítja az adatbázist:
#!/bin/bash pg_ctl start -D `pwd` -l PG_LOG
A pg-stop script leállítja az adatbázist:
#!/bin/bash pg_ctl stop -D `pwd` -l PG_LOG
Elindítjuk a Postgres-t (az előbbi indítóscripttel), utána elindítjuk psql-t.
psql template1
A psql-ben végrehajtjuk:
create database vermes;
Ezzel a saját home-omban csináltam egy privát adatbázis clustert (adatbázisok olyan halmazát, amit ugyanaz a Postmaster kezel), ebben a clusterben jelenleg egy adatbázis van (vermes), amibe azonosítás nélkül (mint lokális UNIX user) bármikor beléphetek, és amiben bármit csinálhatok.
Mivel a jelen demó minden adatbázisobjektumot a konto schema-ban hoz létre, legjobb ezt a schema-t mindjárt megcsinálni. A psql-ben végrehajtjuk:
create schema konto;
Ezután a Postgres adatbázis alkalmas a demó programok kiszolgálására.
1ComFirm BT.
2Azóta támogatva van az SQLite, MySQL és DB2 is.
3 Azt is megtehetjük, hogy ugyanabba a programba egyszerre belinkeljük az Oracle és Postgres interfészkönyvtárat. Ekkor csak egy programváltozatunk lesz, aminek parancssori paraméterrel, vagy környezeti változóval mondjuk meg, hogy melyik adatbázist használja. Az is lehet, hogy egy alkalmazás több adatbázist használ egyszerre, Oracle-t és Postgrest vegyesen.