You are on page 1of 104

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 objektno-
relacioni 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 16
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);
}
}
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()
[<Anketa: Vaš omiljeni programski jezik je?>]

# dobavljanje objekta po ključu, rezultat je jedan element


>>> Anketa.objects.get(id=1)
<Anketa: Vaš omiljeni programski jezik je?>
# selekcija
>>> Anketa.objects.filter(pitanje__startswith="Vaš")
[<Anketa: Vaš omiljeni programski jezik je?>]

# dodavanje izbora u anketu


>>> anketa.izbor_set.create(izbor='Java', glasova=0)
<Izbor: Java>
>>> anketa.izbor_set.create(izbor='Python', glasova=0)
<Izbor: Python>
>>> izbor = anketa.izbor_set.create(izbor='COBOL', glasova=0)
<Izbor: COBOL>

# relacija M-1
>>> izbor.anketa
<Anketa: Vaš omiljeni programski jezik je?>

# obrnuti smer
>>> anketa.izbor_set.all()
[<Izbor: Java>, <Izbor: Python>, <Izbor: COBOL>]

# 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 26
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) {
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
<sqlalchemy.engine.base.ResultProxy object at 0xb75278ac>

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' : 'pera@gmail.com'},
... {'klijent_id' : 1, 'email_adr' : 'pera.peric@mail.ru'},
... {'klijent_id' : 2, 'email_adr' : 'mikic@gmail.com'},
... {'klijent_id' : 2, 'email_adr' : 'mikam@hotmail.com'},
... ])
INSERT INTO adrese (klijent_id, email_adr)
VALUES (%(klijent_id)s, %(email_adr)s)
[{'email_adr': 'pera@gmail.com', 'klijent_id': 1},
{'email_adr': 'pera.peric@mail.ru', 'klijent_id': 1},
Razvoj Python programa korišćenjem SQLAlchemy tehnologije 37
{'email_adr': 'mikic@gmail.com', 'klijent_id': 2},
{'email_adr': 'mikam@hotmail.com', 'klijent_id': 2}]
COMMIT
<sqlalchemy.engine.base.ResultProxy object at 0xb75279cc>

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, 'pera@gmail.com')
(2, 'Mika', 'Mika Mikić', 1, 1, 'pera@gmail.com')
(1, 'Pera', 'Pera Perić', 2, 1, 'pera.peric@mail.ru')
(2, 'Mika', 'Mika Mikić', 2, 1, 'pera.peric@mail.ru')
(1, 'Pera', 'Pera Perić', 3, 2, 'mikic@gmail.com')
(2, 'Mika', 'Mika Mikić', 3, 2, 'mikic@gmail.com')
(1, 'Pera', 'Pera Perić', 4, 2, 'mikam@hotmail.com')
(2, 'Mika', 'Mika Mikić', 4, 2, 'mikam@hotmail.com')

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, 'pera@gmail.com')
(1, 'Pera', 'Pera Perić', 2, 1, 'pera.peric@mail.ru')
(2, 'Mika', 'Mika Mikić', 3, 2, 'mikic@gmail.com')
(2, 'Mika', 'Mika Mikić', 4, 2, 'mikam@hotmail.com')

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
<sqlalchemy.sql.expression._BinaryExpression object at 0xb756910c>

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=='pera@gmail.com',adrese.c.email_adr=='mika@mail.ru'),
... 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 <= :id_1

odnosno isti SQL se dobija sa:


>>> print klijenti.c.name.like('j%') & (klijenti.c.id==adrese.c.klijent_id) & \
... ((adrese.c.email_adr=='pera@mail.ru') | adrese.c.email_adr=='mika@mail.ru')) \
... & ~(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ć, pera@gmail.com',)]

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 == 'pera@gmail.com',
... a2.c.email_adresa == 'pera.peric@mail.ru'
... )
... )
>>> 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': 'pera@gmail.com', 'email_adresa_2': 'pera.peric@mail.ru'}
[(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': 'pera@gmail.com', 'email_adresa_2': 'pera.peric@mail.ru'}
[('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, 'pera@gmail.com'), (1, 'Pera', 'Pera Perić', 2, 1,
'pera.peric@mail.ru')]

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
{}
pera.peric@mail.ru
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
{}
[('pera.peric@mail.ru', 2), ('pera@gmail.com', 1), ('mikic@gmail.com', 3), ('mikam@hotmail.com',
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, 'pera.peric@mail.ru')]

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
<sqlalchemy.engine.base.ResultProxy object at 0xb71cca2c>

>>> # 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
<sqlalchemy.engine.base.ResultProxy object at 0xb71ce5ec>
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
<sqlalchemy.engine.base.ResultProxy object at 0xb71ccf2c>

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
<sqlalchemy.engine.base.ResultProxy object at 0xb71ce60c>

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
<sqlalchemy.engine.base.ResultProxy object at 0xb71cc74c>

>>> # 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
<sqlalchemy.engine.base.ResultProxy object at 0xb71ce46c>

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 "<Klijent('%s','%s', '%s')>" % (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)
<sqlalchemy.orm.mapper.Mapper object at 0xb725a6ac>

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)
<type 'type'>

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)
<class 'sqlalchemy.orm.session.Sess'>

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'}
<Klijent('pera','Pera Perić', 'perinaloznka')>

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
{}
<Klijent('zika','Žika Žikić', 'blabla')>
<Klijent('mika','Mika Mikić', 'tralala')>

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'}
<Klijent('pera','Pera Perić', 'sezameotvorise')>

