LearnGenropy 23

Scritture sul database da ORM Genropy

insert

Per prima cosa importiamo GnrApp

In [2]:
from gnr.app.gnrapp import GnrApp

app = GnrApp('sandboxpg')
mydb = app.db

tbl_prodotto = mydb.table('fatt.prodotto')

L'oggetto table implementa un metodo insert che riceve in modo polimorfico o un dizionario o una bag contenente i dati del nuovo record.

In [3]:
nuovo_prodotto = dict(codice='TTFFG', descrizione='Prodotto Simpatico')
tbl_prodotto.insert(nuovo_prodotto)
mydb.commit()
#l'insert sul database non avviene realmente fin quando non si esegue il comando commit sull'oggetto db

print (nuovo_prodotto)
{'codice': 'TTFFG', 'descrizione': 'Prodotto Simpatico', 'id': 'KCdaL3JIMful1fXF4mB11A', '__ins_ts': datetime.datetime(2020, 6, 10, 10, 22, 26, 483704), '__mod_ts': datetime.datetime(2020, 6, 10, 10, 22, 26, 483737), '__ins_user': None}

Notiamo che noi non abbiamo passato un valore per la primary key eppure il adesso al dizionario (oggetto mutabile) che rappresentava il nostro record sono state aggiunte delle colonne alle quali è stato dato un valore.

  • id
  • __ins_ts
  • __mod_ts
  • __ins_user

Queste sono colonne di sistema che abbiamo definito nel model con la chiamata a sysFields. Se questi campi vengono aggiunti al model della table, vengono valorizzati automaticamente dall'ORM, qualora non fossero già valorizzati al momento della insert.

Assegnazione automatica della primary key

Merita un discorso particolare l'assegnazione automatica della primary key. L'assegnazione automatica avviene se al momento della insert nel record ricevuto non è presente un valore per la colonna primary key. In questo caso viene invocato il metodo di table pkeyValue.

Nel caso che abbiamo appena visto usiamo come primary key della table la colonna di sistema id e possiamo usare quindi la pkeyValue standard che rende un uuid di 22 caratteri.

tbl = pkg.table('prodotto', pkey='id', 
                   name_long='Prodotto')

self.sysFields(tbl)
In [4]:
new_pkey = tbl_prodotto.pkeyValue()
print (new_pkey)
gW6zRisfP6iKjwpD6DM2qg
In [5]:
from gnr.core.gnrlang import getUuid
new_uuid = getUuid()
print (new_uuid) 
7mHuZRx4MFu9_N8YqibM2g

Ovviamente se il metodo insert ricevesse la colonna pkey già valorizzata il sistema utilizzerebbe il valore ricevuto e non farebbe quindi l'auto assegnazione.

In [6]:
new_pkey = '1234567891234567891234'

altro_prodotto = dict(id = new_pkey, codice='GNAM', descrizione='Prodotto buono')
tbl_prodotto.insert(altro_prodotto)

print (altro_prodotto)
{'id': '1234567891234567891234', 'codice': 'GNAM', 'descrizione': 'Prodotto buono', '__ins_ts': datetime.datetime(2020, 6, 10, 10, 22, 39, 592476), '__mod_ts': datetime.datetime(2020, 6, 10, 10, 22, 39, 592508), '__ins_user': None}

primary key alternative a id

Nessuno però mi obbliga ad usare la colonna id di sistema e posso invece voler usare come primary key una colonna normale con valori che vengono ad esempio compilati in form o ricevuti da un processo di importazione da sistemi esistenti.

Se vediamo ad esempio in sandbox la tabella cliente_tipo

tbl = pkg.table('cliente_tipo', pkey='codice', caption_field='descrizione',lookup=True)
self.sysFields(tbl,id=False)
tbl.column('codice' ,size=':5',name_long='!![it]Codice', unique=True)

Notiamo che usa come pkey la colonna codice.

In [7]:
tbl_cli_tipo = mydb.table('fatt.cliente_tipo')
###ATTENZIONE CAMBIARE CODICE ALTRIMENTI DUPLICATE PKEY ERROR####
nuovo_tipo = dict(codice='PR7G5', descrizione='THSDDSD')

tbl_cli_tipo.insert(nuovo_tipo)
mydb.commit()

print(nuovo_tipo)
{'codice': 'PR7G5', 'descrizione': 'THSDDSD', '__ins_ts': datetime.datetime(2020, 6, 10, 10, 22, 43, 275408), '__mod_ts': datetime.datetime(2020, 6, 10, 10, 22, 43, 275449), '__ins_user': None}

Se volessi far fare l'autoassegnazione ad una colonna specifica secondo un criterio specifico posso ridefinire sulla mia tabella il metodo pkeyValue.

Ridefinire pkeyValue

Ridefiniamo la table cliente_tipo come segue:

from random import randint

