Blog

Come partizionare i dati e creare un applicativo multi-tenant

Come partizionare i dati e creare un applicativo multi-tenant

Il partizionamento dei dati può rivelarsi molto utile quando l’applicativo è utilizzato da utenti che hanno necessità di accedere all’applicazione in modo limitato, visualizzando e modificando solo i record loro riservati o da loro stessi inseriti, quando cioè ci troviamo di fronte a un applicativo multi-tenant.

Il partizionamento di per sé è un meccanismo che si può attivare in due semplici step:

1. modificare il model della table come segue:

class Table(object):
      def config_db(self,pkg):
           tbl=pkg.table('cliente', pkey='id', name_long='Cliente',caption_field='ragione_sociale',partition_user_id='user_id')
           self.sysFields(tbl)

2. Nella risorsa relativa, utilizzare nel metodo th_options l’attributo partitioned=True:

def th_options(self):
     return dict(partitioned=True)

Questi due semplici passaggi permettono in modo totalmente automatico di filtrare i record degli utenti, mostrando all’utente che effettua il login esclusivamente i record da lui inseriti. Il sistema infatti andrà in autonomia ad effettuare in modo nascosto una query di carattere AND utilizzando il criterio (o i criteri) che abbiamo definito.

Supponiamo però di voler andare più a fondo nelle logiche sottostanti a questo partizionamento automatico “base” e voler per esempio rendere l’applicativo fruibile a una categoria di user appartenenti a una specifica tabella, ovvero gli Agenti, che avranno accesso ai loro record clienti e alle relative fatture, ma anche a uno o più Amministratori, che potranno filtrare i record per Agente, nonché assegnare agli Agenti dei Clienti, o ad altri user “non Agenti” che manterranno invece i privilegi di default. Questo tipo di partizionamento (non sulla base di gruppi di utenza ma di una tabella esterna) richiederà la ridefinizione parziale della tabella user di default, del component di Login, nonché del metodo partitioning_pkeys che si occupa del partizionamento. È quello che abbiamo fatto in uno dei nostri appuntamenti di LearnGenropy, e di cui vi consigliamo la visione:

 

Vediamo però un’ulteriore procedimento alternativo che è invece attivo sul progetto HomeSweetHome, un piccolo applicativo tuttora in divenire che si occupa della gestione di un inventario di una casa, permettendoci di ricordare i posti e le stanze dove abbiamo riposto specifici oggetti. Il progetto è fruibile liberamente a tutti previa registrazione.

In questo caso l’esigenza era quella di assegnare automaticamente l’utente registrato a un gruppo di utenza (i “Casalinghi”), assegnare lui (almeno) una casa, permettergli poi di modificare all’interno di quella casa posti e oggetti. Il partizionamento verrà quindi applicato sulla base della casa (o delle case) lui assegnate.

Per fare questo ci siamo limitati a ridefinire parte della tabella user per assegnare automaticamente il gruppo di utenza e la casa, nonché il component di Login per permettere all’utente la scelta della casa su cui lavorare. Vediamo quindi nel dettaglio le modifiche effettuate.

Ridefinizione della tabella user

Per ridefinire (parzialmente) la table user del package ADM abbiamo seguito la procedura descritta nel Manuale dei Package, ovvero abbiamo creato nel nostro package di default “base” una cartella _packages, al cui interno abbiamo inserito adm > user.py.

A questo punto abbiamo semplicemente aggiunto una colonna “prima_casa” che verrà inserita contestualmente alla registrazione:

class Table(object): 
      def config_db(self,pkg): 
           tbl=pkg.table('user', pkey='id')
           self.sysFields(tbl)
           tbl.column('prima_casa', plugToForm=True, lbl='Prima casa') 

Si noti che con l’attributo plugToForm = True forziamo la visualizzazione del campo che abbiamo creato nel model anche nella Form. In questo modo creando il campo nel model possiamo evitare di ripetere la modifica anche nella connessa risorsa.

Sfruttiamo poi sempre nella stessa table il concetto di trigger, ovvero dei metodi di sistema di Genropy che permettono di far scattare dei processi al verificarsi di particolari eventi (es: l’inserimento di un nuovo record user). Utilizzando il metodo trigger_onInserting, per esempio, durante la registrazione dell’utente, assegniamo lui il gruppo di utenza “Casalingo”:

        def trigger_onInserting(self, record):
            record['group_code'] = 'HK'

E invece utilizzando il trigger_onInserted, che scatterà dopo che lo user è stato inserito, inseriamo automaticamente anche la nuova casa nella tabella “casa” e la relativa utenza nella tabella “utenza”:

        def trigger_onInserted(self, record): 
            casa_tbl = self.db.table('base.casa') 
            casa_id = casa_tbl.newPkeyValue() 
            nuova_casa = dict(id=casa_id, nome=record['prima_casa'])
            casa_tbl.insert(nuova_casa) 

            utenza_tbl = self.db.table('base.utenza')
            nuova_utenza = utenza_tbl.newrecord(user_id=record['id'], casa_id=nuova_casa['id'])
            utenza_tbl.insert(nuova_utenza)

            self.db.commit()