... 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'}
<Klijent('pera','Pera Perić', 'sezameotvorise')>

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'}
<Klijent('mika','Mika Mikić', 'tralala')>

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'}
[<Klijent('zika','Žika Žikić', 'blabla')>, <Klijent('mika','Mika Mikić', 'tralala')>]

first() ograničava rezultat na 1 element i vraća prvi element kao skalar:


>>> query.first()
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 52
FROM klijenti
WHERE klijenti.ime ILIKE %(ime_1) s
ORDER BY klijenti.id
LIMIT 1 OFFSET 0
{'ime_1': '%ika'}
<Klijent('zika','Žika Žikić', 'blabla')>

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}
[<Klijent('pera','Pera Perić', 'sezameotvorise')>]

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}
[<Klijent('pera','Pera Perić', 'sezameotvorise')>, <Klijent('mika','Mika Mikić', 'tralala')>]

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'}
[<Klijent('pera','Pera Perić', 'sezameotvorise')>]

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
{}
[<Klijent('pera','Pera Perić', 'sezameotvorise')>]

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
{}
(<Klijent('pera','Pera Perić', 'sezameotvorise')>, 'tralala')
(<Klijent('zika','Žika Žikić', 'blabla')>, 'tralala')
(<Klijent('mika','Mika Mikić', 'tralala')>, 'tralala')
(<Klijent('laza','Laza Lazić', 'tatatatira')>, '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 "<Adresa('%s')>" % 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')
... })
<sqlalchemy.orm.mapper.Mapper object at 0x872782c>
>>> mapper(Adresa, adrese_tabela)
<sqlalchemy.orm.mapper.Mapper object at 0x872706c>

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('jeca@gmail.com'))
>>> jeca.adrese.append(Adresa('jelenaj@sun.com'))

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]
<Adresa('jelenaj@sun.com')>
>>> jeca.adrese[1].klijent
<Klijent('jeca','Jelena Jelić', 'asdf')>
>>> treca_jecina_adr = Adresa('jj@yahoo.com')
>>> treca_jecina_adr.klijent = jeca
>>> jeca.adrese
[<Adresa('jeca@gmail.com')>, <Adresa('jelenaj@sun.com')>, <Adresa('jj@yahoo.com')>]

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': 'jeca@gmail.com', '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': 'jelenaj@sun.com', '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': 'jj@yahoo.com', '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
<Klijent('jeca','Jelena Jelić', 'asdf')>

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}
[<Adresa('jeca@gmail.com')>, <Adresa('jelenaj@sun.com')>, <Adresa('jj@yahoo.com')>]

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


<Klijent('jeca','Jelena Jelić', 'asdf')>
>>> print jeca.adrese
[<Adresa('jeca@gmail.com')>, <Adresa('jelenaj@sun.com')>, <Adresa('jj@yahoo.com')>]

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 == 'jeca@gmail.com').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': 'jeca@gmail.com'}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

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 == 'jeca@gmail.com').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': 'jeca@gmail.com'}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

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='jeca@gmail.com').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': 'jeca@gmail.com'}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

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='jeca@gmail.com'). \
... 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': 'jeca@gmail.com', 'ime_1': 'jeca'}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

a isti rezultat se dobija sa prirodnijim upitom:


session.query(Klijent).filter_by(ime='jeca'). \
join('adrese').filter_by(email_adresa='jeca@gmail.com').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 == 'jeca@gmail.com').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': 'jeca@gmail.com'}
[(<Klijent('jeca','Jelena Jelić', 'asdf')>, <Adresa('jeca@gmail.com')>)]

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, jeca@gmail.com i jj@yahoo.com,
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 == 'jeca@gmail.com'). \
... join('adrese', aliased=True).filter(Adresa.email_adresa == 'jj@yahoo.com').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': 'jeca@gmail.com', 'email_adresa_2': 'jj@yahoo.com'}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

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 == 'jeca@gmail.com').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': 'jeca@gmail.com'}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

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='jeca@gmail.com').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 == 'jeca@gmail.com')).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': 'jeca@gmail.com'}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

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'}
[<Adresa('jeca@gmail.com')>, <Adresa('jelenaj@sun.com')>, <Adresa('jj@yahoo.com')>]

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}
[<Adresa('jeca@gmail.com')>, <Adresa('jelenaj@sun.com')>, <Adresa('jj@yahoo.com')>]

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}
[<Adresa('jeca@gmail.com')>, <Adresa('jelenaj@sun.com')>, <Adresa('jj@yahoo.com')>]

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=='jeca@gmail.com').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}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

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}
[<Klijent('jeca','Jelena Jelić', 'asdf')>]

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')
... })
<sqlalchemy.orm.mapper.Mapper object at 0x88682ec>
>>> mapper(Adresa, adrese_tabela)
<sqlalchemy.orm.mapper.Mapper object at 0x886880c>

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 65
>>> 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)
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)
<sqlalchemy.orm.mapper.Mapper object at 0x8720dec>
>>> mapper(BlogClanak, clanci_tabela, properties={
... 'autor' : relation(Klijent, backref=backref('clanci', lazy='dynamic')),
... 'teme' : relation(Tema, secondary=clanak_teme, backref='clanci')
... })
<sqlalchemy.orm.mapper.Mapper object at 0x874f4cc>

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 nextval('"teme_id_seq"')
None
INSERT INTO teme (id, tema)
VALUES (%(id)s, %(tema)s)
{'id': 1L, 'tema': 'python'}

