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:

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.

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:

Web2pyn toimintaperiaate on seuraava:

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ää:

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:

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

Database Abstraction Layer (DAL)

DALin avulla määritellään sovelluksen tietorakenne (model, malli)

Taulujen määrittely

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.

Kenttien määrittely

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):

# 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:

One form for multiple tables

Esimerkki

Viikkotehtävien palautusjärjestelmän lähdekoodi

Viikkotehtävien palautusjärjestelmän lähdekoodi zip-paketissa

Lisätietoa

Lisätietoa web2py-sovelluskehyksestä:

Lue läpi vähintään seuraavat osat web2py-kirjasta:

Korkeampiin arvosanoihin tähtäävien on syytä lukea myös seuraavat:

Käyttäjien kommentit

Kommentoi Lisää kommentti
Kurssimateriaalien käyttäminen kaupallisiin tarkoituksiin tai opetusmateriaalina ilman lupaa on ehdottomasti kielletty!
http://appro.mit.jyu.fi/web-sovellukset/luennot/web2py/
© 2016-05-18 13:19:04
Informaatioteknologia - Jyväskylän yliopiston informaatioteknologian tiedekunta