April 24, 2017 | Author: Miroslav Novta | Category: N/A
Download Zlatan Kadragic - Razvoj Python Programa Koriscenjem SQL Alchemy Tehnologije...
UNIVERZITET U BEOGRADU FAKULTET ORGANIZACIONIH NAUKA
Diplomski rad Tema: Razvoj Python programa korišćenjem SQLAlchemy tehnologije
Mentor: dr Siniša Vlajić, doc. Student: Zlatan Kadragić 318/99
Beograd, decembar 2008.
Sadržaj 1. Uvod.................................................................................................................................................1 1.1 O radu........................................................................................................................................1 1.2 Struktura rada.............................................................................................................................1 1.3 Način pisanja korišćen u radu....................................................................................................2 2. Perzistencija......................................................................................................................................3 2.1 Šta je perzistencija.....................................................................................................................3 2.2 Relacione baze...........................................................................................................................3 2.2.1 Osnovno o SQL-u..............................................................................................................4 2.2.2 Perzistencija u objektno-orijentisanim aplikacijama sa relacionim bazama......................4 2.2.3 Neusaglašenost predstava (impedance mismatch).............................................................5 1. Problem granularnosti....................................................................................................6 2. Problem nasleđivanja.....................................................................................................6 3. Problem identiteta..........................................................................................................7 4. Problemi sa asocijacijama.............................................................................................8 5. Problem sa pristupom podacima....................................................................................9 2.3. Objektne baze...........................................................................................................................9 2.3.1 OSUBP protiv RSUBP.....................................................................................................10 3. Objektno-relaciono preslikavanje...................................................................................................12 3.1 Šta je objektno-relaciono preslikavanje?.................................................................................12 3.2 Zašto koristiti ORM?...............................................................................................................12 3.3 Uzori za preslikavanje.............................................................................................................14 3.3.1 Aktivni slog uzor..............................................................................................................14 3.3.1 Preslikač podataka............................................................................................................18 Kako preslikač radi.....................................................................................................18 Preslikavanje podataka u atribute domenskih objekata..............................................21 Preslikavanje zasnovano na metapodacima................................................................21 Kada koristiti Preslikač...............................................................................................21 Primer implementacije................................................................................................22 Preslikači u praksi.......................................................................................................27 4. Python i SQLAlchemy...................................................................................................................28 4.1 Šta je Python............................................................................................................................28 4.2 Filozofija Pythona....................................................................................................................29 4.2.1 Programer u središtu pažnje.............................................................................................29 4.2.2 Osnovni principi Pythona.................................................................................................30 Princip najmanjeg iznenađenja...................................................................................30 Princip isporuke sa baterijama....................................................................................31 Princip eksplicitnosti..................................................................................................31 4.3 Šta je SQLAlchemy.................................................................................................................32 5. Vodič kroz SQLAlchemy................................................................................................................33 5.1 Uvod u SQL Expression Language..........................................................................................33 5.1.1 Opisivanje veze s bazom..................................................................................................33 5.1.2 Opisivanje i stvaranje tabela............................................................................................33 5.1.3 Insert izrazi.......................................................................................................................35 5.1.4 Upiti.................................................................................................................................37 5.1.5 Operatori..........................................................................................................................38 5.1.6 Upotreba alijasa................................................................................................................40 5.1.7 Spajanje tabela.................................................................................................................41 5.1.8 Dinamički upiti................................................................................................................42 5.1.9 Funkcije............................................................................................................................43 5.1.10 Sortiranje, grupisanje, limitiranje, pomeranje................................................................44
5.1.11 Ažuriranje.......................................................................................................................44 5.1.12 Brisanje..........................................................................................................................45 5.2 Uvod u objektno-relaciono preslikavanje................................................................................45 5.2.1 Povezivanje tabele i klase................................................................................................45 5.2.2 Stvaranje sesije.................................................................................................................47 5.2.3 Čuvanje objekta................................................................................................................47 5.2.4 Upiti.................................................................................................................................50 5.4.5 Uspostavljanje 1-M veze..................................................................................................53 5.2.6 Rad na povezanim objektima i povratnim vezama..........................................................55 5.2.7 Upiti sa spajanjem............................................................................................................57 Relacioni operatori.....................................................................................................59 5.2.8 Brisanje............................................................................................................................62 5.2.9 Uspostavljanje M-M veze................................................................................................64 6. Napredna upotreba ORM paketa....................................................................................................68 6.1 Definisanje tabela....................................................................................................................68 6.1.1 Tipovi kolona...................................................................................................................68 6.1.2 Različito ime kolone u bazi i kolone u Table objektu......................................................68 6.1.3 Refleksija tabela...............................................................................................................69 6.1.4 Složeni ključevi................................................................................................................69 6.1.5 ON UPDATE i ON DELETE...........................................................................................70 6.1.6 Podrazumevane vrednosti kolona....................................................................................70 6.1.6 Ograničenja na kolonama.................................................................................................70 6.2 Podešavanje preslikavanja.......................................................................................................71 6.2.1 Podešavanje preslikavanja kolona...................................................................................71 6.2.2 Preslikavanje nasleđivanja klasa......................................................................................72 Nasleđivanje kroz spajanje tabela...............................................................................72 Nasleđivanje kroz zajedničku tabelu..........................................................................76 Nasleđivanje kroz odvojene tabele.............................................................................76 Nasleđivanje i relacije.................................................................................................78 6.2.3 Napredno preslikavanje relacija.......................................................................................79 Veza 1-1......................................................................................................................79 Agregacija...................................................................................................................79 Vezane liste.................................................................................................................81 Relacije nad upitima...................................................................................................82 Strategije učitavanja relacija.......................................................................................83 6.3 Korišćenje sesija......................................................................................................................85 6.3.1 Način rada sesije..............................................................................................................85 6.3.2 Ažuriranje i spajanje razdvojenih objekata......................................................................87 6.3.3 Izdvajanje objekta iz sesije i zatvaranje sesije.................................................................87 6.3.4 Ponovno učitavanje atributa objekta................................................................................88 6.3.5 Kaskadne relacije.............................................................................................................88 6.3.6 Upravljanje transakcijama................................................................................................88 7. Zaključak........................................................................................................................................91 Dodatak A: Uvod u Python.................................................................................................................92 Dodatak B: Instaliranje korišćenih alata.............................................................................................97 Instaliranje Pythona.......................................................................................................................97 Instaliranje SQLAlchemyja...........................................................................................................97 Instaliranje i podešavanje PostgreSQL sistema.............................................................................98 Literatura..........................................................................................................................................101
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
1
1. Uvod 1.1 O radu U samoj definiciji informatike kao discipline koja se bavi prikupljanjem, razvrstavanjem, čuvanjem, pronalaženjem i širenjem zabeleženog znanja [1] pominje se čuvanje znanja kao jedan od 5 stubova te nauke. Tema ovog rada su savremeni načini za prelazak entiteta iz nepostojanog stanja u postojano, odnosno stanje u kojem se može čuvati i posle prestanka rada programa. Entiteti su zapravo pojavljivanje klasa a persistencija se postiže smeštanjem podataka iz objekata u relacionu bazu. Tu je i obrnuti problem: kako iz relacione baze izvući objekte u memoriju sa svim svojim vezama. Kao primer mosta između objekata i relacione baze kao dva različita sveta uzet je SQLAlchemy. To je biblioteka napisana u programskom jeziku Python i samo u njemu se može koristiti. SQLAlchemy je mlada tehnologija sa mnoštvom novih ideja i odličnom razradom već postojećih. Veliki doprinos tome dao je i sam jezik u kome i za koga je pisan. SQLAlchemy je slobodan softver. Izdat je pod licencom Masačusetskog instituta za tehnologiju (MIT) koja je odobrena od Fondacije za slobodni softver [2] i Inicijative za otvoreni kôd [3] i saglasna je GNU-ovoj opštoj javnoj licenci. MIT licenca je vrlo otvorena, tako da omogućava i zatvaranje kôda uz samo jedan uslov, da se pomenu izvorni autori kôda. Kao sistem za upravljanje bazom podataka korišćen je PostgreSQL [4] kao jedan od najnaprednijih sistema ne samo u okviru slobodnih rešenja (izdat je pod BSD licencom). PostgreSQL je objektnorelacioni sistem saglasan ANSI-SQL 92/99 standardu a implementiran je i deo ANSI-SQL 03 standarda. Sun Microsystems podržava ovaj SUBP i isporučuje ga uz Solaris operativni sistem. PostgreSQL se može koristiti na više platformi i može mu se pristupiti iz mnogih programskih jezika. Programski jezik u kome je stvoren SQLAlchemy i u kojem se koristi je Python [5]. Filozofija koja prati ovaj jezik imala je veliki uticaj na principe oko kojih je SQLAlchemy sazdan. Osim što je Python po prirodi dinamičan, jednostavan, interpretirani jezik, on u sebi nosi kulturu da sve treba biti jednostavno i na prvi pogled očigledno što je sažeto kroz nekoliko principa kojih se autori čvrsto drže. Python je slobodan softver na koji se primenjuje licenca slična licenci pod kojom se izdaje SQLAlchemy. Može se koristiti na više platformi. Iako je po vremenu nastanka stariji od nekih poznatijih jezika (Java, C#) Python je jezik u usponu i razvoju sa mnoštvom biblioteka za razne primene kojima je zajedničko to da su kao i sam jezik izuzetno lake za korišćenje. Pored glavne implementacije u jeziku C, kompanija Sun podržava razvoj implementacije u javi kao jedan od jezika za javinu virtuelnu mašinu pod imenom Jython. Takođe i Microsoft potpomaže razvoj IronPython-a, verzije za .NET platformu.
1.2 Struktura rada U prvom delu rada postavljaju se teorijske osnove. U drugoj glavi se definiše perzistencija, daje prikaz mogućih načina postizanja trajnosti sa naglaskom na relacione baze. Ujedno se razmatra problem koji programeri objektno-orijentisanih jezika imaju u radu sa relacionim bazama. Način prevazilaženja tih problema je tema treće glave. Daju se dva uzora koji se koriste u implementaciji preslikavanja objektnog sveta u relacioni i razmatraju njihove prednosti i mane. U drugom delu rada se prelazi na prikaz SQLAlchemyja kao savremene i jednostavne biblioteke za
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
2
objektno-relaciono preslikavanje. U četvrtoj glavi se govori o Pythonu i njegovoj filozofiji na kojoj je i SQLAlchemy zasnovan. Tu se daju osnovni podaci o tome šta je SQLAlchemy. Pošto je materija suviše složena da bi se obradila u jednom prolazu, u petoj glavi se SQLAlchemy obrađuje punom širinom, ali ne zalazeći u dubinu. Ta glava predstavlja uputstvo i prikaz mogućnosti SQLAlchemyja. U narednoj glavi se znanja iz pete produbljuju. Šesta glava predstavlja novi ciklus prolaska kroz SQLAlchemy uz zalaženje u neke detalje koji su zanemareni u prethodnom ciklusu. Sedma glava predstavlja zaključak celokupnog rada. Za razumevanje ovog rada je potrebno osnovno znanje iz jave i UML (Unified modeling language). Python kôd bi trebalo da bude razumljiv sam po sebi. Neki detalji koji su specifični za Python su obrađeni u prvom dodatku koji preporučujem da se pročita. Instaliranje potrebnih programa, kako bi se primeri iz rada mogli isprobati, obrađeno je u drugom dodatku.
1.3 Način pisanja korišćen u radu Radi lakšeg čitanja sve što je vezano za kôd (javin, Pythonov ili SQL-ov) napisano je slovima jednake širine (programerskim fontom). U primerima su izlazi iz kôda su naglašeni sivom pozadinom, kako bi se razlikovali od generisanog SQL kôda, koji se takođe ispisuje učenja radi. Generisani SQL je formatiran radi lakšeg praćenja. Tekst koji predstavlja digresiju prikazan je uokvireno. Tu se nalaze napomene, objašnjenja Pythona, zanimljivosti i poređenja sa drugim tehnologijama. Njihov cilj je da pomognu boljem razumevanju rada, ali nisu nužne da bi se shvatila celina.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
3
2. Perzistencija U ovoj glavi se obrađuje pojam perzistencije. Kreće se od definisanja pojma perzistencije a potom se prikazuju mogući načini njenog postizanja uz pregled osobina, prednosti i mana svakog načina. Naglasak se stavlja na perzistenciju putem relacionih baza. Ujedno se razmatra problem koji programeri objektno-orijentisanih jezika imaju u radu sa relacionim bazama, jer su upravo ti problemi doveli do razvoja tehnika objektno-relacionog preslikavanja.
2.1 Šta je perzistencija Skoro sve aplikacije zahtevaju postojanost podataka. Perzistencija je jedna od osnovnih zamisli u razvoju aplikacija. Kada informacioni sistem ne bi čuvao podatke i posle prestanka njegovog rada, taj sistemi ne bi bio od velike praktične koristi. Za objekat se kaže je perzistentan ukoliko nastavi da postoji i nakon prestanka rada programa koji ga je stvorio [6]. Bez te sposobnosti podaci, koji postoje samo u memoriji, bi bili izgubljeni kada memorija ostane bez napajanja, kao na primer pri gašenju računara. Persistencija se može ostvariti preko datotečnog sistema i baza. Čuvanje u datotekama je stvar prošlosti, mada i savremeni programski jezici dolaze sa podrškom za serijalizaciju kao načinom da se lako postigne perzistencija čitavog objekta u datoteku. Čuvanje podataka u datotekama je loše iz mnogo razloga. Da pomenem samo neke: teškoće pretraživanja, ili čak nemogućnost pretraživanja dok se objekti ne učitaju u memoriju ako se u datotekama čuvaju u serijalizovanom obliku, teškoće oko istovremenog rada više programa (odnosno niti, procesa) na zajedničkim datotekama, nepostojanje podrške za transakcije itd. Zbog svih tih problema praktično se pod perzistencijom podrazumeva čuvanje podataka u bazi podataka i to gotovo uvek bazi koja je pod relacionim sistemom za upravljanje i vrlo retko, u nekim posebnim primenama, pod objektnim sistemom.
2.2 Relacione baze Relaciona tehnologija je nešto s čime se svakodnevno susrećemo. Ona je svima poznata pa je to dovoljan razlog da se većina organizacija opredeljuje za relacione baze. Ali reći da je to jedini razlog bilo bi daleko od istine. Relacione baze vladaju zato što su vrlo prilagodljive i zbog toga što predstavljaju jasan i čvrst pristup upravljanju podacima. Pošto su u celosti teorijski zasnovane na relacionom modelu podataka, relacione baze mogu efikasno štititi i garantovati integritet podataka, uz ostale poželjne osobine. Neki autori čak tvrde da je poslednji veliki pronalazak u informatici bio upravo relacioni koncept u upravljanju podacima koji je prvi objavio E.F Codd pre više od tri decenije [7]. Relacioni sistemi za upravljanje bazama podataka nisu vezani za bilo koji jezik ili program. Taj važan princip je poznat kao nezavisnost podataka. Drugim rečima, podaci žive duže nego bilo koji program. Relaciona tehnologija obezbeđuje način za deljenje podataka između različitih programa ili između različitih tehnologija. Npr. da jedan program vrši transakcije a drugi daje izveštaje iz iste baze. Relaciona tehnologija je zajednički imenilac mnogih različitih sistema i platformi. Relacioni sistemi za upravljanje bazama podataka imaju programski intrefejs zasnovan na SQL-u, zato se često ti sistemi nazivaju SQL sistemima za upravljanje bazama podataka. Važno je napomenuti da iako se označavaju relacionim, sistemi za upravljanje podacima koji pružaju SQL jezik za rad sa podacima nisu stvarno relacioni i u mnogim stvarima nisu ni blizu izvornoj zamisli. To stvara zabunu, pa SQL stručnjaci krive relacioni model zbog mana u SQL-u, a stručnjaci za upravljanje relacionim podacima krive SQL standard zbog slabe implementacije relacionog modela.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
4
Aplikativni programeri su zaglavljeni negde između ova dva stava i jedinim ciljem: da naprave nešto što radi.
2.2.1 Osnovno o SQL-u Bez punog razumevanja SQL-a i relacionog modela, teško je razumeti i pravilno primeniti savremene načine rada sa relacionim bazama, iako su ti načini upravo i nastali da nas poštede pisanja ponavljajućeg kôda i da nam automatizuju rad sa SQL-om i po mogućstvu u potpunosti sakriju SQL-om od nas. Naredbe SQL-a su svrstane u jednu od tri kategorije: naredbe za kontrolne (upravljačke) funkcije (eng. data control language), naredbe za definisanje podataka (eng. data definition language) i naredbe za manipulisanje podacima (eng. data manipulation language) i [8]. Naredbe za upravljačke funkcije se koriste za kontrolu pristupa podacima u bazi. Ključne reči GRANT i REVOKE se koriste da se nekom korisniku ili nekoj grupi korisnika baze dodele ili oduzmu prava na izvršavanje nekih naredbi nad nekim objektima. Npr. da se korisniku prijavljenom SUBP pod imenom testnikorisnik dozvoli pozivanje upita i unošenje slogova na tabeli klijent potrebno je izvršiti sledeću naredbu: GRANT SELECT, INSERT ON TABLE klijent TO testnikorisnik
Naredbe za definisanje podataka se koriste za stvaranje strukture baze, uključujući redove, kolone, tabele i indekse sa CREATE i ALTER ključnim rečima. Naredbama ovog tipa pripada i DROP za brisanje objekata iz baze, kao i naredbe za definisanje referencijalnog integriteta. Kada se napravi šema baze koristi se SQL naredbe za manipulisanje i dobavljanje podataka. To se postiže ključnim rečima: SELECT, INSERT, UPDATE i DELETE. Dobavljanje se postiže operacijama ograničenja, projekcije i spajanja. Radi efikasnog izveštavanja, SQL može grupisati, uređivati u zadati redosled i sažimati podatke preko GROUP BY, ORDER BY i agregacionih funkcija (min, max, sum...). SQL naredbe se mogu spajati i graditi složene konstrukcije tako što se glavna naredba oslanja na podatke iz podupita.
2.2.2 Perzistencija u objektno-orijentisanim aplikacijama sa relacionim bazama U objektno-orijentisanoj (OO) aplikaciji, perzistencija omogućava objektu da nadživi proces koji ga je stvorio. Stanje objekta se može sačuvati na disku pa se objekat sa istim stanjem može ponovo napraviti u nekom trenutku budućnosti. Ovo nije ograničeno samo na pojedinačne objekte – čitave mreže povezanih objekata se može učiniti perzistentnom i kasnije ponovo stvoriti u novom procesu. Većina objekata nije stalna, privremeni objekat ima ograničen životni vek uokviren trajanjem procesa koji ih je instancirao. Skoro sve aplikacije imaju i stalne i privremene objekte, zato se mora imati podsistem koji će upravljati stalnim objektima. Savremene relacione baze omogućavaju predstavu strukture perzistentnih podataka, manipulaciju, uređivanje, pretragu i sažimanje podataka. Sistemi za upravljanje bazama su odgovorni za rešavanje problema konkurencije i integriteta podataka; oni su odgovorni za deljenje podataka između više korisnika i više aplikacija. Oni garantuju integritet podataka kroz pravila integriteta koja su postavljena kroz ograničenja u naredbama za definisanje podataka. Sistem za upravljanje bazom pruža sigurnost na nivou podataka. Aplikacija sa domenskim modelom ne radi direktno sa poslovnim entitetima, predstavljenim tabelama, već kroz sopstveni OO model. Ako baza za kupovinu preko Interneta ima NARUDZBINA i
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
5
STAVKA tabele tada će aplikacija definisati Narudzbina i Stavka klase.
Zatim će poslovna logika, umesto da direktno radi sa redovima i kolonama rezultata SQL upita, raditi sa tim OO domenskim modelom i mrežom povezanih objekata u vreme izvršavanja programa. Svaki objekat Stavka imaće referencu na objekat Narudzbina, i svaka Narudzbina će imati kolekciju referenci na Stavka objekte. Poslovna logika se ne izvršava u bazi (ne postoje SQL procedure), sve je implementirano u aplikativnom sloju. To omogućava poslovnoj logici da koristi složene OO koncepte kao što su nasleđivanje, polimorfizam i projektne uzore koji od tih koncepata zavise. Ipak ne trebaju sve aplikacije da koriste domenski model. Neke prostije je bolje uraditi bez njega. Ali aplikacije sa složenom poslovnom logikom imaju korist od domenskog modela preko olakšane mogućnosti za ponovnu upotrebu kôda i značajno nižih troškova održavanja. Ako pogledamo relacione baze i domenski model uviđamo nepodudaranje tih obrazaca. SQL operacije, kao što su projekcija i spajanje, uvek daju kao rezultat podatke predstavljene u obliku tabele. To je bitno drugačije od mreže povezanih objekata koji se koriste u radu poslovne logike aplikacije. To su u osnovi različiti modeli a ne samo različiti načini prikazivanja istog modela [7]. Sa tom spoznajom možemo početi da shvatamo probleme u aplikaciji koja kombinuje obe predstave podataka: OO domenskog modela i persistentnog relacionog modela. Opisani jaz između tih predstava je poznat pod nazivom neusaglašenost predstava ili pod imenom neusaglašenost impedance (eng. impedance mismatch) što je termin preuzet iz elektrotehnike.
2.2.3 Neusaglašenost predstava (impedance mismatch) Termin neusaglašentost predstava odnosi se na razlike između objektno-orijentisane i relacione paradigme i teškoće u razvoju aplikacija koje proističu iz tih razlika. Koren ovih problema se nalazi u različitim osnovnim ciljevima te dve tehnologije. U objektnim jezicima objekat (npr. klase Stavka) sadrži referencu na drugi (npr. objekat klase Kategorija), i taj referisani objekat se ne kopira u prvi. Dva objekta klase Stavka koja pripadaju istoj kategoriji imaće reference na isti objekat klase Kategorija. Ta činjenica nas oslobađa od brige za uštedom memorije pri implementacji domenskog modela sa visokim stepenom apstrakcije. Da nije tako, verovatno bismo čuvali identifikator referisane kategorije u stavkama i ostvarivali bismo vezu među njima samo onda kad je to potrebno. U stvari ovo je skoro u potpunosti način na koji se radi u relacionom modelu. OO model
Relacioni model
Klasa, objekti
Tabela, redovi
Atributi
Kolone
Identitet
Primarni ključ
Relacija / referenca ka drugom entitetu
Spoljni ključ
Nasleđivanje / polimorfizam
Nije podržan
Metode
Posredno se može uzeti SQL logika, procedure, okidači
Kôd je portabilan
Nije portabilan u opštem slučaju, zavisi od proizvođača
Tabela 1: neusaglašenost predstava - razlike između objektnog i relacionog sveta
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
6
U objektnom svetu imamo luksuz nasleđivanja i polimorfizma kojeg nema u relacionom. Zatim pored stanja objekti imaju i ponašanje, dok tabele imaju samo redove, kolone i ograničenja bez poslovne logike. Tabela 1 daje pregled nekih razlika objektno-orijentisanog i relacionog sveta [9]. Neki od većih problema nastalih iz nepodudarnosti objektnog i relacionog modela su: 1. Problem granularnosti
U OO svetu, klase su predstavljene kao ugnježdene hijerarhijske strukture. Npr. objekat klase Kupac sadrži mnogo objekata Narudzbina, od kojih se svaki sastoji od mnoštva objekata StavkaNarudzbine. U relacionim svetu sve se predstavlja preko tabela (relacija) koje se sastoje od više redova (zapisa) i kolona (atributa). Drugim rečima, dok se klasama može opisati i predstaviti bilo koji nivo granularnosti, relaciona šema je ograničena na samo četiri osnovna elementa: tabelu, red, kolonu i ćeliju (presek kolone i reda). To ima za posledicu da se bogatstvo i izražajnost objektnog modela često žrtvuje (izbegava se nasleđivanje, asocijacije se uprošćavaju ili čak i uklanjaju) zbog omogućavanja lakšeg preslikavanja na relacioni model [10]. Mnogi relacioni sistemi za upravljanje bazama podataka omogućavaju neku vrstu podrške za korisnički definisane tipove preko takozvanih objektno-relacionih dodataka. Čak je na osnovu toga nastala nova klasa sistema za upravljanje pod nazivom objektno-relacioni sistemi za upravljanje bazama podataka. Na nesreću ta podrška se razlikuje od sistema do sistema i nije prenosiva među njima. I sam SQL standard ima podršku za korisničke tipove (u SQL-u poznatih kao domeni), ali je ona slaba, pa se oni retko koriste. Tako relacione baze i dalje imaju mnogo manju izražajnost od one koja se može postići u domenskom modelu [7]. 2. Problem nasleđivanja
U OO svetu nasleđivanje se ostvaruje preko potklasa i natklasa. Zamislimo aplikaciju za elektronsko poslovanje koja prihvata ne samo plaćanje preko računa već i preko kreditnih i debitnih kartica. Najprirodniji način da se ovo modeluje je upotrebom nasleđivanja klase NacinPlacanja. Moguće je da imamo apstraktnu natklasu NacinPlacanja, sa nekoliko konkretnih potklasa: KreditnaKartica, BankarskiRacun itd. Svaka od njih opisuje drugačije podatke i potpuno drugačije ponašanje koje se izvršava nad njima. UML dijagram klasa na slici 1 prikazuje ovaj model.
Slika 1: korišćenje nasleđivanja za različita plaćanja Pošto tabela nije tip, jasno je da nema nasleđivanja tabela i u klasičnom relacionom modelu ne postoje nadtable i podtabele (mada neki od savremenih objektno-relacionih sistema za upravljanje bazama podataka, kakav je PostgreSQL, imaju podršku za nasleđivanje tabela). Postoji nekoliko načina za prevazilaženje ovog problema. Oni se kreću od potpune normalizacije kada se za svaku konkretnu potklasu pravi zasebna tabela, preko potpune denormalizacije, gde se u jednoj tabeli stave svi atributi svih potklasa i doda diskriminaciona kolona u koju se zapisuje kojoj
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
7
klasi pripada taj zapis, do hibridnog rešenja kad se za svaku tabelu u stablu nasleđivanja napravi tabela samo sa atributima koji tu potklasu razlikuju od natklase i kolonom sa spoljnim ključem ka natklasi [10]. I u ovom hibridnom pristupu, se često koristi diskriminaciona kolona kao nešto što olakšava rad, mada u teoriji nije potrebna. Ali ovo nije jedini problem kod nasleđivanja. Tu je i problem kod polimorfizma. Klasa Korisnik ima vezu ka natklasi NacinPlacanja. To je polimorfna asocijacija koja u vreme izvršenja programa može referisati na bilo koju potklasu klase NacinPlacanja. Slično, ako hoćemo da pišemo polimorfne upite koji ukazuju na NacinPlacanja klasu a vraćaju njene potklase. SQL baze nemaju standardan način da predstave polimorfne asocijacije. Spoljni ključ može ciljati samo na jednu tabelu, ne postoji način da se definiše spoljni ključ kome će sa druge strane biti više tabela. Da bismo uveli neko takvo ograničenje integriteta na kolonu, morali bismo koristiti procedure ili okidače [7]. 3. Problem identiteta
Ovaj problem ima tri aspekta, dva su u OO svetu i jedan u SQL bazama. OO jezici definišu dva različita vida jednakosti: Identitet objekta, koji je ekvivalentan memorijskoj lokaciji objekta (u javi se proverava sa operatorom ==, u Pythonu sa operatorom is)
●
Jednakost po vrednosti koja se utvrđuje implementacijom određenje metode objekta (equals u javi i __eq__ u Pythonu koja se poziva implicitno kad se upotrebi operator == )
●
S druge strane, identitet u redu baze je izražen preko vrednosti primarnog ključa. Ni jedan od dva vida jednakosti objekata ne predstavlja ekvivalent vrednosti primarnog ključa. Nije retkost da više neidentičnih objekata istovremeno predstavljaju isti red u bazi, npr. u višenitnoj aplikaciji. I sama implementacija equals metode za perzistentnu klasu traži dodatnu pažnju programera. Razmotrimo još jedan problem u vezi sa identitetom u bazi. Zamislimo da smo u prethodnom primeru sa elektronskim poslovanjem u tabeli KORSNICI koristili KORISNICKO_IME kao primarni ključ. To nije dobra odluka jer se javlja problem kod promene korisničkog imena, tada ne samo da moramo promeniti KORISNICKO_IME, već i kolonu koja je spoljni ključ u NACIN_PLACANJA. Da bi se problem sprečio preporučljivo je koristiti surogat ključeve, tj. kolonu koja je primarni ključ bez ikakvog značenja za korisnika. Npr. mogli bismo promeniti definicije tabela da izgledaju ovako: create table KORISNICI ( KORISNIK_ID bigint not null primary key, KORISNICKO_IME varchar(15) not null unique, IME varchar(50) not null, ... ) create table NACIN_PALCANJA ( NACIN_PLACANJA_ID bigint not null primary key, BROJ_RACUNA varchar(10) not null unique, TIP_RACUNA varchar(2) not null, KORISNIK_ID bigint foreign key references KORISNIK ... )
KORISNIK_ID i NACIN_PLACANJA_ID kolone sadrže vrednosti koje sam sistem generiše. One postoje
jedino iz razloga poboljšanja modela. Kako će se, ako uopšte i treba, te kolone predstaviti u domenskom modelu je još jedan od vidova problema identiteta [7].
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
8
4. Problemi sa asocijacijama
OO jezici predstavljaju asocijacije preko referenci na objekte, ali u relacioniom svetu se one predstavljaju kao kolona koja je spoljni ključ, sa kopijom vrednosti ključa i referencijalnim ograničenjima koja garantuju integritet podataka. Između te dve predstave postoji suštinska razlika. Reference su u suštini usmerene. To su pokazivači i ako je potrebno da asocijacija između objekata bude dvosmerna, mora se definisati asocijacija dva puta, po jednom u svakoj klasi asocijacije. Npr. u javi bi to izgledalo ovako: public class Korisnik { private Set naciniPlacanja; ... } public class NacinPlacanja { private Korisnik korisnik; ... }
S druge strane, asocijacija spoljnim ključem po prirodi nije usmerena. Navigacija nema smisla u relacionom modelu podataka zato što je moguće napraviti proizvoljne asocijacije upotrebom spajanja tabela i projekcije. Izazov je spojiti model podataka koji je u potpunosti otvoren, koji je nezavistan u odnosu na aplikaciju koja radi sa podacima, sa navigacionim modelom koji zavisi od aplikacije. Nemoguće je utvrditi kardinalnost jednosmerne asocijacije posmatranjem samog kôda klase. Asocijacije mogu imati M-M kardinalnost npr. u javi: public class Korisnik { private Set naciniPlacanja; ... } public class NacinPlacanja { private Set korisnici; ... }
Nasuprot tome, asocijacije u tabelama su uvek 1-M ili 1-1. Kardinalnost se može direktno pročitati iz definicije spoljnog ključa. Sledi primer deklaracije spoljnog ključa u tabeli NACIN_PLACANJA kao asocijacije sa kardinalonšću M-1 (ili ako se čita u suprotnom smeru, 1-M): KORISNIK_ID bigint foreign key references KORISNICI
Primer za 1-1 kardinalnost bi bio: KORISNIK_ID bigint unique foreign key references KORISNICI NACIN_PLACANJA_ID bigint primary key foreign key references KORISNICI
Da bi se predstavila M-M asocijacija u relacionoj bazi, mora se uvesti nova tabela koja će služiti za povezivanje (eng. Association Table). Ona se ne pojavljuje u domenskom modelu. Npr: ako zamislimo odnos između korisnika i načina naplate kao M-M, tada je asocijativna tabela ovakva: create table KORISNIK_NACIN_PLACANJA ( KORISNIK_ID bigint foreign key references KORISNICI, NACIN_NAPLATE_ID bigint foreign key references NACINI_NAPLATE, primary key (KORISNIK_ID, NACIN_NAPLATE_ID) )
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
9
Probleme koje smo do sada razmatrali su bili uglavnom vezani za strukturu i ticali su se statičkog pogleda na sistem. Ali najteži problemi u perzistenciji objekata su problemi dinamike. 5. Problem sa pristupom podacima
Pristup podacima u OO programskim jezicima i u relacionim bazama je u osnovi drugačiji. U programskim jezicima bi podacima o načinu plaćanja pristupili preko poziva metoda na objektu korisnik.dajNacinPlacanja().dajBrojRacuna() ili nešto slično. Ovakav način pristupa podacima se često opisuje kao šetnja kroz mrežu objekata. Ali ovo nije efikasan način za dobijanje podataka iz SQL baze. Najvažnija stvar koja može da se uradi u cilju poboljšanja performansi pristupa podacima je da se umanji broj zahteva ka bazi. Dakle, efikasan pristup relacionim podacima uglavnom zahteva upite sa spajanjima između tabela koje nas zanimaju. Broj tabela koje moraju da se spoje zavisi od dubine mreže objekata kroz koju se krećemo u memoriji. Npr. za prethodni primer bi SQL upit mogao izgledati ovako: select * from KORISNICI k left outer join NACIN_PLACANJA n on n.KORISNIK_ID = k.KORISNIK_ID where k.KORISNIK_ID = 123
S druge strane bilo koje rešenje za perzistenciju objekta omogućuje dobijanje podataka o vezanim objektima tek onda kada se ta veza prvi put upotrebi. Ovakav način pribavljanja podataka „na parče“ je sa gledišta relacionih baza spor i skup jer zahteva izvršavanje SQL naredbi za svaki čvor ili kolekciju u mreži objekata kojima se pristupa. Ovaj problem je poznat pod nazivom problem (n + 1) -og upita jer se za jedan objekat izvršava jedan glavni upit za podatke koji se direktno tiču tog objekta i n drugih upita za objekte sa kojima je u vezi. Ova neusaglašenost objektnog i relacionog modela je najveći krivac za probleme sa performansama. Postoji prirodan sukob između mogućnosti za previše upita i mogućnosti za jedan prevelik upit koji učitava u memoriju nepotrebne podatke.
2.3. Objektne baze Pošto mi radimo sa objektima, čini se da bi bilo idealno kad bi postojao neki način da se ti objekti sačuvaju u bazi bez ikakve potrebe za konverzijom objektnog modela. Sredinom devedestih su objektno-orijentisani sistemi za upravljanje bazama podataka (OSUBP) dobili pažnju. Oni su zasnovani na mrežnom modelu podataka, koji je bio uobičajen pre dolaska relacionog modela. Osnovna ideja je da se čuva mreža objekata i da se ona ponovo stvori kao memorijski graf kasnije [7]. Osim toga što je skladište za čuvanje grafova objekata (zajedno sa identitetima, atributima, asocijacijama i informacijama o nasleđivanju), OSUBP sadrži bar još sistem za upite, sistem za upravljanje konkurentnim izvršavanjem i mehanizmom za oporavak podataka. OSUBP više liči na proširenje aplikacionog okruženja nego na trajnu spoljnu memoriju. Obično je napravljen iz više slojeva sa pozadinskom spoljnom memorijom, kešom objekata i klijentskom aplikacijom, spregnutim čvrsto, koji komuniciraju preko posebnog mrežnog protokola. Zato objektna baza nudi besprekornu integraciju u OO aplikaciono okruženje, što je nešto drugačije u odnosu na model koji se danas koristi u relacionim bazama, gde se interakcija sa bazom ostvaruje preko međujezika SQL a nezavistnost podataka od konkretne aplikacije se poklanja najveća važnost. Postoje dve grupe objektnih baza. U prvoj grupi postoje dva odvojena objektna modela, jedan za aplikaciju i drugi za samu bazu. OSUBP koji su usaglašeni sa Object Data Management Group (ODMG) specifikacijom su tipični
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
10
primeri ove grupe jer zahtevaju da se definiše odvojena šema podataka bez obzira na to što postoji domenski model. Da bi se objekat sačuvao ili izvukao upitom mora da postoji preslikavanje između ta dva različita modela. Za ODMG baze šema se definiše u Object Definition Language (ODL) a domenski model se može ili napisati ručno ili generisati iz ODL šeme. Iako ovo odvajanje između domenskog modela i modela baze ovoj grupi OSUBP daje mogućnost nezavisnosti, te time i prenosivost između aplikacija, programskih jezika i platformi, ono je i uzrok problema jer projektanti aplikacija moraju da rade na održavanju oba ta modele tokom životnog ciklusa aplikacije. Drugim rečima, promene na jednom od tih modela traže od programera da promene izvrše i na drugom. Druga grupa su takozvane čiste objektne baze. U njima se objekti čuvaju bez ikakve potrebe za preslikavanjem u drugi objektni model koji zahteva baza i obratno. Dakle, postoji samo jedan objektni model: domenski model aplikacije. Iako s jedne stane čisti OSUBP pojednostavljuju do krajnjih granica pretragu i čuvanje objektnog modela aplikacije, njihove baze nisu lako prenosive na druge aplikacije, programske jezike i platforme. Šta više, da bi dve ili više aplikacije koristile istu bazu, moraju sa sobom imati iste klase koje se perzistiraju. Za aplikacije napisane u različitim jezicima je čak i teže da dele istu bazu zbog razlika u osnovnim tipovima podataka.
2.3.1 OSUBP protiv RSUBP Pošto smo pogledali osnovna svojstva i vrste OSUBP proučimo njihove prednosti i nedostatke u odnosu na RSUBP. Prednosti OSUBP: Bogat domenski model: pošto OSUBP može čuvati objekte na bilo kom nivou granularnosti i ima ugrađenu podršku za identitet, asocijacije i nasleđivanje, OO projektanti mogu oblikovati model domenskih klasa onoliko bogato i izražajno koliko god žele, bez ograničenja kakva bi im nametnuo relacioni svet.
●
Lakoća održavanja: zato što su objektni model aplikacije i baze međusobno blisko povezani ili čak isti (u čistoj OSUBP), potrebno je manje truda uložiti u održavanje tih modela dok se aplikacija razvija.
●
Brzina razvoja: mogućnost projektanata da stvore bogati domen i domen koji će se održavati sa najmanje napora će dalje dovesti do značajnog smanjenja vremena i troškova razvoja.
●
Performanse: OSUBP bi trebalo da imaju mnogo bolje performanse nego RSUBP, bez obzira da li se koriste alati za objektno-relaciono preslikavanje ili ne, u sistemima sa veoma složenim objektnim modelom zato što ne moraju da se koriste složeni upiti i preslikavanje.
●
Mane OSUBP: Prenosivost: aplikacije, napisane u bilo kom jeziku i bilo kojoj paradigmi i platformi, mogu deliti podatke u RSUBP dok je OSUBP privezan za objektno-orijentisani svet. Slika je i gora za čiste objektne baze jer čak ako više aplikacija dele iste podatke oni se ne mogu koristiti sa različitim domenskim klasama. Jasna odvojenost između relacionog i i objeknog modela još više potpomaže prenosivost u relacionim bazama pošto se ta dva modela mogu menjati nezavisno jedan od drugog.
●
Nasleđene aplikacije: postoji toliko mnogo aplikacija napisanih sa RSUBP kao skladištem podataka da nije praktično prebacivati sve te podatke u OSUBP. I ne samo da podaci trebaju da se prebace u OSUBP, već i same aplikacije koje te podatke moraju da se
●
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
11
prilagode novom načinu pristupa podacima. Zrelost: OSUBP su novina (pojavili se devedesetih), te su zato daleko od RSUBP (koji su se pojavili sedamdesetih) po pitanju dostupnih implementacija, kompatibilnosti između implementacija različitih proizvođača i podrške u vidu alata kao što su programi za izveštavanje, OLAP, transformaciju podataka, servise za klastere, itd.
●
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
12
3. Objektno-relaciono preslikavanje U prethodnoj glavi je bilo reči o neusaglašenosti predstava objektnog i relacinog sveta i problemima koji se zbog toga javljaju. Način prevazilaženja tih problema je tema ove glave. Daju se dva uzora koji se koriste u implementaciji preslikavanja objekata u relacije i razmatraju njihove prednosti i mane.
3.1 Šta je objektno-relaciono preslikavanje? U najkraćem, objektno-relaciono preslikavanje (eng. Object relational mapping, ORM) je automatska i transparentna perzistencija objekata aplikacije u tabele relacione baze, korišćenjem meta podataka koji opisuju preslikavanje između objekata i baze. ORM u suštini pretvara podatke iz jedne u drugu predstavu. To svakako znači da će doći do određenog smanjenja performansi. Međutim postoje mnoge mogućnosti za optimizaciju kakvih nema kod ručnog pisanja perzistencionog sloja. Opisivanje podataka (tj. pisanje metapodataka) koji se transformišu jeste dodatni trošak u vreme razvoja ali je cena toga mnogo manja nego cena održavanja koja bi se dobila u slučaju ručnog pisanja. Čak i objektne baze zahtevaju značajnu količinu metapodataka. Savremena ORM rešenja, posebno kod dinamičkih jezika, značajno smanjuju trošak opisivanja podataka i konfigurisanja. Sve se češće koristi princip podrazumevane konfiguracije. Po tom principu sve je konfigurisano po nekoj podrazumevanoj šemi koja odgovara najvećem broju slučajeva. Programeru ostaje da konfiguriše samo mali broj izuzetaka. Mnoga programska okruženja imaju mogućnost automatizovanja konfiguracije, i polako se prelazi sa pisanja metapodataka u xml datoteke na uključivanje metapodataka u sam kôd (npr. preko anotacija u javi 5). ORM se sastoji iz sledećih delova: programski interfejsa za pozivanje osnovnih operacija (stvaranje, uzimanje, ažuriranje i brisanje) na objektima perzistentnih klasa
●
jezik ili programski interfejs preko kog se zadaju upiti nad objektima i njihovim atributima
●
mogućnost za opisivanje preslikavanja podataka iz objekata u relacije, odnosno povezivanje klasa sa tabelama baze
●
tehniku za rad sa transakcionim objektima, dobavljanje povezanih objekata samo onda kad su potrebni (tzv. lenje veze) i druge optimizacione funkcije
●
3.2 Zašto koristiti ORM? ORM su biblioteke sa jako složenim kôdom, pa se postavlja pitanje zašto uvoditi tako složen infrastrukturni element u naš sistem i da li je to vredno truda. Prvo treba reći da ORM nije tu da zaštiti programere od SQL-a. Pogrešno je mišljenje kako ne treba očekivati od objektno-orijentisanih programera da razumeju SQL i relacione baze i da je njima SQL nešto strano. Naprotiv, programeri moraju imati dovoljan nivo bliskosti sa relacionim modelovanjem i SQL-om kako bi mogli da koriste ORM. Da bi se efikasno koristio ORM programer mora da razume SQL naredbe koje koje ORM stvora i da je svestan njihovog uticaja na performanse.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
13
Pomenimo neke koristi od uvođenja ORM-a. Produktivnost
Kôd vezan za perzistenciju u mnogim aplikacijama je najzamorniji deo. Upotreba ORM uklanja dobar deo tog dosadnog posla i omogućava programerima da se usresrede na poslovnu logiku. Bez obzira da li se primenjuje strategija top-down, kretanjem od domenskog modela ili bottom-up, započinjanjem od postojeće šeme baze, ORM će uz upotrebu odgovarajućih alata značajno smanjiti vreme razvoja. Lakoća održavanja
Što manje linija kôda to je sistem lakše razumeti jer se tada naglašava poslovna logika a ne prosto „fizikalisanje“. Posebno je značajno napomenuti da je sistem sa manje kôda lakše refaktorisati. Automatizovno objektno-relaciono preslikavanje u mnogome smanjuje složenost kôda. Postoje i drugi razlozi za olakšano održavanje kôda korišćenjem ORM-a. U sistemima sa ručno programiranom perzistencijom postoji stalni sukob između dve relacione i objektne predstave. Promene u jednoj od njih uvek uzrokuju promene u onoj drugoj. ORM pruža tampon zonu između ova dva modela izolujući svaki od manjih izmena u drugom. Performanse
Postoji stav među programerima da se ručno programirana perzistencija izvršava brže od automatizovane. Međutim, ta tvrdnja je ispravna samo ako je potrebno uložiti sličan programerski napor za postizanje jednake brzine izvršavanja operacija perzistenicije kad se ona ručno programira i onda kad se automatizuje ORM alatom. U svakoj drugoj situaciji poređenje nije valjano. Jer kad se ORM koristi potrebno je uložiti neuporedivo manje vremena za postizanje zadovoljavajućih rezultata, a kod ručnog kodiranja u pitanju su sati uloženog truda da bi se do persistencije uopšte došlo. I kad se dođe do boljih performansi ta razlika u performansama je marginalna i ne opravdava utrošeno vreme skupog programera, osim u malom broju posebnih slučajeva kad su performanse ključne za aplikaciju. Neke se optimizacije mogu mnogo jednostavnije postići sa ručno kodiranim SQL-om. Međutim, većinu opitmizacija je mnogo lakše primeniti sa automatizovanim objektno-relacionim preslikavanjem. U projektu sa ograničenim vremenom i budžetom, ručna perzistencija uglavnom dozvoljava neke optimizacije. Mnoge ORM implementacije dozvoljavaju mnogo više optimizacija koje mogu da se koriste sve vreme. A pošto ORM povećava produktivnost programera to njemu ostaje više vremena koje može da utroši na fino podešavanje nekoliko preostalih uskih grla. Još jedan izvor poboljšanja performansi kod ORM predstavlja znanje ljudi koji razvijaju ORM. Oni imaju više vremena da istraže ponašanje baze i da shodno tome fino podese preslikavanje. Npr. oni znaju da keširani PreparedStatement objekti predstavljaju značajno ubrzanje kod DB2 drajvera za JDBC, ali da dovode do pucanja drajvera za InterBase. Nezavisnost od implementacije SUBP
ORM apstrahuje SQL bazu i SQL dijalekt. ORM podržava veći broj različitih RSUBP i time obezbeđuje određen nivo prenosivosti kôda aplikacije (eng. portability). Ali ne treba očekivati potpunu prenosivost zbog toga što se mogućnosti baza razlikuju pa bi za postizanje potpune prenosivosti bilo potrebno žrtvovati neke prednosti naprednijih platformi. Ipak, mnogo je lakše razviti višeplatformske aplikacije upotrebom ORM-a. I u slučaju kad trenutno nije potrebna prenosivost, ORM pomaže da se izbegne zavisnosti od jednog proizvođača SUBP. Projekat koji je u
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
14
početku kao cilj imao samo jedan SUBP može u toku svog razvoja preći na drugi, ili podržati više sistema za upravljanje bazama. Još jedna korist od nezavisnosti je mogućnost da se u toku razvoja aplikacije koristi neki drugi SUBP, npr. neki slobodni ili sistem koji manje troši resurse (MySQL, PostgreSQL, SQLite), a da se po završetku razvoja prebaci na skupi produkcioni sistem (Oracle, MS SQL Server).
3.3 Uzori za preslikavanje Postoje dve implementacije ORM-a prva je preko uzora Aktivni slog i druga preko uzora Preslikač podataka.
3.3.1 Aktivni slog uzor Aktivni slog (eng. Active record) obmotava red u tabeli baze, učauruje pristup bazi i dodaje domensku logiku na te podatke [11]. Objekat nosi i podatke i ponašanje. Najveći deo tih podataka treba čuvati u bazi. Ovaj uzor je najočigledniji pristup, stavljajući logiku pristupa podacima u domenski objekat tako da on sam zna kako da čita i upisuje svoje podatke u bazu. Struktura podataka u Aktivnom slogu treba u potpunosti da odgovara onoj u bazi: po atribut za svaku kolonu tabele. Za spoljne ključeve postoje dva rešenja: koristiti referencu na objekat koji predstavlja red tabele na koju spoljni ključ pokazuje ili ostaviti spoljne ključeve kao u tabeli. Klasa aktivnog sloga obično sadrži metode koje rade sledeće stvari: ●
stvaraju objekat aktivnog sloga iz rezultata SQL upita
●
stvaraju nov objekat koja će se kasnije ubaciti u tabelu
statičke metode za pretragu u koje se pakuju SQL upiti koji se često koriste a koji kao rezultat daju objekat aktivnog sloga
●
●
ažuriraju bazu i ubacuju podatke iz aktivnog sloga
●
metode za čitanje i izmenu atributa (akcesori i mutatori)
●
metode u kojima je implementirana poslovna logika
Naredni UML dijagram prikazuje opšti oblik Aktivnog sloga, na kome se vide atributi koji se čuvaju u bazi, statičke metode za dobijanje objekta, metode za proste operacije nad objektom kao i metode poslovne logike:
Slika 2: dijagram klasa Aktivnog sloga
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
15
Aktivni slog je dobar izbor u situaciji kada domenska logika nije previše složena, gde se uglavnom obavljaju proste operacije stvaranja, čitanja ažuriranja i brisanja. U početnom projektovanju domenskog modela glavni izbor je između dva uzora: Aktivnog sloga i Preslikača podataka. Glavna prednost Aktivnog sloga leži u njegovoj jednostavnosti. On se lako piše i lako ga je razumeti. Najveći problem je što Aktivni slog radi dobro samo ako objekti direktno odgovaraju tabelama u bazi. Ako je poslovna logika složena, uskoro ćete poželeti da koristite direktne veze između objekata, kolekcije, nasleđivanje itd. To je teško ostvariti u Aktivnom slogu, i za takvo nešto treba odabrati Preslikača podataka. Još jedna mana Aktivnog sloga leži u činjenici su kod njega jako povezani objektni i relacioni model jer se jednom istom klasom opisuje i domenski objekat i tabela baze. Posledica toga je otežano refaktorisanje u jednom i u drugom modelu. Sledeći java kôd prikazuje osnovni, uprošćeni oblik Aktivnog sloga: class Anketa { private String pitanje; private Date datumObjave; private long id; private final static String vratiStatementString = "SELECT id, pitanje, datum_objave" + " FROM anketa" + " WHERE id = ?"; public static Anketa vrati(long id) { Anketa anketa = (Anketa) Registar.dajAnketu(id); if (anketa != null) return anketa; PreparedStatement vratiStatement = null; ResultSet rs = null; try { vratiStatement = DB.pripremiUpit(vratiStatementString); vratiStatement.setLong(1, id); rs = vratiStatement.executeQuery(); rs.next(); anketa = napuni(rs);
}
return anketa; } catch (SQLException e) { throw new ApplicationException(e); } finally { DB.pocisti(vratiStatement, rs); }
public static Anketa napuni(ResultSet rs) throws SQLException { long id = rs.getLong(1); Anketa anketa = (Anketa) Registar.dajAnketu(id); if (anketa != null) return anketa; String pitanjeArg = rs.getString(2); Date datumObjaveArg = rs.getDate(3); anketa = new Anketa(id, pitanjeArg, datumObjaveArg); Registar.staviAnketu(anketa); }
return anketa;
Razvoj Python programa korišćenjem SQLAlchemy tehnologije private final static String azurirajStatementString = "UPDATE anketa" + " SET pitanje = ?, datum_objave = ?" + " WHERE id = ?"; public void azuriraj() { PreparedStatement azurirajStatement = null; try { azurirajStatement = DB.pripremiUpit(azurirajStatementString); azurirajStatement.setString(1, pitanje); azurirajStatement.setDate(2, datumObjave); azurirajStatement.setInt(3, dajID()); azurirajStatement.execute(); } catch (Exception e) { throw new ApplicationException(e); } finally {
}
}
DB.pocisti(azurirajStatement);
private final static String ubacitStatementString = "INSERT INTO anketa VALUES (?, ?, ?)"; public long ubaci() { PreparedStatement ubaciStatement = null; try { ubaciStatement = DB.pripremiUpit(ubaciStatementString); staviID(dajSledeciId()); ubaciStatement.setInt(1, dajID()); ubaciStatement.setString(2, pitanje); ubaciStatement.setString(3, datum_objave); ubaciStatement.execute(); Registar.staviAnketu(this);
}
return dajID(); } catch (Exception e) { throw new ApplicationException(e); } finally { DB.pocisti(ubaciStatement); }
private static String izbaciStatementString = "DELETE FROM anketa WHERE id = ?"; public void izbaci() { PreparedStatement izbaciStatement = null;
}
try { izbaciStatement = DB.pripremiUpit(izbaciStatementString); izbaciStatement.setInt(1, dajID()); izbaciStatement.execute(); Registar.skiniAnketu(this); staviID(-1); } catch (Exception e) { throw new ApplicationException(e); } finally { DB.pocisti(azurirajStatement); }
16
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
}
17
// metoda poslovne logike public boolean objavljenoDanas() { Calendar cal1 = Calendar.getInstance(); Calendar cal2 = Calendar.getInstance(); cal1.setTime(datumObjave); return cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) && cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR); }
Kada se Aktivni slog koristi kao gotov ORM tada on uključuje nasleđivanje klase koja predstavlja generički domenski objekat. U toj klasi su definisane metode za perzistenciju i programeru preostaje da unese atribute i definiše metode poslovne logike. Zbog svojih osobina, pre svega jednostavnosti, ovaj uzor se često koristi u web okvirima, s obzirom na to da je tu najvažnija lakoća korišćenja te da modeli s kojima se radi nisu previše složeni. Kao primer savremene implementacije ovog uzora navešću ORM u Django Python web frameworku [12]. Sledeći domenski model se sastoji od dve klase Anketa i Pitanje: from django.db import models #iz paketa django.db uvozimo modul models import datetime class Anketa(models.Model): pitanje = models.CharField(maxlength=200) datum_objave = models.DateTimeField() def objavljeno_danas(self): return self.datum_objave.date() == datetime.date.today() def __str__(self): return self.pitanje class Izbor(models.Model): anketa = models.ForeignKey(Anketa) izbor = models.CharField(maxlength=200) glasova = models.IntegerField() def __str__(self): return self.izbor
Ovim su definisana dva domenska objekta sa vezom 1-M. Na osnovu nje će se stvoriti šema baze u odgovarajućem dijalektu RSUBP. NAPOMENA: Pogledajte Dodatak A: Uvod u Python na strani 92 ukoliko imate poteškoća sa razumevanjem Pythonovog kôda u narednom delu rada PYTHON OBJAŠNJENJA: Domenske klase u primeru definišu atribute modela kao klasne atribute (statične atribute u terminologiji jave). Npr. pitanje i datum_objave u klasi Anketa su klasni atributi (u Pythonu sve deklarisano bez prefiksa self. smatra se klasinim atributom, a self postoji samo u nestatičnim metodama). Dinamičnost Pythona je upotrebljena pa se na osnovu tih klasnih, u vreme izvšavanja kôda generišu istoimeni atributi objekta (nestatični atributi). Tako u metodi objavljeno_danas() pristupamo atributu objekta self.datum_objave iako nemamo konstruktor __ init__() u kojem se u Pythonu deklarišu atributi objekta. Čak se i sam konstruktor generiše automatski i kao parametre ima sve atribute objekta, što se vidi u narednom kôdu.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
18
Ovako definisani aktivni slogovi se koriste na sledeći način: # stvaranje domenskog objekta preko automatski generisanog konstruktora >>> anketa = Anketa(pitanje="Vaš omiljeni jezik je?", datum_objave=datetime.now()) # stavljanje objekta u bazu >>> anketa.save() # objekat automatski dobija surogat primarni ključ kao atribut id >>> anketa.id 1 # ažuriranje anketa.pitanje = "Vaš omiljeni programski jezik je?" anketa.save() # pribavnjanje svih anketa, rezultat je lista Anketa.objects.all() [] # dobavljanje objekta po ključu, rezultat je jedan element >>> Anketa.objects.get(id=1) # selekcija >>> Anketa.objects.filter(pitanje__startswith="Vaš") [] # dodavanje izbora u anketu >>> anketa.izbor_set.create(izbor='Java', glasova=0) >>> anketa.izbor_set.create(izbor='Python', glasova=0) >>> izbor = anketa.izbor_set.create(izbor='COBOL', glasova=0) # relacija M-1 >>> izbor.anketa # obrnuti smer >>> anketa.izbor_set.all() [, , ] # brisanje >>> izbor = anketa.izbor_set.filter(izbor__startswith='Java') >>> izbor.delete()
3.3.1 Preslikač podataka Međusloj aplikacije koji odvaja objekte iz memorije od baze je Preslikač podataka. Zadatak Preslikača se sastoji u prenošenju podataka između memorije i baze kako bi ih izolovao. Tako objekti u memoriji ne moraju znati ni da postoji baza. Njima ne treba kôd koji radi sa SQL interfejsom niti znanje o šemi baze [11]. Sam Preslikač je nevidljiv domenskom sloju. Kako preslikač radi
Postoji mnoštvo detalja na koje treba obratiti pažnju, nekoliko načina da se nešto ostvari u ovom uzoru. Zbog svega toga ovaj uzor je jako složen da bi se pravio u sopstvenoj režiji te se gotovo uvek koriste gotovi ORM alati u kojima je implementiran Preslikač podataka. Krenimo od modela iz prethodnog poglavlja: Anketa. Dijagram klasa za ovaj primer prikazan je na slici 3. Preslikač koristi preslikavanje identiteta (heš tabelu) kako bi se svaki objekat učitao samo jednom, a ako se pokuša višestruko učitavanje uvek će se dobiti referenca na isti objekat. Time se
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
19
preslikava identitet u objektnom i relacionom svetu, te se za jedan objekat u bazi dobija samo jedan objekat u memoriji. Ukoliko se objekat sa traženim ključem ne nalazi u heš tabeli tada se izvršava upit na bazi i iz rezultata upita se konstruiše objekat kao što je prikazano dijagramom sekvenci na slici 4.
Slika 3: dijagram klasa Preslikača podataka
Slika 4: dijagram sekvenci za operaciju dobavljanja objekta iz baze Ažuriranje se obavlja tako što klijent zatraži od Preslikača da sačuva domenski objekat. Preslikač potom čita podatke iz objekta i stavlja ih u bazu kao na slici 5. Prosta implementacija Preslikača bi samo povezala tabele baze sa odgovarajućim klasama koje bi za svaku kolonu tabele imale odgovarajući atribut. Ali stvari nisu uvek tako proste. Potrebno je primeniti razne strategije za situacije kada ne postoji preslikavanje jedna kolona - jedan atribut, kad postoji nasleđivanje i razne veze među objektima.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
20
Slika 5: dijagram sekvecnci za operaciju ažuriranja objekta Kod ažuriranja i stavljanja objekta u bazu, ORM sloj mora nekako znati koji su se objekti promenili, koji su u međuvremenu stvoreni a koji su obrisani. I sve to treba da se radi u transakcionom okviru. Da bi se radilo sa objektima oni se najpre moraju učitati iz baze. Obično prezentacioni sloj pokreće učitavanje nekoliko početnih objekata. Preko tih učitanih objekata, putem veza (asocijacija, agregacija i kompozicija) dobijaju se ostali povezani objekti. Primer sa uzimanjem objekta sugeriše da se pri traženju objekta izvršava samo jedan SQL upit, ali to nije uvek tako. Učitavanje narudžbina sa više naručenih stavki može uključivati i učitavanje tih stavki. Učitavanje jednog objekta uobičajeno dovodi do učitavanje čitavog grafa objekata, pri čemu sam Preslikač odlučuje koliko će se tačno objekata učitati u jednom pristupu bazi, mada se može sugerisati strategija učitavanja. Ideja je da se smanje upiti na bazu, zato Preslikač mora znati kako će se traženi objekat koristiti da bi mogao doneti najbolju odluku po pitanju učitavanja povezanih objekata. Ovo pitanje nas dovodi do slučaja kad se jednim upitom puni više objekata. Ako se želi napuniti i narudžbina i njene stavke, sa gledišta RSUBP efikasnije je izvući sve potrebne podatke jednim upitom sa spajanjem tabele NARUDZBINA i table STAVKE_NARUZBINE. Pošto su objekti povezani mora se negde prekinuti sa izvlačenjem vezanih objekata ili će se lako dogoditi da se čitava baza povuče u jednom zahtevu. ORM sloj ima tehnike koje se bave ovim pitanjem korišćenjem lenjog učitavanja kao načina da se vezani objekti zadrže van memorije sve do trenutka kada zatrebaju aplikaciji. Aplikacija može imati jednu preslikačku klasu ili više njih. Ako se preslikačka klasa ručno programira, najbolji pristup je da se piše po jedna za svaku domensku klasu ili osnovnu natklasu domenske hijerarhije. Ako se koristi Metapreslikač uzor tada će se imati samo jedna preslikač klasa. U velikim aplikacijama može se javiti problem održavanja mnoštva metoda za traženje objekata, pa je u takvoj situaciji pametno razdvojiti te metode i grupisati ih po domenskim klasama ili osnovnim natkasama domenske hijerarhije.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
21
Preslikavanje podataka u atribute domenskih objekata
Preslikačke klase moraju imati pristup atributima domenskih objekata kako bi ih punili i čitali. Ovo može predstavljati problem zato što traži postojanje javnih metoda samo za ORM a koje projektanti ne želi u domenskoj logici. Za ovaj problem ne postoji laki odgovor. Jedno od rešenja je da se koristiti približavanje kôda stavljanjem u isti paket, tako da preslikač može pristupiti metodama koje nisu javne. Ali to dovodi do konfuzije i povećava zavisnost a preslikač postaje vidljiv delu sistema kojeg treba da zanimaju samo domenski objekti. Može se koristiti refleksija preko koje je moguće zaobići pravila vidljivosti i pristupi privatnim metodama. To dovodi do pada performansi, što ne mora biti veliki problem. To ipak samo marginalno usporenje u odnosu na ukupno vreme obavljanja neke ORM operacije, u koju ulaze i SQL pozivi. Vezano s ovim je i pitanje stvaranja objekta. U suštini postoje samo dva pristupa. Prvi je da se objekat stvara bogatim konstruktorom, tako da se stvara bar sa svim obaveznim podacima. Drugi je da se stvara prazan objekat a potom da se puni sa obaveznim podacima. Prvom pristupu se daje prednost jer je važno imati dobro stvoreni objekat od početka. To nam omogućava modelovanje klasa kod kojih neki atributi moraju biti konstantni (npr. atribut maticniBroj u klasi Gradjanin). Takav atribut se može modelovati tako što se neće obezbediti metode koje mogu promeniti vrednost tog atributa (npr. neće biti metode setMaticniBroj()), a sama vrednost će se proslediti preko konstruktora. Problem sa bogatim konstruktorom se javlja kod uzajamnih referenci. Ako postoje dva objekta koji se međusobno referenciraju, svaki put kad se jedan učita on će pokušati da učita drugi, koji će pokušati da učita prvi i tako dalje, sve dok se ne potroši stak. Izbegavanje ovoga zahteva poseban kôd, često uz korišćenje lenjog učitavanja. Preslikavanje zasnovano na metapodacima
Jedan od problema koje je potrebno rešiti se tiče načina čuvanja informacija o tome kako atribute domenskih klasa preslikati na kolone u tablama baze. Najlakši, često i najbolji, način da se to obavi je direktno kroz napisani kôd, što zahteva posebnu preslikač klasu za svaki domenski objekat. Te klase preslikavaju kroz dodeljivanje promenljivih i imaju atribute (uobičajeno string konstante) u kojima čuvaju SQL kôd za pristup bazi. Alternativni pristup je preko uzora Preslikavanje metapodacima, gde se metapodaci čuvaju ili u klasi ili u posebnoj datoteci. Velika prednost pristupa sa metapodacima je u mogućnosti da se sve varijacije preslikavanja mogu obuhvatiti kroz podatke bez potrebe za pisanjem dodatnog kôda, bilo korišćenjem generisanja kôda ili refleksijom. Kada koristiti Preslikač
Glavni razlog za korišćenje Preslikača podataka je potreba da se šema baze i objektni model razvijaju i menjaju nezavisno. Uz ovaj uzor je moguće raditi na domenskom modelu potpuno ignorišući bazu kako u projektovanju tako i u procesu razvoja i testiranja. Domenski objekti ne znaju ništa o strukturi baze jer se sva veza uspostavlja posredstvom preslikača. Cena za ovo je postojanje dodatnog sloja kog nema u slučaju uzora Aktivni slog. Stoga se pri razmatranju koji od uzora ORM odabrati treba oceniti složenost poslovne logike. Ako je složena to je pravi znak da se treba koristiti Preslikač podataka. Ukoliko je domenski model jednostavan a sama baza se nalazi pod kontrolom projektanta aplikacije, tada je moguće da domenski objekti pristupaju bazi direktno upotrebom Aktivnog sloga. Ovakvo rešenje u stvari stavlja ponašanje preslikača u same domenske objekte.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
22
Primer implementacije
Sledeći primer je uprošćen do krajnjih granica i služi da se samo stekne uvid u način na koji Preslikač radi u nekom opštem slučaju. Krenućemo sa klasom Student: class Student extends DomenskiObjekat { private String ime; private String prezime; private int godina; ... }
Odgovarajuća šema baze igleda ovako: create table student ( ID int primary key, ime varchar, prezime varchar, godina int, ... )
U ovom primeru se u apstraktni preslikač u smešta ponašanje zajedničko svim preslikačima. Koristi se uzor ponašanja Šablonski metod (eng. Template method). Heš tabela identiteta će biti spojena sa preslikačem radi jednostavnosti. Učitavanje podrazumeva proveru da li je objekat već stavljen u heš tabelu i vađenje podataka iz baze ako nije. abstract class ApstraktniPreslikac { protected Map ucitani = new HashMap(); abstract protected String vratiStatement(); abstract protected DomenskiObjekat izvrsiPunjenje(Long id, ResultSet rs) throws SQLException; protected DomenskiObjekat vratiSablon(Long id) { DomenskiObjekat rez = (DomenskiObjekat) ucitani.get(id); if (rez != null) return rez; PreparedStatement vratiStatement = null; try { vratiStatement = DB.pripremiUpit(vratiStatement()); vratiStatement.setObject(1, id); ResultSet rs = vratiStatement.executeQuery(); rs.next(); rez = napuni(rs);
}
return rez; } catch (SQLException e) { throw new ApplicationException(e); } finally { DB.pocisti(vratiStatement); }
protected DomenskiObjekat napuni(ResultSet rs) throws SQLException { Long id = new Long(rs.getLong(1)); if (ucitani.containsKey(id)) { return (DomenskiObjekat) ucitani.get(id); }
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
23
DomenskiObjekat rez = izvrsiPunjenje(id, rs); ucitani.put(id, rez); } ...
return rez;
}
Zatim pišemo konkretni preslikač: class StudentPreslikac extends ApstraktniPreslikac { ... @Override protected String vratiStatement() { return "SELECT " + KOLONE + " FROM student" + " WHERE id = ?"; } public static final String KOLONE = " id, ime, prezime, godina ..."; public Student vrati(Long id) { return (Student) vratiSablon(id); } @Override protected Student izvrsiPunjenje(Long id, ResultSet rs) throws SQLException { String ime = rs.getString(2); String prezime = rs.getString(3); int godina = rs.getInt(4); ... } ...
return new Student(id, ime, prezime, godina, ...);
}
Primetite da se u klasi ApstraktniPreslikac proverava pretragom u tabeli identiteta da li je traženi objekat učitan. To se čini u metodi vratiSablon() i u napuni(). Mada deluje kao nepotrebno, jer vratiSablon() poziva napuni(), za to postoji opravdanje. U šablonskoj metodi se proverava zato što se može uštedeti na skupom pristupu bazi ako je objekat već učitan. A u napuni() metodi se radi zato što nju neće samo šablonska metoda pozivati. Npr. ako treba da pronađemo i vratimo sve studente koji zadovoljavaju uslov pretrage ne možemo biti sigurni da su svi ti studenti učitani pa se mora izvršiti upit nad bazom. Time je moguće izvući neke redove iz tabele koji odgovaraju studentima koji su već učitani, pa se mora proveriti u tabeli identiteta. public abstract class ApstraktniPreslikac { ... protected List napuniSve(ResultSet rs) throws SQLException { List rez = new ArrayList(); while (rs.next()) rez.add(napuni(rs)); } ... }
return rez;
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
24
class StudentPreslikac extends ApstraktniPreslikac { ... private static String vratiPoPrezimenuStatementString = "SELECT " + KOLONE + " FROM student " + " WHERE prezime ILIKE ?" + " ORDER BY prezime"; public List vratiPoPrezimenu(String prezime) { PreparedStatement stmt = null; ResultSet rs = null; try { stmt = DB.pripremiUpit(vratiPoPrezimenuStatementString); stmt.setString(1, prezime); rs = stmt.executeQuery();
} ...
return napuniSve(rs); } catch (SQLException e) { throw new ApplicationException(e); } finally { DB.pocisti(stmt, rs); }
}
Ovakvi načinom pisanja metode koja vraća objekte u svakoj potklasi klase ApstraktniPreslikac, zahteva mnogo ponavljanja jednostavnog kôda, što možemo sprečiti pravljenjem opštije metode. public abstract class ApstraktniPreslikac { ... public List vratiVise(Upit upit) { PreparedStatement stmt = null; ResultSet rs = null; try { stmt = DB.pripremiUpit(upit.sql()); for (int i = 0; i < upit.argumenti().length; i++) stmt.setObject(i+1, upit.argumenti()[i]); rs = stmt.executeQuery();
} ...
return napuniSve(rs); } catch (SQLException e) { throw new ApplicationException(e); } finally { DB.pocisti(stmt, rs); }
} interface Upit { String sql(); Object[] argumenti(); }
Sada možemo iskoristiti postavljenju osnovu da implementiramo pretragu po imenima kao unutrašnju klasu.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
25
class StudentPreslikac extends ApstraktniPreslikac { ... public List vratiPoPrezimenu2(String prezime) { return vratiVise(new UpitPoPrezimenu(prezime)); } static class UpitPoPrezimenu implements Upit { private String prezime; public UpitPoPrezimenu(String prezime) { this.prezime = prezime; } public String sql() { return "SELECT " + KOLONE + " FROM student " + " WHERE prezime ILIKE ?" + " ORDER BY prezime"; }
} ...
public Object[] argumenti() { return new Object[] { prezime, }; }
}
Slično rešenje se može primeniti i na drugim mestima gde se javlja ponavljanje kôda pri pozivu upita. Programski kôd za ažuriranje je poseban za svaki podtip. class StudentPreslikac extends ApstraktniPreslikac { ... private static final String azurirajStatementString = "UPDATE student " + " SET ime = ?, prezime = ?, godina = ? " + " WHERE id = ?"; public void azuriraj(Student student) { PreparedStatement stmt = null;
} ...
try { stmt = DB.pripremiUpit(azurirajStatementString); stmt.setString(1, student.getIme()); stmt.setString(2, student.getPrezime()); stmt.setInt(3, student.getGodina()); stmt.setObject(4, student.getID()); stmt.execute(); } catch (Exception e) { throw new ApplicationException(e); } finally { DB.pocisti(stmt); }
}
Za ubacivanje objekta u bazu deo kôda se može smestiti u sloj nadtipa.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije public abstract class ApstraktniPreslikac { ... abstract protected String ubaciStatement(); abstract protected void izvrsiUbaci(DomenskiObjekat subjekat, PreparedStatement ubaciStatement) throws SQLException; public Long ubaci(DomenskiObjekat subjekat) { PreparedStatement ubaciStatement = null; try { ubaciStatement = DB.pripremiUpit(ubaciStatement()); subjekat.setID(vradiSledeciDBId()); ubaciStatement.setObject(1, subjekat.getID()); izvrsiUbaci(subjekat, ubaciStatement); ubaciStatement.execute(); ucitani.put(subjekat.getID(), subjekat); return subjekat.getID();
} ...
} catch (SQLException e) { throw new ApplicationException(e); } finally { DB.pocisti(ubaciStatement); }
} class StudentPreslikac extends ApstraktniPreslikac { ... @Override protected String ubaciStatement() { return "INSERT INTO student VALUES (?, ?, ?, ? ...)"; } @Override protected void izvrsiUbaci(DomenskiObjekat subjekat, PreparedStatement stmt) throws SQLException { Student student = (Student) subjekat;
} ...
stmt.setString(2, student.getIme()); stmt.setString(3, student.getPrezime()); stmt.setInt(4, student.getGodina()); ...
}
Kôd za brisanje objekta iz baze je najmanje složen. public abstract class ApstraktniPreslikac { ... abstract protected String izbaciStatement(); public void izbaci(DomenskiObjekat subjekat) { PreparedStatement stmt = null; try { stmt = DB.pripremiUpit(izbaciStatement()); stmt.setObject(1, subjekat.getID()); stmt.execute(); ucitani.remove(subjekat); } catch (SQLException e) {
26
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
} ...
27
throw new ApplicationException(e); } finally { DB.pocisti(stmt); }
} class StudentPreslikac extends ApstraktniPreslikac { ... @Override protected String izbaciStatement() { return "DELETE FROM student WHERE id = ?"; } ... }
Preslikači u praksi
U realnosti se uvek koristi gotov ORM alat zasnovan na preslikavanju podataka. Postoji mnoštvo komercijalnih i besplatnih implementacija za mnoge jezike: TopLink, Hibernate, OpenJPA, Nhibernate, WebObjects, ObjectMapper.Net itd. U njima se ne piše sirovi SQL kao što je u ovom primeru dato (osim ako se ne radi neko posebno optimizovanje performansi), alat sam apstrahuje razne SQL sisteme sa svojim specifičnostima u sintaksi. Zajedničko za njih je da ne zahtevaju da domenski objekti nasleđuju zajedničku klasu. Dobar deo podataka potrebnih za preslikavanje se saznaje refleksijom. Često se kao pomoć refleksiji koristi konvencija u imenovanju atributa i metoda. Ipak refleksija i konvencija nisu svemogući i mogu pomoći samo da se uspostavi podrazumevano ponašanje. Npr. da se na osnovu naziva domenskog objekta Student asociraju sa redovima tabele istog naziva STUDENT. Slično se na osnovu naziva atributa traže kolone istog imena. Ali dovoljno je da postoji nasleđena baza sa tabelom kojoj je ime u množini (STUDENTI) pa da podrazumevano ponašanje ne bude primenjivo. Ili da imamo asocijaciju 1-M koja se podrazumevano učitava samo po potrebi (tzv. lenjo učitavanje) a da je priroda problema takva da nam je potrebno učitavanje u trenutku vađenja objekta na strani „1“ te veze. Mi to moramo na neki način saopštiti ORM sloju. Postoje dva načina na koja se te informacije obezbeđuju. Prvi je da se stvaraju konfiguracioni objekti kojim se prosleđuju metapodaci. To je klasičan, programerski način, kroz kôd konkretnog jezika, vrlo čist i jasan.. Drugi način je deskriptivan. U praksi se deskripcija može raditi u konfiguracionim fajlovima, gde su posebno popularni opisi u raznim XML šemama. Takav opis je pogodan za mašinsko generisanje, a može biti zamoran za programera. Zato se u poslednje vreme javlja alternativni pristup u kome se deskripcija stavlja u sam kôd domenskih objekata. To se radi pomoću jezičkih elemenata koji se u javi zovu anotacije, u Pythonu dekoracija. Jedan od mogućih problema ovakvog pristupa je visoka spregnutost. Dok je XML fajl lako izmeniti kôd drugog pristupa se mora dirati sam kôd, što je skuplja operacija.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
28
4. Python i SQLAlchemy U ovoj glavi se govori o Pythonu kao jeziku u kome i za kojeg je implementiran SQLAlchemy, kao i o njegovoj filozofiji koja je snažno uticala na tu ORM biblioteku. Zatim se u ovoj glavi daju osnovni podaci o tome šta je SQLAlchemy i za šta se sve može koristiti.
4.1 Šta je Python U mnoštvu savremenih jezika, jedan se izdvaja. Ne zato što ima neko novo rešenje, kakvo drugi jezici nastali pre ili kasnije nemaju, već zato što je dobro projektovan i što ima jedinstvenu filozofiju. Python je objektno-orijentisani, imperativni i funkcionalni jezik čiji je autor Guido van Rossum, tada naučni radnik u čuvenom Nacionalnom institutu za informatiku u Amsterdamu (Centrum voor Wiskunde en Informatica, CWI). Python je skript jezik, što znači da se kôd ne prevodi već se direktno izvršava u intepreteru. U ovom jeziku se tip podatka otkriva u toku interpretiranja, ali kad se podatku dodeli tip on se više u programu ne menja. Zbog toga se tip promenljive ne deklariše, on se sam odredi, a svaka promena tipa se mora eksplicitno izvršiti funkcijama za konverziju. ZANIMLJIVOST: Ime jezika potiče od britanske televizijske komedije Monty Python’s Flying Circus, čime je Guido želeo da naglasi svoj osnovni cilj pri pravljenju novog jezika, da programiranje postane zabavno kao i serija po kojoj je jezik nazvan. Python je napravljen sa minimalnom sintaksom s ciljem da bude što lakši za učenje. Jezičke konstrukcije su vrlo prirodne i bliske pseudokodu. Namera autora je bila da napravi jezik koji će programere usmeravati da pišu kvalitetan kôd, da u tom pisanju imaju jedan očigledan način za pretvaranje zamisli u sam kôd. Zato se blokovi prave uvlačenjem kôda, kako bi se sprečilo prljavo programiranje, ne postoji switch case grananje kao odlika lošeg stila programiranja, do while petlja je proglašena nepotrebnom itd. Python je jedan od retkih jezika koji su nastali u akademskim uslovima a da su postigli komercijalni uspeh. Dugo je važilo pravilo da akademske ustanove prave čiste jezike, sa novinama i jakom filozofskom pozadinom, ali da takve jezike retko ko koristi van samih univerziteta. Primer za to su Pascal, Simula, Algol, Haskell, Modula itd. S druge strane, prljavi, brzi, sa gledišta nauke slabi jezici za koje je zajedničko da imaju pragmatičan pristup, jezici pravljeni od hakera za hakere, su bivali prihvaćeni među profesionalcima. Tako je bilo jezicima C, C++, Perl, PHP, Java itd. Python je jedinstven spoj čistog jezika, jasno zamišljenog, dobro projektovanog i implemenitranog, lakog za upotrebu, a opet dovoljno moćnog i brzog da bi bio prihvaćen među programerima. NAPOMENA: Pogledajte Dodatak B: Instaliranje korišćenih alata na strani 97 za uputstva u vezi sa instaliranjem Python interpretera. Za dublje upoznavanje sa širokim mogućnostima i sa izražajnošću Pythona preporučujem besplatnu knjigu Dive into Python koja se može skinuti sa: http://diveintopython.org/ Po TIOBE indeksu [13] Python se nalazi na 6. mestu popularnosti jezika a vrednost indeksa sve više raste. U mnogim školama se uči kao prvi jezik, zamenjujući ostareli Pascal i preterano složenu Javu. Python je danas ono što je BASIC bio ranije, jezik za sve, samo što je mnogo savremeniji i snažniji. Vrlo često ga upotrebljavaju naučnici kojima programiranje nije nešto što spada u domen, ali im je neophodno za razne analize. Zato se Python koristi mnogo u biološkim naukama, npr. za analizu gena, u matematičkim naukama za simulacije, za optimizaciju, kao kompjuterski algebarski sistem,
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
29
alat za numeričku analizu, veštačku inteligenciju, itd. Python je odličan „lepak jezik“, jezik kojim se povezuju postojeći programi, pa im se preko Pythona daje zajednički interfejs. Python se često koristi kao skript mašina u raznim programima: Maya i Blender za 3D animacije, Gimp, Inkscape za grafiku, Civilization IV za kontrolu igre itd. Ipak Python pokazuje pravu snagu u projektima koji su u potpunosti zasnovani na njemu. BitTorrent klijent je izvorno napisan u njemu. Prvi web robot za Google je pisan u Pythonu, a ta kompanija i dalje koristi mnogo njegovog kôda, pravi nove aplikacije i ulaže u ekosistem oko Pythona. Tako je i Guido van Rossum već nekoliko godina zaposlen u toj kompaniji. Youtube je zasnovan na Pythonu a koristi ga i NASA. Instaler za Red Hat linux je napisan u Pythonu i šaljivo nazvan Anaconda. Python postoji u nekoliko implementacija. Referentna je ona u C jeziku koja postoji za sve važnije platforme. Pored nje postoji i verzija koja se izvršava na JVM i koja se zove Jython, a takođe postoji i verzija za .NET pod nazivom IronPython. Verzije za JVM i .NET pored toga što nude sve što i CPython, omogućavaju visoku integraciju sa okruženjem u kome se izvršavaju. To podrazumeva mogućnost prevoda na bajt kod, mogućnost korišćenja biblioteka u JVM i .NET jezicima, čak i mogućnost nasleđivanja klasa u tim jezicima. Te verzije podržavaju SUN i Microsoft time što plaćaju programere koji rade na njihovom razvoju. Pogodnost koju ima Python, kao i svi interpretirani jezici, je interaktivni inerpreter gde je moguće kucati liniju po liniju kôda i tako učiti jezik ili proveravati neke ideje direktno u kôdu. Podrška za Python postoji u Eclipse programskom okruženju, a uvodi se i u Visual Studio i Netbeans. Postoji i nekoliko specijalizvanih okruženja za Python.
4.2 Filozofija Pythona Python ima samo jednu osobinu koja ga izdiže iznad ostalih jezika. To je njegova jedistvena filozofija.
4.2.1 Programer u središtu pažnje Mnogi jezici su nastajali sa nekom idejom i čitav jezik je bio podvrgnut toj ideji, praktično dogmi. Na primer u LISP-u se smatra da se svi podaci mogu predstaviti u vidu ugnježdenih lista pa se čitav program sastoji iz operacija nad listama: (if (= (* (+ 1 2 3) -2) -12) (print "tacno") (print "netacno"))
Tu se na listi brojeva izvršavaju matematičke operacije (operator je prvi član liste), zatim se testira jednakost 2 elementa liste i u zavisnosti od nje prikazuje odgovarajuća poruka. SmallTalk smatra da je sve u objektima pa se i sabiranje dva broja shvata kao slanje poruke (kako se u SmallTalku naziva metoda) objektu klase broj uz prosleđivanje drugog broja. Npr: 1 + 5 * 6 = 31 ifTrue: ['Matematika pobeđuje' printNl] ifFalse: ['OOP dogma pobeđuje' printNl]!
Ovde se objektu 1 šalje poruka + sa argumentom 5. Rezultat toga je novi objekta kojem se šalje poruka * sa argumentom 6 pa dobijeni objekta ima vrednost 36, prkoseći matematici. Zatim se tom broju šalju poruka = sa argumentom 31 čiji je rezultat false objekat. On dobija dve poruke. Prva je ifTrue sa blokom koda kao argumentom. Ona se neće izvšiti na false objektu, ali zato sledeća poruka hoće. Izvšava se blok u kojem se string objektu šalje poruka printNl koja čini da se odštampa 'OOP dogma pobeđuje'. Java sa druge strane misli da je sve klasa pa čak i kad se ne koriste objekti.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
30
Ali takve filozofije su se pokazale suviše neprilagodljivim zbog toga što se primenjuju u svakom delu jezika i tamo gde su od koristi i tamo gde smetaju. Od takvog fundamentalizma ispaštaju programeri, u slučaju SmallTalk-a čak i matematika. Da bi predavač u javi pokazao najprostiji primer („Zdravo svete“) ima dve mogućnosti. Prva je da izbegne razgovor o „sitnicama“ kao što su: šta je klasa, šta znači: public, static, out, println... i da se ponaša po sistemu „kad porasteš kazaće ti se samo“. Druga mogućnost je da objasni sve i time zbuni i uplaši početnike. A sve to zbog dogmatskog držanja autora jave da program bez klase ne postoji. Još je gora situacija u jezicima koji ni nemaju filozofiju ili je bar u početku nisu imali. Propusti koji se naprave u stvaranju jezika teško se nadoknađuju kad jezik zaživi i kad već hiljade aplikacija zavise od od tog jezika. Kako kaže poslovica: što se grbo rodi vrijeme ne ispravi. Primer takvog jezika je PHP. On je nastao bez podrške za module (u javi pakete, imenske prostore u C# itd.), bez podrške za OO programiranje. Sve je to naknadno ubačeno u jezik ali sa manjim uspehom i uticajem nego da je postojalo od početka. Slično u javi gde npr. već deceniju inženjeri ne uspevaju da napišu upotrebljivu biblioteku za rad sa kalendarom i vremenom, ili kasnije dodavanje mogućnosti generičkog programiranja iako je još 1996. godine Bertrand Meyer, najveći teoretičar i praktičar OO programiranja, pisao da javi upravo to nedostaje [14]. Python je primer jezika koji nije dogmatski. Jedina dogma koja postoji je da programiranje mora biti lako i zabavno. Programer je u centru svih odluka u vezi sa dizajnom jezika. Iz svih postojećih dogmi programiranja uzima se ono što je najkorisnije i što najviše doprinosi lepoti programiranja. Kao ni jedan drugi jezik, Python je alat u kome se ideje lako pretaču u dela. Cilj Pythona je da se što manje oseti, da se što manje misli na njega. Čim je neki jezik u prvom planu to znači da samo smeta toku od ideje do njene realizacije kroz kôd. Python jeste objektno-orijentisani jezik toliko da ne postoje prosti tipovi a čak su i funkcije objekti ali je moguće pisati i proceduralni kôd. Time se „Zdravo svete“ svodi na liniju: print 'Zdravo svete'
Tamo gde je funkcionalno programiranje prirodno postoji i podrška za njega. Po tome je Python sličan LISP-u. Tako se sa listama mogu primeniti razne funkcije, moguće je agregirati, filtrirati i generisati liste u jednoj liniji.
4.2.2 Osnovni principi Pythona Python je sazdan na tri jasna principa. Princip najmanjeg iznenađenja
Jedan od principa u jeziku je da za svaki zadatak treba da postoji očigledan način da se to ostvari. Taj zahtev se često naziva Principom najmanjeg iznenađenja, ako nešto iznenađuje iznova programere, znači da nešto nije u redu. Primer kršenja tog principa u najpopularnijem jeziku je nalaženje broja elemenata. Ako je u pitanju niz koristi se atribut length, na stringu je to istoimena metoda, a na kolekcijama metoda size(). Python je redak primer jezika kod koga se događa da u narednoj verziji jezika bude manje konstrukcija i modula. Ako se zaključi da se nešto može uraditi na dva načina, tada se izabere onaj bolji a lošiji se postepeno, kroz nekoliko narednih verzija, označi zastarelim i na kraju izbaci. Ako se nešto novo unosi u jezik prolazi se dug proces razmatranja da li je to vredno unošenja. Teži se tome da većina funkcionalnosti bude u bibliotekama, jer je tako lakše raditi na njoj i razvijati je, a jezik ostaje jednostavan i čist. Tako npr. Python nema direktnu podršku za regularne izraze kao što to ima većina skript jezika, ali zato od samog nastanka ima standardnu biblioteku za tu namenu.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
31
Posledica neiznenađivanja programera je lakoća i brzina programiranja. Python programeri su izuzetno produktivni. Sam kôd je 3 do 5 puta kraći od ekvivalentnog u javi. I drugi jezici omogućavaju brzo programiranje i kratak kôd, ali za razliku od njih u Pythonu se to ne postiže na štetu čitljivosti kôda. Python projektanti više vode računa da kôd bude razumljiv programeru koji ga je pisao, kad ga bude ponovo čitao posle nekog vremena. I ne samo onima koji su pisali kôd već i onima koji nisu učestvovali u njegovom pisanju. Glavni trošak u životnom ciklusu neke aplikacije nije u pisanju već u održavanju. Perl programeri priznaju da posle nekoliko meseci ni sami ne mogu shvatiti šta su programirali bez ulaganja velikog napora. Zato je Python pogodan i za projekte srednje veličine i dužeg životnog ciklusa. Princip isporuke sa baterijama
Drugi princip je da jezik mora doći sa baterijama (eng. Battery included). Znači kao zaokruženi proizvod, spreman za upotrebu. To se realizuje kroz ogromnu standardnu biblioteku. Nešto poput same jave koja dolazi sa mnoštvom paketa i Python dolazi sa mnoštvom modula za najrazličitiju upotrebu. I van standardnih modula, Python spada u jezike koji imaju najviše biblioteka i okvira koje su korisnici izradili i pustili na slobodnu upotrebu. Za Web aplikacije (Django, TurboGears, Zope, Plone), za ORM (SQLObject, ZopeDB, SQLAlchemy), za obradu XML (BeautifulSoup, ElementTree) za aplikacije sa grafičkim interfejsom (wxPython, PyQT, PyGTK) itd. Skoro da ne postoji oblast u kojoj se ne može naći već gotova biblioteka za Python. Princip eksplicitnosti
Treći princip je Princip eksplicitnosti. Po ovom principu sve treba biti otvoreno, vidljivo i jasno. Da se ništa ne dešava magično, podrazumevano, intervencijom kompajlera i interpretera. Tako se npr. referenca objekata nad kojim se izvršava metoda ne prenosi magično u tu metodu kao this u javi, niti se magično nasleđuje vrhovna natklasa kao što se dešava sa Object klasom. Ideje kojima je prožet Python su toliko snažne da se šire i osećaju u čitavom Python okruženju. Čak je skovana reč pythonizam kojom se taj duh označava. Prepoznaćemo ove ideje u samom SQLAlchemy alatu. ZANIMLJIVOST: Tim Peters je napisao Zen Pythona koji se sastoji iz stavova koji prožimaju jezik. Tekst se može dobiti kad se u interpeteru otkuca import this a neki od stavova su: ●
lepo je bolje od ružnog
●
eksplicitno je bolje od implicitnog
●
prosto je bolje od složenog, složeno je bolje od komplikovanog
●
čitljivost je važna
●
specijalni slučajevi nisu toliko specijalni da ponište pravilo, ali ipak praktičnost pobeđuje čistotu
●
pri dvoumljenju odolite iskušenju da pogađate, treba postojati jedan i poželjno samo jedan, očigledan način da se nešto uradi
●
ako je implementaciju teško objasniti, ona je ona loša, ako je implementaciju lako objasniti, onda je ona možda i dobra.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
32
4.3 Šta je SQLAlchemy SQLAlchemy je SQL alat i objektno-relacioni preslikač za Python. Napravljen je da projektantima programa pruži punu moć i fleksibilnost SQL-a. Današnje kolekcije objekata se sve manje ponašaju kao tabele i redovi, pa veličina i perfromanse sve više postaju problem. Sve se više koristi apstrakcija u domenskom modelu. SQLAlchemy teži da odgovori na oba ova izazova. SQLAlchemy ne posmatra bazu samo kao skup tabela, već je vidi kao mašinu za relacionu algebru. To je objektno-relacioni preslikač koji omogućava da se klase preslikaju u bazu na više od jednog načina. SQL konstrukcije ne rade samo upite nad jednom tabelom, moguće je spajati više tabela JOIN operacijom, praviti podupite i unije. Dakle, relacije u bazi i domenski objekti se mogu lepo razdvojiti od početka, omogućavajući da se obe strane razvijaju do granica svojih mogućnosti. ORM deo SQLAlchemyja je implemenitran preko uzora Preslikač podataka. Koristi se pristup preko metapodataka, gde se metapodaci prosleđuju u obliku konfiguracionih objekata. Osim dela za ORM postoji i deo za direktan rad sa SQL-om preko Python metoda koji se zove SQL Expression Lanaguage (SQL-EL). Programeri mogu da koriste ova dva dela odvojeno ili da ih kombinuju. Sam ORM deo je izgrađen od SQL-EL, konfigurisanje preslikača i zadavanje upita se radi preko njega. SQLAlchemy je projekat slobodnog kôda sa MIT licencom. Razvoj traje tri godine i vrlo je dinamičan, gotovo svaki mesec se izbacuju nove verzije sa ispravkama, a u proseku na 6 meseci izađe i verzija sa novim dodacima i krupnijim izmenama. SQLAlchemy sadrži dijalekte za SQLite, PostgreSQL, MySQL, Oracle, MS-SQL, Firebird, MaxDB, MS Access, Sybase i Informix relacione sisteme za upravljanje bazama podataka. IBM je izdao DB drajver. Da bi se SQLAlchemy mogao raditi sa nekom kojom bazom potrebno je da za nju postoji implemenitiran standardni interfejs za rad za bazama u Pythonu pod nazivom DB-API 2.0 (Python Database API Specification v2.0). SQLAlchemy je napravljen tako da se prilagođava širokom spektru potreba. Dovoljno je moćan i za najsloženije zadatke kao što su trenutno učitavanje grafa objekata i njihovih zavisnosti preko upita spajanja i sinhronizacija čitavog grafa sa bazom u jednom koraku, simultani rad sa nekoliko baza, korišćenje dvofaznih transakcija kao i ugnježdenih transakcija itd, što se naziva skalabilnost ka gore. A opet je skalabilan i ka dole što znači da je izuzetno lagan za osnovne zadatke kao što su stvaranje SQL upita iz izraza u Pythonu i učitavanje objekata iz baze i upisivanje izmena na objektu nazad u bazu, što se naziva CRUD ciklus (Create, Retrieve, Update, Delete). Još jedna izvor moći ovog alata leži u njegovoj modularnosti i proširivosti. Različiti delovi SQLAlchemyja se mogu koristiti nezavisno jedni od drugih i mogu se proširiti dodacima na raznim mestima predviđenim za proširivanje. Već sada postoji mnoštvo dodataka koji su razvijeni nezavisno od projekta SQLAlchemyja. Tu ima dodatka koji pomažu sinhronizovanje promena u domenskom modelu sa šemom baze, koji dodaju deklarativni sloj pa se metapodaci daju kroz dizajn domenskog objekta, pretvaraju SQLAlchemy u Aktivni slog, generišu HTML forme za domenske objekte u Web aplikacijama itd. Iako je mlad projekat, SQLAlchemy se već koristi kao osnova nekoliko popularnih Web okvira uključujući Pylons, Turbogears i Grok, a započeta je integracija za Django, Trac i Zope. NAPOMENA: Pogledajte Dodatak B: Instaliranje korišćenih alata na strani 97 za uputstva u vezi sa instaliranjem SQLAlchemy paketa
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
33
5. Vodič kroz SQLAlchemy U ovom poglavlju daje se brzi pregled većine mogućnosti SQLAlchemyja, dok se u narednom ide u detaljni opis svih pojedinosti vezanih za rad sa ORM delom. Pošto je oblast obimna ovo je najbolji način predstavljanja za lako sticanje znanja o SQLAlchemyju. Prvo se obrađuje SQL-EL jer je gradivna osnova za ORM. Primeri su predstavljeni kao ulaz i izlaz iz komandne linije interpretera.
5.1 Uvod u SQL Expression Language SQL Expression Language služi da se kroz Python kôd, na programerima prirodni način, komunicira sa bazom. SQL-EL zapravo pretvara Python izraze u SQL i obrnuto. U programiranju na klasičan način sa bazama se radi pomoću SQL kôda koji je strano telo u programu, praktično običan string. S druge strane pomoći SQLAlchemyja je moguće iskoristiti svu podršku koju pružaju savremena programska okruženja u vidu kompletiranja kôda i pronalaženje grešaka pri samom programiranju. Ako se napravi i najmanja omaška u SQL stringu ta će se greška otkriti tek u vreme izvršenja programa a sam izveštaj o problemu će biti od male koristi. Često će ukazivati na pogrešnu lokaciju greške, neretko će izostati informacija o tome šta je u SQL kôdu izazvalo problem... Običan SQL string može sadržati specifičnosti za neku SQL mašinu. Moguće je držati se discipline i koristiti standardni SQL, ali to iziskuje dodatni napor i gubljenje nekih finih podešavanja koja su specifična nekom sistemu za upravljanje bazama podataka. SQL-EL pravi apstrakciju i preko uzora Strategija (eng. Strategy pattern) implementira posebne SQL stringove za SQLAlchemy izraze u zavisnosti od vrste SQL mašine sa kojom se radi. Strategije se u SQLAlchemyju prave nasleđivanjem sqlalchemy.engine.Dialect klase. Uz instalaciju dolaze odgovarajuće Dialect potklase za SQLite, PostgreSQL, MySQL, Oracle, MS-SQL, Firebird, MaxDB, MS Access, Sybase i Informix relacione sisteme.
5.1.1 Opisivanje veze s bazom Za uspostavljanje veze sa bazom koristi se funkcija create_engine(): >>> from sqlalchemy import create_engine >>> engine = create_engine('postgres://fon:lozinka@veles:5432/sqlalchemy', echo=True)
Funkciji se prosleđuje string URL po standardu RFC-1738. Prvi deo ukazuje na dijalekat koji će se koristiti, zatim sledi korisničko ime i lozinka kojom se pristupa bazi, posle znaka „ @“ je naziv računara na kome je SUBP i broj porta na kojem SUBP očekuje vezu. Na samom kraju stringa je naziv baze. Parametar echo određuje ispisivanje dodatnih inforamacija preko standardnog logging mehanizma u Pythonu, koji podrazumevano ispisuje poruke u konzolu. Tako možemo videti kakav je SQL kôd stvoren što je korisno u ispravljanju grešaka, optimizovanju programa i učenju. Zato tu opciju uključujemo. Povratna vrednost funkcije je objekat klase sqlalchemy.engine.base.Engine.
5.1.2 Opisivanje i stvaranje tabela U SQL-EL kolone se najčešće predstavljaju preko objekata klase sqlalchemy.schema.Column koji su povezani sa objektom sqlalchemy.schema.Table. Skup Table objekata i njihovih zavisnih objekata su metapodaci baze i iz njih se može generisati šema baze. Metapodaci su u SQLAlchemyju predstavljeni klasom MetaData. Da bi se onemogućilo postojanje tabele koja nije pridružena MetaData objektu, pri pozivu konstruktora Table objekta mora se proslediti pojava klase MetaData. U SQLAlchemyju je moguće krenuti i obrnutim putem: da se automatski uveze skup Table objekata iz postojeće šeme (taj postupak je opisan na 69. strani u poglavlju 6.1.3 Refleksija tabela). Znajući
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
34
sve ovo možemo opisati jednu šemu: >>> >>> >>> ... ... ... ... >>> ... ... ... ...
from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey metadata = MetaData() klijenti = Table('klijenti', metadata, Column('id', Integer, primary_key=True), Column('ime', String(40)), Column('puno_ime', String(100)), ) adrese = Table('adrese', metadata, Column('id', Integer, primary_key=True), Column('klijent_id', None, ForeignKey('klijenti.id')), Column('email_adr', String(50), nullable=False) )
Ova šema se može opisati sledećim dijagramom objekata i veza (eng. Entity Relationship diagram):
Slika 6: dijagram objekata i veza za 1-M relaciju PYTHON OBJAŠNJENJA: Konstruktor Table objekta je odličan primer mogućnosti prenosa argumenata. Potpis konstruktora je: __init__(self, name, metadata, *args, **kwargs) što znači da ima dva obavezna argumenta kod poziva: name i metadata. Treći parametar prihvata promenljiv broj argumenata (što se u javi naziva varargs i postoji od jave 5) i preko njega prosleđujemo Column objekte. Poslednji parametar predstavlja promenljiv broj imenovanih argumenata. Taj parametar u gornjem slučaju nije upotrebljen, ali se primer njegovog korišćenja može videti u poglavlju 6.1.3 Refleksija tabela na strani 69. Objašnjenje o parametrima Python funkcija i metoda možete pročitati na strani 93 u poglavlju Dodatak A: Uvod u Python. Python ima mnogo razvijeniji sistem paketa, modula i sadržaja modula nego drugi jezici. Treba znati da svaki paket i sam predstavlja modul (poseban samo po tome što sadrži u sebi druge module), pa i sam može imati kao sadržaj klase, funkcije i promenljive. Osim toga svaki modul može da u svoj sadržaj uključi klase, funkcije i promenljive definisane u drugim modulima. Tako u prethodnom primeru imamo npr. klasu sqlalchemy.schema.Column (znači iz paketa sqlalchemy, modula schema), koju smo importovali sa: from sqlalchemy import Column. Dakle iz modula/paketa sqlalchemy, što znači da je klasa Column u tom modulu samo importovana dok se stvarna definicija nalazi u modulu sqlalchemy.schema. Na objektu u kome se čuvaju metapodaci pozivamo metod create_all() koji generiše šemu, opisanu preko Table objekata, na bazi s kojom smo u vezi preko Engine objekta: >>> metadata.create_all(engine) SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name' : 'klijenti'} SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s { 'name' : 'adrese'}
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
35
CREATE TABLE klijenti ( id SERIAL NOT NULL, ime VARCHAR(40), puno_ime VARCHAR(100), PRIMARY KEY(id) ) COMMIT CREATE TABLE adrese ( id SERIAL NOT NULL, klijent_id INTEGER, email_adr VARCHAR(50) NOT NULL, PRIMARY KEY(id), FOREIGN KEY(klijent_id) REFERENCES klijenti(id) ) COMMIT
U generisanom SQL kôdu se vidi da SQLAlchemy prvo proverava za svaku tabelu da li već postoji tabela sa istim imenom. To radi preko upita nad tabelama kataloga baze što je usko povezano sa konkretnim SUBP-om. Zatim se DDL naredbama stvaraju tabele. PYTHON OBJAŠNJENJA: SQL kôd koji generiše SQLAlchemy često predstavlja string šablon koji se puni podacima iz prosleđene heš mape. Za to se koristi operacija formatiranja odnosno interpolacije string šablona. Šablon se stvara tako što se sa znakom „ %“ označe mesta koja će se naknadno popunjavati podacima iz mape. Zatim se u zagradici stavi ključ pomoću kog će se u mapi tražiti vrednost za umetanje i na kraju se stavi oznaka tipa u koji se vrednost iz mape konvertuje („s“ za string). SQLAlchemy pri ispisivanju generisanog SQL-a prvo prikaže string šablon, a u novom redu mapu kojom puni šablon. Tako se ova linija iz prethodnog primera: SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname=current_schema() AND lower(relname)=%(name)s {'name': 'klijenti'}
posle interpolacije sa prikazanom mapom pretvara u: SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname=current_schema() AND lower(relname)='klijenti'
5.1.3 Insert izrazi INSERT SQL izraz je instanca sqlalchemy.sql.expression.Insert klase i obično se stvara iz Table objekta nad kojim se želi primeniti INSERT: >>> ins = klijenti.insert()
Pozivom str() funkcije možemo videti stvoreni SQL kôd: >>> str(ins) 'INSERT INTO klijenti (id, ime, puno_ime) VALUES (:id, :ime, :puno_ime)'
Insert navodi svaku kolonu tabele klijenti, ali to se može ograničiti preko values parametra insert() metode gde se navodi asocijativni niz (heš tabela) sa imenima kolona kao ključevima i vrednostima tih kolona: >>> ins = klijenti.insert(values={'ime':'Pera', 'puno_ime':'Pera Perić'}) >>> str(ins) 'INSERT INTO klijenti (ime, puno_ime) VALUES (:ime, :puno_ime)'
Kao što se vidi još uvek stvarni podaci nisu popunili parametre :ime i :puno_ime u VALUES iskazu. Podaci se čuvaju ali će se uneti u SQL kôd tek pri stvarnom izvršenju. Ipak možemo zaviriti u njih: >>> ins.compile().params
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
36
{'puno_ime': 'Pera Perić', 'ime': 'Pera'}
Samo izvršavanje INSERT naredbe se može uraditi na više načina. Možemo od objekta Engine zatražiti konekciju u obliku sqlalchemy.engine.base.Connection objekta pa preko nje izvršiti upis u bazu: >>> conn = engine.connect() >>> resultat = conn.execute(ins) SELECT nextval('"klijenti_id_seq"') None INSERT INTO klijenti (id, ime, puno_ime) VALUES (%(id)s, %(ime)s, %(puno_ime)s) {'puno_ime': 'Pera Perić', 'ime': 'Pera', 'id': 1L} COMMIT
Vide se tri SQL naredbe. U prvoj naredbi se za kolonu id koja je tipa serial generiše sledeća vrednost iz sekvence klijeti_id_seq. Serial je tip podatka PostgreSQL sistema koji je ekvivalentan tipu autonumber u MS Accessu tj. identity u MS SQL Serveru, kolone tog tipa se popunjavaju jedinstvenim generisanim brojem i služe kao surogat ključ tabele. PostgreSQL to realizuje tako što za svaku serial kolonu PostgreSQL automatski pravi sekvenca pomoću koje se dobijaju ti jedinstveni brojevi. Podrazumenvano sekvenca počinje brojem 1 i korak pravljenja niza je 1, dakle 1, 2, 3, ... , n, n+1, ... U drugoj naredbi se popunjava lista parametara sa već zadatim vrednostima uz dodatak parametara id sa vrednošću dobijenom iz sekvence. Na kraju u trećoj naredbi potvrđujemo transakciju sa COMMIT. U slučaju nekog drugog SUBP naredbe bi bile prilagođene tom sistemu. Promenljiva result je referenca na objekat klase sqlalchemy.engine.base.ResultProxy. Objekti te klase su analogni DBAPI kursorima. U slučaju naredbe INSERT mogu se saznati neke važne informacije, kao npr. vrednost generisanog primarnog ključa: >>> result.last_inserted_ids() [1L]
Rezultat je lista zato što SQLAlchemy podržava složene generisane primarne ključeve. Prethodni primer ipak nije uobičajen način za izvršenje INSERT naredbe. Obično se vrednosti parametara ne vezuju direktno za Insert objekat, već se prosleđuju execute() metodi Connection objekta: >>> ins = klijenti.insert() >>> conn.execute(ins, ime='Mika', puno_ime='Mika Mikić') SELECT nextval('"klijenti_id_seq"') None INSERT INTO klijenti (id, ime, puno_ime) VALUES (%(id)s, %(ime)s, %(puno_ime)s) {'puno_ime': 'Mika Mikić', 'ime': 'Mika', 'id': 2L} COMMIT
Moguće je izvršiti više INSERT naredbi jednim pozivom execute metode, tako što se prosledi lista asocijativnih nizova sa vrednostima parametara za svaki pojedinačni INSERT: >>> conn.execute(adrese.insert(), [ ... {'klijent_id' : 1, 'email_adr' : '
[email protected]'}, ... {'klijent_id' : 1, 'email_adr' : '
[email protected]'}, ... {'klijent_id' : 2, 'email_adr' : '
[email protected]'}, ... {'klijent_id' : 2, 'email_adr' : '
[email protected]'}, ... ]) INSERT INTO adrese (klijent_id, email_adr) VALUES (%(klijent_id)s, %(email_adr)s) [{'email_adr': '
[email protected]', 'klijent_id': 1}, {'email_adr': '
[email protected]', 'klijent_id': 1},
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
37
{'email_adr': '
[email protected]', 'klijent_id': 2}, {'email_adr': '
[email protected]', 'klijent_id': 2}] COMMIT
5.1.4 Upiti Osnovni način za stvaranje SELECT naredbe je preko select() funkcije: >>> from sqlalchemy.sql import select >>> s = select([klijenti]) >>> result = conn.execute(s) SELECT klijenti.id, klijenti.ime, klijenti.puno_ime FROM klijenti {}
Ovako stvoreni SELECT upit vraća sve kolone tabele. Druga osobina ovog upita je da mu nedostaje WHERE uslov. Zbog toga asocijativni niz preko kojega se prosleđuju vrednosti za parametre u WHERE izrazu ne sadrži ni jedan par ključ-vrednost. Sama povratna vrednost select() funkcije je referenca na objekat klase sqlalchemy.sql.expression.Select. Objekat koji je rezultat upita pripada klasi ResultProxy. Najlakši način da se iz njega dobiju redovi je da se samo iterira: >>> for red in result: ... print red (1, 'Pera', 'Pera Perić') (2, 'Mika', 'Mika Mikić')
Drugi uobičajeni način je preko asocijativnog niza, korišćenjem imena kolona u string obliku: >>> red = result.fetchone() >>> print 'ime:', red['ime'], '; puno_ime:', red['puno_ime'] ime: Pera ; puno_ime: Pera Perić
Isto ovo je moguće i pomoću indeksa: >>> red = result.fetchone() >>> print 'ime:', red[1], '; puno_ime:', red[2] ime: Mika ; puno_ime: Mika Mikić
Još jedan način, čija će korisnost kasnije postati jasnija, je da se koristi Column objekat kao ključ u asocijativnom nizu: >>> for red in conn.execute(s): ... print 'ime:', red[klijenti.c.ime], '; puno_ime:', red[klijenti.c.puno_ime] SELECT klijenti.id, klijenti.ime, klijenti.puno_ime FROM klijenti {} ime: Pera ; puno_ime: Pera Perić ime: Mika ; puno_ime: Mika Mikić
ResultProxy objekat koji se dobije kao rezultat execute() metode treba eksplicitno zatvoriti. Iako
će se zatvaranje obaviti automatski kad ga garbage collector pokupi, bolje je da se to uradi odmah po prestanku potrebe sa objektom: >>> result.close()
Ako bismo želeli da u upitu odaberemo redosled i broj kolona koje želimo imati u rezultatu tada moramo funkciji select() proslediti niz kolona koje nas zanimaju. One se prosleđuju kao atributi
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
38
Table objekta nad kojim se izvršava upit. Sve kolone Table objekta se čuvaju u okviru atributa c (skraćeno od columns) pa im se preko njega pristupa kao u prethodnom i sledećem primeru: >>> s = select([klijenti.c.ime, klijenti.c.puno_ime]) >>> result = conn.execute(s) SELECT klijenti.id, klijenti.ime, klijenti.puno_ime FROM klijenti {} >>> for red in result: ... print red ('Pera', 'Pera Perić') ('Mika', 'Mika Mikić')
Zanimljivo je kako mi nigde ne određujemo direktno sadržaj FROM dela u upitu. Napisan je samo sadržaj SELECT dela (kao listu kolona ili tabela) a iz te liste se implicitno popunjava FROM lista. Tako je moguće napisati sledeće: >>> for red in conn.execute(select([klijenti, adrese])): ... print red SELECT klijenti.id, klijenti.ime, klijenti.puno_ime, adrese.id, adrese.klijent_id, adrese.email_adr FROM klijenti, adrese {} (1, 'Pera', 'Pera Perić', 1, 1, '
[email protected]') (2, 'Mika', 'Mika Mikić', 1, 1, '
[email protected]') (1, 'Pera', 'Pera Perić', 2, 1, '
[email protected]') (2, 'Mika', 'Mika Mikić', 2, 1, '
[email protected]') (1, 'Pera', 'Pera Perić', 3, 2, '
[email protected]') (2, 'Mika', 'Mika Mikić', 3, 2, '
[email protected]') (1, 'Pera', 'Pera Perić', 4, 2, '
[email protected]') (2, 'Mika', 'Mika Mikić', 4, 2, '
[email protected]')
Time smo dobili Dekartov proizvod tabela klijenti i adrese. Da bismo dobili upit sa nekim smislom treba nam WHERE član koji se zadaje kao drugi argument select() funkcije: >>> s = select([klijenti, adrese], klijenti.c.id == adrese.c.klijent_id) >>> for red in conn.execute(s): ... print red SELECT klijenti.id, klijenti.ime, klijenti.puno_ime, adrese.id, adrese.klijent_id, adrese.email_adr FROM klijenti, adrese WHERE klijenti.id = adrese.klijent_id {} (1, 'Pera', 'Pera Perić', 1, 1, '
[email protected]') (1, 'Pera', 'Pera Perić', 2, 1, '
[email protected]') (2, 'Mika', 'Mika Mikić', 3, 2, '
[email protected]') (2, 'Mika', 'Mika Mikić', 4, 2, '
[email protected]')
5.1.5 Operatori U prethodnom primeru smo zadali uslov spajanja dve tabele. Uslov je uspostavljen preko Pythonovog operatora == sa kolonama tabela kao operandima. Kao što bismo napisali 1 == 1 sa rezultatom True ili 1 == 2 sa False. Ali pogledajmo šta se stvarno događa:
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
39
>>> klijenti.c.id == adrese.c.klijent_id
Iznenađujuće, rezultat nije ni True, ni False već objekat, kao što su to Insert i Select koje smo ranije videli. I ne samo to oni su svi potklase klase sqlalchemy.sql.ClauseElement. Jasno je da su autori SQLAlchemyja iskoristili uzor Sastav (eng. Comosite pattern) da bi mogli napraviti stablo objekata preko koje bi se predstavili hijerarhiju delova i celina koje grade SQL izraze. Ako se kao operand upotrebi literal (direktno kodirana vrednost), proizvodi se vezani parametar: >>> print klijenti.c.id == 7 klijenti.id = :id_1
Literal 7 se umeće u ClauseElement, što možemo videti istim trikom koji smo primenili na Insert objekat: >>> print (klijenti.c.id == 7).compile().params {'id_1': 7}
Po analogiji sa == i većina ostalih Python operatora proizvodi SQL izraze: >>> print klijenti.c.id != 7 klijenti.id != :id_1 >>> print klijenti.c.ime == None # None je isto što i null u Javi klijenti.ime IS NULL >>> print 'laza' > klijenti.c.ime klijenti.ime < :ime_1
Sabiranje integer kolona ili spajanje stringova se radi na sledeći način, a slično je i sa ostalim aritmetičkim operacijama za koje postoje SQL ekvivalenti: >>> print klijenti.c.id + adrese.c.id klijenti.id + adrese.id >>> print klijenti.c.ime + klijenti.c.puno_ime klijenti.ime || klijenti.puno_ime
Za logičke operatore postoje ekvivalente funkcije, mada je moguće umesto njih koristiti i operatore nad bitovima: >>> print and_(klijenti.c.ime.like('j%'), klijenti.c.id==adrese.c.klijent_id, ... or_(adrese.c.email_adr=='
[email protected]',adrese.c.email_adr=='
[email protected]'), ... not_(klijenti.c.id > 5)) klijenti.ime LIKE :ime_1 AND klijenti.id = adrese.klijent_id AND (adrese.email_adr = :email_adr_1 OR adrese.email_adr = :email_adr_2) AND klijenti.id >> print klijenti.c.name.like('j%') & (klijenti.c.id==adrese.c.klijent_id) & \ ... ((adrese.c.email_adr=='
[email protected]') | adrese.c.email_adr=='
[email protected]')) \ ... & ~(klijenti.c.id>5)
Kad se sve ovo sklopi dobija se moćan alat. Recimo da treba vratiti korisnika koji ima email adresu na Gmail ili na Hotmail, čije ime počinje slovom između „n“ i „u“. Pri tome treba stvoriti kolonu koja će prikazati njihovo puno ime spojeno sa email adresom. Koristićemo dve nove konstrukcije: beetwen() koja proizvodi istoimeni SQL operator i label() koji proizvodi ključnu reč AS: >>> s = select([(klijenti.c.puno_ime + ', ' + adrese.c.email_adr).label('naslov')], ... and_( ... klijenti.c.id==adrese.c.klijent_id, ... klijenti.c.ime.between('n', 'z'), ... or_( ... adrese.c.email_adresa.like('%@gmail.com'), ... adrese.c.email_adresa.like('%@hotmail.com') ... )
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
40
... ) ... ) >>> print conn.execute(s).fetchall() SELECT klijenti.puno_ime || %(puno_ime_1)s || adrese.email_adr AS naslov FROM klijenti, adrese WHERE klijenti.id = adrese.klijent_id AND klijenti.ime BETWEEN %(ime_1)s AND %(ime_2)s AND ( adrese.email_adra LIKE %(email_adr_1)s OR adrese.email_adr LIKE %(email_adr_2)s ) {'puno_ime_1': ', ', 'email_adr_1': '%@gmail.com', 'ime_1': 'n', 'email_adr_2': '%@hotmail.com', 'ime_2': 'z'} [('Pera Perić,
[email protected]',)]
Ponovo je SQLAlchemy iz našeg select() iskaza zaključio šta treba ući u FROM član. U stvari ne samo iz select() iskaza, FROM član će se izgraditi i iz svih ostalih delova u kojima se može referisati na tabele, što uključuje i WHERE član kao i neke članove koje da sada nismo pominjali: ORDER BY, GROUP BY i HAVING. U prethodnom primeru se vidi kako SQLAlchemy stvara prarametrizoveni upit čak i kad su argumenti za početna slova i email adrese konstantni. Sada ćemo pokazati kao je moguće napraviti sopstvene parametre. Mi smo u primerima za Insert objekte već koristili parametre, samo su se oni tada automatski generisali. Uzmimo za primer da želimo pretražiti klijenti tabelu po delu imena klijenta, a da vrednost koja se koristi bude parametrizovana: >>> from sqlalchemy.sql import bindparam >>> s = select([klijenti], klijenti.c.ime.ilike(bindparam('sablon'))) >>> print s SELECT klijenti.id, klijenti.ime, klijenti.puno_ime FROM klijenti WHERE lower(klijenti.ime) LIKE lower(:sablon)
Funkcija bindparam stvara parametar po imenu koje mu se prosledi kao argument. Metoda ilike() je verzija like() koja ne razlikuje velika i mala slova. Pri pozivu metode execute() treba proslediti vrednosti svim parametima upita: >>> conn.execute(ss, sablon = '%era').fetchall() SELECT klijenti.id, klijenti.ime, klijenti.puno_ime FROM klijenti WHERE lower(klijenti.ime) LIKE lower(:sablon) {'sablon': '%era'} [(1, 'Pera', 'Pera Perić')]
Primetite kako se SQL upit promenio u WHERE delu. Kada smo štampali sadržaj Select objekta on tada nije bio povezan ni sa jednim dijalektom. Pri izvršenju upita bilo je poznato da je u pitanju PostgreSQL mašina te se upit prilagodio tom saznanju.
5.1.6 Upotreba alijasa Alijasi su verzije tabele ili neke relacije nazvane drugim imenom. Prave se preko ključne reči AS iza koje sledi novo ime. Alijasi su od izuzetne važnosti jer omogućavaju da se ukaže na istu tabelu više od jednom, npr. u spajanju tabele sa sobom samom ili kad se glavna tabela spaja sa zavisnom više puta. Npr. tražimo klijenta koji ima 2 poznate email adrese:
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
41
>>> s = select([klijenti], ... and_( ... klijenti.c.id == a1.c.klijent_id, ... klijenti.c.id == a2.c.klijent_id, ... a1.c.email_adresa == '
[email protected]', ... a2.c.email_adresa == '
[email protected]' ... ) ... ) >>> print conn.execute(s).fetchall() SELECT klijenti.id, klijenti.ime, klijenti.puno_ime FROM klijenti, adrese AS a1, adrese AS a2 WHERE klijenti.id = a1.klijent_id AND klijenti.id = a2.klijent_id AND a1.email_adresa = %(email_adresa_1)s AND a2.email_adresa = %(email_adresa_2)s {'email_adresa_1': '
[email protected]', 'email_adresa_2': '
[email protected]'} [(1, 'Pera', 'Pera Perić')]
Moguće je i korišćenje anonimnih alijasa, kada se metodi alias() ne prosledi naziv alijasa. Tada se prepušta SQLAlchemyju da generiše neki naziv. Alijasi se mogu upotrebiti i za čitav podupit. Možemo spojiti tabelu klijenti sa sobom samom. Metoda correlate(None) sprečava SQLAlchemy da spoji unutrašnju i spoljašnju tabelu: >>> a1 = s.correlate(None).alias() >>> s = select([klijenti.c.ime], klijenti.c.id == a1.c.id) >>> print conn.execute(s).fetchall() SELECT klijenti.ime FROM klijenti, (SELECT klijenti.id AS id, klijenti.ime AS ime, klijenti.puno_ime AS puno_ime FROM klijenti, adrese AS a1, adrese AS a2 WHERE klijenti.id = a1.klijent_id AND klijenti.id = a2.klijent_id AND a1.email_adresa = %(email_adresa_1)s AND a2.email_adresa = %(email_adresa_2)s ) AS anon_1 WHERE klijenti.id = anon_1.id {'email_adresa_1': '
[email protected]', 'email_adresa_2': '
[email protected]'} [('Pera',)]
5.1.7 Spajanje tabela U prethodnim primerima smo već spajali tabele tako što smo u select() funkciji ukazivali na više tabela, bilo direktno ili preko njenih kolona. Ali ako nam je potreban pravi JOIN ili OUTER JOIN, koristimo join() i outerjoin() metode, najčešče preko leve tabele u upitu: >>> print klijenti.join(adrese) klijenti JOIN adrese ON klijenti.id = adrese.klijent_id
Uslov spajanja je SQLAlchemy sam zaključio na osnovu ForeignKey objekta iz tabele adrese sa početka vodiča. Naravno moguće je spajati bilo koji izraz; npr. ako želimo spojiti sve klijente koji koriste isto ime u svojim email adresama kao i u korisničkom imenu: >>> print klijenti.join(adrese, adrese.c.email_adr.like(klijenti.c.ime + '%')) klijenti JOIN adrese ON adrese.email_adr LIKE klijenti.ime || :ime_1
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
42
Kad koristimo select(), SQLAlchemy pretražuje referisane tabele i stavlja ih u FROM član, kao što smo opisali. Međutim kada se koristi JOIN, mi tad eksplicitno kažemo šta želimo u FROM delu, pa zato u ovom slučaju koristimo from_obj parametar: >>> s = select([klijenti.c.puno_ime], from_obj=[ ... klijenti.join(adrese, adrese.c.email_adresa.ilike(klijenti.c.ime + '%')) ... ]) >>> print conn.execute(s).fetchall() SELECT klijenti.puno_ime FROM klijenti JOIN adrese ON adrese.email_adresa ILIKE klijenti.ime || %(ime_1)s {'ime_1': '%'} [('Pera Perić',), ('Pera Perić',), ('Mika Mikić',)]
Funkcija outerjoin() je ista kao join() samo što proizvodi LEFT OUTER JOIN: >>> s = select([klijenti.c.puno_ime], from_obj=[klijenti.outerjoin(adrese)]) >>> print s SELECT klijenti.puno_ime FROM klijenti LEFT OUTER JOIN adrese ON klijenti.id = adrese.klijent_id
5.1.8 Dinamički upiti U dosadašnjem izlaganju su upiti napravljeni preko SQLAlchemy konstrukcija predstavljeni kao lakši i bolji način rada od neposrednog pisanja statičkog SQL-a. Međutim SQLAlchemy ne služi samo kao napredniji SQL već nas dovodi do nivoa na kom je moguće pisati programski generisan SQL koji može da se menja po potrebi u scenariju izvršenja programa. Da bi to ostvario select() konstrukcija mora podržavati konstruisanje iz malih „zalogaja“ nasuprot pristupu „sve odmah“ kakav smo do sada koristili. Zamislimo da pišemo funkciju za pretraživanje koja prima kriterijum i tek onda stvara odgovarajući upit. Počinjemo sa osnovnim upitom dobijenim od metode prečice na klijenti tabeli: >>> upit = klijenti.select() >>> print upit SELECT klijenti.id, klijenti.ime, klijenti.puno_ime FROM klijenti
Nailazimo na kriterijum da ime mora biti 'Pera': >>> upit = upit.where(klijenti.c.ime == 'Pera')
Sledeći zahtev je da rezultat bude uređen u punom imenu u opadajućem redosledu: >>> upit = upit.order_by(klijenti.c.puno_ime.desc())
Zatim imamo potrebu samo za onim klijentima koji imaju adrese na Gmail-u. Brzi način za to je upotreba EXISTS operatora nad podupitom uz povezivanje sa klijenti tabelom iz spoljašnjeg upita: >>> upit = upit.where( ... exists([adrese.c.id], ... and_( ... adrese.c.klijent_id == klijenti.c.id, ... adrese.c.email_adresa.like('%@gmail.com')) ... ).correlate(klijenti))
I na kraju aplikaciji je potrebno da vidi i listu email adresa. Za to nam je potreban OUTER JOIN sa adrese tabelom. Tu vrstu spajanja koristimo da bi se i klijenti bez adrese obuhvatili. To nam u ovom
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
43
scenariju ne treba jer ako klijent nema adresu on svakako neće zadovaljiti prethodni uslov da ima adresu na Gmail-u. Ali pošto koristimo dinamičko sastavljanje upita možda u nekom drugom izvršenju programa neće postojati prethodni scenario pa bismo bez OUTER JOIN spajanja rizikovali gubitak klijenata u rezultatu. Zato što i u tabeli klijenti i u tabeli adrese postoji kolona po imenu id, moramo ih odvojiti korišćenjem apply_labels() metode, da ne bi došlo do sukoba imena u rezultatu. Metoda će ispred imena kolona koje stvaraju problem dodati ime pripadajuće tabele (klijenti_id i adrese_id): >>> upit = upit.column(adrese).select_from(klijenti.outerjoin(adrese)).apply_labels()
Metoda column() vraća novi Select objekat dopunjen sa kolonama koje su joj prosleđene. Pogledajmo šta smo na kraju sastavili: >>> conn.execute(upit).fetchall() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, adrese.id AS adrese_id, adrese.klijent_id AS adrese_klijent_id, adrese.email_adr AS adrese_email_adr FROM klijenti LEFT JOIN adrese ON klijenti.id = adrese.klijent_id WHERE klijenti.ime = %(ime_1)s AND (EXISTS (SELECT adrese.id FROM adrese WHERE adrese.klijent_id = klijenti.id AND adrese.email_adr LIKE %(email_adr_1)s )) ORDER BY klijenti.puno_ime DESC {'email_adresa_1': '%@gmail.com', 'ime_1': 'Pera'} [(1, 'Pera', 'Pera Perić', 1, 1, '
[email protected]'), (1, 'Pera', 'Pera Perić', 2, 1, '
[email protected]')]
Počeli smo sa malim upitom, dodavali male promene i na kraju dobili velik SQL iskaz koji pri tome stvarno radi.
5.1.9 Funkcije SQL funkcije se stvaraju posredstvom singleton objekta func iz modula sqlalchemy.sql: >>> from sqlalchemy.sql import func >>> print func.now() now() >>> print func.concat('x', 'y') concat(:param_1, :param_2)
Funkcije se uobičajeno koriste za stvaranje izračunatih kolona upita i tim kolonama se može dodeliti naziv kao i tip. Preporučljivo je davati naziv da bi se koloni moglo pristupiti preko stringa sa njenim imenom kada se rezultat upita obrađuje red po red. Tip je neizbežan kada je zbog dalje obrade potrebno konvertovati rezultat funkcije. Npr. Unicode konverzija stringa i konverzija datuma. U sledećem primeru koristimo funkciju scalar() da bismo očitali samo prvu kolonu prvog reda reda rezultata i da bi taj objekat odmah zatvorili: >>> print conn.execute( ... select([func.max(adrese.c.email_adr, type_=String).label('maxemail')]) ... ).scalar() SELECT max(adrese.email_adr) AS maxemail FROM adrese {}
[email protected]
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
44
5.1.10 Sortiranje, grupisanje, limitiranje, pomeranje Funkcija select() ima parametre order_by, group_by (kao i having), limit i offset. Postoji i distinct parametar čija je podrazumevana vrednost False. Nabrojani parametri postoje i kao metode za dinamičkio sastavljanje upita. Metoda order_by() može imati modifikatore asc() ili desc() za rastući ili opadajući redosled. >>> s = select([addrese.c.klijent_id, func.count(addrese.c.id)]).\ ... group_by(addrese.c.klijent_id).having(func.count(addrese.c.id) > 1) >>> print conn.execute(s).fetchall() SELECT adrese.klijent_id, COUNT(adrese.id) AS count_1 FROM adrese GROUP BY adrese.klijent_id HAVING COUNT(adrese.id) > %(count_2)s {'count_2': 1} [(2, 2L), (1, 2L)] >>> s = select([adrese.c.email_adr, adrese.c.id]).distinct(). \ ... order_by(adrese.c.email_adr.desc(), adrese.c.id) >>> print conn.execute(s).fetchall() SELECT DISTINCT adrese.email_adr, adrese.id FROM adrese ORDER BY adrese.email_adr DESC, adrese.id {} [('
[email protected]', 2), ('
[email protected]', 1), ('
[email protected]', 3), ('
[email protected]', 4)] >>> s = select([adrese]).offset(1).limit(1) >>> print conn.execute(s).fetchall() SELECT adrese.id, adrese.klijent_id, adrese.email_adresa FROM adrese LIMIT 1 OFFSET 1 LIMIT 1 OFFSET 1 {} [(2, 1, '
[email protected]')]
5.1.11 Ažuriranje Konačno smo došli do UPDATE iskaza. Rad sa njim je sličan radu sa INSERT, osim što ovde imamo dodatak sa WHERE koji može da se zada. >>> # menjamo Peru u Đuru >>> conn.execute(klijenti.update(klijenti.c.ime == 'Pera'), ime='Đura') UPDATE klijenti SET ime = %(ime)s WHERE klijenti.ime = %(ime_1)s {'ime_1': 'Pera', 'ime': 'Đura'} COMMIT >>> # rad sa parametrima >>> k = klijenti.update(klijenti.c.ime == bindparam('staro_ime'), ... values={'ime' : bindparam('novo_ime')}) >>> conn.execute(k, staro_ime='Pera', novo_ime='Đura') UPDATE klijenti SET ime = %(novo_ime)s WHERE klijenti.ime = %(staro_ime)s {'novo_ime': 'Đura', 'staro_ime': 'Pera'} COMMIT
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
45
>>> # ažuriranje kolone izrazom >>> conn.execute(klijenti.update(values={klijenti.c.puno_ime : \ ... 'Puno ime: ' + klijenti.c.ime})) UPDATE klijenti SET puno_ime = (%(ime_1)s || klijenti.ime) {'ime_1': 'Puno ime: '} COMMIT
Povezano ažuriranje omogućava ažuriranje tabele upotrebom upita nad drugom tabelom ili nad sopstvenom: >>> s = select([adrese.c.email_adr], adrese.c.klijent_id == klijenti.c.id).limit(1) >>> conn.execute(klijenti.update(values={klijenti.c.puno_ime : s})) UPDATE klijenti SET puno_ime = (SELECT adrese.email_adr FROM adrese WHERE adrese.klijent_id = klijenti.id LIMIT 1 ) {} COMMIT
5.1.12 Brisanje Poslednja od CRUD (Create, Read, Update, Delete) funkcija je najlakša: >>> # obrisati sve iz tabele >>> conn.execute(adrese.delete()) DELETE FROM adrese {} COMMIT >>> # brisanje klijenata čije je ime lekikografski veće od slova 'M' >>> conn.execute(klijenti.delete(klijenti.c.ime > 'M')) DELETE FROM klijenti WHERE klijenti.ime > %(ime_1)s {'ime_1': 'M'} COMMIT
5.2 Uvod u objektno-relaciono preslikavanje Konačno smo došli do najzanimljivijeg dela SQLAlchemyja: pretvaranje objekata u relacije i obratno. Samo preslikavanje je nadgradnja na SQL Expression Language pa će nam mnoge stvari biti poznate ili bar slične onima iz prethodnih primera.
5.2.1 Povezivanje tabele i klase Prvo ćemo definisati domensku klasu Klijent kao najobičniju Python klasu koja, za razliku od uzora Aktivni slog, ne nasleđuje nikakvu posebnu natklasu: >>> class Klijent(object): ... def __init__(self, ime, puno_ime, lozinka): ... self.ime = ime ... self.puno_ime = puno_ime ... self.lozinka = lozinka ...
Razvoj Python programa korišćenjem SQLAlchemy tehnologije ... ... ...
46
def __repr__(self): return "" % (self.ime, self.puno_ime, self.lozinka)
Zatim opisujemo vezu sa bazom: >>> from sqlalchemy import create_engine >>> engine = create_engine('postgres://fon:lozinka@veles:5432/sqlalchemy', echo=True) >>> from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
A onda opisujemo i stvaramo tabelu koja odgovara klasi Klijent, kao i što smo i ranije radili: >>> metadata = MetaData() >>> klijenti_tabela = Table('klijenti', metadata, ... Column('id', Integer, primary_key=True), ... Column('ime', String(40)), ... Column('puno_ime', String(100)), ... Column('lozinka', String(15)), ... ) >>> metadata.create_all(engine) SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name': 'klijenti'} CREATE TABLE klijenti ( id SERIAL NOT NULL, ime VARCHAR(40), puno_ime VARCHAR(100), lozinka VARCHAR(15), PRIMARY KEY (id) ) COMMIT
PYTHON OBJAŠNJENJA: Kao što se vidi na primeru klase Klijent u Pythonu se ne koristi u javi poznati obrazac da su atributi privatni i da im se pristupa preko get/set metoda. Razlog za to je što Python ima podršku za pretvaranje atributa u properties. To znači da se uobičajenom sintaksom za pristup atributima mogu čitati i menjati atributi preko get/set metoda koje se pozivaju implicitno. Klase se u Pythonu pišu sa public atributima i njima se pristupa direktno. Ako se ikada pojavi potreba za pretvaranjem atributa u private opseg i pristup preko get/set metoda to se radi na jednostavan način (primer za atribut ime klase Klijent): class Klijent(object): def __get_ime(self): if self.__ime: return self.__ime else: return 'Nepoznato ime' def __set_ime(self, val): self.__ime = val.capitalize() def __init__(self, ime, puno_ime, lozinka): self.__ime = ime . . . ime = property(__get_ime, __set_ime)
Kôd koji je ranije direktno pristupao atributu ime i dalje će raditi. Ovako nešto nije moguće uraditi u javi zbog nesavršenosti tog jezika. Zato se get/set metode pišu unapred za sve atribute, iz opreznosti da iako u tom trenutku za njima nema potrebe, kasnije ako se potreba ukaže programeri neće moći lako da prebace direktan pristup atributu u pristup preko get/set metoda.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
47
Sada trebamo nekako povezati klijenti_tabela objekat sa klasom Klijenti. Za to nam služi sqlalchemy.orm paket. Koristićemo mapper() funkciju: >>> from sqlalchemy.orm import mapper >>> mapper(Klijent, klijenti_tabela)
Funkcija mapper() stvara Mapper objekat i čuva ga za dalju upotrebu. Ona takođe uspostavlja vezu između atributa klase Klijenti i kolona u klijenti_tabela. Kolone id, ime, puno_ime i lozinka su sada povezane sa Klijent klasom, što znači da će pratiti sve promene na tim atributima i da može sačuvati i učitati njihove vrednosti u i iz baze. Pokažimo to na primeru: >>> pera_klijent = Klijent('pera', 'Pera Peric', 'perinaloznka') >>> pera_klijent.ime 'pera' >>> str(pera_klijent.id) 'None'
5.2.2 Stvaranje sesije Sada smo uradili sve što je potrebno da bismo započeli komunikaciju sa bazom. ORM uspostavlja vezu sa bazom preko sesije. Kada aplikacija počne sa izvršavanjem, ona prilikom inicijalizacije uspostavlja vezu sa bazom preko create_engine() funkcije, a da bi se koristio deo SQLAlchemyja zadužen za preslikavanje biće nam potreban još jedan objekat (koji je zapravo klasa) iz funkcije sessionmaker(). Ova funkcija služi za stvaranje i konfigurisanje klase preko koje se stvaraju sesije. Nju je potrebno samo jednom pozvati: >>> from sqlalchemy.orm import sessionmaker >>> Session = sessionmaker(bind=engine, autoflush=True, transactional=True) >>> type(Session)
Time smo dobili klasu sqlalchemy.orm.session.Sess povezanu sa tačno određenom bazom. Nije potrebno unapred znati bazu na koju se klasa sesije odnosi. Moguće je naknadno konfigurisati klasu objektom sqlalchemy.engine.base.Engine: >>> Session.configure(bind=engine)
Kad je preko Engine objekta klasa Session konačno povezana na bazu, može se upotrebiti za pravljenje sesija sa transakcionim osobinama kakve smo joj dodelili. Kad god nam je potrebna konverzacija sa bazom napravićemo objekat klase: >>> session = Session() >>> type(session)
Ovako napravljen objekat još uvek nema otvorenu konekciju na bazu. Kad se prvi put bude upotrebio povući će konekciju iz keširanih konekcija i držati je otvorenom sve dok se ne potvrde sve promene (sa COMMIT) i/ili ne zatvori objekat sesije. Pošto smo postavili da je transactional=True, automatski će se odvijati transakcije. Moguće je promeniti ovakvo ponašanje, ali ovo je uobičajeni način na koji se radi.
5.2.3 Čuvanje objekta Čuvanje objekata je jednostavno, potrebno je samo pozvati metodu save(): >>> session.save(pera_klijent)
Kao što se vidi ništa se nije dogodilo, jer se nikakav SQL nije izvršio na SUBP. Pokušajmo da
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
48
pronađemo klijenata. To ćemo uraditi pomoću query() metode. Prvo ćemo napraviti upit koji će predstavljati skup svih Klijent objekata a zatim ćemo rezultat filtrirati na klijente čije je ime "pera". Zatim pozivamo metodu first() koja kaže Query objektu da želimo prvi rezultat u listi: >>> session.query(Klijent).filter_by(ime='pera').first() BEGIN SELECT nextval('"klijenti_id_seq"') None INSERT INTO klijenti (id, ime, puno_ime, lozinka) VALUES (%(id)s, %(ime)s, %(puno_ime)s, %(lozinka)s) {'puno_ime': 'Pera Perić', 'ime': 'pera', 'lozinka': 'perinaloznka', 'id': 1L} SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.ime = %(ime_1)s ORDER BY klijenti.id LIMIT 1 OFFSET 0 {'ime_1': 'pera'}
POREĐENJE SA JPA: U JPA se podešavanja veze sa bazom čuvaju u persistence.xml datoteci. S druge strane SQLAlchemy osnovna podešavanja čuva u Engine objektu. Pošto se taj objekat koristi i za SQL-EL parametri specifični za ORM deo SQLAlchemyja se prosleđuju sessionmaker() funkciji zajedno sa Engine objektom. Sama sessionmaker() funkcija je ekvivalent Persistence.createEntityManagerFactory() metodi. Session objekta/klasa koja je rezultat sessionmaker() funkcije je ekvivalentan EntityManagerFactory objektu. Stvaranje objekta sesije se u SQLAlchemyju radi konstruktorom Session klase a u JPA pozivanjem metode createEntityManager() nad EntityManagerFactory objektom. Podešavanje preslikavanja se u JPA radi ili u samoj klasi anotacijama ili u orm.xml fajlu a moguće je i kombinacija ova dva načina. Za klasu se moraju upotrebiti bar 2 anotacije @Entity i @Id, ostalo se može povezati podrazumevanim načinima (preko imena atributa klase i kolona tabela ili imena get/set metoda i imena kolona). SQLAlchemy koristi Table objekat da definiše izgled tabele u bazi i mapper() funkciju da poveže klasu sa njenom tabelom. Atribut id se automatski stvara. Autori SQAlchemyja su iskoristili dinamičnost Pythona da stvaranje sesije učine logičnijim: opiše se veza preko Engine objekta, konfiguriše se Session klasa i od nje se prave sesije. Nikakva potreba za statičnom factory metodom kao u javi. I tu se potvrđuje istina da što je jezik bolji to mu manje projektnih uzora treba. S druge strane naziv funkcije sessionmaker() može programera odvesti na krivi put, jer se kao rezultat dobija klasa a ne sama sesija, time se krši jedan od osnovnih temelja Python kulture Princip najmanjeg iznenađenja. U JPA imena metoda ne ostavljaju mesta za pogrešno tumačenje. I uspeli smo da dobijemo našeg klijenta. Iz SQL kôda se vidi da je objekat sesije prvo izvršio INSERT naredbu pa onda SELECT. Objekat sesije pamti sve što se se stavi u memoriju i u određenim trenucima izvršava povezivanje sa bazom i zapisivanje nastalih promena iz memorije u relacije. Ovo pražnjenje se može i ručno uraditi metodom flush(), međutim kada je sesija konfigurisana sa kao autoflush, to uglavnom nije potrebno. Primetite da u SQL kôdu postoji BEGIN transakcije ali da nema COMMIT naredbe. Ako bi se sada uspostavila druga sesija ili veza (npr. preko pgAdmin III programa) videli bi da je tabela klijenata još uvek prazna.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
49
Uradimo još nekoliko operacija. Unesimo još tri nova klijenta: >>> session.save(Klijent('zika', 'Žika Žikić', 'blabla')) >>> session.save(Klijent('mika', 'Mika Mikić', 'tralala')) >>> session.save(Klijent('laza', 'Laza Lazić', 'tatatatira'))
Takođe, odlučeno je da Pera nema dovoljno bezbednu lozinku pa ćemo je promeniti: >>> pera_klijent.lozinka = 'sezameotvorise'
Sada ćemo trajno sačuvati sve ono što se promenilo i dodati u bazu ono što je u međuvremenu stvoreno. To se radi preko metode commit(): >>> session.commit() UPDATE klijenti SET lozinka = %(lozinka)s WHERE klijenti.id = %(klijenti_id)s {'lozinka': 'sezameotvorise', 'klijenti_id': 1L} SELECT nextval('"klijenti_id_seq"') None INSERT INTO klijenti (id, ime, puno_ime, lozinka) VALUES (%(id)s, %(ime)s, %(puno_ime)s, %(lozinka)s) {'puno_ime': 'Žika Žikić', 'ime': 'zika', 'lozinka': 'blabla', 'id': 2L} SELECT nextval('"klijenti_id_seq"') None INSERT INTO klijenti (id, ime, puno_ime, lozinka) VALUES (%(id)s, %(ime)s, %(puno_ime)s, %(lozinka)s) {'puno_ime': 'Mika Mikić', 'ime': 'mika', 'id': 3L, 'lozinka': 'tralala'} SELECT nextval('"klijenti_id_seq"') None INSERT INTO klijenti (id, ime, puno_ime, lozinka) VALUES (%(id)s, %(ime)s, %(puno_ime)s, %(lozinka)s) {'puno_ime': 'Laza Lazić', 'ime': 'laza', 'id': 4L, 'lozinka': 'tatatatira'} COMMIT
Rezultat commit() metode je pražnjenje preostalih promena u bazu i potvrđivanje transakcije. Konekcija na bazu koju je sesija koristila se oslobađa i skladišti u keš konekcija za ponovno korišćenje. Naredne operacije u ovoj sesiji će biti deo nove transakcije koja će ponovo uzeti konekciju kad joj bude prvi put zatrebala. Ako pogledamo Perin id atribut, koji je ranije bio None, on sada ima dodeljenu vrednost: >>> pera_klijent.id 1L
Posle svake INSERT operacije, sesija dodeljuje novostvorenu identifikaciju kao i sve podrazumevane vrednosti (preko DEFAULT postavki u definiciji kolone) se preslikavaju u odgovarajuće atribute objekta. Izuzetno je značajno shvatiti da se u sesiji svaki objekat kešira na osnovu primarnog ključa. Keširanje se ne radi toliko zbog performansi koliko zbog očuvanja jedinstvenosti preko preslikavanja identiteta pomoću heš tabele. Ova tabela garantuje da ćemo kad god u sesiji radimo sa konkretnim objektom klase Klijent, uvek dobiti istu instancu nazad. Kao što pokazuje primer: >>> pera_klijent is session.query(Klijent).filter_by(ime='pera').one() BEGIN SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
50
FROM klijenti WHERE klijenti.ime = %(ime_1)s ORDER BY klijenti.id LIMIT 2 OFFSET 0 {'ime_1': 'pera'} True
Dakle pomoću upita smo dobili referencu na objekat koji je već bio u sesiji a ne neki novi sa istim atributima uključujući isti id. Pri korišćenju get() metode za upit SQL kôd se neće ni izvršiti ako za traženi primarni ključ već postoji objekat u sesiji: >>> pera_klijent is session.query(Klijent).get(pera_klijent.id) True
5.2.4 Upiti U ovom poglavlju ćemo proći kroz zadavanje upita. Upit je objekat klase sqlalchemy.orm.query.Query i stvara se iz objekta sesije: >>> query = session.query(Klijent)
Kad je stvoren objekat upita možemo krenuti sa učitavanjem klijenata. Inicijalno Query objekat predstavlja sve objekte klase za koju je stvoren. Moguće je proći kroz sve objekte direktno u petlji: >>> for klijent in query: ... print klijent.ime ... SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti ORDER BY klijenti.id {} pera zika mika laza
... i SQL će se izvršiti u trenutku kad se na Query objekat primenjuje operator in koji objektu nalaže da se ponaša kao lista. Ako se primene operatori odsecanja liste pre iteracije tada će se na upit primeniti LIMIT i OFFSET: >>> for k in query[1:3]: ... print k ... SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti ORDER BY klijenti.id LIMIT 2 OFFSET 1 {}
Sužavanje rezultata pretrage se postiže ili sa metodom filter_by(), koja koristi imenovane argumente: >>> for k in session.query(Klijent).filter_by(ime='pera', puno_ime='Pera Perić'): ... print k ...
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
51
SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.puno_ime = %(puno_ime_1)s AND klijenti.ime = %(ime_1)s ORDER BY klijenti.id {'puno_ime_1': 'Pera Perić', 'ime_1': 'pera'}
... ili sa filter(), koja koristi SQL expression language izraze. To nam omogućava da koristimo obične Pythonove operatore sa atributima na nivou klase (u javi poznate kao statičke atribute) koji su preslikani sa istoimenih atributa instanci: >>> for k in session.query(Klijent).filter(Klijent.ime == 'pera'): ... print k ... SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.ime = %(ime_1)s ORDER BY klijenti.id {'ime_1': 'pera'}
Uobičajeni SQL operatori su prisutni, kao npr. LIKE: >>> session.query(Klijent).filter(Klijent.ime.like('%ka'))[1] SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.ime LIKE %(ime_1)s ORDER BY klijenti.id LIMIT 1 OFFSET 1 {'ime_1': '%ka'}
Primetite kako se uzimanje 2. elementa liste pravilno odrazilo na SQL kôd kroz LIMIT i OFFSET. Metode all(), one() i first() odmah izvršavaju SQL, tako da nam je nepotrebna petlja ili indeks liste da bismo pristupili elementima. all() vraća listu: >>> query = session.query(Klijent).filter(Klijent.ime.ilike('%ika')) >>> query.all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.ime ILIKE %(ime_1)s ORDER BY klijenti.id {'ime_1': '%ika'} [, ]
first() ograničava rezultat na 1 element i vraća prvi element kao skalar: >>> query.first() SELECT klijenti.id klijenti.ime klijenti.puno_ime klijenti.lozinka
AS AS AS AS
klijenti_id, klijenti_ime, klijenti_puno_ime, klijenti_lozinka
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
52
FROM klijenti WHERE klijenti.ime ILIKE %(ime_1) s ORDER BY klijenti.id LIMIT 1 OFFSET 0 {'ime_1': '%ika'}
a one(), ograničava rezultat na 2 elementa, i ako nije vraćen tačno jedan red baca izuzetak jer ne dozvoljava da nema rezultata ili da je on višestruki. Osim te razlike metoda ima isto ponašanje kao i first(): >>> try: ... klijent = query.one() ... except Exception, e: ... print "Izuzetak:", e ... SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.ime ILIKE %(ime_1) s ORDER BY klijenti.id LIMIT 2 OFFSET 0 {'ime_1': '%ika'} Izuzetak: Multiple rows returned for one()
Metode Query objekta, koje ne vraćaju rezultat upita, umesto rezultata daju novi Query objekat na koji su primenjene izmene. Time je omogućeno da se metode vežu jedna za drugu u niz da bi se izgradio kriterijum upita: >>> session.query(Klijent).filter(Klijent.id < 2).filter_by(ime='pera'). \ ... filter(Klijent.puno_ime == 'Pera Perić').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.id < %(id_1) s AND klijenti.ime = %(ime_1) s AND klijenti.puno_ime = %(puno_ime_1) s ORDER BY klijenti.id {'puno_ime_1': 'Pera Peri\xc4\x87', 'ime_1': 'pera', 'id_1': 2} []
Možemo koristiti i logičke operacije na isti način na koji smo to radili u SQL-EL pomoću funkcija and_(), or_() i not_(): >>> session.query(Klijent).filter( ... and_(Klijent.id < 224, or_(Klijent.ime == 'pera', Klijent.ime == 'mika')) ... ).all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.id < %(id_1) s AND(klijenti.ime = %(ime_1) s OR klijenti.ime = %(ime_2) s) ORDER BY klijenti.id {'ime_2': 'mika', 'ime_1': 'pera', 'id_1': 224} [, ]
Parametrizovane upite stvaramo pomoću bindparam() funkcije, a parametre popunjavamo metodom params():
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
53
>>> from sqlalchemy import bindparam >>> session.query(Klijent) \ ... .filter( ... and_(Klijent.id < bindparam('vrednost'), Klijent.ime == bindparam('ime')) ... ) \ ... .params(vrednost=224, ime='pera').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.id < %(vrednost)s AND klijenti.ime = %(ime)s ORDER BY klijenti.id {'vrednost': 224, 'ime': 'pera'} []
Moguće je stvarati select() izraze i tako koristiti punu snagu SQL-EL opisanu u prethodnim poglavljima: >>> session.query(Klijent).from_statement( ... select( ... [klijenti_tabela], ... select([func.max(klijenti_tabela.c.ime)]).label('max_ime') == klijenti_tabela.c.ime) ... ).all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE(SELECT MAX(klijenti.ime) AS max_1 FROM klijenti) = klijenti.ime {} []
Takođe postoji način da se kombinuje skalarni rezultat sa objektima, korišćenjem metode add_column(). Ova mogućnost se često koristi uz funkcije i agregaciju. Rezultat je nepromenljiva lista: >>> for r in session.query(Klijent). \ ... add_column(select([func.max(Klijent.lozinka)]).label('max_lozinka')): ... print r ... SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka, (SELECT MAX(klijenti.lozinka) AS max_1 FROM klijenti ) AS max_lozinka FROM klijenti ORDER BY klijenti.id {} (, 'tralala') (, 'tralala') (, 'tralala') (, 'tralala')
5.4.5 Uspostavljanje 1-M veze Dosad smo se bavili samo jednom klasom i jednom tabelom. Pogledajmo kako se SQLAlchemy koristi sa dve tabele koje su vezane jedna za drugu. Recimo da klijenti u našem sistemu mogu imati bilo koji broj email adresa povezanih sa njihovim korisničkim imenom. To podrazumeva osnovnu jedan prema više asocijaciju iz Table objekta klijenti_tabela ka novoj tabeli u kojoj ćemo čuvati
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
54
email adrese koju ćemo nazvati adrese. Takođe želimo da tu vezu napravimo pomoću spoljnog ključa: >>> from sqlalchemy import ForeignKey >>> adrese_tabela = Table('adrese', metadata, ... Column('id', Integer, primary_key=True), ... Column('email_adresa', String(100), nullable=False), ... Column('klijent_id', Integer, ForeignKey('klijenti.id')) )
Sad treba da ponovo pozovemo create_all() metodu koja će preskočiti pravljenje tabele klijenti (koja već postoji) a stvoriće tabelu adrese: >>> metadata.create_all(engine) SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name': 'klijenti'} SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name': 'adrese'} CREATE TABLE adrese ( id SERIAL NOT NULL, email_adresa VARCHAR(100) NOT NULL, klijent_id INTEGER, PRIMARY KEY(id), FOREIGN KEY(klijent_id) REFERENCES klijenti(id) ) {} COMMIT
Da bismo inicijalizovali ORM potrebno je da krenemo iz početka. Prvo moramo zatvoriti sve objekte klase Session i očistiti sve Mapper objekte: >>> from sqlalchemy.orm import clear_mappers >>> session.close() >>> clear_mappers()
Naša Klijent klasa još uvek postoji ali je sada samo obična stara klasa (nema više onih atributa klase koji odgovaraju kolonama, npr. Klijent.ime). Napravimo klasu Adresa koja će predstavljati email adresu klijenta: >>> class Adresa(object): ... def __init__(self, email_adresa): ... self.email_adresa = email_adresa ... ... def __repr__(self): ... return "" % self.email_adresa
Sada ponovo opisujemo preslikavanje klasa i tabela kao i veze između njih pomoću funkcije relation(). Preslikavanje možemo definisati u bilo kom redosledu: >>> from sqlalchemy.orm import relation >>> mapper(Klijent, klijenti_tabela, properties={ ... 'adrese' : relation(Adresa, backref='klijent') ... }) >>> mapper(Adresa, adrese_tabela)
U gornjem kôdu možemo videti jednu novinu, Klijent ima relaciju pod imenom adrese. Ta relacija
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
55
će se pretvoriti u atribut instance klase Klijent i biće lista adresa tog klijenta. Kako se zna da je to lista? SQLAlchemy to sam zaključuje na osnovu spoljnog ključa koji iz tabele adrese referiše na tabelu klijenti. Suprotna strana relacije je atribut klijent u instancama Adresa klase i on je definisan preko imenovanog argumenta backref.
5.2.6 Rad na povezanim objektima i povratnim vezama Stvorimo još nekog klijenta i on će automatski imati kolekciju adresa: >>> jeca = Klijent('jeca', 'Jelena Jelić', 'asdf') >>> jeca.adrese []
Možemo slobodno dodavati Adresa objekte a sesija će se starati o svemu za nas: >>> jeca.adrese.append(Adresa('
[email protected]')) >>> jeca.adrese.append(Adresa('
[email protected]'))
Pre nego što sačuvamo ove promene u sesiji, razmotrimo šta se sve događa. Kolekcija adrese postoji kôd našeg klijenta zato što smo dodali relation() sa tim imenom. Ali smo takođe u toj funkciji preko imenovanog argumenta backref stvorili povratnu vezu. Ovaj argument označava da želimo dvosmernu relaciju. To u suštini znači da smo stvorili ne samo jedan prema više vezu pod nazivom adrese u Klijent klasi, već smo stvorili i više prema jedan vezu na Adresa klasi. Ova veza je samoažurirajuća, bez ikakvih podataka poslatih samoj bazi. Nije bitno sa koje strane se pravi veza, ona će uvek biti pravilno uspostavljena i u domenskom i u relacionom svetu. Sve se ovo vidi na primeru: >>> jeca.adrese[1] >>> jeca.adrese[1].klijent >>> treca_jecina_adr = Adresa('
[email protected]') >>> treca_jecina_adr.klijent = jeca >>> jeca.adrese [, , ]
POREĐENJE SA JPA: samoažurirajuće ponašanje pri uspostavi veze ne postoji u JPA. Dvosmerna veza između objekata u domenskom modelu kod JPA se mora ručno postaviti na oba njena kraja (stavljanje u kolekciju na „1“ strani i postavljanje reference na „M“ strani) što je posledica statičke prirode samog java jezika. Ako se dvosmerna veza u JPA uspostavi u objektu samo sa „1“ strane, ne samo da se neće postaviti automatski na „M“ već se neće ni upisati u bazu, samim tim veza neće biti trajna. Razlog za to je što je „M“ strana vlasnik veze u relacionim bazama (na toj strani je spoljni ključ) pa pošto u objektu na „M“ strani nema reference na objekat „1“ u tabeli će se za spoljni ključ upisati NULL vrednost. Sad ćemo sačuvati promene u sesiju, potom je zatvoriti i napraviti novu, tako da možemo videti šta se dešava sa Jecom i njenim email adresama: >>> session.save(jeca) >>> session.commit() BEGIN SELECT nextval('"klijenti_id_seq"') None INSERT INTO klijenti (id, ime, puno_ime, lozinka) VALUES (%(id)s, %(ime)s, %(puno_ime)s, %(lozinka)s) {'puno_ime': 'Jelena Jelić', 'ime': 'jeca', 'lozinka': 'asdf', 'id': 5L}
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
56
SELECT nextval('"adrese_id_seq"') None INSERT INTO adrese (id, email_adresa, klijent_id) VALUES (%(id)s, %(email_adresa)s, %(klijent_id)s) {'email_adresa': '
[email protected]', 'klijent_id': 5L, 'id': 1L} SELECT nextval('"adrese_id_seq"') None INSERT INTO adrese (id, email_adresa, klijent_id) VALUES (%(id)s, %(email_adresa)s, %(klijent_id)s) {'klijent_id': 5L, 'email_adresa': '
[email protected]', 'id': 2L} SELECT nextval('"adrese_id_seq"') None INSERT INTO adrese (id, email_adresa, klijent_id) VALUES (%(id)s, %(email_adresa)s, %(klijent_id)s) {'email_adresa': '
[email protected]', 'klijent_id': 5L, 'id': 3L} COMMIT >>> session = Session()
U poslednjem redu gornjeg primera napravili smo novu sesiju. Potražimo sada preko nje Jecu upitom: >>> jeca = session.query(Klijent).filter_by(ime='jeca').one() BEGIN SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.ime = %(ime_1)s ORDER BY klijenti.id LIMIT 2 OFFSET 0 {'ime_1': 'jeca'} >>> jeca
Primetite da se u gornjem upitu ne dobavljaju Jecine adrese iz baze. Pogledajmo adrese u dobijenom objektu. Primetite SQL: >>> jeca.adrese SELECT adrese.id AS adrese_id, adrese.email_adresa AS adrese_email_adresa, adrese.klijent_id AS adrese_klijent_id FROM adrese WHERE %(param_1)s = adrese.klijent_id ORDER BY adrese.id {'param_1': 5} [, , ]
Kada smo pristupili kolekciji adresa, SQL kôd je tek tada izvršen. To je primer relacije sa odloženim, lenjim učitavanjem (eng. lazy loading relation). Ako se želi smanjiti broj upita (u nekim slučajevima mnogostruko), možemo primeniti trenutno učitavanje (eng. eager load) na upit. Očistićemo sesiju da bismo imali ponovno puno učitavanje: >>> session.clear()
Potom ćemo iskoristiti metodu options() na Query objektu i preko nje podesiti upit tako da se adrese učitaju trenutno. Za to nam je potrebna eagerload() funkcija. SQLAlchemy zatim konstruiše JOIN upit na tabelama klijenti i adrese:
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
57
>>> from sqlalchemy.orm import eagerload >>> jeca = session.query(Klijent) \ ... .options(eagerload('adrese')) \ ... .filter_by(ime='jeca').one() SELECT anon_1.klijenti_id AS anon_1_klijenti_id, anon_1.klijenti_ime AS anon_1_klijenti_ime, anon_1.klijenti_puno_ime AS anon_1_klijenti_puno_ime, anon_1.klijenti_lozinka AS anon_1_klijenti_lozinka, adrese_1.id AS adrese_1_id, adrese_1.email_adresa AS adrese_1_email_adresa, adrese_1.klijent_id AS adrese_1_klijent_id FROM (SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka, klijenti.id AS klijenti_oid FROM klijenti WHERE klijenti.ime = %(ime_1)s ORDER BY klijenti.id LIMIT 2 OFFSET 0 ) AS anon_1 LEFT OUTER JOIN adrese AS adrese_1 ON anon_1.klijenti_id = adrese_1.klijent_id ORDER BY anon_1.klijenti_id, adrese_1.id {'ime_1': 'jeca'} >>> print jeca >>> print jeca.adrese [, , ]
Iako smo primenili trenutno učitavanje rezultat je potpuno isti. Strategija učitavanja se koristi samo za optimizaciju. Bez obzira na to koji kriterijumi se zadaju u upitu uključujući i sortiranje, limitiranje, spajanje i dr. upit treba da vrati identični rezultat za bilo koju kombinaciju trenutnog i odloženog učitavanja relacija. Trenutno učitavanje se može primeniti na više relacija, i na relacije tih relacija tako što se imena relacija odvoje tačkama: query.options(eagerload('narudzbine'), eagerload('narudzbine.stavke'), \ eagerload('narudzbine.stavke.kategorije'))
Gornji izraz bi se mogao uprostiti sa tri posebna poziva eagerload() funkcije na samo jedan preko eagerload_all(): query.options(eagerload_all('narudzbine.stavke.kategorije'))
5.2.7 Upiti sa spajanjem Šta ako želimo napraviti spajanje koje menja rezultat? Jedan od načina da spojimo dve tabele je da sastavimo SQL-EL. U sledećem primeru pravimo upit sa spajanjem preko id i klijent_id atributa: >>> session.query(Klijent).filter(Klijent.id == Adresa.klijent_id). \ ... filter(Adresa.email_adresa == '
[email protected]').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti, adrese WHERE klijenti.id = adrese.klijent_id AND adrese.email_adresa = %(email_adresa_1)s ORDER BY klijenti.id
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
58
{'email_adresa_1': '
[email protected]'} []
Drugi način je da koristimo JOIN SQL naredbu. Naredni primer pokazuje spajanje upotrebom join() metode klase Table, koja pravi Join objekata, a potom taj objekat šaljemo u Query da se koristi kao deo FROM izraza: >>> session.query(Klijent).select_from(klijenti_tabela.join(adrese_tabela)). \ ... filter(Adresa.email_adresa == '
[email protected]').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti JOIN adrese ON klijenti.id = adrese.klijent_id WHERE adrese.email_adresa = %(email_adresa_1)s ORDER BY klijenti.id {'email_adresa_1': '
[email protected]'} []
Primetite da join() bez poteškoća shvata pravi uslov spajanja između tabela jer objekat ForeignKey koji smo konstruisali sadrži informacije o tome. Najlakši način da se spajanje obavi automatski je preko join() metode Query objekta. Samo se metodi preda putanja od klase A do klase B, atributa koji ih veže. Atribut se može proslediti u obliku stringa 'atributi' kao u narednom primeru ili kao klasni atribut npr. Klijent.adrese: >>> session.query(Klijent).join('adrese'). \ ... filter_by(email_adresa='
[email protected]').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti JOIN adrese ON klijenti.id = adrese.klijent_id WHERE adrese.email_adresa = %(email_adresa_1)s ORDER BY klijenti.id {'email_adresa_1': '
[email protected]'} []
Pod putanjom „A do B“, misli se na direktnu povezanost ili na vezu preko drugih entiteta. U prethodnom primeru imali smo samo Klijent–>adrese–>Adresa, ali da samo imali npr. tri entiteta: Klijent, Faktura i StavkaFakture vezanih na sledeći način: Klijent–>fakture–>Faktura– >stavke–>StavkaFakture, spajanje sva tri entiteta bi izgledalo ovako: session.query(Klijent).join(['fakture', 'stavke']).filter(...)
Svaki put kada se join() pozove na Query objektu, tačka spajanja upita se pomera na kraj spoja. To se može objasniti na primeru spajanja Klijent entiteta sa Adresa entitetima. Kriterijum zadat preko filter_by() metode se odnosi na pojave Adresa klase jer je ona poslednja u nisu entiteta koji se spajaju (Klijent–>adrese–>Adresa). Kada se ponovo pozove join() metoda, tačka spajanja iznova počinje od prvog entiteta u nizu (Klijent). Možemo se eksplicitno vratiti na početak preko metode reset_joinpoing(). Ona će smestiti tačku spajanja entitet Klijenti, pa će se kriterijum zadat u prvoj sledećoj filter_by() metodi primeniti na taj entitet: >>> session.query(Klijent).join('adrese'). \ ... filter_by(email_adresa='
[email protected]'). \ ... reset_joinpoint().filter_by(ime='jeca').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
59
FROM klijenti JOIN adrese ON klijenti.id = adrese.klijent_id WHERE adrese.email_adresa = %(email_adresa_1)s AND klijenti.ime = %(ime_1)s ORDER BY klijenti.id {'email_adresa_1': '
[email protected]', 'ime_1': 'jeca'} []
a isti rezultat se dobija sa prirodnijim upitom: session.query(Klijent).filter_by(ime='jeca'). \ join('adrese').filter_by(email_adresa='
[email protected]').all()
Možemo zahtevati da u isto vreme dobijemo objekat Klijenta i njemu odgovarajuće objekte Adresa klase. Kao rezultat dobija se lista parova: >>> session.query(Klijent).add_entity(Adresa).join('adrese'). \ ... filter(Adresa.email_adresa == '
[email protected]').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka, adrese.id AS adrese_id, adrese.email_adresa AS adrese_email_adresa, adrese.klijent_id AS adrese_klijent_id FROM klijenti JOIN adrese ON klijenti.id = adrese.klijent_id WHERE adrese.email_adresa = %(email_adresa_1)s ORDER BY klijenti.id {'email_adresa_1': '
[email protected]'} [(, )]
Još jedan uobičajeni scenario je potreba da se uradi spajanje sa istom tabelom više puta. Npr. ako želimo da saznamo koji klijent ima dve različite email adrese,
[email protected] i
[email protected], moramo uraditi JOIN sa tabelom adrese dva puta. SQLAlchemy sadrži Alias objekte koji nam omogućuju da to postignemo, ali je mnogo lakše reći join() metodi da napravi alias: >>> session.query(Klijent). \ ... join('adrese', aliased=True).filter(Adresa.email_adresa == '
[email protected]'). \ ... join('adrese', aliased=True).filter(Adresa.email_adresa == '
[email protected]').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti JOIN adrese AS adrese_1 ON klijenti.id = adrese_1.klijent_id JOIN adrese AS adrese_2 ON klijenti.id = adrese_2.klijent_id WHERE adrese_1.email_adresa = %(email_adresa_1)s AND adrese_2.email_adresa = %(email_adresa_2)s ORDER BY klijenti.id {'email_adresa_1': '
[email protected]', 'email_adresa_2': '
[email protected]'} []
Relacioni operatori
Objekat koji trenutno posmatramo, tj. od koga polazimo u razmatranju, zvaćemo roditeljem, a objekte koji su vezani za roditelja zvaćemo decom. Pregled svih operatora koji se mogu koristiti u relacijama:
Razvoj Python programa korišćenjem SQLAlchemy tehnologije ●
60
Filtriranje po eksplicitnom uslovu nad kolonom kombinovano sa spajanjem. Kriterijum može sadržati sve podržane SQL operatore i izraze: >>> session.query(Klijent).join('adrese'). \ ... filter(Adresa.email_adresa == '
[email protected]').all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti JOIN adrese ON klijenti.id = adrese.klijent_id WHERE adrese.email_adresa = %(email_adresa_1) s ORDER BY klijenti.id {'email_adresa_1': '
[email protected]'} []
filter_by na kriterijumu koji je predat kao imenovani argument. Sve ostalo je isto kao i u
predhodnom primeru: >>> session.query(Klijent).join('adrese'). \ ... filter(email_adresa='
[email protected]').all() ●
Filtriranje po eksplicitnom uslovu nad kolonom preko any() metode (za kolekcije) ili has() (za skalare). To je sažetiji način u odnosu na join(), jer se EXISTS podupit stvara automatski. Metoda any() znači „nađi sve roditeljske, gde bilo koji dete u roditeljskoj kolekciji, zadovoljava uslov“: >>> session.query(Klijent). \ ... filter(Klijent.adrese.any(Adresa.email_adresa == '
[email protected]')).all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE EXISTS (SELECT 1 FROM adrese WHERE klijenti.id = adrese.klijent_id AND adrese.email_adresa = %(email_adresa_1)s ) ORDER BY klijenti.id {'email_adresa_1': '
[email protected]'} []
has() znači „nađi sve roditeljske čije dete zadovoljava kriterijum“: >>> session.query(Adresa). \ ... filter(Adresa.klijent.has(Klijent.ime == 'jeca')).all() SELECT adrese.id AS adrese_id, adrese.email_adresa AS adrese_email_adresa, adrese.klijent_id AS adrese_klijent_id FROM adrese WHERE EXISTS (SELECT 1 FROM klijenti WHERE klijenti.id = adrese.klijent_id AND klijenti.ime = %(ime_1)s ) ORDER BY adrese.id {'ime_1': 'jeca'} [, , ]
I any() i has() mogu primiti imenovane argumente.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije ●
61
filter_by() gde je kriterijum identitet objekta, tj. sam uslov je dete objekat. U najvećem
broju slučajeva neće biti potrebe da se referencira dete tabela. Samo dete sadrži dovoljno informacija za izgradnju kriterijuma koji će se koristiti u upitu za roditelje. Za veze M-1 i 1-1, rezultat sadrži sve objekte koji referenciraju dato dete: # lociramo klijenta >>> jeca = session.query(Klijent).filter(Klijent.ime == 'jeca').one() # koristimo ga u filter_by >>> session.query(Adresa).filter_by(klijent=jeca).all() SELECT adrese.id AS adrese_id, adrese.email_adresa AS adrese_email_adresa, adrese.klijent_id AS adrese_klijent_id FROM adrese WHERE %(param_1)s = adrese.klijent_id ORDER BY adrese.id {'param_1': 5} [, , ]
Oblik sa filter() je: session.query(Adresa).filter(Adresa.klijent == jeca).all()
Slično se može postići sa metodom with_parent(). U odnosu na filter() i filter_by() ona relaciju posmatra u suprotnom smeru, ne od objekta koji se traži već ka objektu koji se traži. U gornjem primeru smo tražili pojave klase Adresa kod koje je atribut klijent tačno zadat objekat, a ovde ćemo tražiti adrese koje su sadržane u zadatom roditeljskom objektu. Ako roditelj ima više kolekcija/atributa u kojima se dete može naći (kad postoje višestruke 1-M/1-1 veze ka istom entitetu) potrebno je zadati naziv kolekcije/atributa u kojoj se dete traži preko imenovanog argumenta property (u narednom kôdu se taj argument može izostaviti, ali je stavljen radi primera): >>> session.query(Adresa).with_parent(jeca, property='adrese').all() SELECT adrese.id AS adrese_id, adrese.email_adresa AS adrese_email_adresa, adrese.klijent_id AS adrese_klijent_id FROM adrese WHERE %(param_1) s = adrese.klijent_id ORDER BY adrese.id {'param_1': 5} [, , ]
Ako se u filter() metodi koristi operator != stvara se negirana EXISTS klauzula: >>> session.query(Adresa).filter(Adresa.klijent != jeca).all() SELECT adrese.id AS adrese_id, adrese.email_adresa AS adrese_email_adresa, adrese.klijent_id AS adrese_klijent_id FROM adrese WHERE NOT (EXISTS (SELECT 1 FROM klijenti WHERE klijenti.id = adrese.klijent_id AND klijenti.id = %(id_1)s )) ORDER BY adrese.id {'id_1': 5} []
Ako se u ovom poređenje vrši sa None objektom, stvara se IS NULL klauzula ako je operator == ili IS NOT NULL ako je !=:
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
62
>>> session.query(Adresa).filter(Adresa.klijent == None).all() SELECT adrese.id AS adrese_id, adrese.email_adresa AS adrese_email_adresa, adrese.klijent_id AS adrese_klijent_id FROM adrese WHERE adrese.klijent_id IS NULL ORDER BY adrese.id {}
Za M-M i 1-M veze, rezultat će biti svi objekti koji sadrže u svojoj kolekciji dati dete objekat: >>> adresa = session.query(Adresa). \ ... filter(Adresa.email_adresa=='
[email protected]').one() >>> session.query(Klijent).filter_by(adrese=adresa).all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE klijenti.id = %(param_1)s ORDER BY klijenti.id {'param_1': 5} []
Oblik sa filter() koristi contains() metodu: session.query(Klijent).filter(Klijent.adrese.contains(adresa)).all() ●
Filtriranje preko liste instanci na 1-M vezi gde se operator jednakosti može koristiti dajući višestruke EXISTS klauzule: >>> session.query(Klijent).filter(Klijent.adrese == adrese).all() SELECT klijenti.id AS klijenti_id, klijenti.ime AS klijenti_ime, klijenti.puno_ime AS klijenti_puno_ime, klijenti.lozinka AS klijenti_lozinka FROM klijenti WHERE(EXISTS (SELECT 1 FROM adrese WHERE klijenti.id = adrese.klijent_id AND adrese.id = %(id_1)s )) AND(EXISTS (SELECT 1 FROM adrese WHERE klijenti.id = adrese.klijent_id AND adrese.id = %(id_2)s )) AND(EXISTS (SELECT 1 FROM adrese WHERE klijenti.id = adrese.klijent_id AND adrese.id = %(id_3)s )) ORDER BY klijenti.id {'id_2': 2, 'id_3': 3, 'id_1': 1} []
5.2.8 Brisanje Pokušajmo da obrišemo Jecu i vidimo šta se događa. Označićemo objekat kao izbrisan u sesiji, potom ćemo izvršiti count() upit da potvrdimo da red ne postoji u tabeli:
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
63
>>> session.delete(jeca) >>> session.query(Klijent).filter_by(ime='jeca').count() UPDATE adrese SET klijent_id = %(klijent_id)s WHERE adrese.id = %(adrese_id)s {'klijent_id': None, 'adrese_id': 1} UPDATE adrese SET klijent_id = %(klijent_id)s WHERE adrese.id = %(adrese_id)s {'klijent_id': None, 'adrese_id': 2} UPDATE adrese SET klijent_id = %(klijent_id)s WHERE adrese.id = %(adrese_id)s {'klijent_id': None, 'adrese_id': 3} DELETE FROM klijenti WHERE klijenti.id = %(id)s {'id': 5} SELECT COUNT(1) AS count_1 FROM klijenti WHERE klijenti.ime = %(ime_1) s {'ime_1': 'jeca'} 0L
Kao što se vidi, brisanjem klijenta se ne brišu i njegove adrese, već se klijent_id kolona postavlja na NULL. SQLAlchemy ne pretpostavlja kaskadno brisanje, moramo sami podesiti takvo ponašanje. Zato opozovimo sve što smo uradili, i krenimo iz početka, od podešavanja relacija u preslikaču: >>> session.rollback() ROLLBACK >>> session.clear() >>> clear_mappers()
Moramo reći relaciji adrese na Klijent klasi da želimo kaskadno brisanje zavisnih Adresa objekata. Takođe želimo da objekti Adresa klase koji budu odvojeni od roditeljskog Klijent objekta budu obrisani, bez obzira na to da li je roditelj obrisan ili ne. Za opisano ponašanje koristimo dve kaskadne opcije: all i delete-orphan, u relation() funkciji: >>> mapper(Klijent, klijenti_tabela, properties={ ... 'adrese' : relation(Adresa, backref='klijent', cascade='all, delete-orphan') ... }) >>> mapper(Adresa, adrese_tabela)
Sad kad učitamo pojavu Klijent klase, pa iz kolekcije u atributu adrese uklonimo jedan Adresa objekat (operatorom del) , to će dovesti do brisanja objekta ne samo u memoriji već i u bazi: # učitamo jecu ponovo u sesiju >>> jeca = session.query(Klijent).get(jeca.id) # obrišemo jednu od adresa (što pokreće odloženo učitavanje) >>> del jeca.adrese[1] SELECT adrese.id AS adrese_id, adrese.email_adresa AS adrese_email_adresa, adrese.klijent_id AS adrese_klijent_id FROM adrese WHERE %(param_1)s = adrese.klijent_id ORDER BY adrese.id {'param_1': 5}
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
64
#ostalo je 2 adrese >>> session.query(Adresa).filter(Adresa.klijent == jeca).count() DELETE FROM adrese WHERE adrese.id = %(id)s {'id': 2} SELECT COUNT(1) AS count_1 FROM adrese WHERE %(param_1)s = adrese.klijent_id {'param_1': 5} 2L
5.2.9 Uspostavljanje M-M veze Kao primer uzećemo zamišljenu blog aplikaciju, gde korisnici mogu pisati blog članke koji imaju oznake tematike povezane sa njima. Prvo nove tabele: >>> >>> ... ... ... ... ... >>> ... ... ... >>> ... ... ...
from sqlalchemy import Text clanci_tabela = Table('clanci', metadata, Column('id', Integer, primary_key=True), Column('klijent_id', Integer, ForeignKey('klijenti.id')), Column('naslov', String(255), nullable=False), Column('sadrzaj', Text) ) teme_tabela = Table('teme', metadata, Column('id', Integer, primary_key=True), Column('tema', String(50), nullable=False, unique=True) ) clanak_teme = Table('clanci_teme', metadata, Column('clanak_id', Integer, ForeignKey('clanci.id')), Column('teme_id', Integer, ForeignKey('teme.id')) )
Tabela clanak_teme uspostavlja M-M vezu između tabela clanci_tabela i teme_tabela. Takve tabele se nazivaju asocijativne tabele. Ovu šemu, zajedno sa ranije definisanim tabelama, možemo predstaviti sledećim dijagramom objekata i veza:
Slika 7: dijagram objekata i veza za M-M relaciju
Razvoj Python programa korišćenjem SQLAlchemy tehnologije >>> metadata.create_all(engine) SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name': 'klijenti'} SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name': 'adrese'} SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name': 'clanci'} SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name': 'teme'} SELECT relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = current_schema() AND lower(relname) = %(name)s {'name': 'clanci_teme'} CREATE TABLE clanci ( id SERIAL NOT NULL, klijent_id INTEGER, naslov VARCHAR(255) NOT NULL, sadrzaj TEXT, PRIMARY KEY (id), FOREIGN KEY(klijent_id) REFERENCES klijenti (id) ) {} COMMIT CREATE TABLE teme ( id SERIAL NOT NULL, tema VARCHAR(50) NOT NULL, PRIMARY KEY (id), UNIQUE (tema) ) {} COMMIT CREATE TABLE clanci_teme ( clanak_id INTEGER, teme_id INTEGER, FOREIGN KEY(clanak_id) REFERENCES clanci (id), FOREIGN KEY(teme_id) REFERENCES teme (id) ) {} COMMIT
Potom klase: >>> class BlogClanak(object): ... def __init__(self, naslov, sadrzaj, autor=None): ... self.autor = autor ... self.naslov = naslov ... self.sadrzaj = sadrzaj ... ... def __repr__(self): ... return 'BlogClanak(%r, %r, %r)' % (self.naslov, self.sadrzaj, self.autor)
65
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
66
>>> class Tema(object): ... def __init__(self, tema): ... self.tema = tema
I na kraju preslikački objekti: >>> from sqlalchemy.orm import backref >>> mapper(Tema, teme_tabela) >>> mapper(BlogClanak, clanci_tabela, properties={ ... 'autor' : relation(Klijent, backref=backref('clanci', lazy='dynamic')), ... 'teme' : relation(Tema, secondary=clanak_teme, backref='clanci') ... })
Tri nove stvari se vide u ovim podešavanjima: ●
Klijent relacija ima backref, ko i ranije, samo što je ovaj put to funkcija pod imenom backref(). Ova funkcija se koristi kad je potrebno podesiti povratnu vezu
●
opcija u funkciji backref() je lazy='dynamic' čime se postavlja podrazumevana strategija učitavanja tog atributa, u ovom slučaju strategija omogućava delimično učitavanje rezultata
●
relacija teme koristi imenovani argument secondary da označi asocijativnu tabelu za M-M vezu između BlogClanak i Tema entiteta
Upotreba nije mnogo drugačija od onog što smo do sada radili. >>> clanak = BlogClanak('Jecin članak o SQLAlchemyju', 'Test', jeca) >>> session.save(clanak)
Sada napravimo nekoliko tema vezanih za članak: >>> clanak.teme.append(Tema('python')) >>> clanak.teme.append(Tema('ORM'))
Sada možemo pretražiti sve blog članke koji za temu imaju 'python'. Koristićemo operator nad kolekcijama any() da lociramo „blog članke gde je jedna od tema 'python'“: >>> session.query(BlogClanak).filter(BlogClanak.teme.any(tema='python')).all() SELECT nextval('"clanci_id_seq"') None INSERT INTO clanci (id, klijent_id, naslov, sadrzaj) VALUES (%(id)s, %(klijent_id)s, %(naslov)s, %(sadrzaj)s) {'klijent_id': 5, 'naslov': 'Jecin članak o SQLAlchemyju', 'id': 1L, 'sadrzaj': 'Test'} SELECT None INSERT VALUES {'id':
nextval('"teme_id_seq"')
SELECT None INSERT VALUES {'id':
nextval('"teme_id_seq"')
INTO teme (id, tema) (%(id)s, %(tema)s) 1L, 'tema': 'python'}
INTO teme (id, tema) (%(id)s, %(tema)s) 2L, 'tema': 'ORM'}
INSERT INTO clanci_teme (clanak_id, teme_id) VALUES (%(clanak_id)s, %(teme_id)s) [{'clanak_id': 1L, 'teme_id': 1L}, {'clanak_id': 1L, 'teme_id': 2L}] SELECT clanci.id clanci.klijent_id clanci.naslov clanci.sadrzaj
AS AS AS AS
clanci_id, clanci_klijent_id, clanci_naslov, clanci_sadrzaj
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
67
FROM clanci WHERE EXISTS (SELECT 1 FROM clanci_teme, teme WHERE clanci.id = clanci_teme.clanak_id AND teme.id = clanci_teme.teme_id AND teme.tema = %(tema_1)s ) ORDER BY clanci.id {'tema_1': 'python'} [BlogClanak('Jecin članak o SQLAlchemyju', 'Test', )]
Ako želimo pretražiti samo Jecine članke, možemo pomoću nje kao roditelja da suzimo pretragu: >>> session.query(BlogClanak).with_parent(jeca). \ ... filter(BlogClanak.teme.any(tema='python')).all() SELECT clanci.id AS clanci_id, clanci.klijent_id AS clanci_klijent_id, clanci.naslov AS clanci_naslov, clanci.sadrzaj AS clanci_sadrzaj FROM clanci WHERE %(param_1)s = clanci.klijent_id AND(EXISTS (SELECT 1 FROM clanci_teme, teme WHERE clanci.id = clanci_teme.clanak_id AND teme.id = clanci_teme.teme_id AND teme.tema = %(tema_1)s )) ORDER BY clanci.id {'param_1': 5, 'tema_1': 'python'} [BlogClanak('Jecin članak o SQLAlchemyju', 'Test', )]
Ili možemo koristiti Jecinu teme relaciju, koja je podešena kao dinamička, da preko nje pravimo upit: >>> jeca.clanci.filter(BlogClanak.teme.any(tema='python')).all() >>> session.query(BlogClanak).with_parent(jeca) \ ... .filter(BlogClanak.teme.any(tema='python')) \ ... .all() SELECT clanci.id AS clanci_id, clanci.klijent_id AS clanci_klijent_id, clanci.naslov AS clanci_naslov, clanci.sadrzaj AS clanci_sadrzaj FROM clanci WHERE %(param_1) s = clanci.klijent_id AND(EXISTS (SELECT 1 FROM clanci_teme, teme WHERE clanci.id = clanci_teme.clanak_id AND teme.id = clanci_teme.teme_id AND teme.tema = %(tema_1) s )) ORDER BY clanci.id {'param_1': 5, 'tema_1': 'python'} [BlogClanak('Jecin članak o SQLAlchemyju', 'Test', )]
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
68
6. Napredna upotreba ORM paketa U prethodnoj glavi dat je opšti pogled na SQLAlchemy kao SQL alat i kao objektno-relacioni preslikač, bez zalaženja u detalje i dublja objašnjenja. U ovom poglavlju se obrađuju neka složenija pitanja u vezi sa preslikavanjem relacija u objekte i obnuto.
6.1 Definisanje tabela Ovo poglavlje se bavi opisom šeme baze podataka kroz SQLAlchemy. Iz tog opisa je moguće stvoriti šemu. Suprotna mogućnost je da šema već postoji pa da se iz nje stvori opis u SQLAlchemyju. I poslednja mogućnost je da šema baze i opis u SQLAlchemyju žive nezavisno uz ručno usklađivanje.
6.1.1 Tipovi kolona Postoje ugrađeni tipovi koji su generički i imaju implementaciju za svaki dijalekat. Oni predstavljaju presek tipova poznatijih sistema. U sledećoj tabeli je dat njihov pregled: Tip
Opis tipa
String/Unicode
Stvara VARCHAR u DDL. Argument length zadaje najveći broj znakova koje polje može primiti. Ako se argument length izostavi tip se ponaša kao SQL tip TEXT kod kog nema ograničenje na broj znakova. Argument enconding podešava kodiranje stringova, sa Unicode tipom encoding je utf-8
Text/UnicodeText
Predstavljaju neograničene verzije String/Unicode tipova. Pretvaraju se u TEXT ili CLOB tip u tabeli baze
Numeric
Daje DECIMAL ili NUMERIC. Ima argumente precision i length za zadavanje ukupne dužine broja i broj decimala
Float
Vraća Python float broj. Takođe ima precision argument
Datetime/Date/Time
Vraća objekte iz datetime modula Pythona
Interval
Radi sa datetime.timedelta objektima. U PostgreSQL koristi se ugrađeni INTERVAL tip, za ostale se koristi vreme od epohe (1.1.1970)
Binary
Stvara BLOB ili BYTE
Boolean
Stvara BOOLEAN ili SMALLINT kolonu a vraća True ili False
PickleType
Podtip Binary tipa i primenjuje pickle.dump() metodu na prosleđene objekte. Ta metoda serijalizuje Python objekte i čuva ih u bazi. Tabela 2 : tipovi kolona u SQLAlchemyju
Pored ovih tipova, SQLAlchemy ima čitavu paletu tipova vezanih za pojedine dijalekte. Npr. za PostgreSQL postoje kolone tipa niza (sqlalchemy.databases.postgres.PGArray) ili ENUM za MySQL (sqlalchemy.databases.mysql.MSEnum).
6.1.2 Različito ime kolone u bazi i kolone u Table objektu Do sada smo stvarali tabele u bazi preko Table objekta. Svaka kolona tog objekta davala je
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
69
istoimenu kolonu u tabeli baze. Upotrebom key argumenta možemo zadati drugačije ime kolone u Table objektu, od imena kolone u bazi: >>> zaposleni = Table('zaposleni', metadata, ... Column('zaposleni_id', Integer, primary_key=True), ... Column('ime_zaposlenog', String(60), nullable=False, key='ime')) CREATE TABLE zaposleni ( zaposleni_id SERIAL NOT NULL, ime_zaposlenog VARCHAR(60) NOT NULL, PRIMARY KEY (zaposleni_id) ) >>> print zapolsleni.c.ime zaposleni.ime_zaposlenog
Ako bismo pokušali da pristupimo zaposleni.ime_zaposlenog dobili bismo grešku koja kaže da ne postoji atribut sa tim imenom.
6.1.3 Refleksija tabela Programer često nema tu lagodnost da radi projekat od početka. A u prošlim primerima smo pretpostavljali da tabele koje koristimo ne postoje u baze već ih mi pravimo iz kôda. Često tabele već postoje, i u tom slučaju možemo Table objekat napraviti iz postojeće šeme preko refleksije. Tako će se učitati ne samo imena kolona i njeni tipovi, već i spoljni i primarni ključevi i u nekim slučajevima i podrazumevane vrednosti. Da bi se izvršila refleksija treba proslediti Engine instancu Table konstruktoru i postaviti autoload argument na True: >>> poruke = Table('poruke', metadata, autoload_with=engine, autoload=True) >>> for kolona in poruke.columns: print repr(kolona) Column(u'id', PGInteger(), table=, primary_key=True, nullable=False, default=PassiveDefault()) Column(u'tekst', PGString(length=60, convert_unicode=False, assert_unicode=None), table=) Column(u'vreme', PGDateTime(timezone=True), table=)
SQLAlchemy pretražuje katalog baze i iz njega izvlači metapodatke o tabeli poruke. Izvučene su kolone id koja je primarni ključ, test koja je varchar i vreme timestamp. Objekti koji predstavljaju te podatke su specifični za PostgreSQL SUBP što se vidi iz prefiksa PG. Ako se učitava podatak o tabeli koja preko spoljnog ključa referencira drugu tabelu učitaće se i ta tabela. Za pojedinačne kolone moguće je nadjačati refleksiju i dati svoju izmenjenu definiciju kolone. To se zadaje nizom Column objekata u konstruktor Table objekta kao u slučaju kad nema refleksija. Moguće je odrediti na koje se kolone obavlja refleksija preko imenovanog argumenta include_columns kome se prosledi lista imena kolona za koje želimo refleksiju.
6.1.4 Složeni ključevi Da bi se predstavio složeni ključ potrebno je argumentom primary_key=True označiti kolone koje ga čine. Za složene spoljne ključeve postoji posebna klasa ForeignKeyConstraint kojoj se prosleđuje niz kolona tekuće tabele koje referenciraju ključ druge i niz kolona koje se referenciraju: stavka_racuna = Table('stavke_racuna', metadata, Column('racun_id', Integer, ForeignKey('racuni.id'), pirmary_key=True), Column('rb', Integer, primay_key=True), Column('artikal_id', Integer, ForeignKey('artikli.id'), Column('cena', Numeric(precision=10, length=3), Column('kolicina'. Integer)) otpremnica_robe = Tabela('otpremnica_robe', metadata, Column('datum_slanja', Date, nullable=Flase), Column('racun_id', Integer, nullable=False),
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
70
Column('stavka_id', Integer, nullable=False), ForeignKeyConstraint(['racun_id', 'stavka_id'], ['stavke_racuna.racun_id', 'stavke_racuna.rb']))
6.1.5 ON UPDATE i ON DELETE Postavljanje referencijalnih akcija se vrši preko onupdate i ondelete argumenata konstruktora ForeignKeyConstraint objekta: stavka_racuna = Table('stavke_racuna', metadata, Column('racun_id', Integer, pirmary_key=True), Column('rb', Integer, primay_key=True), Column('artikal_id', Integer, ForeignKey('artikli.id'), Column('cena', Numeric(precision=10, length=3), Column('kolicina'. Integer), ForeignKeyConstraint(['racun_id'], ['racuni.id], onupdate="CASCADE", ondelete="CASCADE"))
6.1.6 Podrazumevane vrednosti kolona Mogu se zadati za INSERT i za UPDATE operacije, a sama podrazumevana vrednost može dobiti ili kroz Python kôd ili preko SQL izraza: # funkcija koja vraća rastuči niz brojeva 1, 2, 3 ... i = 0 def sekvenca(): global i i += 1 return i t = Table("test1", metadata, # podrazumevana vrednost iz funkcije Column('id', Integer, primary_key=True, default=mydefault), # skalarna podrazumevana vrednost Column('ime', String(10), default="nepoznato"), # podrazumevana vrednost za update Column('vreme_azuriranja', DateTime, onupdate=datetime.now), # podrazumevana vrednost kao SQL izraz Column('vreme_nastanka', DateTime, default=func.now()) )
Funkcija sekvenca nam u stvarnosti nije potrebna jer SQLAlchemy ima podršku za SQL sekvence (identity, autonumber) kao podrazumevane vrednosti pomoću Sequence klase: Column('sifra_artikla', Integer, Sequence('artikal_id_seq'), primary_key=True)
6.1.6 Ograničenja na kolonama Postoji 2 vrste ograničena: UNIQUE i CHECK. mytable = Table('test2', metadata, # UNIQUE na nivou kolone Column('kol1', Integer, unique=True), Column('kol2', Integer), Column('kol3', Integer), # složeno UNIQUE ograničenje, 'name' je opcioni argument UniqueConstraint('kol2', 'kol3', name='uiq_1') # CHECK na nivou kolone Column('kol4', Integer, CheckConstraint('kol5 > 5')),
)
Column('kol5', Integer), Column('kol6', Integer), # CHECK na nivou tabele, 'name' je opcioni argument CheckConstraint('kol5 > kol6 + 5', name='check_1')
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
71
6.2 Podešavanje preslikavanja Ovo poglavlju govori o povezivanju opisa šeme baze (iz prethodnog poglava) sa domenskim modelom. Tu su podešavanja preslikavanja kolona u atribute, određivanje načina predstavljanja nasleđivanja i podešavanje preslikavanja relacija.
6.2.1 Podešavanje preslikavanja kolona Podrazumevano ponašanje Mapper objekta je da preslika kolone u Table objektu u atribute objekta. Ovo se ponašanje može izmeniti u nekoliko smerova, kao i poboljšati SQL izrazima. Da se učita samo deo kolona kolona iz tabele koristi se include_properties i exclude_properties argumenti: mapper(Klijent, klijenti_tabela, include_properties=['klijent_id', 'ime_klijenta']) mapper(Adresa, adrese_tabela, exclude_properties=['ulica', 'grad', 'drzava', 'postanski_br'])
U prošlim primerima kolona u Table objektu se preslikavala u atribut domenskog imena preko podudaranja imena. Ako želimo da atribut ima drugačije ime od kolone tada ručno povezujemo ime atributa i kolonu u properties asocijativnom nizu: mapper(Klijent, klijenti_tabela, properties={ 'id' : klijenti_tabela.c.klijent_id, 'ime' : klijenti_tabela.c.ime_klijenta, })
Moguće je odložiti učitavanje određenih kolona. Slično kao kôd asocijacija gde se odlaže učitavanje kolekcije objekata samo što se ovde uopštava priča na bilo koju kolonu. Primenjuje se kad želimo izbeći učitavanje velikih tekstualnih ili binarnih polja u memoriju sve dok zaista ne budu potrebni. Pojedinačne kolone se mogu povezati u skupove koji će se učitati zajednički. strucni_clanci = Table('clanci', metadata, Column('clanci_id', Integer, primary_key=True), Column('naslov', String(200), nullable=False), Column('ukratko_o_clanku', String(2000)), Column('tekst', String), Column('slika'1, Binary), Column('slika2', Binary), Column('slika3', Binary) ) class Clanak(object): pass # definiše se preslikavanje koje će učitati sadrzaj kad se prvi put tom # atributu pristupi i učitati sve slike kad se bilo kojoj od njih pristupi mapper(Clanak, strucni_clanak, properties = { 'sadrzaj' : deferred(strucni_clanci.c.tekst), 'slika1' : deferred(strucni_clanci.slika_1, group='slike'), 'slika2' : deferred(strucni_clanci.slika_2, group='slike'), 'slika3' : deferred(strucni_clanci.slika_3, group='slike'), })
Sadržaj članka i slike kao objekti koji opterećuju memoriju su označeni za odloženo učitavanje. Pri tom su slike grupisane, pa se pristupom bilo kojoj od njih pokreće učitavanje i ostalih. Ako se u samom Mapper objektu ne podesi odloženo učitavanje, moguće je pri zadavanju upita to uraditi pomoću defer(), undefer(), defer_group() i undefer_group(): query = session.query(Clanak) query.options(defer('ukratko_o_clanku')).all() query.options(undefer('sadržaj')).all()
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
72
Preslikavanje ne mora biti samo atribut – kolona. Atribut se može povezati sa SQL-EL izrazom preko funkcije column_property(). Izraz treba da vrati skalar, a kolone od kojih se izraz sastoji ne mogu se menjati, već samo čitati: mapper(Korisnik, korisnici_tabela, properties={ 'ime_prezime' : column_property( (korisnici_tabela .c.ime + " " + korisnici_tabela.c.prezime).label('ime_prezime')
})
#korelisani upit kao atribut 'broj_adresa' : column_property( select( [func.count(adrese_tabela.c.adresa_id)], adrese_tabela.c.korisnik_id == korisnici_tabela.c.korisnik_id ).label('broj_adresa') ) )
6.2.2 Preslikavanje nasleđivanja klasa SQLAlchemy podržava tri oblika nasleđivanja (poput JPA): nasleđivanje kroz zajedničku tabelu, gde se nekoliko tipova klasa čuva u jednoj tabeli, nasleđivanje kroz odvojene tabele, u kojem svaka klasa ima svoju sopstvenu tabelu i nasleđivanje kroz spajanje tabela, gde se roditelj/dete klase čuvaju u sopstvenim tabelama u kojima se nalaze samo kolone koje čine razliku između deteta i roditelja, pa se čitava dete tabela dobija preko upita spajanjem roditeljske tabele sa dete tabelom. Kad se preslikači podese sa nasleđivanjem, SQLAlchemy je sposoban da učita elemente polimorfno, što znači da se jednim upitom mogu vratiti objekti različitih tipova. Pretpostavimo sledeće veze između klasa: class Zaposlen(object): def __init__(self, ime): self.ime = ime def __repr__(self): return self.__class__.__name__ + " " + ' '.join(self.__dict__.values()) class Rukovodilac(Zaposlen): def __init__(self, ime, podaci_o_rukovodiocu): super(Rukovodilac, self).__init__(ime) self. podaci_o_rukovodiocu = podaci_o_rukovodiocu class Inzenjer(Zaposlen): def __init__(self, ime, inzenjerske_informacije): super(Inzenjer, self).__init__(ime) self.ime = ime self.inzenjerske_informacije = inzenjerske_informacije
Nasleđivanje kroz spajanje tabela
U ovoj vrsti nasleđivanja skup svih atributa za određenu pojavu klase predstavlja rezultat spajanja svih tabela, od te tabele pa naviše po hijerarhiji nasleđivanja. Za naš primer, prvo ćemo definisati tabelu koja predstavlja Zaposlen klasu. Ona će imati primarni ključ, i po kolonu za svaki atribut te klase: zaposleni = Table('zaposleni', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('tip', String(30), nullable=False) )
Tabela sadrži i kolonu tip. Strogo se savetuje da se, i u slučaju nasleđivanja spajanjem i u slučaju
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
73
nasleđivanja kroz zajedničku tabelu, u korenu (zajedničku) tabelu napravi kolona čija je svrha samo da označi klasu objekta koji je predstavljen tim redom tabele. Ta kolona služi da možemo razdvojiti redove po tome kakvu vrstu objekta čuvaju, te se ta kolona naziva diskriminaciona kolona. id
ime
tip
1
"pera"
"zaposlen"
2
"mika"
"inzenjer"
3
"laza"
"rukovodilac"
4
"zika"
"inzenjer"
5
"djole"
"zaposlen"
Tabela 3: primer sadržaja korene tabele zaposleni sa diskriminacionom kolonom Potom definišemo posebne tabele za Inzenjer i Rukovodilac klase, sa atributima koji pripadaju samo tim klasama. Svaka tabela takođe ima primarni ključ i u većini slučajeva spoljni ključ koji referencira roditeljsku tabelu. Standardno se jedna ista kolona koristi u obe svrhe. inzenjeri = Table('inzenjeri', metadata, Column('zaposlen_id', Integer, ForeignKey('zaposleni.zaposlen_id'), primary_key=True), Column('inzenjerske_informacije', String(50)), ) rukovodioci = Table('rukovodioci', metadata, Column('zaposlen_id', Integer, ForeignKey('zaposleni.zaposlen_id'), primary_key=True), Column('podaci_o_rukovodiocu', String(50)), )
Odnos između tih tabela možemo prikazati dijagramom objekata i veza:
Slika 8: dijagram objekata i veza za nasleđivanje kroz spajanje tabela Sada konfigurišemo Mapper objekte na dosadašnji način, uz dodatni argument koji ukazuje na nasleđivanje, diskriminacionu kolonu i polimorfni identitet svake klase (vrednost koja će se čuvati u diskriminacionoj koloni). mapper(Zaposlen, zaposleni, polymorphic_on=zaposleni.c.tip, polymorphic_identity='zaposlen') mapper(Inzenjer, inzenjeri, inherits=Zaposlen, polymorphic_identity='inzenjer') mapper(Rukovodilac, rukovodioci, inherits=Zaposlen, polymorphic_identity='rukovodilac')
Query objekat uključuje neke pomoćne metode za rad sa nasleđivanjem spajanjem. To su with_polimorphic() i of_type() metode.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
74
Prva metoda utiče na određene potklase tabela iz kojih se dobija rezultat upita. Uobičajeni upit: >>> session.query(Zaposlen).filter(Zaposlen.ime == 'pera').one() SELECT zaposleni.zaposlen_id AS zaposleni_zaposlen_id, zaposleni.ime AS zaposleni_ime, zaposleni.tip AS zaposleni_tip FROM zaposleni WHERE zaposleni.ime = %(ime_1)s ORDER BY zaposleni.zaposlen_id LIMIT 2 OFFSET 0 {'ime_1': 'pera'} Zaposlen pera
pretražuje samo zaposleni tabelu. Šta ako želimo upit nad Zaposlenima ali takođe i nad Inzenjerima? Mogli bismo samo izvršiti upit nad Inzenjer klasom i rešili bismo problem. Ali ako bismo koristitli kriterijum filtriranja po više potklasa (koje se ne nasleđuju direktno jedna iz druge), tražili bismo spoljno spajanje (OUTER JOIN) svih tih tabela. Metoda with_polimorphic() govori Query objektu iz kojih se potklasa želi rezultat: >>> session.query(Zaposlen).with_polymorphic(Inzenjer). \ ... filter(Inzenjer.inzenjerske_informacije.ilike('%aviomehan%')).all() SELECT zaposleni.zaposlen_id AS zaposleni_zaposlen_id, zaposleni.ime AS zaposleni_ime, zaposleni.tip AS zaposleni_tip, inzenjeri.zaposlen_id AS inzenjeri_zaposlen_id, inzenjeri.inzenjerske_informacije AS inzenjeri_inzenjerske_informacije FROM zaposleni LEFT OUTER JOIN inzenjeri ON zaposleni.zaposlen_id = inzenjeri.zaposlen_id WHERE inzenjeri.inzenjerske_informacije ILIKE %(inzenjerske_informacije_1)s ORDER BY zaposleni.zaposlen_id {'inzenjerske_informacije_1': '%aviomehan%'} [Inzenjer laza mašinski inženjer smera za aviomehaniku]
Čak i bez kriterijuma može se upotrebiti with_polimorphic() metoda da bi se iskoristila prednost učitavanja u jednom rezultatu svih pojava klase i potklasa. Za ovakvu optimizaciju metoda prihvata džoker '*' koji označava da se sve tabele trebaju spojiti: >>> session.query(Zaposlen).with_polymorphic('*').all() SELECT zaposleni.zaposlen_id AS zaposleni_zaposlen_id, zaposleni.ime AS zaposleni_ime, zaposleni.tip AS zaposleni_tip, rukovodioci.zaposlen_id AS rukovodioci_zaposlen_id, rukovodioci.podaci_o_rukovodiocu AS rukovodioci_podaci_o_rukovodiocu, inzenjeri.zaposlen_id AS inzenjeri_zaposlen_id, inzenjeri.inzenjerske_informacije AS inzenjeri_inzenjerske_informacije FROM zaposleni LEFT OUTER JOIN rukovodioci ON zaposleni.zaposlen_id = rukovodioci.zaposlen_id LEFT OUTER JOIN inzenjeri ON zaposleni.zaposlen_id = inzenjeri.zaposlen_id ORDER BY zaposleni.zaposlen_id {} [Zaposlen pera, Rukovodilac mika dipl. menadžer sa Megatrend fakulteta u Lajkovcu, Inzenjer laza mašinski inženjer smera za aviomehaniku]
Ako postoji relacija sa natklasom, moguće je spajanje suziti na određene potklase. Npr. ako zaposleni tabela predstavlja kolekciju zaposlenih koji su u vezi sa Preduzece objektom. Dodaćemo preduzece_id u zaposleni tabelu: preduzeca = Table('preduzeca', metadata, Column('preduzece_id', Integer, primary_key=True), Column('naziv', String(50)) )
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
75
zaposleni = Table('zaposleni', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('tip', String(30), nullable=False), Column('preduzece_id', Integer, ForeignKey('preduzeca.preduzece_id')) ) class Preduzece(object): pass mapper(Preduzece, preduzeca, properties={ 'zaposleni' : relation(Zaposlen) })
Kada bi želeli da spojimo preduzeće ne sa bilo kojim zaposlenim već samo sa inženjerima, metoda join(), kao i operatori any()/has(), bi nam po podrazumevanom ponašanju stvorila JOIN konstrukciju nad tabelama preduzeca i zaposleni bez uključivanja inženjera i rukovodilaca. Ako želimo da se ograničimo samo na Inzenjer klasu to saopštavamo preko of_type() operatora: >>> session.query(Preduzece).join(Preduzece.zaposleni.of_type(Inzenjer)). \ ... filter(Inzenjer.inzenjerske_informacije.ilike('%avio%')).all() SELECT preduzeca.preduzece_id AS preduzeca_preduzece_id, preduzeca.naziv AS preduzeca_naziv FROM preduzeca JOIN (SELECT zaposleni.zaposlen_id AS zaposleni_zaposlen_id, zaposleni.ime AS zaposleni_ime, zaposleni.tip AS zaposleni_tip, zaposleni.preduzece_id AS zaposleni_preduzece_id, inzenjeri.zaposlen_id AS inzenjeri_zaposlen_id, inzenjeri.inzenjerske_informacije AS inzenjeri_inzenjerske_informacije FROM zaposleni JOIN inzenjeri ON zaposleni.zaposlen_id = inzenjeri.zaposlen_id ) AS anon_1 ON preduzeca.preduzece_id = anon_1.zaposleni_preduzece_id WHERE anon_1.inzenjeri_inzenjerske_informacije ILIKE %(inzenjerske_informacije_1)s ORDER BY preduzeca.preduzece_id {'inzenjerske_informacije_1': '%avio%'} []
Operatori any() i has() se mogu slagati sa of_type() kada se umetnuti uslov zasniva na nekoj potklasi: >>> session.query(Preduzece).filter(Preduzece.zaposleni.of_type(Inzenjer). \ ... any(Inzenjer.inzenjerske_informacije.ilike('%avio%'))).all() SELECT preduzeca.preduzece_id AS preduzeca_preduzece_id, preduzeca.naziv AS preduzeca_naziv FROM preduzeca WHERE EXISTS (SELECT 1 FROM zaposleni JOIN inzenjeri ON zaposleni.zaposlen_id = inzenjeri.zaposlen_id WHERE preduzeca.preduzece_id = zaposleni.preduzece_id AND inzenjeri.inzenjerske_informacije ILIKE %(inzenjerske_informacije_1)s ) ORDER BY preduzeca.preduzece_id {'inzenjerske_informacije_1': '%avio%'} []
EXISTS podupit se vrši nad spojom tabela zaposleni i inzenjeri i ima uslov koji povezuje podupit sa roditeljskom tabelom preduzeca (korelisani podupit).
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
76
Nasleđivanje kroz zajedničku tabelu
U ovoj vrsti nasleđivanja atributi osnovne klase kao i svih njenih potklasa predstavljeni su jednom tabelom. Za svaki atribut osnovne klase i njenih potklasa postoji kolona. One kolone koje pripadaju samo jednoj potklasi mogu imati i vrednost NULL. Podešavanje izgleda kao i kôd nasleđivanja spojenim tabelama, samo što je sada u pitanju samo jedna tabela. Diskriminaciona kolona je obavezna jer nikako drugačije ne bismo mogli da razlikujemo kojoj klasi pripada koji red u tabeli. Tabela se specificira samo u osnovnom Mapper objektu, za nasleđene klase treba ostaviti parametar table praznim: zaposleni_tabela = Table('zaposleni', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('podaci_o_rukovodiocu', String(50)), Column('inzenjerske_informacije', String(50)), Column('tip', String(20), nullable=False) ) zaposlen_mapper = mapper(Zaposlen, zaposleni_tabela, polymorphic_on=zaposleni_tabela.c.tip, polymorphic_identity='zaposleni') rukovodilac_mapper = mapper(Rukovodilac, inherits=zaposlen_mapper, polymorphic_identity='rukovodilac') inzenjer_mapper = mapper(Inzenjer, inherits=zaposlen_mapper, polymorphic_identity='inzenjer')
Primetite da se u Mapper objektima za potklase Rukovodilac i Inzenjer izostavlja povezivanje sa tabelom jer se ono nasleđuje iz Mapper objekta za natklasu Zaposlen. Izostavljanje tabele za nasleđene Mapper objekte u nasleđivanju kroz zajedničku tabelu je obavezno. Nasleđivanje kroz odvojene tabele
Kôd ovog načina nasleđivanja svaka se klasa preslikava u posebnu tabelu kao u primeru: zaposleni_tabela = Table('zaposleni', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), ) rukovodioci_tabela = Table('rukovodioci', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('podaci_o_rukovodiocu', String(50)), ) inzenjeri_tabela = Table('inzenjeri', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('inzenjerske_informacije', String(50)), )
U ovom obliku nasleđivanja nema diskriminacione kolone. Ako polimorfno učitavanje nije potrebno, nema nikakve koristi od inherits argumenta, te je potrebno samo definisati posebne Mapper objekte za svaku klasu: mapper(Zaposen, zaposleni_tabela) mapper(Rukovodilac, rukovodioci_tabela) mapper(Inzenjer, inzenjeri_tabela)
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
77
Za polimorfno učitavanje potrebno je koristiti select_table argument. U ovom slučaju moramo stvoriti uniju sve tri tabele. SQLAlchemy dolazi sa pomoćnom funkcijom za stvaranje tih polimorfnih unija koja će povezati različite kolone u upite sa istim brojem kolona i istim imenima kolona, a takođe će generisati virtualnu diskriminacionu kolonu za svaki podupit: unija = polymorphic_union({ 'zaposlen' : zaposleni_tabela, 'rukovodilac' : rukovodioci_tabela, 'inzenjer' : inzenjeri_tabela }, 'tip', 'unija') zaposlen_mapper = mapper(Zaposlen, zaposleni_tabela, select_table=unija, polymorphic_on=unija.c.tip, polymorphic_identity='zaposlen') rukovodilac_mapper = mapper(Rukovodilac, rukovodioci_tabela, inherits=zaposlen_mapper, concrete=True, polymorphic_identity='rukovodilac') inzenjer_mapper = mapper(Inzenjer, inzenjeri_tabela, inherits=zaposlen_mapper, concrete=True, polymorphic_identity='inzenjer')
Prvo smo funkciji polymorphic_union() predali asocijativni niz u kome smo definisali koje će vrednosti biti upisane u diskriminacionu kolonu za koju tabelu. Sledeći argument određuje kako će se zvati diskriminaciona kolona, a poslednji je alijas koji će se koristiti u UNION upitu. Samo za definiciju preslikavanja najviše klase koristi se select_table i polymorphic_on argumenti, a za ostale definicije ti se argumenti ne pišu jer se nasleđuju preko interits argumenta. Nasleđeni Mapper objekti takođe imaju argument concrete sa vrednošću True. Sva tri Mapper objekta imaju polymorphic_identity argument preko kog se iz zajedničke unije vrši odabiranje odgovarajućih redova (preko diskriminacione kolone) kako bi se preslikali u objekte za koje je taj Mapper odgovoran. Rezultat polimorfnog upita je sledeći SQL kôd: >>> session.query(Zaposlen).all() SELECT unija.zaposlen_id unija.ime unija.tip unija.inzenjerske_informacije unija.podaci_o_rukovodiocu FROM (SELECT zaposleni.zaposlen_id zaposleni.ime CAST(NULL AS VARCHAR(50)) CAST(NULL AS VARCHAR(50)) 'zaposlen' FROM zaposleni
AS AS AS AS AS
unija_zaposlen_id, unija_ime, unija_tip, unija_inzenjerske_informacije, unija_podaci_o_rukovodiocu
AS AS AS AS AS
zaposlen_id, ime, podaci_o_rukovodiocu, inzenjerske_informacije, tip
UNION ALL SELECT rukovodioci.zaposlen_id rukovodioci.ime rukovodioci.podaci_o_rukovodiocu CAST(NULL AS VARCHAR(50)) 'rukovodilac' FROM rukovodioci UNION ALL
AS AS AS AS AS
zaposlen_id, ime, podaci_o_rukovodiocu, inzenjerske_informacije, tip
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
78
SELECT inzenjeri.zaposlen_id AS zaposlen_id, inzenjeri.ime AS ime, CAST(NULL AS VARCHAR(50)) AS podaci_o_rukovodiocu, inzenjeri.inzenjerske_informacije AS inzenjerske_informacije, 'inzenjer' AS tip FROM inzenjeri ) AS unija ORDER BY unija.zaposlen_id {} [Zaposlen Petar Petrović, Rukovodilac Jovan Jovanović direktor marketinga, Inzenjer Zoran Milov arhitekta]
Nasleđivanje i relacije
U nasleđivanju kroz spajanje tabela i nasleđivanju kroz zajedničku tabelu postoji mogućnost da se te tabele koriste kao jedna strana relacije i to polimorfno. Dakle, može se uspostaviti relacija sa natklasom (tj. njenom tabelom) a da se i objekti potklasa smeju upotrebiti u relaciji. Slično ovome, Mapper objekti koji nasleđuju drugi Mapper (preko inherits argumenta) mogu imati svoje posebne relacije, koje se dalje mogu nasleđivati kao što je u prethodnom paragrafu napisano. Izmenimo raniji primer relacije između klasa Zaposlen i Preduzece, tako da sada uspostavljamo dvosmernu vezu: preduzeca = Table('preduzeca', metadata, Column('preduzece_id', Integer, primary_key=True), Column('naziv', String(50)) ) zaposleni = Table('zaposleni', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('preduzece_id', Integer, ForeignKey('preduzeca.preduzece_id')) ) class Preduzece(object): pass mapper(Preduzece, preduzeca, properties={ 'zaposleni' : relation(Zaposlen, backref='preduzece') })
U scenariju sa nasleđivanjem kroz odvojene tabele, preslikavanje relation() veze je teži posao, jer odvojene tabele nemaju zajedničku tabelu u kojoj bi bio spoljni ključ. Zato se relacija može uspostaviti samo ako se u svakoj tabeli koja učestvuje u njoj postoji spoljni ključ ka drugoj strani relacije: preduzeca = Table('preduzeca', metadata, Column('preduzece_id', Integer, primary_key=True), Column('naziv', String(50)) ) zaposleni_tabela = Table('zaposleni', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('preduzece_id', Integer, ForeignKey('preduzeca.preduzece_id')) ) rukovodioci_tabela = Table('rukovodioci', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('podaci_o_rukovodiocu', String(50)), Column('preduzece_id', Integer, ForeignKey('preduzeca.preduzece_id')) )
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
79
inzenjeri_tabela = Table('inzenjeri', metadata, Column('zaposlen_id', Integer, primary_key=True), Column('ime', String(50)), Column('inzenjerske_informacije', String(50)) Column('preduzece_id', Integer, ForeignKey('preduzeca.preduzece_id')) ) zaposlen_mapper = mapper(Zaposlen, zaposleni_tabela, select_table=unija, polymorphic_on=unija.c.tip, polymorphic_identity='zaposlen') rukovodilac_mapper = mapper(Rukovodilac, rukovodioci_tabela, inherits=zaposlen_mapper, concrete=True, polymorphic_identity='rukovodilac') inzenjer_mapper = mapper(Inzenjer, inzenjeri_tabela, inherits=zaposlen_mapper, concrete=True, polymorphic_identity='inzenjer') mapper(Preduzece, preduzeca, properties={ 'zaposleni' : relation(Zaposlen) })
6.2.3 Napredno preslikavanje relacija U glavi 5 smo videli kako se uspostavljaju uobičajene 1-M/M-1 i M-M veze kod kojih se u bar jednom od objekata u relaciji nalazi kolekcija objekata sa druge strane relacije. Međutim nekada nam je potrebna 1-1 veza. Veza 1-1
U relacionom modelu su veze fizički realizovane kao 1-M odnosno M-1 u suprotnom smeru. Kada pravimo 1-1 vezu mi moramo da uvedemo ograničenja na 1-M/M-1. To radimo tako što preko uselist=False u relation() funkciji označavamo da na „1“ strani ne želimo kolekciju objekata sa „M“ strane već skalar. Ako imamo tabele a_tabela i b_tabela sa fizičkom vezom 1-M (b_tabela ima spoljni ključ ka a_tabeli) imali bismo sledeće preslikavanje: mapper(A_klasa, a_tabela, properties={ 'b':relation(B_klasa, uselist=False, backref='a') })
A sa suprotne strane imali bismo: mapper(B_klasa, b_tabela, properties={ 'a':relation(A_klasa, backref=backref('b', uselist=False)) })
Agregacija
Agregacija je vrsta M-M veze gde se u tabeli koja povezuje levu i desnu strane M-M veze (koja se uobičajeno zove asocijativna tabela) čuvaju i dodatne kolone pored onih koje su spoljni ključevi na levu i desnu tabelu. Tako asocijacija prerasta u agregaciju. U uvodu u objektno-relaciono preslikavanje videli smo da se u slučaju obične M-M veze asocijativna tabela prosleđuje mapper() funkciji preko secondary argumenta. U slučaju agregacije se ne koristi taj argument već se agregaciona tabela preslikava u posebnu klasu. Leva strana relacije referencira agregacioni objekat vezom 1-M, a agregaciona klasa referencira desnu stranu M-1 vezom.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
80
leva_tabela = Table('leva', metadata, Column('id', Integer, primary_key=True)) desna_tabela = Table('desna', metadata, Column('id', Integer, primary_key=True)) agregaciona_tabela = Table('agregacija', metadata, Column('leva_id', Integer, ForeignKey('leva.id'), primary_key=True), Column('desna_id', Integer, ForeignKey('desna.id'), primary_key=True), Column('podatak', String(50)) )
Dijagram objekata i veza za ovaj slučaj je:
Slika 9: dijagram objekata i veza za M-M relaciju sa agregacijom mapper(Leva_klasa, leva_tabela, properties={ 'desni_elementi' : relation(Agregacija) }) mapper(Agregacija, agregaciona_tabela, properties={ 'desni_objekat' : relation(Desna_klasa) }) mapper(Desna_klasa, desna_tabela)
Dvostrana veza bi dodala backref na obe relacije: mapper(Leva_klasa, leva_tabela, properties={ 'desni_elementi' : relation(Agregacija, backref='levi_objekat') }) mapper(Agregacija, agregaciona_tabela, properties={ 'desni_objekat' : relation(Desna_klasa, backref='levi_elementi') }) mapper(Desna_klasa, desna_tabela)
Rad sa agregacijom zahteva da se desni objekat prvo poveže sa agregacionim pa zatim da se doda u kolekciju levog objekta. Stoga pristup desnom objektu iz levog se ostvaruje kroz agregacioni objekat: # pravimo levi objekat, dodajemo desni preko objekta asocijacije l = Leva_klasa() a = Agregacija() a.desni_objekat = Desna_klasa() a.podatak = 'neki podatak o agregaciji' l.desni_elementi.append(a) # iteracija kroz desne objekte preko asocijacije, uključujući i #atribute agregacije (podatak) for a in l.desni_elementi: print a.podatak print a.desni_objekat
SQLAlchemy obezbeđuje i dodatak associationproxy kojim se može u potpunosti sakriti agregacioni objekat tako da je moguće direktno dodati desni objekat u kolekciju levog.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
81
Vezane liste
Jedan od poznatijih relacionih uzora su vezane liste, gde tabela sadrži spoljni ključ koji referencira sopstvenu tabelu. To je najčešći način da se hijerarhijski podaci predstave u tabelama. U narednom primeru ćemo predstaviti stablo u tabeli cvorovi_stabla: cvorovi = Table('cvorovi_stabla', metadata, Column('id', Integer, primary_key=True), Column('roditelj_id', Integer, ForeignKey('cvorovi_stabla.id')), Column('podatak', String(50)), )
Grafički bi se vezana lista predstavila sledećim dijagramom:
Slika 10: dijagram objekata i veza za vezane liste Stablo oblika:
Slika 11: hijerarhijsko stablo – vezana lista bi se u tabeli cvorovi_stabla predstavio kao: Id
roditelj_id
podatak
1
NULL
"koren"
2
1
"dete1"
3
1
"dete2"
4
3
"dete2.1"
5
3
"dete2.2"
6
1
"dete3"
Tabela 4: prmer sadržaja tabele cvorovi_stabla sa redovima prikazanim kao vezana lista
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
82
Upotreba mapper() funkcije za samoreferencijrajuće 1-M relacije je ista kao kod „normalne“ veze. Kad SQLAlchemy otkrije da spoljni ključ iz cvorovi_stabla uspostavlja vezu sa cvorovi_stabla tada se pretpostavlja da je veza 1-M osim ako se ne podesi drugačije: class Cvor(object): pass mapper(Cvore, cvorovi, properties={ 'deca' : relation(Cvor) })
Da bi se napravila M-1 veza od deteta ka roditelju, potrebno je upotrebiti dodatni argument remote_side, kom se dodeljuje Column objekat koji opisuje drugu stranu relacije: mapper(Cvor, cvorovi, properties={ 'roditelj' : relation(Cvor, remote_side=[cvorovi.c.id]) })
A dvosmerna verzija je kombinacija prethodnih: mapper(Cvor, cvorovi, properties={ 'deca' : relation(Cvor, backref=backref('roditelj', remote_side=[cvorovi.c.id])) })
Upit nad ovakvom strukturom se radio na uobičajen način: session.query(Cvor).filter(Cvor.podatak == 'dete2').all()
Što se spajanja u upitima tiče, za samoreferencirajuće strukture je nužno koristiti alijase kako bi se ista tabela mogla referencirati više puta u FROM delu: # vrati sve čvorove nazvane 'dete2.1' sa roditeljem koji se zove 'dete2' >>> print session.query(Cvor).filter(Cvor.podatak == 'dete2.1'). \ ... join('roditelj', aliased=True).filter(Cvor.podatak == 'dete2').one().podatak SELECT cvorovi_stabla.id AS cvorovi_stabla_id, cvorovi_stabla.roditelj_id AS cvorovi_stabla_roditelj_id, cvorovi_stabla.podatak AS cvorovi_stabla_podatak FROM cvorovi_stabla JOIN cvorovi_stabla AS cvorovi_stabla_1 ON cvorovi_stabla_1.id = cvorovi_stabla.roditelj_id WHERE cvorovi_stabla.podatak = %(podatak_1)s AND cvorovi_stabla_1.podatak = %(podatak_2)s ORDER BY cvorovi_stabla.id LIMIT 2 OFFSET 0 {'podatak_2': 'dete2', 'podatak_1': 'dete2.1'} dete2.1
Relacije nad upitima
Funkcija relation() koristi spoljne ključeve koji su uspostavljeni između leve i desne tabele da na osnovu njih sastavi primarni uslov spajanja tih tabela. Ako je veza M-M tada imamo i sekundarni uslov spajanja. U nekim situacijama SQLAlchemy ne može sam sastaviti te uslove spajanja. To se dešava ako se radi sa tabelama na kojima ne postoji spoljni ključ. Razlog zbog kog on možda ne postoji jer nema ni primarnog ključa ili kolona nije unique, ili je Table objekat stvoren refleksijom a sam SUBP nema mogućnost da iz metapodataka sazna spoljni ključ (to je slučaj sa MySQL-om). Uslov spajanja može biti mnogo složeniji od proste veze preko spoljnog ključa. U oba slučaja treba koristiti primaryjoin i po potrebi secondaryjoin argumente relation() funkcije i preko njih uspostaviti uslov spajanja. U ovom primeru pravimo relaciju beogradske_adrese preko koje će se učitati samo adrese kod kojih je za grad stavljeno 'Beograd':
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
83
class Klijent(object): pass class Adresa(object): pass mapper(Adresa, adrese_tabela) mapper(Klijenti, klijenti_tabela, properties={ 'beogradske_adrese' : relation(Adresa, primaryjoin= and_(klijenti_tabela.c.klijent_id == adrese_tabela.c.klijent_id, adresse_tabela.c.grad == 'Beograd')) })
M-M veze se mogu podesiti preko primaryjoin i secondaryjoin. U sledećem primeru se oni postavljaju onako kako bi se i podrazumevano postavili: class BlogClanak(object): pass class Tema(object): pass mapper(Tema, teme_tabela) mapper(BlogClanak, clanci_tabela, properties={ 'teme' : relation(Tema, secondary = clanci_teme_tabela, primaryjoin=clanci_tabela.c.clanak_id == clanci_teme_tabela.c.clanak_id, secondaryjoin=clanci_teme_tabela.c.tema_id == teme_tabela.c.tema_id) })
Kad se koristi primaryjoin i secondaryjoin SQLAlchemy mora znati koje kolone u relaciji referenciraju druge. Kao što smo rekli u najvećem broju slučajeva postoje spoljni ključevi koji se staraju o tome, ali kad njih nema moraju se ručno odrediti preko foreign_keys kolekcije: mapper(Adresa, adrese_tabela) mapper(Klijent, klijenti_tabela, properties={ 'adrese' : relation(Adresa, primaryjoin=klijenti_tabela.c.klijent_id == adrese_tabela.c.klijent_id, foreign_keys=[adrese_tabela.c.klijent_id]) })
Strategije učitavanja relacija
U uvodu u ORM smo predstavili trenutno učitavanje relacije. Njega smo zadavali preko option() metode nad Query objektom i kao rezultat imali smo učitavanje relacije u isto vreme kad i objekat sa leve strane relacije u jednom upitu: jeca = session.query(Klijent) \ .options(eagerload('adrese')) \ .filter_by(ime='jeca').one()
U SQLAlchemyju se sve relacije učitavaju odloženo, osim ako se ne podesi drugačije. Ovakvo ponašanje odudara od onog u JPA gde se 1-1 veze učitavaju trenutno po podrazumevanom podešavanju. Podešavanje strategije učitavanja se vrši preko argumenta lazy u relation() funkciji. Kao što smo rekli podrazumevana vrednost tog argumenta je True. U sledećem primeru tom argumentu dodeljujemo vrednost False tako da se relacija deca učitava trenutno: mapper(Roditelj, roditelji_tabela, properties={ 'deca' : relation(Dete, lazy=False) })
Strategija postavljena u Mapper objektu se može izmeniti u Query objektu u metodi options(), kao što smo u primeru sa početka funkcijom eagerload() učitali trenutno. Slično se preko funkcije
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
84
lazyload() nadjačava podešavanje preslikača i menja ga u trenutno učitavanje: session.query(Roditelj).options(lazyload('deca')).all()
Pored trenutnog i odloženog učitavanja, postoje još dve strategije. Jedna od njih je dinamičko učitavanje relacija. To je vrsta relacije čija relation() funkcija vraća Query objekat umesto kolekcije kad joj se pristupi. Na tu relaciju se može primenjivati filter() kriterijum, kao i limit/offset SQL operacije preko Python operatora za odsecanje niza. Dinamička relacija se direktno uspostavlja metodom dynamic_loader(), a kao povratna referenca se uspostavlja preko lazy argumenta metode backref(). Slede primeri za oba načina: mapper(Klijent, klijenti_tabela, properties={ 'clanci' : dynamic_loader(BlogClanak, backref='autor') }) mapper(BlogClanak, clanci_tabela, properties={ 'autor' : relation(Klijent, backref=backref('clanci', lazy='dynamic')) }) >>> jeca = session.query(Klijent).filter_by(ime='jeca').one() >>> clanci = jeca.clanci.filter(BlogClanak.c.naslov.ilike('%SQLAlchemy%')) >>> type(clanci) >>> clanci.all() SELECT clanci.id AS clanci_id, clanci.klijent_id AS clanci_klijent_id, clanci.naslov AS clanci_naslov, clanci.sadrzaj AS clanci_sadrzaj FROM clanci WHERE %(param_1) s = clanci.klijent_id AND clanci.naslov ILIKE %(naslov_1)s ORDER BY clanci.id {'naslov_1': '%SQLAlchemy%', 'param_1': 5} [BlogClanak('Jecin Blog clanak o SQLAlchemyju', 'Ovo je test', ), BlogClanak('Izašao SQLAlchemy 0.5 beta 2', 'Izašao je SQLAlchemy 0.5 beta 2 sa mnogo novosti i poboljšanja', ), BlogClanak('IBM podržava SQLALchemy', 'IBM je izbacio podršku za SQLAlchemy na DB2 RSUBP', )] >>> clanci = jeca.clanci[1:3] >>> clanci.all() SELECT clanci.id AS clanci_id, clanci.klijent_id AS clanci_klijent_id, clanci.naslov AS clanci_naslov, clanci.sadrzaj AS clanci_sadrzaj FROM clanci WHERE %(param_1)s = clanci.klijent_id ORDER BY clanci.id LIMIT 2 OFFSET 1 {'param_1': 5} [BlogClanak('Foo', 'Bar', ), BlogClanak('Izašao SQLAlchemy 0.5 beta 2', 'Izšao je SQLAlchemy 0.5 beta 2 sa mnogo novosti i poboljšanja', )]
Dinamičke relacije imaju ograničenja na operacije koje upisuju i menjaju podatke. Izmene se mogu vršiti jedino preko append() i remove() metoda. Pošto dinamička relacija uvek izvršava upit, promene na kolekciji neće biti upisane u bazu sve dok se ne isprazni sesija metodom flush(): >>> clanak = session.query(BlogClanak).filter_by(naslov='Foo').one() >>> print clanak BlogClanak('Foo', 'Bar', ) # sledeća promena ne prenosi se na bazu >>> clanak.naslov *= 4
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
85
# mora se eksplicitno pozvati pražnjenje sačuvanih promena # u sesiji iako je sesija konfigurisana kao autoflush=True >>> session.flush() UPDATE clanci SET naslov = %(naslov)s WHERE clanci.id = %(clanci_id)s {'naslov': 'FooFooFooFoo', 'clanci_id': 3} >>> jeca.clanci.remove(clanak) >>> jeva.clanci.append(BlogClanak('Test naslov', 'Test sadržaj')) >>> jeca.clanci.all() UPDATE clanci SET klijent_id = %(klijent_id)s WHERE clanci.id = %(clanci_id)s {'klijent_id': None, 'clanci_id': 3} SELECT nextval('clanci_id_seq'::regclass) None INSERT INTO clanci (id, klijent_id, naslov, sadrzaj) VALUES (%(id)s, %(klijent_id)s, %(naslov)s, %(sadrzaj)s) {'klijent_id': 5, 'naslov': 'Test naslov', 'id': 7L, 'sadrzaj': 'Test sadržaj'} SELECT clanci.id AS clanci_id, clanci.klijent_id AS clanci_klijent_id, clanci.naslov AS clanci_naslov, clanci.sadrzaj AS clanci_sadrzaj FROM clanci WHERE %(param_1)s = clanci.klijent_id ORDER BY clanci.id {'param_1': 5} [BlogClanak('Jecin Blog clanak o SQLAlchemyju', 'Ovo je test', ), BlogClanak('Izašao SQLAlchemy 0.5 beta 2', 'Izašao je SQLAlchemy 0.5 beta 2 sa mnogo novosti i poboljšanja', ), BlogClanak('IBM podržava SQLALchemy', 'IBM je izbacio podršku za SQLAlchemy na DB2 RSUBP', ), BlogClanak('Test naslov', 'Test sadržaj', )]
Primetite da rezultat remove() operacije nad blog člancima nije dovelo do brisanja tog članka, već je samo izbrisana veza članka i autora. To je posledica podrazumevanog ponašanja SQLAlchemyja. Poslednja strategija učitavanja je kad učitavanja nema, tj. lazy argumentu se prosledi None objekat: mapper(Roditelj, roditelji_tabela, properties={ 'deca' : relation(Dete, lazy=None) })
Ponašanje je suprotno dinamičkim relacijama. Kolekcija dece se može menjati, svaka promena će se sačuvati u bazi kao i u sesiji za čitanje. Ali samo u vreme kad se ti objekti dodaju. Međutim kad se instanca Roditelj klase učita iz baze kolekcija dece će biti prazna.
6.3 Korišćenje sesija Ovo poglavlje govori o tome kako se objekat stavlja i uklanja iz sesije i šta se dešava sa povezanim objektima i životnom ciklusu objekta u sesiji. Obrađuje se i upotreba transakcija.
6.3.1 Način rada sesije Instance domenskih objekata se u odnosu na sesiju mogu naći u jednom od četiri stanja: 1. privremenosti (eng. transient) – objekat nije u sesiji i nije sačuvana u bazi. Jedina veza sa SQLAlchemyjem je da za klasu tog objekta postoji pridruženi Mapper objekat. 2. čekanja (eng. pending) – kada se objekat u privremenom stanju prosledi save() metodi prelazi u stanje čekanja. Promena se samo čuva u sesiji te još uvek nije prosleđena bazi ali
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
86
će se poslati već pri prvom sledećem pražnjenju. Kao što smo već videli pražnjenje se kontroliše pomoću flush() metode i preko autoflush imenovanog argumenta pri pozivu metode sessionmaker(). 3. trajnosti (eng. persistent) – kada se objekat nalazi u sesiji i zapisana je u bazi. U ovo stanje se ulazi ili kad se instanca na čekanju upiše u bazu pri pražnjenju sesije, ili kad se upitom iz baze vrati postojeća instanca. 4. razdvojenosti (eng. detached) – objekat je zapisan u bazi, ali više nije u sesiji. Nema ničeg lošeg u ovome, objekat se može normalno koristiti kad je izdvojen iz sesije, osim što se neće izvršavati upiti za učitavanje kolekcija ili atributa koji nisu bili učitani do razdvajanja. Isto važi i za atribute i kolekcije koje su označene kao „istekle“. Ova stanja kao i prelazi između njih su predstavljeni dijagramom prelaza stanja na slici 12.
Slika 12: dijagram prelaza stanja objekta u odnosu na sesiju Sesija se ne ponaša kao keš tj. ona ne služi da ubrza obavljanje operacija smeštanjem podataka sa diska u memoriju i minimizovanjem pristupa disku. Kao kod keširanja, sesija ima mapu identiteta u kojoj čuva objekte po njihovom primarnom ključu. Međutim, sesija ne radi nikakvo keširanje upita. Kada se zada neki upit, npr. session.query(Osoba).filter_by(ime='pera'), čak i ako se Osoba(ime='pera') nalazi u mapi, sesija o tome ne zna ništa. Tek kad se izvrši upit i izuče slog iz baze, sesija će po primarnom ključu znati da li se taj objekat nalazi ili ne nalazi u sesiji. Jedino kad se koristi query.get({neki primarni ključ}) sesija može da izvuče objekat direktno iz mape. Dakle glavni zadatak sesije je sprečavanje da se jedan objekat iz baze (predstavljen kao red tabele) pojavi u sesiji više puta (predstavljen kao objekat). Ako se više puta zahteva neki objekat samo će se prvi put on stvoriti i popuniti podacima iz baze. Svaki sledeći put sesija će vratiti referencu na već učitani objekat. Python ima automatsko upravljanje memorijom (slično javinoj virtualnoj mašini), što znači da se memorija oslobađa ubrzo pošto se eliminiše i poslednja referenca na objekat. Ovakav pristup je zadovoljavajući u većini situacija, ali ponekad je potrebno pratiti objekat koji neko drugi koristi i to praćenje ograničiti samo dok je objekat u upotrebi. Na nesreću, praćenje objekta znači da će uvek postojati referenca na taj objekat samo radi praćenja, čak i onda kad se objekat više ne bude koristio. Da bi se ipak omogućilo automatsko brisanje takvih objekata koristi se
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
87
weakref.WeakValueDictionary klasa koja čuva slabe (eng. weak) reference na objekte. Upravo je mapa identiteta pojava te klase (osim ako se ne specificira weak_identity_map=False u pozivu sessionmaker() funkcije). To znači da će objekti na koje više ne postoji referenca biti automatski
uklonjeni iz sesije. Jedino ako je objekat u stanju čekanja izlazak iz sesije će biti odložen dok se promene ne upišu u bazu.
6.3.2 Ažuriranje i spajanje razdvojenih objekata Metoda update() se koristi kad imamo objekat u razdvojenom stanju koji želimo da vratimo u sesiju. Pošto je između stanja privremenosti i razdvojenosti mala razlika ali po pitanju koje je SQLAlchemy striktan pa se save() može primeniti samo na privremene objekte a update() samo na razdvojene, napravljena je metoda save_or_update() koja korisnika lišava potrebe da vodi računa u kom od ova dva stanja se nalazi objekat: # učitavamo klijenta u jednu sesiju klijent = sess1.query(Klijent).get(5) # izdvajamao ga iz sesije sess1.expunge(klijent) # prenosimo ga u drugu sesiju sess2.save_or_update(klijent)
Metoda merge() je kao i update(), osim što stvara kopiju objekta u sesiji i vraća taj objekat. Objekat koji se predaje u merge() nikad se ne stavlja u sesiju. Metoda će proveriti da li je objekat sa istim primarnim ključem prisutan u sesiji. Ako ne, učitaće ga po primarnom ključu, a potom će prekopirati atribute sa predatog objekta na onaj koji je upravo pronađen. Metoda je korisna za objekte koji su se učitali iz serijalizacije (npr. objekti čuvani u HTTP sesiji, preneti preko mreže itd), gde postoji mogućnosti da se objekat već nalazi u sesiji: # deserijalizovanje objekta iz fajla obj = pickle.load(fajl) # spaja, ako u sesiji već postoji onda će se vratiti taj objekat obj = session.merge(obj)
6.3.3 Izdvajanje objekta iz sesije i zatvaranje sesije Metoda expunge() uklanja objekat iz sesije i tako prevodi objekat iz stanja trajnosti u stanje razdvojenosti., a objekte iz stanja čekanja u stanje privremenosti: session.expunge(obj)
Ova se metoda koristi kada želimo da u potpunosti uklonimo objekat iz memorije, npr. pre poziva del naredbe, čime se sprečava pojava fantomskih operacija kada se prazni sesija. Metoda clear() je ekvivalentna pozivu expunge() nad svim objektima sesije: session.clear()
Treba znati da clear() ne resetuje transakciju ili resurse vezane za konekciju, tako da obično mesto ove metode treba pozvati close(). Ova metoda izvršava clear() oslobađa sve transakcione i resurse konekcije. Kad se konekcija vrati u keš konekcija, transakcija se poništava bez obzira na stanje u kom se nalazi. Tada se Session objekat nalazi u stanju u kom je bio pri stvaranju i može se ponovo koristiti kao da je tek stvoren.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
88
6.3.4 Ponovno učitavanje atributa objekta Atributi pojedinačnih objekata se mogu trenutno učitati iz baze ili se mogu označiti kao „istekli“ što će uzrokovati da se ponovo učitaju pri prvom sledećem pristupu bilo kom atributu koji je preslikan iz baze. Ovo uključuje i sve relacije. One sa odloženim učitavanjem će se ponovo inicijalizovati, one se trenutnim učitavanjem će se ponovo učitati. Sve promene na objektu se odbacuju: # trenutno učitava atribute session.refresh(obj)
obj
# označava objekat kao istekao, atributi će biti ponovo učitani na sledećem pristupu session.expire(obj)
Ove dve metode podržavaju i učitavanje tako određenih atributa koji se prosleđuju u vidu liste njihovih naziva kao stringova: session.refresh(obj, ['atr1', 'atr5']) session.expire(obj, ['atr1', 'atr5'])
6.3.5 Kaskadne relacije Preslikač podržava podešavanje kaskadnog ponašanja na relacijama. Ovo ponašanje određuje kako će Session objekat tretirati pojave povezane odnosom roditelj-dete, gde je roditelj objekat čijom referencom manipulišemo. Kao što smo već videli kaskade se podešavaju preko stringa u kome se daje lista opcija razdvojena zarezima sa mogućim vrednostima: all, delete, save-update, merge, expunge, refresh-expire i delete-orphan: mapper(Narudzbina, narudzbina_tabela, properties={ 'stavke' : relation(Stavka, cascade="all, delete-orphan"), 'klijent' : relation(Klijent, secondary=klijent_narudzbina_tabela, cascade="save-update"), })
U ovom primeru smo u relaciji stavke podesili da se sa Narudzbina objekta prenose save(), merge(), expunge(), refresh() i expire() operacije na pridružene Stavka objekte. Opcija delete-orphan znači da kad stavka više nije povezan sa narudžbinom tada treba da se stavka izbriše. Druga relacija specificira samo save-update vrednost, označavajući da će samo save() i update() operacije da se prenesu na Stavka objekte. Podrazumevana vrednost cascade imenovanog atributa je 'save-update, merge'.
6.3.6 Upravljanje transakcijama Sesija može automatski upravljati transakcijama. To uključuje i upravljanje transakcijama i preko više Engine objekata istovremeno. Kad je sesija u transakciji ona zahteve za izvršenjem SQL naredbi čuva odvojeno za svaki objekat klase Connection/Engine održavajući transakciono stanje. U vreme izvršenja commit() metode, svi podaci koji nisu ispražnjeni se šalju u bazu i za svaku pojedinačnu transakciju se izvršava COMMIT. Ako SUBP podržava dvofazni COMMIT, on će se koristiti ukoliko su u sesiji uključene dvofazne transakcije. Sa transakcijama se najlakše radi ako se sesija proglasi transakcionom. Sesija će ostati u transakciji sve vreme: # transakciona sesija Session = sessionmaker(transactional=True) sess = Session()
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
89
try: stavka = sess.query(Stavka).get(1) stavka.naziv = 'test' # potvrda tansakcije - po izvršenju se odmah započinje nova transakcija sess.commit() except: # opoziv transakcije - po izvršenju se odmah započinje nova transakcija sess.rollback()
Kada se koristi transakciona sesija i dogodi se greška u flush() ili commit() metodama, mora se pozvati ili rollback() ili close(). Ako se desi izuzetak (eng. exception) u metodi flush(), automatski će se izvršiti ROLLBACK na bazi, ali stanje sesije će ostati nedefinisano sve dok korisnik ne odluči da li će pozvati rollback() ili close(). Poziv commit() metode bezuslovno izvršava flush(). Posebno kad se sesija podesi sa transactional=True u kombinaciji sa autoflush=True. Tada direktni pozivi flush() metode uglavnom nisu potrebni. Transakcije možemo i ručno pokretati sa begin(): # transakciona sesija Session = sessionmaker(transactional=False) sess = Session() sess.begin() try: stavka = sess.query(Stavka).get(1) stavka.naziv = 'test' sess.commit() except: sess.rollback()
Ovde važe iste napomene kao i za prethodni primer. Sesije imaju podršku za with naredbu (ekvivalent using naredbi iz C#) koja olakšava rad sa transakcijama jer se po izlasku iz with bloka automatski pozva commit() ili rollback() zavisno od ishoda pa se prethodni primer skraćuje na: Session = sessionmaker(transactional=False) sess = Session() with sess.begin(): stavka = sess.query(Stavka).get(1) stavka.naziv = 'test'
Podtransakcije se mogu stvoriti uzastopnim pozivanjem begin() metode. Za svaku transakciju koja se započne sa begin() uvek se mora pozvati ili commit() ili rollback(). To uključuje i implicitne transakcije koje se stvaraju u transakcionoj sesiji. Kad se započne podtransakcija ona preuzima ulogu tekuće transakcije sesije. Pozivom commit() metode izvršava se COMMIT na podtransakciji, a ulogu tekuće transakcije sesije uzima najbliža spoljna transakcija. Sa rollback() se takođe za tekuću uzima najbliža spoljna, ali se ROLLBACK na bazi izvršava na najbližoj spoljnoj transakciji koja podržava opoziv. Obično to znači da će se opozvati početna (korena) transakcija, osim u slučaju kada se koriste ugnježdene trasankcije preko begin_nested() metode. MySQL i PostgreSQL (a uskoro i Oracle) imaju podršku za ugnježdene transakcije preko SAVEPOINT SQL naredbe: Session = sessionmaker(transactional=False) sess = Session() sess.begin() sess.save(o1) sess.flush()
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
90
# započinjemo ungeždenu transakciju preko SAVEPOINT-a sess.begin_nested() sess.save(o2) sess.rollback() # poništava o2 a zadržava o1 sess.commit() # izvršava COMMIT nad o1
I na kraju, za MySQL, PostgreSQL i uskoro Oracle, sesija može biti podešena da koristi dvofazno potvrđivanje. Preko njega se usaglašava potvrđivanje transakcija na više baza tako da se transakcija ili potvrdi ili opozove na svim bazama. Da bi se koristila dvofazna transakcija treba postaviti twophase=True u metodi sessionmaker(): engine1 = create_engine('postgres://baza1') engine2 = create_engine('postgres://baza2') Session = sessionmaker(twophase=True, transactional=True) # vezujemo Klijent operacija na engine1, Racun operacije na engine2 Session.configure(binds={Klijent:engine1, Racun:engine2}) sess = Session() # ... radimo sa klijentima i računima # commit, sesija će izvršiti flush() na svim bazama pre potvrde transakcija sess.commit()
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
91
7. Zaključak U ovom radu sam predstavio pojam perzistencije, pokazao njen značaj, razmotrio puteve njenog ostvarivanja i probleme vezane za nju. Zatim sam prikazao današnje načine postizanja trajnosti sa naglaskom na objektno-relaciono preslikavanje putem uzora Aktivni slog. Smatram da sam odabrao implementaciju tog uzora koja je među najboljim u ovom trenutku, a svakako je najelegantnija jer koristi principe na kojima je sazdan i jezik u kome je ORM alat razvijen. Trudio sam se da pokažem kako je uz SQLAlchemy moguće uporedo razvijati i bogat domenski model i složen relacioni model, da je moguće uzeti najbolje iz oba sveta, da nije nužna suprostavljenost objektne i relacione predstave podataka. SQLAlchemy je još uvek u punom razvoju i tek treba da se stabilizuje API. U svakoj verziji se dešavaju krupne promene, što je razumljivo s obzirom na to koliko je ovo mlad projekat. Raduje to što je svako novo izdanje korak u dobrom smeru koji olakšava korišćenje i povećava izražajnost. Mnogi projekti počinju da uvode SQLAlchemy kao svoj gradivni sastojak. To je jedna vrsta priznanja autorima, znak da je biblioteka kvalitetna i da ima budućnost. Licenca pod kojom se izdaje i otvorenost razvoja SQLAlchemyja podstiče njegovo širenje i prihvatanje. Snaga SQLAlchemyja leži u tome što je za proste stvari jednostavan a za složene sposoban. Ne mora se mnogo znati da bi se krenulo sa njegovom upotrebom. Sa druge strane nije poput mnogih takvih projekata gde se ORM napravi za rešavanje malog i strogo definisanog skupa zadataka, na kojima sve radi lako i savršeno. Ali sa takvim implemenacijama je problem u tome što svi zadaci koji izlazi iz tog od autora zamišljenoj okvira postaju programerska zona sumraka. Takve implementacije nisu dovoljno prilagodljive da bi mogle da se koriste u stvarnom svetu. SQLAlchemy nije samo ORM alat. Često je potrebno imati jaku vezu sa relacionom bazom, pa u tom slučaju se može spustiti na niži nivo apstrakcije i raditi sa sirovim podacima iz baze preko SQL-EL izbegavajući direktan rad sa SQL-om. Slično je i kad se koristi ORM ali automatsko generisanje SQL kôda ne daje dovoljno dobre rezultate. Tada programer može putem SQL-EL preuzeti kontrolu nad preslikavanjem i tako povećati performanse aplikacije. I dok se kod drugih ORM alata preslikavanje opisuje oslanjanjem na druge tehnologije u SQLAlchemyju je to izvedeno najprirodnijim putem preko samog Python jezika u obične izvorne datoteke bez potrebe za konfiguracionim fajlovima. Nadam se da će ideje koje su se u SQLAlchemyju pokazale kao dobre uticati i na druge ORM biblioteke, mada sumnjam da će moći ovako uspešno da se implementiraju, pre svega zbog ograničenja koja su prisutna u drugim jezicima, poput statičnog otkrivanja tipova i nemogućnosti izmene definicije klase u toku izvršenja programa (dodavanje metoda i atributa).
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
92
Dodatak A: Uvod u Python Sintaksa Pythona je jednostavna a ovde ćemo se dotaći samo osnovnih pojmova nužnih za razumevanje ovog rada. Za početak je dovoljno znati da je u Pythonu sve objekat pa su i paketi, moduli, metode, funkcije čak i literali poput True, False i None (null vrednost iz jave) su objekti. Kao i u svakom drugom jeziku kôd se organizuje u celine koje se u Python nazivaju paketi i moduli i da bi se pristupilo tim celinama koriste se dve konstrukcije: import random from sqlalchemy.orm import Query, mapper, relation
Naredba import je slična istoimenoj u javi, samo što se uvoze moduli (jedan ili lista više modula) dok se u javi uvozi klasa. Modul je jedna *.py datoteka koja može sadržati više klasa, funkcija i promenljih. Paket je direktorijum sa više *.py datoteka. Važi analogija direktorijum = paket, datoteka = modul. Sadržaju modula se pristupa preko imena modula. Naredba from iz modula orm, kao dela paketa sqlalchemy, uvozi pojedinačni sadržaj modula, što može biti i sam modul jer su paketi posebna vrsta modula. U gornjem primeru uvozimo jednu klasu i dve funkcije. Moguće je sa from sqlalchemy.orm import * uvesti čitav sadržaj modula. Sadržaju koji je uveden na ovaj način se pristupa bez navođenja imena modula. ZANIMLJIVOST: U javi iako se u jednoj *.java datoteci može nalaziti više klasa, samo jedna od njih može biti javna i mora imati ime kao datoteka. Zato se kod pristupa klasi piše paket.Klasa. Zbog navedenih ograničenja ta klasa je sigurno javna i po njenom imenu se sigurno zna u kojem direktorijumu i kojoj datoteci se nalazi. U Pythonu ne postoje takva ograničenja pa datoteka (tj. modul u Python terminologiji) može imati neograničen broj klasa, funkcija i promenljivih i zato se pri pristupu navodi i modul u kojem se nalazi ta klasa, funkcija ili promenljiva. Python ima vrlo fleksibilne tipove. Liste su direktno ugrađene u jezik i imaju posebnu sintaksu: voce = ['jabuke', 'tresnje', 'slive'] voce.append('dunje') for i in voce: print "volim da jedem " + i
Primetite da se nigde ne deklariše kog je tipa promenljiva voce. Interpreter sam otkriva da je to promenljiva koja čuva referencu na listu. Iskazi se završavaju prelaskom u novi red bez stavljanja znaka „;“ kao u javi. Blok se u Pythonu stvara navođenjem dvotačke i uvlačenjem kôda koji pripada bloku, kako to obično i rade programeri u jezicima sa blokovima označenim zagradicama. Vrlo sličan listama je tip podatka koji se naziva tupe. Označava se običnim zagradicama i za razliku od lista ima konstantne članove. To znači da se novi članovi ne mogu dodavati niti stari uklanjati, odnosno da je broj članova konstantan. Ni već unesene članove nije moguće zameniti nekim drugim. voce = ('jabuke', 'tresnje', 'slive') # ovo je moguće u listi ali u tupeu izbacuje grešku voce[0] = 'dunja'
Tip podataka koji se u Pythonu naziva dictionary, a poznat je i kao mapa, heš tabela ili asocijativan niz je takođe direktno podržan:
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
93
azbuka = {'a': 'а', 'b':'б', 'c':'ц'} azbuka['d'] = 'д' print azbuka['c']
U narednom kôdu pozivamo funkciju randint() koja je uvežena preko import naredbe, stoga moramo prvo navesti ime modula: broj = random.randint(1, 100) if broj < 50: # ovo je jednolinijski komentar u pythonu print 'donja polovina' elif broj > 50: print 'gornja polovina' else: print 'u sredini' i = 1 while True: if random.randint(1, 10) == 5: print 'iz', i, 'pokusaja stvoren je broj 5' break i += 1
Granjanje je kao i u javi sa izuzetkom spajanja ključnih reči else i if u elif. Konstrukcija switch ne postoji. Python podržava proceduralno programiranje preko funkcija: def razlika(a=0, b=0): return a - b print 'razlika 3 i 1 je', razlika(3, 1) print 'razlika 1 i 3 je', razlika(b=3, a=1) print razlika()
Python ima mogućnost da parametrima dodeli podrazumevanu vrednost, kao i da prilikom prosleđivanja argumenata imenuje paramatar kojem se argument dodeljuje, što nam omogućava da ne moramo pamtiti redosled parametara kao što to radimo u javi. Pošto se ne deklarišu tipovi parametara i povratne vrednosti, Python kao potpis funkcije (i metoda) uzima samo ime, pa ime funkcije (metode) mora biti jedinstveno. Nedostatak preopterećivanja (eng. overloading) se rešava izuzetno bogatim načinom prosleđivanja argumenta. Ne samo da postoje podrazumevane vrednosti parametara već je moguće imati promenljiv broj argumenata koji se prihvataju u funkciji (metodi) kao lista: def suma(*argumenti): rez = 0 for arg in argumenti: rez += arg return rez print 'suma niza 1, 2, 3, 4 je', suma(1, 2, 3, 4)
Moguće je imati i promenljiv broj imenovanih argumenata koji se prihvataju kao heš tabela: def metoda(**kljuc_argumenti): for kljuc, vrednost in kluc_argumenti.items(): print 'kljuc_argumenti[%s] = %s' % (kljuc, vrednost) metoda(c=4, d=7) kljuc_argumenti[c] = 4 kljuc_argumenti[d] = 7
Primetite kako smo koristili interpolaciju stringova kao u printf() funkciji C jezika. Takođe je prikazan način iteracije kroz heš tabelu for petljom.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
94
Sve navedene načine prenošenja argumenata je moguće kombinovati čime se dobijaju veoma prilagodljive funkcije, što Python programeri obilato koriste. Python podržava objektno-orijentisano programiranje. Međutim iz te paradigme programiranje je uzeta suština bez komplikacija koje postoje u drugim OO jezicima: class Student(object): """dokumentacioni komentar kao string u vise redova ostaje u i u vreme izvosenja i moze se videti preko poziva help(Student) funkcije u interpreteru ili sa print Student.__doc__ """ klasni_atribut = 'je zapalio zito' def __init__(self, br_indexa, ime, prezime): self.br_indexa = br_indexa # atributi instance self.ime = ime self.prezime = prezime def __str__(self): return 'Student ' + self.br_indexa + " " + Student.klasni_atribut def ime_prezime(self): "metoda koja vraca ime i prezime studenta" return self.ime + ' ' + self.prezime s = Student('187/07', 'Pera', 'Perić') print s print s.ime_prezime.__doc__ print s.ime_prezime() help(Student)
Python ima samo javne i private atribute i metode. Ako ime atributa/metode počinje s dve donje crtice npr. __ime tada je atribut/metoda privatna. Nasleđivanje se deklariše navođenjem natkasa u zagradice kao u jezicima C++ i C#. Čak i kad se ni jedna klasa ne nasleđuje mora se naslediti object - najviša klasa u Pythonu (ekvivalent Object klasi u javi), ali se za razliku od jave gde se to automatski (implicitno) radi u Pythonu je potrebno navesti natklasu object. Slično je i kod prenošenja reference na tekući objekat kod metoda objekta. U javi se referenca na tekući objekat (objekat nad koji se poziva metoda) prenosi na implicitini parametar this. Taj parametar se ne deklariše u listi paramatara metode ali se automatiski stvara u svakoj nestatičnoj metodi. U Pythonu paramatar koji prihvata referencu na teukći objektat deklariše kao i svaki drugi. To je uvek prvi parametar nestatičke metode i po konvenciji se naziva self. Ove razlike prikazujem ekvivalentim primerom u javi i Pythonu (zbog ekvivalencije koda prekršena je Pythonova konvencija pa je umesto sa self prvi parametar nazvan sa this kao u javi): // java primer class Tacka { // stvarno je: class Tacka extends Object int x = 0; int y = 0; void setX(int x) { // stvarno je: void setX(Test this, int x) this.x = x; // this se pojavljuje magično } ...
}
public static void main(String[] args) { Tacka t = new Tacka(); t.setX(5); // stvarno: setX(t, 5), t se prosleđuje automatski a magično se prima kao this }
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
95
# Python primer class Tacka(object): # eksplicitno se nasleđuje vrhovna klasa object def __init__(this, x=0, y=0): this.x = x this.y = y def setX(this, x): # eksplicitno se prima referenca na tekući objekat kao prvi parametar this.x = x t = Tacka() t.setX(5) // stvarno je: setX(t, 5), t se prosleđuje automatski a eksplicitno se prima kao this
Obavezno je koristiti self prilikom pristupa metodama i atributima unutar klase jer se samo tako mogu razdvojiti lokalne promenljive od atributa i metode od funkcija. Npr. self.ime = 'Pera' je pristup atributu ime, a ime = 'Pera' je pristup lokalnoj promenljivoj metode, self.radi() je metoda a radi() je funkcija. U javi se this može izostaviti u svim primerima koji su ekvivalentni navedenim u prethodnoj rečenici. Jedno se this mora koristiti u slučaju postojanja lokalne promenljive ili parametra metode istog imena pa se preko this referiše na atribute. Specijalne metode koje se nasleđuju iz klase object se razlikuju od drugih metoda po tome što imaju dve donje crte na početku i kraju imena. Metoda __str__() je isto što i toString() u javi. Metoda __init__() predstavlja konstruktor. U njemu se deklarišu i inicijalizuju atributi objekta i to obavezno preko reference na tekući objekat tj. self. Pristup tim atributima u drugim metodama se takođe mora obaviti preko self reference. Atributi klase se deklarišu izvan __init__() metode. Njima se pristupa preko imena klase kao u javi. Instanciranje objekata se obavlja kao što je uobičajeno u drugim jezicima, jedina razlika je što u Pythonu ne postoji operator new, jer konstruktor nije specijalna funkcija sa drugačijim načinom pozivanja. Izvršavanje Python skripte se postiže pozivom interpreter i prosleđivanjem *.py datoteke: $ python moj_program.py
Postoji i mogućnost da se u prvoj liniji *.py datoteke navede interpreter, tako da se program može izvršiti direktno pozivom datoteke (uz prethodno davanje prava na izvšenje datoteci): $ ./moj_program.py
Datoteka koja se interpretira izvršava se redno liniju po liniju. Ne postoji main metoda ili funkcija od koje bi program trebalo da krene sa izvršavanjem. Interpreter funkcioniše slično JVM, dakle prvo prevede datoteku (ono što radi javac) pa krene sa izvršavanjem. Samo što su kod jave te dve faze striktno odvojene. Moguće je i u Pythonu sačuvati međukod (*.pyc datoteke, ekvivalent *.class datotekama) i time preskočiti stalno prevođenje i potrebu za *.py datotekama. Dobra osobina Pythona je podrška za dokumentacione komentare u prevedenim datotekama i u vreme izvršenja programa. To znači da čak i kad imamo samo *.pyc datoteke bez dokumentacije u nekom drugom obliku, mi i dalje možemo pristupati dokumentacionim komentarima koje je autor pisao u kôdu. Lakoća Pythona se ogleda i u tome što postoji interaktivni interpreter u kome je moguće direktno kucati kôd i odmah videti njegov rezultat. To je sjajan način za učenje jezika i eksperimentisanje sa kôdom. U ovom radu je prikaz rada u interaktivnom interpreteru obilato korišćen kao najočigledniji način prikazivanja rada sa SQLAlchemy bibliotekom. Interaktivni interpreter se pokreće sa: $ python
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
96
Odzivni znak interpertera je „>>>“ ali kad se piše neka konstrukcija u više redova prompt se menja u „. . .“ kao na slici 13. Ukoliko je izraz previše dug da bi stao u jedan red tada se na mestu prekida izraza stavi znak „\“ a u novom redu se nastavi sa pisanjem izraza: >>> print "zbir prvih 1000 brojeva je:", \ ... sum(range(1, 1001)) zbir prvih 1000 brojeva je: 500500
Linije bez ikakvog odzivnog znaka predstavljaju izlaz iz prethodne konstrukcije. U prethodnom primeru je to string „zbir prvih 1000 brojeva je: 500500“. Iz intepretera se izlazi pozivom funkcije exit() ili slanjem EOF znaka (CTRL+D na *nix-ima ili CTRL+Z na Windowsima).
Slika 13: izgled Pythonovog interaktivnog interpretera u emulatoru terminala Pored ovog postoji i ipython interaktivni interpreter sa naprednim mogućnostima za listanje lokalnih promenljivih, atributa i metoda objekta (eng. code completion). Slične osobine ima i IDLE grafički interpreter koji dolazi uz instalaciju Pythona. Za potpuniji uvod u Python pročitajte besplatnu knjigu A Byte of Python koju možete preuzeti sa http://www.swaroopch.com/notes/Python
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
97
Dodatak B: Instaliranje korišćenih alata U ovom dodatku ću opisati instaliranje i podešavanje alata koje sam koristio u primerima za SQLAlchemy. Opis se odnosi na Ubuntu linux distribuciju, zbog lakoće instaliranja, rasprostranjenosti i zato što je na toj distribuciji i pisan ovaj rad. Za ostale *nix platforme koristi se drugačiji alat za instaliranje, ali postupak ostaje suštinski isti. Windows korisnici će morati ručno skidaju i instaliraju pakete, kako to inače i rade.
Instaliranje Pythona Uobičajeno je da linux distribucije dolaze sa već instaliranim Python interpreterom. Windows korisnici mogu skinuti poslednju verziju Pythona sa http://www.python.org/download/releases/. Preporučljivo je da se putanja do python.exe programa stavi u PATH promenljivu Windowsa.
Instaliranje SQLAlchemyja SQLAlchemy je moguće instalirati na nekoliko načina. Najlakši način u Ubuntu je putem apt-get programa za instaliranje: $ sudo apt-get install python-sqlalchemy
Problem sa ovim pristupom je u kašnjenju za najnovijim izdanjem ORM alata. SQLAlchemy je jako dinamičan projekat sa puno izdanja što ljudi koji održavaju Ubuntu repozitorijume ne mogu pratiti dovoljno brzo. Zato je preporučen način instaliranja preko programa easy_install. Tim programom se instaliraju Python paketi direktno sa Interneta, a ima mogućnost instaliranja iz arhiva sa kôdom ili direktno Subversion repozitorijuma. Potrebno je prvo instalirati easy_install što je u Ubuntu moguće uraditi direktno sa: $ sudo apt-get install python-setuptools
Instaliranje nevezano za platformu (uključujući i Windows) se radi tako što se sa http://peak.telecommunity.com/DevCenter/EasyInstall#installation-instructions skine ez_setup.py i pokrene u komandnoj liniji sa: python ez_setap.py
Sad kad smo dobili easy_install možemo instalirati SQLAlchemy. Ubuntu korisnici će pokrenuti: $ sudo easy_install SQLAlchemy
Windows korisnici će pokrenuti: \Scripts\easy_install SQLAlchemy
Za povezivanje SQLAlchemyja i PostgreSQL sistema potrebno je instalirati psycopg 2 drajver: $ sudo easy_install psycopg2
Odnosno za Windows korisnike: \Scripts\easy_install psycopg2
Na Ubuntu se ovo može obaviti i uobičajenim načinom kroz apt-get i paket python-psycopg2.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
98
Instaliranje i podešavanje PostgreSQL sistema Ubuntu linux distribucija ne dolazi sa instaliranim PostgreSQL-om. Međutim PostgreSQL se nalazi u repozitorijumu, tako da se instalira direktno sa Interneta komandom: $ sudo apt-get install postgresql
čime se instalira najnovija verzija servera. U toku instalirana PostgreSQL-a kreira se linux korisnik postgres i SUBP se podiže kao linux servis tog korisnika . Sistemu se po podrazumevanom podešavanju može prići samo iz lokalne mašine i preko postgres korisnika. Da bi se moglo pristupiti serveru potreban nam je klijentski deo koji se instalira sa: $ sudo apt-get install postgresql-client
U klijentski deo spada psql aplikacija za pristup serveru iz komandne linije, kao i mnoge pomoćne komande za otvaranje korisnika, stvaranje baze i ostale administrativne zadatke. Na instaliranom PostgresSQL-u postoji samo jedan korisnik SUBP pod imenom postgres (isto ime kao i za linux korisnika) i prva stvar koju je potrebno uraditi je davanje lozinke za tog korisnika: $ sudo -u postgres psql template1
preko koje se konektujemo koristeći psql aplikaciju (koju pokrećemo kao postgres linux korisnik) na template1 predefinisanu bazu čime dobijamo sledeći prompt: Welcome to psql 8.3.3, the PostgreSQL interactive terminal. Type:
\copyright for distribution terms \h for help with SQL commands \? for help with psql commands \g or terminate with semicolon to execute query \q to quit
template1=#
zatim otkucamo SQL naredbu za promenu lozinke i izađemo iz psql: template1=# ALTER USER postgres WITH ENCRYPTED PASSWORD 'lozinka'; template1=# \q
Ako će se PostgreSQL-u pristupati sa lokalne mašine tada se može preći na stvaranje korisnika SUBP. Međutim ako je potrebno povezati se na PostgreSQL sa nekog drugog računara u mreži, potrebno je dodatno podešavanje. Prvo treba izmeniti datoteku postgresql.conf u omiljenom editoru npr: sudo nano /etc/postgresql/8.3/main/postgresql.conf tako što će se linija: #listen_addresses = 'localhost'
promeniti u: listen_addresses = '*'
čime smo podesili server da prihvate TCP/IP konekcije sa svih IP adresa. Potom u datoteku /etc/postgresql/8.3/main/pg_hba.conf unesemo sledeći red: host
all
all
192.168.1.1/24
md5
gde je prva kolona tip konekcije koja se prihvata (host je obična ili SSL TCP/IP konekcija), druga je baza za koju se dozvoljava pristup, treća korisnik kome se dozvoljava, četvrta je mreža sa koje se dozvoljava pristup (u primeru je lokalna mreža 192.168.1.1 sa maskom 255.255.255.0) i poslednja kolona je metod autenitfikacije serveru. U primeru je to autentifikacija putem lozinke, pri čemu se preko mreže ne prenosi tekst lozinke već njen heš stvoren md5 algoritmom.
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
99
Da bi načinjene promene bile učitane potrebno je restartovati server: $ sudo /etc/init.d/postgresql-8.3 restart
Sad smo spremni da napravimo nove korisnike u SUBP preko pomoćnog programa createuser: $ sudo -u postgres createuser -P testnikorisnik
Program će nam postaviti pitanja u vezi sa ovlašćenjima koja se daju korisniku. Treba napraviti jednog korisnika koji će imati sva prava i njega koristiti za administraciju baze. Sad smo spremni da se konektujemo na SUBP bez ograničenja putem TCP/IP konekcije: $ psql -h localhost -d template1 -U testnikorisnik
Prvi argument pri pokretanju psql programa je adresa mašine na kojoj je podignut server baze. Ako je to lokalna mašina upisuje se localhost odnosno 127.0.0.1, ako nije tad se ukucava IP adresa udaljenog računara ili njegovo ime (pod uslovom da DNS može na osnovu njega doći do IP adrese, ili je ona upisana u /etc/hosts). Drugi argument je naziv baze na koju se priključujemo a poslednji argument je korisničko ime pod kojim se prijavljujemo. Sad pravimo novu bazu (pod uslovom da smo korisniku dozvolili da stvara baze): template1=> create database testdb; CREATE DATABASE
Vlasnik baze će biti korisnik koji ju je stvorio. Ako nam je potrebno da zadamo drugog vlasnika tada ćemo izvršiti: create database testdb with owner = drugi_korisnik; Zatim se povežemo na novostvorenu bazu: template1=> \c testdb You are now connected to database "testdb".
Dalje se radi kao sa svakom bazom u SQL standardu, npr. stvaranje tabele, unošenje zapisa i zadavanje upita, listanje baza, listanje tabela i pregled podataka o tabeli: testdb=> create table test_tabela(a integer, b varchar(10)); CREATE TABLE testdb=> insert into test_tabela values (1, 'test test'); testdb=> select * from test_tabela; a | b ---+----------1 | test test (1 row) testdb=> \l
List of databases Name | Owner | Encoding -----------------+----------------+---------foobar | sysadmin | UTF8 postgres | postgres | UTF8 prvi_django | zlatan | UTF8 sqlalchemy | fon | UTF8 template1 | postgres | UTF8 testdb | testnikorisnik | UTF8 (6 rows) testdb=> \dt test* List of relations Schema | Name | Type | Owner --------+-------------+-------+---------------public | test_tabela | table | testnikorisnik (1 row)
Razvoj Python programa korišćenjem SQLAlchemy tehnologije testdb=> \d test_tabela Table "public.test_tabela" Column | Type | Modifiers --------+-----------------------+----------a | integer | b | character varying(10) | (2 rows)
Za psql postoji i grafički interfejs pgAdmin III (slika 14) koji se instalira sa: $ sudo apt-get install pgadmin3
Slika 14: izgled pgAdmin III grafičkog klijenta za PostgreSQL server
100
Razvoj Python programa korišćenjem SQLAlchemy tehnologije
101
Literatura 1: Merriam-Webster Online Dictionary, http://www.merriam-webster.com/dictionary/information+science 2: Spisak licenci uz komentare o usklađenosti sa definicijom slobodnog softvera koju je dao GNU, http://www.gnu.org/philosophy/license-list.html 3: Spisak licenci koje su koje su u skladu sa Inicijativom za otvoreni kod, http://www.opensource.org/licenses/alphabetical 4: Zvanična stranica PostgreSQL projekta, www.postgresql.org 5: Zvanična stranica Python projekta, www.python.org 6: dr SIniša Vlajić, Projektovanje programa (skirpta), 2004 7: Christian Baure, Gavin King, Java Persistence with Hibernate, 2007 8: dr Branislav Lazarević, dr Zoran Marjanović, mr Nenad Aničić, Slađan Babarogić, Baze podataka, 2003 9: Debu Panda, Reza Rahman, Derek Lane, EJB 3 in Action, 2007 10: Buu Nguyen, The Legend of Data Persistence - Part 1, http://www.buunguyen.net/blog/the-legend-of-data-persistence-part-1.html/ 11: Martin Fowler; David Rice; Matthew Foemmel; Edward Hieatt; Robert Mee; Randy Stafford, Patterns of Enterprise Application Architecture, 2002 12: Zvanična stranica Django web framework-a, www.djangoproject.org 13: TIOBE indeks programskih jezika, http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html 14: Bertrand Meyer, Objektno orijentisano konstruisanje softvera , 1997 15: Zvanična SQLAlchemy dokumentacija, http://www.sqlalchemy.org/docs/04/