class Table(object):
    def config_db(self, pkg):
        tbl = pkg.table('cliente_tipo', pkey='codice', name_long='!![it]Cliente tipo', 
                        name_plural='!![it]Cliente tipi',caption_field='descrizione',lookup=True)
        self.sysFields(tbl,id=False)
        tbl.column('codice' ,size=':5',name_long='!![it]Codice')
        tbl.column('descrizione',name_long='!![it]Descrizione')

    def pkeyValue(self, record):
        iniziale = record['descrizione'][0]
        numero_casuale = str(randint(100,1000))
        return '{iniziale}{numero_casuale}'.format(iniziale=iniziale, numero_casuale=numero_casuale)
In [8]:
nuovo_tipo_auto_code = dict(descrizione='Autoassegno')
tbl_cli_tipo.insert(nuovo_tipo_auto_code)
print (nuovo_tipo_auto_code)
{'descrizione': 'Autoassegno', 'codice': 'A680', '__ins_ts': datetime.datetime(2020, 6, 10, 10, 22, 49, 776404), '__mod_ts': datetime.datetime(2020, 6, 10, 10, 22, 49, 776439), '__ins_user': None}

Benché esista questa possibilità, suggeriamo di utilizzare se possibile l'id di sistema di Genropy così da potersi dimenticare del tutto di questa problematica e di averla gestita totalemtne dal framework.

Chiavi numeriche

In caso di pkey relative a colonne numeriche il comportamento di default della pkeyValue è di restituire il valore numerico incrementato rispetto all'ultimo presente.

class Table(object):
def config_db(self,pkg):
    tbl = pkg.table('foo', pkey='serial_number', name_long='!![it]Foo')
    self.sysFields(tbl,id=False)
    tbl.column('serial_number',dtype='L',name_long='!![it]Serial Nr.', unique=True)
In [10]:
foo_tbl = mydb.table('fatt.foo')
foo_tbl.pkeyValue()
Out[10]:
1
In [11]:
new_foo = dict(description='Cicciobello')
foo_tbl.insert(new_foo)
mydb.commit()
print (new_foo)
{'description': 'Cicciobello', 'serial_number': 1, '__ins_ts': datetime.datetime(2020, 6, 10, 10, 24, 0, 409021), '__mod_ts': datetime.datetime(2020, 6, 10, 10, 24, 0, 409081), '__ins_user': None}

Trigger

Parlando di scrittura su database vogliamo anche accennare a dei metodi di hook che è possibile definire sulle table.

  • trigger_onInserting / trigger_onInserted
  • trigger_onUpdating / trigger_onUpdated

Questi metodi vengono eseguiti rispettivamente prima e dopo l'inserimento o modifica del record e ricevono come parametro il record. I metodi in ing vengono eseguiti prima della scrittura effettiva e tipicamente si usano per effuare controlli o calcolare il valore di colonne in modo automatico secondo la business logic di applicazione.

I metodi in ed vengono invece eseguiti dopo la scrittura e possono innescare operazioni su altre tabelle, ma non su quella dove è definito.

def trigger_onInserting(self, record):
    record['description'] = record.get('description') or 'UNKNOWN'
    record['iniziale'] = record['description'].upper()[0]
In [13]:
foo_tbl = mydb.table('fatt.foo')
new_rec = dict(description='Kinom')
foo_tbl.insert(new_rec)
print (new_rec)
{'description': 'Kinom', 'serial_number': 15, '__ins_ts': datetime.datetime(2020, 5, 25, 19, 1, 44, 97453), '__mod_ts': datetime.datetime(2020, 5, 25, 19, 1, 44, 97559), '__ins_user': None, 'iniziale': 'K'}
In [11]:
unknown_rec = dict()
foo_tbl.insert(unknown_rec)
print (unknown_rec)
{'serial_number': 9, '__ins_ts': datetime.datetime(2020, 5, 25, 18, 22, 38, 312083), '__mod_ts': datetime.datetime(2020, 5, 25, 18, 22, 38, 312137), '__ins_user': None, 'description': 'UNKNOWN', 'iniziale': 'U'}
In [23]:
mydict = dict()
mydict['descr'] = mydict.get('descr') or 'ciao'
print (mydict['descr'])
ciao

Update

Per effettuare una scrittura di update di un singolo record, la table definisce il metodo update.

Questo metodo riceve due parametri di tipo dizionario (o Bag) che rappresentano rispettivamente il record con i dati nuovi da aggiornare e il record allo stato precedente, ovvero così come l'abbiamo letto.

In [34]:
#leggo il record che voglio aggiornare con parametro for_update = True
rec_trapano = tbl_prodotto.record(where='$codice=:cod_prod', 
                                  cod_prod='T1',
                                  for_update=True).output('dict')

print (rec_trapano)
{'id': '_ra0_FchP7CHqJI47e8q_Q', '__ins_ts': datetime.datetime(2014, 5, 7, 18, 6, 34, 270412), '__del_ts': None, '__mod_ts': datetime.datetime(2020, 6, 2, 18, 23, 2, 762246), '__ins_user': None, 'codice': 'T1', 'descrizione': 'Trapano Bosch', 'prodotto_tipo_id': 'cNobZCoGNkWwO6VG6bDfRA', 'prezzo_unitario': Decimal('110.00000000000001'), 'tipo_iva_codice': 'A', 'foto_url': '/_site/prodotti/immagini/T1.jpg?_pc=842&v_x=125&v_y=108.5&v_z=0.30&v_r=0&v_h=100&v_w=100', 'caratteristiche': None}