SELECT nextval('"teme_id_seq"')
None
INSERT INTO teme (id, tema)
VALUES (%(id)s, %(tema)s)
{'id': 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 AS clanci_id,


clanci.klijent_id AS clanci_klijent_id,
clanci.naslov AS clanci_naslov,
clanci.sadrzaj AS 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', <Klijent('jeca','Jelena Jelić', 'asdf')>)]

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', <Klijent('jeca','Jelena Jelić', 'asdf')>)]

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', <Klijent('jeca','Jelena Jelić', 'asdf')>)]
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=<poruke>, primary_key=True, nullable=False,
default=PassiveDefault(<sqlalchemy.sql.expression._TextClause object at 0x891e68c>))
Column(u'tekst', PGString(length=60, convert_unicode=False, assert_unicode=None),
table=<poruke>)
Column(u'vreme', PGDateTime(timezone=True), table=<poruke>)

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%'}
[<__main__.Preduzece object at 0x869260c>]

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%'}
[<__main__.Preduzece object at 0x869260c>]

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 AS unija_zaposlen_id,
unija.ime AS unija_ime,
unija.tip AS unija_tip,
unija.inzenjerske_informacije AS unija_inzenjerske_informacije,
unija.podaci_o_rukovodiocu AS unija_podaci_o_rukovodiocu
FROM
(SELECT zaposleni.zaposlen_id AS zaposlen_id,
zaposleni.ime AS ime,
CAST(NULL AS VARCHAR(50)) AS podaci_o_rukovodiocu,
CAST(NULL AS VARCHAR(50)) AS inzenjerske_informacije,
'zaposlen' AS tip
FROM zaposleni

UNION ALL

SELECT rukovodioci.zaposlen_id AS zaposlen_id,


rukovodioci.ime AS ime,
rukovodioci.podaci_o_rukovodiocu AS podaci_o_rukovodiocu,
CAST(NULL AS VARCHAR(50)) AS inzenjerske_informacije,
'rukovodilac' AS tip
FROM rukovodioci

UNION ALL
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)
<class 'sqlalchemy.orm.query.Query'>

>>> 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', <Klijent('jeca','Jelena Jelić',
'asdf')>), BlogClanak('Izašao SQLAlchemy 0.5 beta 2', 'Izašao je SQLAlchemy 0.5 beta 2 sa mnogo
novosti i poboljšanja', <Klijent('jeca','Jelena Jelić', 'asdf')>), BlogClanak('IBM podržava
SQLALchemy', 'IBM je izbacio podršku za SQLAlchemy na DB2 RSUBP', <Klijent('jeca','Jelena
Jelić', 'asdf')>)]

>>> 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', <Klijent('jeca','Jelena Jelić', 'asdf')>), BlogClanak('Izašao
SQLAlchemy 0.5 beta 2', 'Izšao je SQLAlchemy 0.5 beta 2 sa mnogo novosti i poboljšanja',
<Klijent('jeca','Jelena Jelić', 'asdf')>)]

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', <Klijent('jeca','Jelena Jelić', 'asdf')>)

# 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', <Klijent('jeca','Jelena Jelić',
'asdf')>), BlogClanak('Izašao SQLAlchemy 0.5 beta 2', 'Izašao je SQLAlchemy 0.5 beta 2 sa mnogo
novosti i poboljšanja', <Klijent('jeca','Jelena Jelić', 'asdf')>), BlogClanak('IBM podržava
SQLALchemy', 'IBM je izbacio podršku za SQLAlchemy na DB2 RSUBP', <Klijent('jeca','Jelena
Jelić', 'asdf')>), BlogClanak('Test naslov', 'Test sadržaj', <Klijent('jeca','Jelena Jelić',
'asdf')>)]

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 obj
session.refresh(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:


<direktorijum_u_kom_je_python>\Scripts\easy_install SQLAlchemy

Za povezivanje SQLAlchemyja i PostgreSQL sistema potrebno je instalirati psycopg 2 drajver:


$ sudo easy_install psycopg2

Odnosno za Windows korisnike:


<direktorijum_u_kom_je_python>\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 100
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


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/