MVC, web2py ja Google App Engine
Tällä luennolla käydään läpi minkälainen on MVC-arkkitehtuuri ja kokeillaan MVC:tä käytännössä web2py-frameworkin avulla. Tutustutaan myös Google App Engineen.
MVC-arkkitehtuuri
MVC-arkkitehtuuri (sanoista model-view-controller eli malli–näkymä–käsittelijä) on ohjelmistoarkkitehtuurityyli, jonka tarkoituksena on käyttöliittymän erottaminen sovellusaluetiedosta. Ohjelma jaetaan kolmeen osaan:
- Malli (Model), joka on sovelluksen asetuksien ja tietokannan kuvaus
- Näkymä (View) määrittää käyttöliittymän eli käytännössä HTML-merkkauksen
- Käsittelijä (Controller) hoitaa varsinaisen sovelluslogiikan ja muuttaa tarpeen mukaan mallia ja näkymää
Web-sovelluksissa MVC-arkkitehtuurin noudattaminen lopettaa ohjelmakoodin ja html-koodin sotkemisen keskenään ja pitää sovelluksen selkeämpänä.
Google App Engine
Google App Engine (GAE) on Googlen tarjoama pilvipalvelualusta web-sovellusten kehittämiseen ja julkaisuun.
- Sovellukset suoritetaan useilla palvelimilla
- Automaattinen resurssien skaalaus
- Ilmainen vähäisessä käytössä
- Tukee useita ohjelmointikieliä: Python, Java, Go ja PHP
- Ei salli kirjoittamista tiedostoihin vaan täytyy käyttää tietokantaa. Tämä aiheuttaa rajoituksia ja muutoksia esim. web2py-sovelluskehyksen joihinkin toimintoihin.
- Ilmaiseksi voi käyttää vain hajautettua BigTable-tietokantaa. Relaatiotietokannan ja SQL-tuen saa maksusta (Google Cloud SQL).
- Hajautetun tietokannan käyttäminen aiheuttaa pieniä ongelmia. Esimerkiksi tietokantaan tehdyt lisäykset eivät välttämättä ole heti näkyvissä tietokannan sisällössä vaan vasta pienen viiveen jälkeen. Tämä pitää osata huomioida sovelluksen rakenteessa ja toiminnassa.
- Voi kehittää ja testata omalla koneellaan jos käyttää Google App Engine SDK:ta
- Libraries in Python 2.7
Lisätietoa Google App Enginesta:
web2py
Web2py on web-sovellusten kehittämiseen tarkoitettu sovelluskehys. Web2py käyttää python-kieltä. web2py muistuttaa Ruby on Rails ja Django-sovelluskehyksiä. Web2py noudattaa MVC-mallia. Web2py käyttää WSGI-rajapintaa.
web2py sisältää tärkeimmät ominaisuudet joita web-sovelluskehykseltä tarvitaan:
- Autentikointi, roolit
- Tietokantarajapinta, joka ei edellytä SQL-koodia ja toimii suoraan erilaisilla tietokannoilla
- Internationalisointi ja lokalisointi
- Tuki jQuerylle
- Sessiot, evästeet, HTTP-rajapinta
- Välimuisti
- WWW-pohjainen ylläpitosovellus
- jne.
Web2pyn toimintaperiaate on seuraava:
- Suoritetaan www-osoitetta vastaava funktio käsittelijässä (vrt. Flaskin route)
- Käsittelijä palauttaa tuloksen merkkijonona tai dictinä
- Näkymä esittää käsittelijän palauttaman datan HTML- tai jossain muussa muodossa
- introduction
Rakenne
Web2py-sovellus rakentuu tietynlaiseen kansiorakenteeseen:
|-- applications | |-- admin ylläpitosovellus | |-- examples esimerkkejä | `-- welcome Mallisovellus | |-- controllers Sovelluksen käsittelijät | |-- cron Ajastetut toiminnot | |-- languages Monikielisyys | |-- models Mallit | |-- modules | |-- private | |-- static staattiset tiedostot kuten javascript, css, kuvat | `-- views Näkymät | `-- default |-- examples Asetustiedostojen malleja |-- extras | |-- build_web2py | `-- icons |-- gluon Varsinainen web2py-sovelluskehys |-- handlers Eri ympäristöihin tarvittavat käsittelijät |-- scripts Sekalaisia skriptejä
Valmiilla ylläpitosovelluksella (admin) voi yleensä luoda uusia sovelluksia ja muutella niiden asetuksia yms. Käytettäessä Googlen App Enginea (GAE) kaikki nämä ominaisuudet eivät kuitenkaan toimi.
Google App Engine launcherilla suoritettaessa web2py-sovellusta on web2py-kansiorakenteen juureen lisättävä appengine_config.py-niminen tiedosto josta löytyvät seuraavat rivit:
import sys sys.platform = 'linux3'
Models (malli)
Models-kansiossa olevat tiedostot suoritetaan aakkosjärjestyksessä. Kaikki models-kansiossa olevissa tiedostoissa olevat muuttujat näkyvät myös kontrollereille ja näkymille.
Controllers (käsittelijä)
Oletuksena sovelluksessa on kaksi käsittelijää:
- appadmin.py - Valmis ylläpitosovellus tietokannan käsittelyyn
- default.py - Oman sovelluksen käsittelijä
Sivun osoite muodostuu seuraavasti:
http://palvelin:portti/sovelluksen_kansio/käsittelijän_nimi/funktion_nimi
Esimerkiksi: http://127.0.0.1:8000/myapp/default/index
Views (näkymä)
Näkymät rakentuvat kontrollerien mukaan. Views-kansiossa on alakansio, jonka nimi on sama kuin kontrollerin nimi esim. default. Tämän kansion alla on jokaiselle funktiolle oma näkymä. Nimen pitää olla sama kuin funktion nimi. Esim. index
Jos kontrollerille ei löydy vastaavaa näkymää niin web2py käyttää lokaalisti (App Engine Launcherilla) suoritettaessa generic.html-näkymää. Julkaistaessa sovellus verkkoon App Engine alustalle tämä ei enää toimi vaan aiheuttaa virheen. Muista siis aina luoda jokaista funktiota varten myös oma näkymä.
Näkymätiedoston tiedostopääte esim. html on myös merkityksellinen. Oletuksena käytetään .html-päätteistä näkymää mutta jos funktiota (sivua) kutsutaan esimerkiksi muodossa http://127.0.0.1:8000/myapp/default/index.json niin yritetäänkin käyttää index.json-nimistä näkymää. Myös tässä tapauksessa käytetään geneeristä näkymää jos valmista ei ole olemassa. Jos haluaa sallia geneeriset näkymät myös tuotantokäytössä niin se onnistuu lisäämällä malliin rivi:
response.generic_patterns = ['*']
Näkymään voi sisällyttää python-koodia {{ ja }}-merkintöjen väliin. Näkymässä voi käyttää kaikkia niitä muuttujia jotka palauttaa kontrollerin return-lauseessa (vrt. Flask):
return dict(message="Hello from MyApp")
Edellä näkyvää message-muuttujaa voi näkymässä käyttää seuraavasti:
<h1>{{=message}}</h1>
Näkymään voi lisätä myös debuggausinfoa:
{{=response.toolbar()}}
Kts. web2py overview
Sessiot
Sessiot toimivat samaan tapaan kuin Flaskissa mutta eivät edellytä mitään erityisiä alustustoimenpiteitä:
if not session.counter: session.counter = 1 else: session.counter += 1
request
Web2py sisältääm samantyyppisen request-objektin kuin Flask:
- request.env.request_method sisältää käytetyn metodin (GET/POST)
- request.get_vars sisältää GET-metodilla tuodut parametrit
- request.post_vars sisältää POST-metodilla tuodut parametrit
- request.vars sisältää sekä GET- että POST-metodilla tuodut parametrit
response
response-objektin tarkempi sisältö kannattaa katsoa suoraan web2py-kirjasta
URL
URL-funktiolla saa luotua haluttuun funktioon osoittavan urlin:
URL('index')
Samalla funktiolla voi luoda osoitteen myös staattisiin tiedostoihin (static-kansioon sijoitettuihin) esim. css-tiedostoon:
URL('static', 'css/oma.css')
Mallia voi katsoa esim. welcome-sovelluksen näkymätiedostoista kuten views/layout.html
- response.flash
- response.menu
- response.title
Database Abstraction Layer (DAL)
DALin avulla määritellään sovelluksen tietorakenne (model, malli)
db.define_table('person', Field('name'), id=id, rname=None, redefine=True common_filter, fake_migrate, fields, format, migrate, on_define, plural, polymodel, primarykey, redefine, sequence_name, singular, table_class, trigger_name)
Taulun esitysformaatti esim. alasvetovalikoissa määrätään format-ominaisuudella:
format='%(nimi)s %(matka)s %(kilpailu)s'
formatissa luetellaan kentät joita halutaan näyttää
Oma id-kenttää ei kannata keksiä vaan on parempi käyttää web2pyn automaattisesti luomaa id:tä. web2py luo id-kentän kaikille tauluille.
Field(fieldname, type='string', length=None, default=None, required=False, requires='<default>', ondelete='CASCADE', notnull=False, unique=False, uploadfield=True, widget=None, label=None, comment=None, writable=True, readable=True, update=None, authorize=None, autodelete=False, represent=None, compute=None, uploadfolder=None, uploadseparate=None,uploadfs=None, rname=None)
string IS_LENGTH(length) default length is 512 text IS_LENGTH(65536) blob None boolean None integer IS_INT_IN_RANGE(-1e100, 1e100) double IS_FLOAT_IN_RANGE(-1e100, 1e100) decimal(n,m) IS_DECIMAL_IN_RANGE(-1e100, 1e100) date IS_DATE() time IS_TIME() datetime IS_DATETIME() password None upload None reference <table> IS_IN_DB(db,table.field,format) list:string None list:integer None list:reference <table> IS_IN_DB(db,table.field,format,multiple=True) json IS_JSON() bigint None big-id None big-reference None
Tietokannan rakennetta ei kannata Googlen NoSQL-tietokannassa normalisoida liikaa. Kannattaa ennemmin käyttää list:string ja list:integer-tietorakenteita ja ympätä yhteen tauluun mahdollisimman paljon tietoa. NoSQL-tietokannassa jokaisen erillisen tietueen hakeminen on arvokasta eikä SQL:ssä toimivia liitoksia voida käyttää kyselyissä.
list:string-rakenteeseen voi tallentaa python-listan, joka sisältää mitä tahansa stringejä. Jos haluaa tallentaa vielä monimutkaisempia tietorakenteita niin sekin onnistuu jos muuntaa ne ensin json-muotoon.
Json-tyyppiin voi myös tallentaa json-muodossa isohkojakin rakenteita.
list:integer toimii kuten list:string mutta kelpuuttaa vain integereja sisältävän listan.
Jos haluaa, että jotain kenttää ei näytetä lomakkeilla niin kentälle voi erikseen määritellä sen luku- ja kirjoitusoikeuden. Tämä ei vaikuta siihen voiko ohjelmallisesti manipuloida kyseistä kenttää vaan vain lomakkeilla näkymiseen.
db.joukkue.rastit.readable = False db.joukkue.rastit.writable = False
Kentille voi määrätä myös monimutkaisiakin oletusarvoja. Esim kellonaika halutussa muodossa:
Field('aika', 'datetime', required=True, compute=lambda x: datetime.datetime.fromtimestamp(time.time()+2*60*60).strftime('%Y-%m-%d %H:%M:%S'
required vs requires
required-parametri määrittelee onko kenttä pakollinen vai ei. Tämä tarkistus tehdään tietokantatasolla. Myös None on kelvollinen arvo.
requires-parametri määrittelee kentän tarkistukset lomaketasolla. SQLFORM tuuppaa kenttään arvoksi None jos kenttään ei syötä mitään joten myös requires-parametrille on annettava sopiva tarkistus jos haluaa varmistaa, että kentän arvo ei jää tyhjäksi tai None:ksi.
Erilaisia tarkistusfunktioita (validators):
- IS_LENGTH(length)
- IS_INT_IN_RANGE(min, max)
- IS_FLOAT_IN_RANGE(min, max)
- IS_DECIMAL_IN_RANGE(min, max)
- IS_DATE()
- IS_TIME()
- IS_DATETIME()
- IS_IN_DB(db, table.field, format)
- IS_NOT_IN_DB(db, table.field, format)
- IS_JSON
- IS_EMPTY()
- IS_NOT_EMPTY()
- IS_URL()
- IS_IN_SET()
# ei saa olla tyhjä db.henkilo.nimi.requires = IS_NOT_EMPTY() # ei saa olla tyhjä, kustomoidaan virheilmoitus ja lisäksi tarkistetaan että ei saa olla jo olemassa tietokannassa samaa nimeä db.henkilo.nimi.requires = [IS_NOT_EMPTY(error_message='Ei saa olla tyhjä'), IS_NOT_IN_DB(db, 'henkilo.nimi')]
reference
Jos kenttä saa sisältää vain toisessa taulussa olevaa sisältöä niin käytetään references-ominaisuutta ja sen lisäksi requires-ominaisuudessa IS_IN_DB-funktiota määräämään mitä sisältöä lomakkeella tälle kentälle esitetään:
Field('joukkue', 'reference joukkue', requires=IS_IN_DB(db,'joukkue.id','%(nimi)s'))
reference joukkue tekee kentästä viiteavaintyyppisen eli arvoksi kelpaavat samat arvot kuin mitkä löytyvät joukkue-taulun id-kentästä. Requires-ominaisuudessa määrätään mitä halutaan antaa vaihtoehtoina lomakkeella ja tässä tapauksessa käytetään joukkue.id-kenttää. Lomakkeelle luotavassa alasvetovalikossa listataan kuitenkin joukkueiden nimet (%(nimi)s) eikä id-kentän arvoa, koska se ei kertoisi kenellekkään mitään.
Oletusvalinnan voi määritellä zero-parametrilla. Jos arvoksi laittaa None niin oletuksena on listan ensimmäinen:
Field('joukkue', 'reference joukkue', requires=IS_IN_DB(db,'joukkue.id','%(nimi)s'), zero=None)
migrate
Tietokannan rakennetta muuteltaessa web2py päivittää uuden rakenteen aina heti kun sovelluksessa ladataan yksikin sivu. Valitettavasti joskus tietokannan rakenteen muunnos (migrate) ei oikein pysy perässä vaan jotain voi jäädä solmuun tai web2py ei jostain syystä tajua tehdä haluttua muutosta rakenteeseen. Web2pyn saa pakotettua muodostamaan taulu uudelleen lisäämällä taulun luomiseen redefine=True
db.define_table('henkilo', Field('nimi'), redefine=True)
Varsinkin reference-tyyppisten kenttien kohdalla saattaa joskus tulla kummallisia ongelmia eikä haluttua alasvetovalikkoa ilmesty tai valikko ilmestyy mutta sen sisältö on tyhjä. Silloin kannattaa kokeilla edellämainittua redefine-parametria. Tämän lisäksi kannattaa muuttaa ongelmallisen kentän (kentän, joka ei näy alasvetovalikossa) nimi toiseksi. Tämän pitäisi pakottaa web2pyn poistamaan vanhan kenttä ja luomaan uuden.
update_or_insert
Tietokannan tietoja voi helposti päivittää update_or_insert-funktiolla, joka lisää uuden jos samoilla arvoilla ei jo löydy vastaavaa tietuetta jolloin tehdäänkin päivitys:
db.person.update_or_insert(db.person.name=='John', name='John',birthplace='Chicago')
Order by
Jos haluat järjestää DAL-kyselyn tuloksen niin se onnistuu order by-ominaisuudella:
db().select(db.henkilo.ALL, orderby=db.henkilo.nimi)
Jos järjestysehtoja on useampia niin ne erotellaan |-merkillä:
db().select(db.henkilo.ALL, orderby=db.henkilo.nimi|db.henkilo.ika)
Jos halutaan jättää huomiotta pienet ja isot kirjaimet. Huom. Tämä ei toimi App Enginen kanssa!
db().select(db.henkilo.ALL, orderby=db.henkilo.nimi.upper())
Vastaava voidaan tehdä pythonilla kts. Custom sorting with key.
def sorttaa(a): # järjestää arvosanan mukaan return a.arvosana # järjestää palautteen mukaan aakkosjärjestykseen # return a.palaute # järjestää palautteen mukaan aakkosjärjestykseen eikä välitä isoista ja pienistä kirjaimista # return a.palaute.lower() # järjestää palautteen mukaan aakkosjärjestykseen eikä välitä isoista ja pienistä kirjaimista ja tarvittaessa järjestää vielä arvosanan mukaan # return a.palaute.lower() + str(a.arvosana) def palaute(): palautteet = db().select(db.palaute.ALL) palautteet = palautteet.sort(sorttaa) ...
belongs
belongs-metodilla voi rajoittaa kyselyä listalla:
Lomakkeet
SQLFORM-funktio osaa suoraan luoda tietokannan sisällön pohjalta sopivan lomakkeen.
SQLFORM(table, record=None, deletable=False, linkto=None, upload=None, fields=None, labels=None, col3={}, submit_button='Submit', delete_label='Check to delete:', showid=True, readonly=False, comments=True, keepopts=[], ignore_rw=False, record_id=None, formstyle='table3cols', buttons=['submit'], separator=': ', **attributes)
Oleellisimpia SQLFORMin parametreja:
- showid määrää näytetäänkö id-kenttää lomakkeella vai ei
- delete_label määrää tekstin, joka näytetään delete-checkboxin vieressä
- submit_button määrää submit-painikkeen tekstin
- fields-parametriin voi sijoittaa dictin jossa on lueteltu kentät, jotka halutaan lomakkeella näyttää. Avain tarkoittaa kenttää ja jos avaimelle on määritelty jokin arvo on se kentän label.
- deletable määrää sallitaanko poistaminen
Esimerkki
Viikkotehtävien palautusjärjestelmän lähdekoodi
Viikkotehtävien palautusjärjestelmän lähdekoodi zip-paketissa
- db.py (malli)
- default.py (käsittelijä)
- views (näkymät)
Lisätietoa
Lisätietoa web2py-sovelluskehyksestä:
- Complete Reference Manual (kirja, hyvä!)
- Luokkadokumentaatio
- Keskusteluryhmä
- Esimerkkejä
Lue läpi vähintään seuraavat osat web2py-kirjasta:
- Introduction
- Simple Examples
- Adding authentication
- More on admin
- Applications
- API
- request
- response
- sessions
- The views
- Basic syntax
- HTML helpers
- Page layout
- Connection strings
- DAL, Table, Field
- Migrations
- insert
- commit and rollback
- Query, Set, Rows
- select
- Other methods
- One to many relation
- Many to many
- Google NoSQL (Datastore)
- FORM
- SQLFORM
- Custom forms
- Validators
Korkeampiin arvosanoihin tähtäävien on syytä lukea myös seuraavat:
Käyttäjien kommentit