for_update e lock ottimistico

Se faccio la lettura con for_update = True mi sto riservando il record per modificarlo. Questo parametro riguarda tutti i metodi di lettura, non solo record, ma anche query.

Posso infatti fare una intera query in modalità for_update ed aggiornare tutti i record della selezione e fare alla fine un unico commit.

In [37]:
#faccio una copia del record originale prima di modificarlo
old_record = dict(rec_trapano)

#lo incremento del 10%
rec_trapano['prezzo_unitario'] = rec_trapano['prezzo_unitario'] * 3
In [38]:
tbl_prodotto.update(rec_trapano, old_record)
mydb.commit()

nuovo_prezzo = tbl_prodotto.readColumns(pkey = rec_trapano['id'], columns='$prezzo_unitario')
print (nuovo_prezzo)
330.00000000000003

L'importanza di old_record

Perché è buona norma passare old_record? Perché nei trigger onUpdating e onUpdated qualcuno potrebbe voler testare quali campi sono stati cambiati e come sono stati cambiati, per innescare eventi di business logic.

    def trigger_onUpdating(self, record, old_record):

        if record['prezzo_unitario'] < old_record['prezzo_unitario']:
            self.notificaRibassoProdotto(record)


Se voglio verificare il cambiamento di un set di campi esiste anche il metodo di table fieldsChanged

     def trigger_onUpdating(self, record, old_record):
        if self.fieldsChanged('prezzo_unitario,prodotto_tipo', record, old_record):
            self.faiQualcosa(record)

ricapitolando il funzionamento della update

  • leggo uno o più record in modalità for_update = True
  • faccio una copia del record prima di modificarne i valori
  • modifico i valori
  • chiamo il metodo di update passando il record modificato e l'originale.

non c'è un modo più comodo e veloce?

Ne esistono ben 2!

Nel caso in cui io voglia aggiornare una selezione di record posso usare il metodo di table batchUpdate, nel caso in cui io voglia leggere e modificare un singolo record posso usare il metodo recordToUpdate. Adesso li vediamo nel dettaglio.

batchUpdate

Questo metodo è per metà query e per metà update, infatti ha dei parametri comuni con il metodo query che servono a definire quali record voglio selezionare e dei parametri invece che servono a definire come i record devono essere modificati. Proprio come lo statement UPDATE di sql, questo metodo opera il cambiamento sui record che soddisfano le condizioni del parametro where.

Senza questo metodo e solo con update dovrei fare...

In [32]:
prodotti = tbl_prodotto.query(where='$descrizione ILIKE :desc' , desc='Tavolo%', for_update = True).fetch()

for p in prodotti:
    old_rec = dict(p)
    p['prezzo_unitario'] = 49
    tbl_prodotto.update(p, old_rec)
    
In [39]:
#invece con batchUpdate posso fare
# con updater "statico" dizionario
tbl_prodotto.batchUpdate( dict(prezzo_unitario = 49),
                         where='$descrizione ILIKE :desc',
                         desc='Tavolo%')
Out[39]:
['xJTymtQxPbGPhXJO9F12Jw',
 '4rOP1y4_O2iVAwWvxLK6Bg',
 'xW2oU9g4Pxa81v_VSkC5tg',
 'EISv0nJXPAeTHH59vU8gzw',
 'wANc1ou5NySJH_4H4Dz4UA']
In [58]:
def myupdater(rec):
    
    if rec['prezzo_unitario'] < 100:
        rec['prezzo_unitario'] = rec['prezzo_unitario'] + 11
    else:
        rec['prezzo_unitario'] = rec['prezzo_unitario'] - 5
        
    rec['descrizione'] = 'PROMO {desc}'.format(desc = rec['descrizione'])
    

updated_pkeys = tbl_prodotto.batchUpdate(myupdater,
                                         where='$descrizione ILIKE :desc',
                                         desc='Tavolo%')

mydb.commit()

recordToUpdate

Questa modalità si avvale dell'apertura di un contesto: all'interno del contesto avviene la lettura del record for_update, fuori dal contesto il lock viene rilasciato ed avviene la commit

In [54]:
with tbl_prodotto.recordToUpdate(codice='T1') as rec_prodotto:
    rec_prodotto['prezzo_unitario'] = rec_prodotto['prezzo_unitario'] + 1


mydb.commit()
In [55]:
prodotti = tbl_prodotto.query(where='$prezzo_unitario > :soglia' , soglia=100).fetch()
for prodotto in prodotti:
    with tbl_prodotto.recordToUpdate(where='$id=:prod_id AND $prezzo_unitario>:soglia',
                                    prod_id=prodotto['id'], 
                                    soglia=100, 
                                    ignoreMissing = True) as rec_prodotto:
        if rec_prodotto:
            rec_prodotto['prezzo_unitario'] = rec_prodotto['prezzo_unitario'] + 1
            print(rec_prodotto['prezzo_unitario'])
    
mydb.commit()
143
239.25
286.500
124.09658203125
186.3890625
339.00000000000003