The SmartWeb Project

21 novembre 2005

Impaginare che passione

Grazie al contributo dell'instancabile pedi i castagnaru e del sempre aggiornato melu abbiamo trovato la soluzione più o meno definitiva all'ormai insostenibile problema dell'impaginazione: smettiamola di usare le tabelle!
Esiste un metodo più semplice e di gran lunga più flessibile delle tabelle per sistemare i contenuti delle pagine, sia HTML che JSP: i CSS.
Finora avevamo utilizzato i CSS per dare colore e qualche altro ritocco all'impaginazione che comunque rimaneva sempre basata sulle tabelle: io stesso ho realizzato gli ultimi siti utilizzando la tecnica delle tabelle.
Abbiamo trovato un modo più semplice per impaginare anche molto meglio le pagine, inoltre il codice che esce fuori può essere molto facilmente riadattato: i moduli di smartweb e tutti i lavori che sappiamo dovremo personalizzare di volta in volta è fondamentale che siano, d'ora in avanti, sviluppati con i CSS ed i DIV.

Non voglio proporvi un completo trattato su questa tecnica perchè sarebbe troppo lungo e ripetitivo, ci sono già una marea di siti che discutono ed approfondiscono l'argomento quindi vi rimando a delle ricerche mirate ed a questo articolo suggeritomi dal caro melu: layout a tre colonne con i div ed i css.

17 novembre 2005

Domain

Ogni package dovrebbe avere la classe Domain con il nome più specifico....
se necessito nel package registro del domain di anagrafica devo specificare tutto il nome del package

16 novembre 2005

valorize e populate

Se ho un bean contenente oggetti diversi da tipi nativi e Enumeration ho la necessità
di impostare (sul form o sul bean) manualmente questi valori DOPO aver fatto il valorize o il populate.
Sarebbe bello se il valorize e il populate svolgessero questo lavoro (chiamando opportunamente le operazione di find sul domain) utilizzando a tal fine l'ID.

Esempio:
per fare l'update di Sezione mi ritrovo a dover eseguire le seguenti istruzioni:

1) super.valorize(form, classe, request.getLocale());

2) String idSezione = request.getParameter("id-sezione");
3) classe.setSezione(domain.findSezione(idSezione));

4) domain.updateClasse(classe);


è da considerare che l'oggetto Classe ha soltano un tipo non nativo (Sezione)
ma se gli oggetti non nativi fossero 10 mi ritroverei ad eseguire

2 x 10 Istruzioni + 2 = Almeno 22 Istruzioni nel metodo Update di UNA ACTION !
( e come sappiamo meno istruzioni ci sono in una action meglio è......)

Supporto al lock ottimistico

Quando si lavora in concorrenza sugli stessi dati, cosa alquanto probabile con una applicazione web, è d'obbligo stabilire una politica di blocco dei dati.
Esistono due approcci speculari al lock dei dati:
  • ottimistico, ovvero si presuppone che nella maggior parte dei casi non si verifichino collisioni quindi si consente a tutti di lavorare e si segnala l'errore solo quando si tenta di aggiornare dati resi obsoleti da una modifica antecedente;
  • pessimistico, dove si presuppone una forte incidenza di collisioni in modifica sui dati e quindi si provvede a rendere inaccessibile il dato anche in pre-modifica fino a che il dato non è stato aggiornato.
Il framework fornisce già il supporto al lock ottimistico ereditandolo da Hibernate: su ciascun BusinessObject è stata definita una proprietà version da utilizzare a questo scopo.

Facciamo un esempio in cui due client, A e B, desiderano entrambi modificare le informazioni salvate sul database relativamente ad uno stesso oggetto O con id 10. Ipotizziamo questa sequenza di eventi:
  1. A richiede i dati di O
  2. B richiede i dati di O
  3. A richiede il salvataggio di O
  4. B richiede il salvataggio di O
E' evidente che al punto 4 si verifica una condizione di collisione individuabile dal lock ottimistico altrimenti il salvataggio operato da A sarebbe andato perso inevitabilmente! Ora ripetiamo la stessa sequenza di operazioni considerando il numero di _versione_ dell'oggetto O:
  1. A richiede i dati di O (la versione di O è 1)
  2. B richiede i dati di O (la versione di O è 1)
  3. A richiede il salvataggio di O
    1. La versione di O a disposizione di A viene confrontata con quella presente sul database
    2. I dati vengono salvati e la versione di O incrementata a 2
  4. B richiede il salvataggio di O
    1. La versione di O a disposizione di B viene confrontata con quella presente sul databas
  5. Le versioni non coincidono e viene segnalato un conflitto all'utente B mediante una LockException
