venerdì 4 luglio 2008

LISTEN/NOTIFY con SQLAlchemy su Postgresql

In questo periodo sto sviluppando una semplice applicazione wxPython basata su SQLAlchemy che ha il compito di gestire gli ordini di una piccola sagra. Una delle richieste è stata quella di visualizzare su una label il totale degli ordini complessivi fatti in un certo momento. Le installazioni previste per questo programma sono più d'una e volevo fare in modo che la label si aggiornasse automaticamente a ogni nuovo ordine.

Visto che come database uso il (portentoso) postgresql ho pensato di avvalermi di una delle feature presenti in questo DBMS: il LISTEN/NOTIFY. In questo modo riesco a essere notificato degli eventi (CRUD) che coinvolgono una certa tabella.

Ovviamente SQLAlchemy non permette l'accesso diretto a queste funzioni, bisogna passare attraverso psycopg2, ma la procedura è abbastanza semplice. Quando l'applicazione viene eseguita viene fatto partire un thread che si mette in ascolto di una certa tabella:

def thread_notifica_ordini(aggiorna_componente):
conn = pg_db.connect()
conn.detach()
conn.connection.set_isolation_level(0)
curs = conn.connection.cursor()

#mi metto in attesa sulla tabella ordini
curs.execute("listen ordini")

print "In attesa di un NOTIFY"
while continua_thread:
if select.select([curs],[],[],None)==([],[],[]):
print "Timeout"
else:
if curs.isready() and continua_thread:
wx.CallAfter(aggiorna_componente)
print "Ricevuta notifica: %s" % str(curs.connection.notifies.pop())


la variabile "continua_thread" ha sempre valore True a meno che l'utente non decida di chiudere l'applicazione. La select.select scritta in quel modo si mette in attesa per un tempo indefinito, il listening sulla tabella ordini non va mai in timeout.
Alla funzione viene passata un'altra funzione "aggiorna_componente" che si limita ad aggiornare il componente grafico (la label) che visualizza il totale ordini complessivo. Devo usare la chiamata a wx.CallAfter perchè quando si lavora con i thread le chiamate a wx devono "tornare" al thread principale.


La funzione di notifica vera e propria è la seguente (che viene eseguita quando salvo un ordine):


def notifyordine():
""" apro una connessione per eseguire la notifica """
conn = pg_db.connect()

conn.detach()
conn.connection.set_isolation_level(0)
curs = conn.connection.cursor()

curs.execute("notify ordine")
conn.close()


Ovviamente usando l'intera procedura ci si lega a postgresql ma questo nel mio caso non è un problema, non ho nessuna legacy da dover supportare e mi è stata data ampia libertà in merito :)

2 commenti:

Joril ha detto...

Bello! :)
Due domande:
1) Cosi' com'e', il thread d'ascolto esegue continuamente query sul DB, o ho capito male?
2) Qual e' la differenza tra l'usare questo sistema e una semplice select count(*) from ordini ?

Emanuele Gesuato ha detto...

Vediamo se riesco a essere chiaro:

1) Malgrado il nome possa trarre in inganno la chiamata select.select non c'entra niente con le query. Essa si limita ad attendere che termini il flusso I/O sul primo parametro (gli altri definiscono il timeout).

2) Quello che voglio fare non è contare gli ordini ma saperne il totale complessivo in un dato momento. Questo totale non è persistito e sì avrei potuto fare una query che calcolasse al volo il totale pagato meno il resto per ogni ordine. La cosa che trovo poco elegante è che avrei dovuto eseguire questa query ogni x secondi visto che vorrei che il client A venisse notificato degli ordini fatti dal client B.
Questo metodo mi pare più elegante e non mi intasa l'applicazione di continue query :)