Blogini jäi jumiin Django 1.3:een django-nonrelin käytön takia, joten päätin hankkiutua siitä eroon. Jos haluaa kuitenkin käyttää Djangon kanssa MongoDB:tä, niin käytännössä ainoa vaihtoehto on nykyään MongoEngine.

MongoEngine eroaa django-nonrelistä ja django-mongodb-enginestä siinä mielessä, että se ei yritäkään olla yhteensopiva Djangon omien tietomallien kanssa. Sen sijaan MongoEngine tarjoaa Document-luokan, jolla voi rakentaa erilliset tietomallit.

Koska Document-tietomallit eivät ole suoraan Djangon kanssa yhteensopivia, tarvitaan muutama apukirjasto:

Ylläolevista linkeistä pari osoittaa omiin forkkeihini projekteista, sillä olen joutunut korjaamaan muutamia epäyhteensopivuuksia Djangon nykyisen version kanssa. Django-mongoadmin on lisäksi melko vaatimaton kirjasto, sillä se ei tarjoa kovinkaan täydellistä MongoEngine-yhteensopivuutta Djangon ylläpitokäyttöliittymään. Sillä kuitenkin pärjää jotenkuten.

MongoEnginen erityispiirteitä

Kun olemassaolevan MongoDB-tietokannan siirtää MongoEnginen tietomallien alle, kannattaa huomioida seuraavat asiat:

  • MongoEngine käyttää relaatioihin (ReferenceField) oletuksena DBRef-tyyppisiä kenttiä. Niiden sijaan kannattaa käyttää yksinkertaisempia ObjectId-kenttiä, mikä onnistuu dbref=False -argumentilla.
  • Edellämainitut ObjectId-kentät tallentuvat tietokantaan ilman _id-päätettä. Toisin sanoen jos Comment-luokassa on viittaus Article-luokkaan, se ei tallennu kenttään nimeltä article_id vaan article. Tätä varten saattaa joutua ajamaan pienen konversioskriptin MongoDB:n shellissä.
  • Monet kentät joutuu vaihtamaan vähän toisen nimisiksi (CharField ja TextField muuttuvat StringFieldeiksi, IntegerField muuttuu IntFieldiksi, ja niin edelleen).
  • DateTimeFieldeillä ei ole auto_now- ja auto_now_add-toimintoja. Niiden sijaan täytyy ohjelmoida oma save()-metodi ja asettaa ne käsin siellä.
  • Perinteinen "class Meta" on MongoEnginessä muodossa meta = {...}, mutta sillä voi tehdä suunnilleen samat asiat.

Vanhasta tietokannasta siirrettyihin dokumentteihin kannattaa laittaa tällaiset meta-optiot:

class Article(Document):
    meta = {
        'collection': 'blog_article',       # Taulun (kokoelman) nimi tietokannassa
        'allow_inheritance': False,         # Välttää turhaa sotkua tietokannassa
        'queryset_class': ArticleQuerySet,  # Kustomoitu queryset (kuten Djangon Manager)
        'ordering': ['-created'],           # Oletusjärjestys
        'indexes': ['-created' ,'+words'],  # Automaattisesti luotavat MongoDB-indeksit
    }

Jos on tekemässä uutta projektia, 'collectionin' voi tietysti jättää pois. Oletuksena se on mallin nimi ilman blog_-etuliitettä. Kustomoidun 'queryset_classin' tarvitsee vain, jos haluaa tehdä omia apumetodeita kyselyjä varten. Jos 'allow_inheritancea' ei ole asetettu Falseksi, MongoEngine edellyttää, että jokaisessa tietokantataulussa täytyy olla ylimääräisiä kenttiä perintää varten. Se kyllä luo ne automaattisesti, mutta olemassaolevassa tietokannassa niitä ei luultavasti ole.

Kuvatiedostot ja GridFS

MongoEnginessä on ImageField-kenttä, joka tallentaa kuvat MongoDB:n GridFS:ään. Tästä on tiettyjä haittapuolia, mutta myös etuja. Tiedostojen käsittely yksinkertaistuu, kun ne sijaitsevat samassa tietokannassa niihin viittaavien dokumenttien kanssa. Koko sovelluksen voi varmuuskopioida tai dumpata kerralla, eikä tarvitse miettiä, missä polussa tiedostot ovat ja zipata niitä erikseen.

Haittapuolia on kaksi:

  • Tiedostojen palveleminen GridFS:stä vaatii erityistä tukea webbiserveriltä, eikä se ole yhtä tehokasta kuin tavallisten tiedostojen palveleminen.
  • Tiedostojen tallentaminen ulkoiseen palveluun, kuten S3:een, ei onnistu. MongoEngine ei nimittäin tue Djangon Storage APIn käyttöä tiedostojen tallentamiseen, mikä olisi suotavaa.

Itse ratkaisin ensimmäisen ongelman siten, että tein Django-sovellukseen yksinkertaisen näkymän, joka palvelee tiedostot GridFS:stä suunnilleen näin:

def article_gridfs_image(request, article_id):
    article = get_document_or_404(Article, id=article_id)
    if not article.image:
        return HttpResponseNotFound('No such image')
    return HttpResponse(article.image.read(), mimetype=article.image.content_type)

Tämä on tietenkin hyvin tehotonta, sillä jokainen kuvahaku menee Python-tulkin läpi MongoDB:hen asti. Onneksi Nginx-palvelimesta löytyy tähän helppo ratkaisu, kun käyttää uwsgi-rajapintaa. Omassa tapauksessani se menee näin:

uwsgi_cache_path /var/www/cache/gridfs levels=1 keys_zone=gridfs:1m max_size=500m;

location /gridfs {
        uwsgi_cache gridfs;
        uwsgi_cache_valid 200 365d;
        uwsgi_cache_key $request_uri;
        uwsgi_pass uwsgi_kfalcknet;
        include uwsgi_params;
}

location / {
        uwsgi_pass uwsgi_kfalcknet;
        include uwsgi_params;
}

Näillä asetuksilla Nginx tallentaa kaikki /gridfs-polun alta ladatut kuvat omaan välimuistiinsa ja pitää niitä siellä vuoden ajan. Seuraavalla kerralla ladattaessa kuvaa ei enää haeta Djangolta, vaan se palvellaan suoraan välimuistista.

Jos käyttää Gunicornia tai jotain muuta vastaavaa ratkaisua uWSGI:n sijaan, samaan tarkoitukseen voi soveltaa Nginxin proxy_cache-rajapintaa, joka toimii samalla tavalla kuin uwsgi_cache.

uWSGI

Itse päätin siirtyä käyttämään uWGSI:tä, sillä se tulee nykyään Ubuntussa valmiina pakettina, ja siihen on myös helppo konfiguroida Django-sovelluksen käyttämä virtualenv. Ubuntun uWSGI:ssä omat sovellukset konfiguroidaan tekemällä /etc/uwsgi/apps-available -polun alle niille konfiguraatiotiedostot. Enää ei siis tarvitse tehdä niille omia Upstart- tai init.d-skriptejä.

Jos luit tänne asti, onneksi olkoon!

Published 2.1.2013