Per ottenere il supporto a questa gestione del lock, che ritengo la più appropriata nella maggior parte dei casi, è sufficiente definire un mappaggio apposito nei file .hbm, secondo quanto già riportato nella documentazione di Hibernate. Se si utilizza XDoclet per la generazione dei file .hbm, operazione fortemente consigliata perchè consente di tenere tutte le informazioni in un unico file, allora si dovrà provvedere a ridefinire il metodo version in questo modo:

/**
* @hibernate.version
*/
public void getVersion() { return super.getVersion(); }


La semplice dichiarazione riportata sopra risolve il problema in via definitiva e non introduce rallentamenti in quanto qualunque compilatore Java accettabile è in grado di ottimizzare il metodo (l'operazione si chiama method inlining).

Ricordo che una volta introdotta questa nuova proprietà è necessario inserire nei moduli di modifica dei BusinessObject versionati un nuovo campo nascosto version attraverso il quale trasportare la nuova informazione.

Mi ripeto: questa operazione, cioè inserire il campo nascosto nel form, diventa necessaria altrimenti non riuscirete più ad aggiornare un oggetto dopo averlo creato!

13 novembre 2005

Form, quanti reset...

Se nelle Action vi ritrovate a resettare in continuazione i form allora questo è il messaggio che fa per voi.
Per default Struts salva il contenuto dei form nella sessione, questo per consentire di realizzare facilmente interfacce utente basate sui wizard senza impazzire nel passare di pagina in pagina i valori impostati.
Questo approccio però comporta che ogni volta che entriamo nel wizard è necessario resettare il form per svuotarlo.
Quando il form viene utilizzato soltanto in una sola JSP però non stiamo realizzando un wizard quindi possiamo smetterla di effettuare tutti quei reset e di sprecare prezioso spazio nella sessione!

Anche in questo caso Struts ci viene in aiuto consentendoci di impostare, attraverso il file di configurazione, l'ambito in cui vengono salvati i form passando dalla sessione, che ripeto è l'ambito di default, alla richiesta, che è un ambito sufficiente nel 90% dei nostri casi.
In questo modo possiamo smetterla di eseguire continuamente il reset e risparmiamo un bel pò di memoria!
Sarebbe buona norma quindi specificare sempre e comunque l'attributo scope nei mapping di Struts, così da tenere sotto controllo quelli che sono in richiesta e quelli che sono in sessione.

E speriamo che decidano di impostarli a request di default...

Queste maledette action!

Guardando un pò di Action mi sono accorto che non sempre è chiaro un principio che ritengo fondamentale e che provo a riassumere in una semplice frase.
Se un metodo di una Action supera le cinque (5) linee di codice dobbiamo chiederci almeno due (2) volte se non stiamo sbagliando da qualche parte.

Nella stragrande maggioranza dei casi infatti cinque righe di codice sono più che sufficienti per realizzare quanto è necessario per richiedere una operazione.
Una eccezione a questa regola possono essere i wizard o pezzi di interfaccia complicati da molteplici fattori che si intrecciano in un sol punto.

11 novembre 2005

Usi e costumi del modulo auth

Il modulo auth consente di risolvere in maniera semplice il problema sia dell'autenticazione che dell'autorizzazione, ma soltanto se si adottano delle modalità di utilizzo proficue.
Purtroppo è possibile comprendere quale sia l'utilizzo proficuo soltanto dopo avere effettuato qualche sperimentazione al riguardo quindi, a vantaggio di coloro che seguiranno, riporto quì alcuni consigli maturati dopo molte discussioni sull'argomento e qualche giorno di litigi col suddetto modulo.

Le domande che ci siamo posti (il dinamico pedi i castagnaru, l'affettuoso melu ed io) sono state:
  • Dove devono essere effettuate le verifiche sulle autorizzazioni?
    Di regola questa operazione viene effettuata al livello più basso possibile, eventualmente consentendo ai livelli superiori una verifica preventiva per evitare perdite di tempo. Di conseguenza la risposta sarebbe "sulle BusinessObjectFactory ma con visibilità anche alle JSP".
    Questo è solo parzialmente vero visto che è opportuno anche consentire nuove riaggregazioni delle classi per risolvere problemi che vertono sugli stessi elementi di business. Ad esempio due sistemi necessitano dell'autenticazione e dell'autorizzazione, in uno solamente gli amministratori devono poter inserire utenti nei gruppi, in un altro anche i gestori, ma solo nel proprio gruppo. Questa flessibilità è raggiungibile soltanto se i controlli vengono fatti a livello di Domain;
  • Come effettuare i controlli sulle autorizzazioni?
    La soluzione migliore in questo caso sarebbe un file di configurazione che associa un ruolo specifico ad un metodo del Domain, che è l'obiettivo posto per la prossima versione del framework (aiutatemi che non ce la faccio), per il momento è necessario scrivere una if all'ingresso del metodo che verifica la mancanza di autorizzazioni e lancia una AuthorizationException.

References fetching: eager vs. lazy

Precisiamo una volta per tutte cosa è il fetching dei riferimenti e gli effetti che produce...

Quando un BusinessObject fa riferimento ad un altro ed entrambi sono stati definiti come classi mappate da Hibernate allora abbiamo stabilito una mapping reference e quindi risulta possibile reperire gli oggetti associati attraverso l'apposita proprietà.

Sulla proprietà è possibile definire la modalità di fetching da seguire per reperire il o i riferimenti collegati:
  • se definiamo la proprietà lazy Hibernate non recupera subito i dati dalla tabella associata, ma aspetta che venga fatta una richiesta esplicita, evitando di leggere dati non necessari.
  • se definiamo la proprietà eager, o non lazy, Hibernate recupera i dati dalla tabella associata facendo una outer join ed evitando di eseguire una seconda query.

Per default i riferimenti sono definiti come eager, quindi lazy="false", per evitare le eccezioni di tipo LazyInitializationException.

Visto il comportamento predefinito di Hibernate occorre esplicitare i riferimenti lazy altrimenti rischiate di andare a leggere ogni volta tutta la base dati... (momento di riflessione)

Rimando al sesto capitolo della documentazione di Hibernate per maggiori informazioni.
Siccome non lo cito mai, il tenero pedi i castagnaru ha approfondito il discorso insieme a me...

page vs. list

Se fosse necessario, come spesso accade a dire il vero, definire nuovi metodi che elencano dati filtrandoli con criteri complessi allora è quasi d'obbligo definire due versioni dello stesso metodo: una con prefisso list e l'altra con prefisso page.
Sia list che page devono comunque entrambe restituire una Collection di elementi e prendere in input possibilmente DTO o Serializable.

10 novembre 2005

Find before remove

Ieri sera è successo un fatto strano: ho detto una cavolata!
Discutendo dell'opportunità o meno di avere il metodo setId() privato e di effettuare una findByKey() prima di operare una remove(), con la mia solita presunzione (tipica di tutti i programmatori) ho individuato la cosidetta "soluzione ottimale" che consisteva in setId() protetto e non privato e di effettuare una rimozione in questo modo:

  try {
    MyObject obj = new MyObject();
    obj.setId(id);
    MyFactory.getInstance().remove(obj);
  } catch (DAOException daoe) {
    throw new BusinessException(daoe);
  }


Niente di più sbagliato poteva partorire questa mia saccente testolina! È sempre preferibile fare una find e poi una remove !!!


In questo modo ci ritroveremmo con una lista non indifferente di problemi, tra cui:
  1. mancata rimozione di eventuali oggetti collegati;
  2. impossibilità di rimuovere l'oggetto se erano state definite delle proprietà not-null visto che il caro Hibernate le verifica per scollegare altri oggetti.
Quindi continuate a farvi domande e continuiamo a discuterne, anche quando sfodero la parte peggiore del mio caratere state certi che le vostre critiche e riflessioni ci fanno maturare tutti.

Domain & DTO

Il Domain è una classe che ha un scopo ben definito: rappresenta il Business Façade del framework.

In questo ruolo la classe Domain svolge alcune funzionalità ben definite interponendosi tra l'interfaccia utente (le Action) e lo strato di business (i BusinessObject) e persistenza (le BusinessObjectFactory).

È compito del Domain, all'interno dei suoi metodi:
  • effettuare i cast verso i tipi appropriati prima di restituire gli oggetti al chiamante, ad esempio castare ad User quando invoca UserFactory.findByKey();
  • effettuare i cast o comunque convertire nei tipi appropriati prima di invocare i metodi sulle factory o sugli oggetti di business;
  • convertire, specializzare o dettagliare eventuali eccezioni non gestibili dall'utente;
  • nascondere all'interfaccia utente la complessità, in termini di classi ed interazioni tra di esse, insita nello strato di business;
  • accorpare operazioni multiple che devono essere eseguite in maniera atomica o che devono apparire come una sola operazione.
Tra i tanti vantaggi quello forse meno apparente però è quello che è facile rendere un'applicazione distribuibile su più server semplicemente trasformando la classe Domain in un EJB session stateless, compito che sarà assolto dalla classe EnterpriseDomain.

A questo punto entrano in gioco i DTO, ovvero DataTransferObject, che garantiscono, essendo serializzabili che questa operazione sia davvero realizzabile: se tutto quanto passa attraverso il Domain implementa l'interfaccia DataTransferObject o Serializable, potete star sicuri che non avremo mai problemi di scalabilità.

In caso contrario...

09 novembre 2005

Paginator, questo sconosciuto

Non è un caso che Paginator sia una interfaccia e che erediti sia da Collection che da Iterator.


Esistono nel framework ben tre implementazioni dell'interfaccia Paginator, ciascuna con scopi ed usi differenti:
  • CollectionPaginator serve per paginare su collezioni già create e non deve essere utilizzato per paginare liste di oggetti reperiti poco prima dal database perchè sarebbe uno dei metodi meno efficienti;
  • SQLPaginator serve per paginare in tutti quei casi in cui la query è necessario scriverla in SQL senza appoggiarsi ad Hibernate;
  • BusinessObjectFactory.Paginator serve per paginare nel resto dei casi ovvero quando si vuole paginare su un Criteria (anche su una Query nella prossima release) di Hibernate (speriamo il 99% delle volte).

Usi e costumi del Paginator

Sconsiglio fortemente chiunque dall'utilizzare il Paginator come unico modo per la restituzione di risultati multipli perchè in tutti quei casi in cui è necessario scorrere completamente la lista questo diventa un modo veramente poco efficente.
Vista la minima complessità nel realizzare la doppia versione, paginata e non, di un accesso con risultati multipli è consigliabile realizzarli sempre entrambi.

La paginazione comunque è sempre da considerarsi una operazione lazy nel senso che i dati vengono estratti realmente dal database soltanto alla loro prima richiesta che in genere avviene attraverso l'uso dell'apposito tag.

Se per un motivo a me del tutto sconosciuto avete necessità di utilizzare un paginatore in un punto diverso dall'interfaccia utente allora dovreste chiedervi se non ci sia qualcosa di sbagliato in quel che fate visto che la paginazione è destinata a rendere più leggera la GUI.

Many-to-many cascaded relationship

Giusto per cominciare...
Oggi ho avuto modo di scontrarmi con le relazioni molti-a-molti di Hibernate e nello specifico con la clausula cascade.
Ho potuto sperimentare che nelle relazioni molti-a-molti, diciamo tra User e Group del modulo auth, avevo utilizzato la clausula cascade in maniera errata.
Ritenevo che, volendo eliminare anche la riga dalla tabella di relazione, group_user nello specifico, quando eliminavo un utente dovevo specificare sulla relazione la clusula cascade="delete": niente di più errato!
Specificando cascade="delete" ottengo un effetto quanto mai caustico: eliminando l'utente elimino anche il gruppo!
Aggiungeteci poi che la stessa clausula era specificata sulla relazione inversa (da gruppo ad utente e sempre per la stessa ragione) ho ottenuto che cancellando l'utente sono riuscito a cancellare il gruppo e tutti gli utenti che appartenevano a quel gruppo e tutti gli altri gruppi associati a ciascun utente... e così via a cascata! Con un delete mi sono ritrovato con tutti gli utenti eliminati!


Fate quindi molta attenzione: la clausola cascade ha effetto sugli elementi della relazione, non sulle associazioni che vengono già rimosse per default!


Nel mio caso era sufficiente rimuovere l'utente da tutti i gruppi e poi cancellare l'utente per effettuare l'operazione che desideravo. Vorrei farvi notare che la rimozione dell'utente dai gruppi è necessaria solo perchè la relazione da utente a gruppo è definita come inversa.

08 novembre 2005

Benvenuti

Vorrei tenere traccia in qualche modo di tutte le cose che scopriamo, man mano che utilizziamo il framework, sia sul suo utilizzo che per quanto riguarda migliorie da apportare, idee e suggerimenti.

Postate tutto quello che ritenete interessante insomma, anche magari trucchi o informazioni su Struts o Hibernate, ma anche JBoss o JSP.

Il materiale che raccoglieremo quì andrà a comporre una base di conoscenza per coloro che ci seguiranno e fornirà spunto per le evoluzioni del framework.