Applicazioni Web in Python: I Modelli

Applicazioni web in PythonAnalizziamo in questo articolo l’ultimo elemento del modello MVC. Precedentemente abbiamo analizzato i controller e le viste, oggi tocca ai modelli.

Il modello è quel componente che fornisce i dati all’applicazione, interagendo con una base di dati di qualsiasi tipo, anche se il caso più comune, come anche nel nostro tutorial, prevede l’interazione con un DBMS.

Abbiamo già parlato in precedenza della flessibilità dei componenti di Pylons, anche se fino ad ora abbiamo sempre scelto la via facile, utilizzando per ogni compito il componente consigliato (Routes, Mako, etc…). Questa volta abbandoniamo di poco la retta via, andando ad utilizzare Elixir per la gestione dei modelli, anzichè SQLAlchemy (sul quale si basa anche Elixir).

Definizione

I modelli di Pylons si trovano all’interno della cartella [app]/lib/model.

Facciamo l’esempio di un blog con Articoli, Autori e Commenti. Creiamo un nuovo file [app]/lib/model/blog.py come segue:

from datetime import datetime, date
from elixir import *
from elixir import events
from baggettone.model import Session, metadata
from sqlalchemy.schema import UniqueConstraint

class Article(Entity):
    title = Field(Unicode(100), required=True)
    excerpt = Field(Unicode(1000))
    content = Field(UnicodeText(65535), required=True)
    created_at = Field(Datetime, required=True, default=datetime.now)

Abbiamo così definito il modello Article che come per tutti i modelli Elixir, estende la classe Entity e che possiede 4 campi che vengono definiti impostando delle proprietà di classe Field, il cui primo parametro rappresenta il tipo di campo, seguito da una serie opzionale di specifiche. I tipi di campi supportati sono gli stessi di SQLAlchemy, ed è possibile trovare la lista completa sulla relativa pagina della sua documentazione.

Alcune particolarità da tener presente in relazione al modello appena definito sono:

  1. Il campo ID viene definito automaticamente come chiave primaria auto-incrementante
  2. Il parametro default accetta sia un valore (ES: default=0) che una funzione, come nell’esempio (default=datetime.now)

Completiamo ora la definizione dei modelli, integrando le classi mancanti e legandole tra loro:

class Article(Entity):
    title = Field(Unicode(100), required=True)
    excerpt = Field(Unicode(1000))
    content = Field(UnicodeText(65535), required=True)
    created_at = Field(Datetime, required=True, default=datetime.now)
    author = ManyToOne('Author', ondelete='cascade', required=True)

class Author(Entity):
    name = Field(Unicode(100), required=True)
    email = Field(String(255))

class Comment(Entity):
    name = Field(Unicode(100), required=True)
    email = Field(String(255))
    comment = Field(Unicode(1000))
    article = ManyToOne('Article', ondelete='cascade', required=True)

Le righe evidenziate mostrano un nuovo tipo di proprietà per i modelli Elixir: le relazioni. Le relazioni possono essere di tipo ManyToOne, OneToOne, OneToMany e ManyToMany.

Nell’esempio, utilizzando ManyToOne, abbiamo attribuito il padre a una relazione padre-figli e può essere letta come: “Gli Article hanno un author che è di classe Author” e “I Comment hanno un article che è di classe Article“.

ManyToOne è la proprietà che effettivamente viene tradotta in linguaggio SQL in una foreing key, ma in Elixir è necessario specificare anche una “reverse” per la relazione, ovvero:

class Article(Entity):
    title = Field(Unicode(100), required=True)
    excerpt = Field(Unicode(1000))
    content = Field(UnicodeText(65535), required=True)
    created_at = Field(Datetime, required=True, default=datetime.now)
    author = ManyToOne('Author', ondelete='cascade', required=True)
    comments = OneToMany('Comment')

class Author(Entity):
    name = Field(Unicode(100), required=True)
    email = Field(String(255))
    articles = OneToMany('Article')

class Comment(Entity):
    name = Field(Unicode(100), required=True)
    email = Field(String(255))
    comment = Field(Unicode(1000))
    article = ManyToOne('Article', ondelete='cascade', required=True)

Le relazioni OneToMany evidenziate non aggiungono niente al livello DBMS, ma servono semplicemente alle classi Elixir per sapere come recuperare i figli di una relazione padre-figli partendo dal padre.
Le relazioni così definite si possono leggere come: “Author ha tanti articles di classe Article” e “Article ha tanti comments di classe Comment

Ovviamente a questi modelli possono essere aggiunti eventualmente tutta una serie di metodi che possono tornare utili per lo sviluppo dell’applicazione, come per esempio un metodo in Article per recuperare tutti gli altri articoli dello stesso autore.

Creazione del Database

Una volta definiti i modelli possiamo procedere con la creazione del database e delle tabelle da “mappare” ai modelli.
Il database da utilizzare va specificato nei file development.ini e production.ini (si può quindi avere 2 database separati per sviluppo e produzione) alla riga “sqlalchemy.url” (ne abbiamo già parlato nel post che illustrava la struttura di pylons).

I possibili valori per sqlalchemy.url sono descritti nella documentazione di SQLAlchemy al capitolo Database Engines.

Come già detto per inizializzare il database dobbiamo utilizzare il comando

paster setup-app development.ini

ma possiamo prima personalizzare i dati inseriti alla creazione modificando [app]/web_setup.py e aggiungendo ad esempio un autore, con anche un test in fondo.

# -*- coding: utf-8 -*-
import logging

import pylons.test

from myapp.config.environment import load_environment

log = logging.getLogger(__name__)

from paste.deploy import loadapp
from pylons import config
from elixir import *
from myapp.model import metadata, Session
from myapp.model.blog import *

def setup_app(command, conf, vars):
    # Don't reload the app if it was loaded under the testing environment
    if not pylons.test.pylonsapp:
        config = load_environment(conf.global_conf, conf.local_conf)

    metadata.create_all()
    author = Author(
        name = u"Massimiliano Torromeo"
        email = u"massimilianotorromeo@artera.it"
    )
    Session.commit()

    # Check the status
    a = Author.query.filter_by(name=u"Massimiliano Torromeo").all()
    assert len(a) == 1
    assert a[0] == author

Interrogazioni

Vediamo velocemente alcuni esempi di interrogazioni:

from sqlalchemy import desc
from sqlalchemy.sql import func
from myapp.model import Session
from myapp.model.blog import *

# Articolo con titolo "Articolo 1"
a = Article.query.filter_by(title=u"Articolo 1").one()

# Articoli con autore "Massimiliano Torromeo" ordinati inversamente per data
Article.query.filter(Article.author.has(name=u"Massimiliano Torromeo")).order_by(desc(Article.created_at)).all()

# Numero di commenti per "Articolo 1" usando direttamente SQLAlchemy
Session.query(func.count(Comment.id)).filter(Comment.article == a).one()

Come sempre, le documentazioni di SQLAlchemy e Elixir forniranno gli approfondimenti necessari.