Si noti che la procedura per l’inserimento automatico di una casa e di un’utenza è esattamente la stessa, ma a scopi didattici nel primo caso usiamo un semplice dizionario, nel secondo invece utilizziamo il metodo newrecord, che si occupa non solo di produrre il nuovo record per la nuova tabella, ma anche di generare automaticamente i sysfields (incluso quindi l’id che invece nel primo caso abbiamo dovuto creare con il metodo newPkeyValue).

Una volta eseguiti questi passaggi procediamo con le modifiche al component di login.

Modifiche al login

A questo punto possiamo customizzare il login component affinché recepisca il meccanismo di partizionamento che abbiamo definito.

Per fare questo nelle risorse del nostro pkg “base” creiamo una risorsa login.py dove ridefiniamo la classe LoginComponent per recepire quanto segue:

  • una volta inserito lo user nella schermata di login, si dovrà distinguere un comportamento per il configuratore, per un “Casalingo” con una casa e per un “Casalingo” con più case
  • la casa scelta dovrà essere poi riportata come variabile in tutte le pagine dell’applicativo
  • in fase di registrazione deve essere possibile anche inserire una nuova casa (la “prima_casa” dello step precedente)

Procediamo quindi con il primo step:

class LoginComponent(BaseComponent): 
      def onUserSelected_base(self,avatar,data): 
          utenze = self.db.table('base.utenza' ).query(where='$user_id=:user_id', user_id=avatar.user_id).fetch()

          if not utenze: 
               data['configuratore'] = True 
               return 
          elif len(utenze)==1: 
               utenza = utenze[0] 
               data['casa_id'] = utenza['casa_id']
               data['casa_singola'] = True

Sfruttiamo il metodo onUserSelected_nomedelpackage, una sorta di trigger che all’identificazione dello username nella schermata di login ci permette di eseguire una query per vedere se ci sono delle utenze, e a questo punto definire due comportamenti:

  • se non ci sono utenze allora lo user deve essere l’amministratore del sistema, e in questo caso non compiamo azioni ma ci mettiamo da parte una variabile “configuratore”
  • se invece c’è una sola utenza, ci mettiamo da parte una variabile “casa_singola” che useremo più avanti e anche il valore della casa_id

A questo punto proseguiamo nella ridefinizione della classe LoginComponent aggiungendo una dbselect che all’inserimento dell’utente mostri le case a sua disposizione:

        def rootenvForm_base(self,fb):
              fb.dbSelect(value='^.casa_id',dbtable='base.casa', condition='@utenze.user_id=:user_id', condition_user_id='=gnr.avatar.user_id', selected_nome='.nome_casa', disabled='^.casa_singola',hasDownArrow=True, lbl='Casa')

rootenvForm_nomedelpackage è quindi un secondo hook che ci permette di effettuare modifiche al formbuilder del login (in questo caso aggiungendo la nostra dbselect). Si noti che utilizzando disabled=’^.casa_singola’ di fatto stiamo “bloccando” la dbselect in caso di utenze con solo una casa. Inoltre alla posizione gnr.avatar.user_id identifichiamo lo user_id inserito (esattamente come abbiamo fatto nel metodo precedente durante la query), e su questo imponiamo la condizione di uguaglianza, in modo da indentificare solo le sue case.

Utilizziamo adesso un terzo hook per metterci da parte nel rootenv la casa_id, in modo che in ogni pagina dell’applicativo questa sia sempre disponibile:

       def onAuthenticating_base(self, avatar, rootenv=None): 
             rootenv['current_casa_id'] = rootenv['casa_id']

In questo modo in server.dbEnv.current_casa_id sarà sempre leggibile il valore della casa_id che abbiamo selezionato, e basterà porre l’uguaglianza tra questa e la casa_id per avere una costante corrispondenza. Ad esempio, nella tabella oggetto:

       def defaultValues(self): 
           return dict(quantita='1', casa_id=self.db.currentEnv['casa_id'])

Infine, andiamo a modificare il formbuilder della registrazione utente per includere l’inserimento di una casa (riportiamo per brevità solo il campo aggiunto):

      def login_newUser_form(self,form): 
          ...
          fb.textbox(value='^.prima_casa',lbl='!!Casa',validate_notnull=True)

All’inserimento della casa scatteranno tutti i trigger che abbiamo descritto nel passaggio precedente.


In definitiva, nel nostro appuntamento di LearnGenropy e in questo tutorial abbiamo visto come in pochi passaggi sia possibile attivare meccanismi di partizionamento dei dati, e come sfruttando trigger, hook e semplice codice Python sia possibile con semplicità ridefinirne il funzionamento standard e sfruttare le enormi potenzialità offerte dal framework.


Ti stai avvicinando al mondo Genropy e desideri saperne di più? Seguici sui social per tenerti in contatto con le ultime novità che Genropy ha da offrire:

Genropy Seguici su Facebook
Pagina Facebook

 

Genropy Seguici Gruppo Facebook
Gruppo Facebook

 

Genropy Gruppo Telegram
Gruppo Telegram