OR Mappers: altre testimonianze
Wednesday, March 31st, 2010Vedo che anche altri sono più o meno del mio parere1.
Mi sembra interessante anche SQLAlchemy, credo varrà la pena di dargli un’occhiata.
Vedo che anche altri sono più o meno del mio parere1.
Mi sembra interessante anche SQLAlchemy, credo varrà la pena di dargli un’occhiata.
Premessa doverosa: non ho mai usato Core Data anche se ho usato in lungo e in largo l’EOF, fino a lasciarlo perdere. I post1 di Brent Simmons in cui dice di aver rinunciato a usare Core Data sull’iPhone per problemi di prestazioni, quindi, non poteva sfuggirmi, tanto più che ha generato diverse reazioni interessanti2.
Quello che mi interessa discutere non è la scelta in sé, che è solo buon senso applicato alla programmazione, ovvero, lavora sempre al più alto livello di astrazione possibile, scendi sotto solo se sei obbligato.
Quello che continuo a non capire è come si possa sostenere che una cosa come update newsItems set read = 1 where... stia a un livello di astrazione più basso di un loop in Objective-C o qualche altro linguaggio.
Non mi sfuggono gli aspetti problematici del fare interagire un programma con un database né che secondo alcuni Core Data non è un database o un ORM3, ma è proprio questo arguire intorno al cosa sia Core Data mi rende sospettoso perché queste discussioni sembrano spostare il problema nell’àmbito delle aspettative sbagliate.
Secondo la documentazione ufficiale:
The technically correct way to describe Core Data is as an object-graph management and persistence framework. In down-to-earth terms, this means that Core Data organizes the application’s model layer into a set of defined in-memory data objects. Core Data tracks changes to these objects and can reverse those changes on demand, such as when a user performs an undo command. Then, when it is time to save changes to your application’s data, Core Data takes care of archiving the objects to a persistent store. (…)
Core Data builds on some of the concepts of enterprise-class database application frameworks, such as the Enterprise Objects Framework in WebObjects. However, make no mistake, Core Data is not an object-relational database access framework. Instead, it uses concepts from the database world to take application data management to a new level.
Ora, se diamo per buona questa descrizione — e non vedo perché non dovremmo — bisogna chiedersi: le aspettative di Brent Simmons erano ragionevoli? Credo di sì, perché se “usi dei concetti dal mondo dei database” non è strano aspettarsi un modo efficiente di fare un update. Giusto?
Tempo fa, quando spiegavo le novità di PostgreSQL 8.4 nelle funzioni, avvertivo anche che la combinazione dei parametri di default con l’overloading degli argomenti delle funzioni poteva condurre ad ambiguità.
Mi ero però limitato alla parte negativa, mentre è proprio grazie ai default che diventa possibile sbarazzarsi di funzioni dallo scopo identico, che variano solo per il numero di argomenti.
In questo post si spiegano i vantaggi (piuttosto ovvi, in verità) di questo approccio e si accennano alle novità in arrivo per la versione 8.5.
Nei momenti di sciatteria mi vien da pensare che in fondo, per la maggior parte degli usi, MySQL va più che bene. Sì, d’accordo, PostgreSQL è un’altra cosa ma sono differenze che spesso non si vedono.
Poi mi capitano cose come quella che segue e capisco che è una sciocchezza.
C’è questa applicazione PHP+MySQL da mettere in sicurezza, prima della riscrittura da zero. Provo alcune modifiche in locale e, essendo tutto a posto, riporto gli aggiornamenti sul server di deployment.
Verifico che funzioni tutto e noto alcune stranezze per cui inizio a fare tutte le possibili verifiche ma sembra tutto a posto, fin quando provo l’SQL a mano e vedo che una query dà errore dicendo che la tabella è inesistente (????).
A questo punto guardo la documentazione di MySQL e scopro che:
In MySQL, databases correspond to directories within the data directory. Each table within a database corresponds to at least one file within the database directory (and possibly more, depending on the storage engine). Consequently, the case sensitivity of the underlying operating system plays a part in the case sensitivity of database and table names. This means database and table names are not case sensitive in Windows, and case sensitive in most varieties of Unix. One notable exception is Mac OS X, which is Unix-based but uses a default file system type (HFS+) that is not case sensitive.
A quel punto capisco: sto sviluppando su Mac OS X e il server è Linux.
Che dire? tutto questo è ridicolo. Non so cosa dica lo standard SQL: se il riferimento ai nomi debba esser case sensitive o meno ma che una query smetta di funzionare a causa del sistema operativo sottostante è veramente un delirio. Non vedo l’ora di mandare in pensione questa applicazione!
Qualche anno fa mi ero messo a scrivere un framework di autenticazione e autorizzazione per WebObjects. L’obiettivo era quello di creare un sistema integrato in cui i principali aspetti della sicurezza fossero affrontati e messi in collegamento con il DirectToWeb. Fu un mezzo fallimento.
Funzionava, sì, ma era troppo macchinoso da impostare e utilizzare. Uno dei motivi fu quello di voler usare un database per memorizzare le configurazioni, obbligandomi anche a prevedere un’interfaccia di gestione per aspetti che è quasi sempre preferibile affrontare con un buon editor di testo.
L’altro ieri mi trovavo a mettere le mani su un’applicazione fatta in Joomla (oh, the horror… Risparmio le imprecazioni per un codice che grida vendetta).
Anche Joomla usa un database dietro le quinte per archiviare le sue configurazioni e consente un approccio semplice ad alcune delle funzioni di CMS ma rende le operazioni di staging molto, troppo complicate: non posso provare una configurazione in locale, fare il commit nella repository e poi fare il deployment ma devo replicare le modifiche sul database di produzione; inoltre non ho nessun controllo di versione se non facendo il dump delle tabelle.
Anche in WordPress, come segnalavo qualche giorno fa, alcune operazioni concettualmente semplici e standard sono più complicate del previsto.
A questo punto, comincio a sospettare seriamente che questa soluzione sia in realtà un anti-pattern bello e buono; forse dovremmo essere un po’ più parsimoniosi nell’uso dei database.
Nelle applicazioni che sfruttano una connessione persistente verso il database e implementano al loro interno la logica di autenticazione e autorizzazione, può presentarsi la necessità di passare al database delle informazioni relative all’utente dell’applicazione, che sarà diverso da quello usato per connettersi al database.
In molti casi, se si tratta solo di inserire le informazioni di auditing su poche tabelle, è sufficiente che sia l’applicazione a passare esplicitamente queste informazioni, durante il salvataggio dei record. Se l’auditing va fatto su tutte le tabelle si può progettare il codice in modo da rendere l’operazione più o meno trasparente.
Non sempre questo è possibile — le modifiche al codice sarebbero troppo onerose o impossibili — né adeguato. Ad esempio, quando vogliamo tracciare eventuali attività svolte al di fuori del controllo dell’applicazione. In questi casi la soluzione dev’essere trovata al livello del database.
Il problema diventa quindi come passare in modo trasparente i dati sull’utente dell’applicazione. Una possibilità consiste nell’usare le cosiddette opzioni custom in combinazione con le trigger.
Al login sull’applicazione verrà chiamata una funzione che imposta il valore dell’opzione custom:
CREATE OR REPLACE FUNCTION begin_sess(staffid text) RETURNS void AS $$ BEGIN PERFORM set_config( 'mysess.curr_user', coalesce(staffid,''), false ); END; $$ LANGUAGE 'plpgsql' VOLATILE
Con questa informazione, basta agganciare una trigger ad ogni tabella che ci interessa:
... DECLARE curr_user staff.staff_id%TYPE; BEGIN SELECT current_setting('mysess.curr_user') INTO curr_user; ...
Se il valore di mysess.curr_user fosse vuoto, ad es. perché l’operazione viene fatta direttamente sul database, basterà ripiegare sull’utente a livello del database.
Le informazioni riportate qui sono state ricavate più o meno pedissequamente da questo thread: Audit Trigger puzzler.
Anche se non amo i NULL, questo “trucco” che ho letto sulla mailing list di PostgreSQL è troppo carino per non citarlo.
Come dice il titolo, vogliamo imporre un vincolo di unicità su più colonne, una delle quali può essere NULL. In PostgreSQL, se abbiamo tre colonne A, B e C — con C che può essere NULL — e creiamo uno UNIQUE INDEX con tutte e tre, posso inserire due record con valori duplicati per A e B.
La soluzione, una volta tanto, è semplice: basta creare un indice parziale:
CREATE INDEX idx ON table_name(A,B) WHERE (C IS NULL);
Qualche giorno fa parlavo di pgtap e accennavo alla possibilità di usarlo per usare il TDD direttamente con il database, sfruttando il fatto che PostgreSQL consente l’uso di transazioni anche per
i comandi di definizione delete database.
Questo è un grosso vantaggio perché posso definire nuovi oggetti dello schema senza che eventuali errori si propaghino al di fuori della sessione corrente.
NB: L’esempio che segue è prolisso e probabilmente simile a innumerevoli altri esempi sulle virtù del “test infected” ma mi sembra comunque degno di nota che si possa usare questo approccio anche all’interno di un database. Bene, cominciamo:
\set ECHO \set QUIET 1 \pset format unaligned \pset tuples_only true \pset pager \set ON_ERROR_ROLLBACK 1 \set ON_ERROR_STOP true \set QUIET 1
Dopo aver impostato alcuni parametri per psql, proviamo a definire un nuovo domain che verrà poi utilizzato da una nuova tabella.
Ho installato pgtap in uno schema a sé stante, in modo che le sue funzioni non si mischino con quelle dello schema “vero”.
Visto che vogliamo sviluppare in modalità TDD, creiamo subito il test per verificare la presenza del domain, che ovviamente fallirà.
BEGIN; SET search_path TO tap, public; SELECT plan(1); SELECT has_domain( 'new_domain', 'verifichiamo la presenza del domain'); SELECT * FROM finish(); ROLLBACK; 1..1 not ok 1 - verifichiamo la presenza del domain # Failed test 1: "verifichiamo la presenza del domain" # Looks like you failed 1 test of 1
Aggiungiamo il domain:
BEGIN; CREATE DOMAIN new_domain int CHECK (VALUE > 0 AND VALUE < 5); SET search_path TO tap, public; SELECT plan(1); SELECT has_domain( 'new_domain', 'verifichiamo la presenza del domain'); SELECT * FROM finish(); ROLLBACK; 1..1 ok 1 - verifichiamo la presenza del domain
Funziona! Adesso tocca alla tabella:
BEGIN; CREATE DOMAIN new_domain int CHECK (VALUE > 0 AND VALUE < 5); SET search_path TO tap, public; SELECT plan(2); SELECT has_domain( 'new_domain', 'verifichiamo la presenza del domain'); SELECT has_table( 'new_table', 'verifichiamo la presenza di new_table'); SELECT * FROM finish(); ROLLBACK; 1..2 ok 1 - verifichiamo la presenza del domain not ok 2 - verifichiamo la presenza di new_table # Failed test 2: "verifichiamo la presenza di new_table" # Looks like you failed 1 test of 2
Solita storia: facciamo in modo di passare anche questo secondo test.
BEGIN; CREATE DOMAIN new_domain int CHECK (VALUE > 0 AND VALUE < 5); CREATE TABLE new_table (col new_domain); SET search_path TO tap, public; SELECT plan(2); SELECT has_domain( 'new_domain', 'verifichiamo la presenza del domain'); SELECT has_table( 'new_table', 'verifichiamo la presenza di new_table'); SELECT * FROM finish(); ROLLBACK;
Aggiungiamo qualche test “perimetrale”:
BEGIN; CREATE DOMAIN new_domain int CHECK (VALUE > 0 AND VALUE < 5); CREATE TABLE new_table (col new_domain); SET search_path TO tap, public; SELECT plan(4); SELECT has_domain( 'new_domain', 'verifichiamo la presenza del domain'); SELECT has_table( 'new_table', 'verifichiamo la presenza di new_table'); SELECT col_type_is('new_table', 'col', 'new_domain'); SELECT col_hasnt_default('new_table', 'col'); SELECT * FROM finish(); ROLLBACK;
Uno degli aspetti interessanti è che possiamo testare l’efficacia della validazione del domain separatamente dal suo uso effettivo in qualche tabella. Nell’esempio che segue creiamo una tabella che ha il solo scopo di testare gli insert:
BEGIN; CREATE DOMAIN new_domain int CHECK (VALUE > 0 AND VALUE < 5); CREATE TEMP TABLE dummy (col new_domain); SET search_path TO tap, public; SELECT plan(3); SELECT throws_ok( 'INSERT INTO dummy (col) values(5)', '23514', 'value for domain new_domain violates check constraint "new_domain_check"', 'Non sono consentiti valori superiori a 4' ); SELECT throws_ok( 'INSERT INTO dummy (col) values(0)', '23514', 'value for domain new_domain violates check constraint "new_domain_check"', 'Non sono consentiti valori inferiori a 0' ); SELECT lives_ok( 'INSERT INTO dummy (col) values(1)', '1 è un valore consentito' ); SELECT * FROM finish(); ROLLBACK; 1..3 ok 1 - Non sono consentiti valori superiori a 4 ok 2 - Non sono consentiti valori inferiori a 0 ok 3 - 1 è un valore consentito
Una volta che siamo soddisfatti delle modifiche basterà estrarre gli statement DML e sottometterli, lasciando alla nostra suite di test.
Fino ad ora ho sempre testato il database attraverso un qualche strumento esterno, come i vari JUnit, TestNG o tcltest.
Sono librerie che funzionano bene e che sono familiari a chiunque pratichi un minimo di TDD ma non particolarmente tagliate per testare i database (con l’eccezione forse di TestNG, che non uso da anni): vuoi perché bisogna prevedere delle fixture elaborate che vanno poi eliminate; vuoi perché, a ben guardare, bisogna attraversare tutta una serie di strati software anche solo per verificare un constraint.
Così ho fatto una ricerca e ho trovato due utilità per PostgreSQL — pgTAP e PGUnit — che permettono di creare delle suite di test eseguite direttamente nel database.
I vantaggi sono diversi: oltre all’accesso più diretto e prestazioni presumibilmente migliori, la gestione delle fixture risulta semplificata perché basta fare il rollback alla fine dei test; inoltre, dato che in PostgreSQL i comandi DDL sono transazionali, diventa possibile testare le modifiche al database istantaneamente, senza doverlo fare in modo permanente.
Per ora ho scelto pgTAP. Mette a disposizione una serie di funzioni che possono venire molto comode per scrivere i test.
Le prime impressioni sono buone e penso che il miglioramento sarà anche più marcato quando potrò disegnare direttamente da zero uno schema in modalità TDD.
Le window functions permettono di usare valori aggregati direttamente sulle singole righe e senza dover utilizzare la clausola GROUP BY.
Prima della versione 8.4 avrei dovuto scrivere una cosa del genere (usando l’esempio in documentazione):
SELECT
depname,
empno,
salary, avg_salary
FROM
empsalary
JOIN (
SELECT
depname,
avg(salary) as avg_salarys
FROM empsalary
GROUP BY depname
) AS avg_salaries using(depname);
Oltre a semplificare le query, possiamo anche andare oltre e, con la funzione rank() “classificare” i record della tabella in base al loro salario o calcolare dei totali progressivi; il tutto espresso in modo molto naturale.
Se penso alle contorsioni da fare in SQL “normale” (o comunque una versione sufficientemente vecchia) per fare cose del genere…