Nella lezione precedente #
Se hai seguito correttamente tutti i passi del tutorial fino a questo punto, il modello dovrebbe apparire come in figura:
Dal punto di vista dei requisiti abbiamo un sistema che consente di gestire informazioni su impiegati (dati personali, organizzazione in team) e progetti (nome, stato e committente). Impiegati e progetti sono collegati da alcune relazioni (gli impiegati hanno incarichi che li legano a diversi progetti, un progetto ha almeno un responsabile tecnico).
Dal punto di vista della modellazione abbiamo un modello abbastanza complesso: abbiamo introdotto nuove classi e le abbiamo collegate mediante associazioni e composizioni, caratterizzate da regole di navigabilità e molteplicità differenti; le classi fanno inoltre uso di attributi derivati di tipo math e di personalizzazioni dell’Application Schema, tra cui valori di default e warning.
Abbiamo raggiunto quindi una buona comprensione degli strumenti e delle tecniche di modellazione procedendo a piccole dosi sui requisiti. Tuttavia, il modello attuale è ancora troppo incentrato sulla classe Employee e mancano delle informazioni per completare lo scenario.
Completiamo il modello dati #
In questa lezione introduciamo due potenti strumenti di Livebase: gli attributi query e i filtri a livello applicativo, Il nostro obiettivo è corredare la classe Project di tutte le informazioni necessarie al controllo di gestione. Dovremo tener conto dei costi (tra cui quelli dello staff) e dei ricavi provenienti dai pagamenti dei clienti, informazioni attualmente non presenti nel modello.
Vedremo anche una nuova tipologia di classe: la classe Enum, molto utile per rappresentare un insieme di oggetti con proprietà fisse e note a priori.
Arresta Workforce. Come al solito, prima di aprire il Designer ti consigliamo di archiviare la versione corrente di TutorialEngine.
Gli attributi query #
Quando abbiamo rimosso gli attributi position e team, modellandoli come classi, abbiamo ottenuto l’effetto collaterale di perdere i campi relativi a essi dall’oggetto Employee; questo è accaduto perché ogni oggetto consente di mostrare solo attributi definiti direttamente sulla classe.
È di nostro interesse poter visualizzare nella tabella anche alcuni attributi delle classi associate a Employee. Per realizzare ciò possiamo far uso degli attributi derivati di tipo query.
Abbiamo già visto cos’è un attributo derivato; in particolare abbiamo lavorato con gli attributi math, definiti a partire da espressioni matematiche. Le espressioni possono prendere in input solo valori provenienti dagli attributi presenti nella classe su cui è dichiarato l’attributo derivato. Al contrario, un attributo query dipende, appunto, da una query che estrae il valore di un attributo presente in una classe target, collegata attraverso un cammino di relazioni. Si tratta quindi di un attributo derivato che può dipendere da attributi presenti in classi diverse da quella in cui è definito.
Un attributo query è definito quindi dal percorso che lo lega all’attributo target (analogo di una FROM
). Inoltre, come vediamo tra poco, l’espressione della query può eventualmente includere una condizione di filtraggio (analogo di una clausola WHERE
) e un operatore di aggregazione (analogo di una GROUP BY
o COUNT
).
1. Aggiungi un attributo query semplice #
Creiamo un attributo query che riporti in Employee il nome del team di cui l’impiegato fa parte: apri il Designer, e dal Class menu di Employee seleziona New derived attribute
→ Query
per aprire il Query expression editor.
Sotto Path è presente una lista di tutti gli attributi raggiungibili a partire da Employee navigando i suoi ruoli . Da qui possiamo selezionare l’attributo target della query espandendone il percorso. Ovviamente, da questa posizione non avrebbe senso scegliere un attributo di Employee, pertanto questi ultimi appaiono disabilitati. Ciascun tipo di attributo è raffigurato con un’icona diversa:
- attributi nativi: ;
- attributi math: ;
- attributi query: .
Espandi il ruolo team e seleziona l’attributo name. In basso viene mostrato il percorso della query (team.name
). Per ora ignora le altre opzioni, perciò clicca su OK
per chiudere l’editor, e rinomina l’attributo /team1
in /team_name
.
Analogamente a quanto accade per gli attributi math, passando il mouse su un attributo query viene mostrato un tooltip che riporta l’espressione che lo definisce. Selezionando l’attributo query, l’attributo target corrispondente viene evidenziato in rosso. Viceversa, selezionando un attributo qualsiasi, vengono cerchiati in blu anche tutti gli attributi query definiti a partire da esso.
Questo meccanismo è importante perché ci consente di conoscere il cammino compiuto dalla query per raggiungere l’attributo. Questa informazione non è scontata perché ci possono essere più percorsi che collegano la classe su cui è definita la query e la classe puntata dalla query. La raggiungibilità delle classi non dipende dalla navigabilità dei ruoli; infatti, è sufficiente che esista una relazione tra due classi affinché essa sia percorribile in entrambi i versi.
2. Aggiungi una query su più valori #
/team_name
è un attributo query che attraversa Employee.team, un ruolo con cardinalità Zero or one (01)
(un impiegato fa parte al più di un team); pertanto il suo valore coinciderà col valore di Team.name. Vediamo cosa succede se proviamo a portare in Employee anche l’attributo Qualification.label.
Trascina label da Qualification a Employee: si apre di nuovo il Query expression editor con l’attributo label già selezionato. Stiamo attraversando un ruolo con cardinalità a molti, pertanto la query estrae un insieme di valori (tutte le qualifiche dell’impiegato) e non uno solo; dobbiamo quindi stabilire in che modo aggregare questi valori così da poterli rappresentare in un unico attributo. Osserva il menu a tendina di fianco ad Aggregation function
: sono disponibili diversi operatori generici, più alcuni specifici in base al tipo di dato dell’attributo sorgente. Vediamoli brevemente:
Funzione | Significato |
---|---|
Count all | L’attributo contiene il conteggio dei record trovati. |
Count distinct | Come sopra, ma non include record con valori duplicati. |
Concat (comma separated) | L’attributo è una stringa contenente tutti i valori dei record trovati, separati tra loro da virgole. |
Concat distinct (comma separated) | Come sopra, ma non include record con valori duplicati. |
Max / Min | Restituisce il valore massimo/minimo tra i record trovati. |
Se la sorgente è un attributo numerico sono disponibili ulteriori operatori:
Funzione | Significato |
---|---|
Sum | Restituisce la somma dei valori dei record trovati. |
Average | Restituisce il valore medio tra quelli dei record trovati. |
Standard deviation | Restituisce la deviazione standard tra i valori dei record trovati. |
Per attributi booleani abbiamo inoltre:
Funzione | Significato |
---|---|
Logical AND | Restituisce TRUE se tutti i record sono veri, FALSE altrimenti |
Logical OR | Restituisce TRUE se almeno un record è vero, FALSE altrimenti |
Per Qualification ci interessa mostrare tutte le qualifiche dell’impiegato; perciò scegli Concat distinct (comma separated)
e clicca su OK
per chiudere l’editor.
È comparso un attributo query con un nome decisamente lungo! Questo perché il Designer sceglie il nome di default di una query in questo modo:
- se il ruolo ha cardinalità a uno, il nome di default è nomeRuolo_nomeAttributo;
- se il ruolo ha cardinalità a molti, il nome di default include in testa la funzione di aggregazione usata;
- in entrambi i casi, se l’attributo Source è l’unico attributo di quella classe, nomeAttributo viene omesso.
Rinominiamo /concat_distinct_comma_separated_qualifications
, ricordando sempre che attributi e ruoli condividono lo stesso spazio di nomi per una classe. Pertanto, in questo caso ci conviene ripristinare il nome di default per il ruolo qualifications: apri il suo Role menu e seleziona Reset default name
: il suo nome tornerà a essere _qualification__. Dopodiché, rinomina la query in /qualifications
.
Il tooltip ci permette di distinguere rapidamente le query a uno da quelle a molti. Per queste ultime esso riporta anche la funzione di aggregazione usata (nel nostro caso CONCAT_DS_CS
, cioè Concat distinct – comma separated).
In questo modo abbiamo ripristinato nell’oggetto di Employee la visione delle colonne relative a qualifiche e team, mantenendo allo stesso tempo queste informazioni in classi separate.
3. Aggiungi query con opzioni di filtraggio #
Ora che sappiamo usare le query possiamo sofisticare la relazione che lega Employee e Project_assignment. Un incarico può essere considerato “attivo” in due situazioni:
- quando non ha una data di termine, cioè quando il valore di end_date è nullo;
- quando end_date non è nullo, ma la data corrente è compresa nel periodo tra data di inizio e data di fine dell’incarico.
Rappresentiamo questa logica con un’espressione booleana: crea la math /is_active
dentro Project_assignment digitando l’espressione isNull(end_date) || (__System.date >= start_date && __System.date <= end_date)
.
Fatto questo, definiamo la seguente query su Employee: “un impiegato è attivo se ha almeno un incarico attivo”. Innanzitutto, trascina /is_active da Project_assignment a Employee.
Come puoi vedere, dal momento che ci sono due cammini possibili che collegano Employee e Project_assignment, è apparso il Query expression path selector; passando su uno dei due percorsi, vengono evidenziate in rosso tutte le classi e le relazioni coinvolte. Ciò è molto utile, soprattutto in presenza di cammini multipli e complessi. Vogliamo attraversare la composizione, perciò scegli il primo path Employee.assignments.is_active
.
Scegliamo una funzione di aggregazione: dato che /is_active
è booleano e un impiegato ha più incarichi, possiamo scegliere il Logical OR
; rinomina anche questo attributo su Employee in /is_active
(possiamo farlo perché /is_active di Project_assignment non occupa lo spazio dei nomi di Employee).
Ora definiamo delle query su Team per fornire qualche informazione utile sui suoi membri.
Per prima cosa, possiamo contare il numero di membri di un team risalendo il ruolo members; ma quale attributo di Employee scegliamo per la query? Dato che abbiamo bisogno di un attributo univoco, possiamo sfruttare il Platform attribute __id; abilitalo su Employee, e poi trascinalo su Team. Scegli Count distinct
come funzione di aggregazione (in questi casi anche Count
va bene) e rinomina l’attributo in /num_members
.
Aggiungiamo poi un altro attributo per contare il numero di membri attivi, cioè coloro che hanno almeno un incarico attivo. Per farlo, crea un’altra query trascinando __id da Employee a Team, ma stavolta aggiungi una condizione di filtraggio dal Query expression editor: clicca su Add filter
e digita l’espressione Employee.is_active
. Usa una Count distinct
anche in questo caso, e rinomina l’attributo in /num_active_members
.
La regola che abbiamo appena aggiunto ci ha consentito di rendere più specifica la query: il conteggio degli impiegati include solo gli oggetti che soddisfano la condizione – cioè quelli che hanno il booleano /is_active pari a TRUE
– mentre tutti gli altri vengono filtrati.
Ora proviamo qualcosa di più avanzato: vogliamo mostrare, per ciascun team, il nome dell’impiegato più attivo, cioè quello col maggior numero di incarichi attivi. Procediamo per tre passi:
- contiamo il numero di incarichi attivi per ciascun impiegato: abilita il Platform attribute __id su Project_assignment, trascina quest’ultimo su Employee (scegliendo il percorso
Employee.assignments.__id
), imponi la condizione di filtraggioProject_assignment.is_active
e aggrega conCount distinct
; infine rinomina l’attributo in/num_active_assignments
. Così facendo abbiamo un conteggio simile a quello che abbiamo definito per /num_active_members su Team; - memorizziamo in Team il valore massimo del numero di incarichi attivi: trascina /num_active_assignments da Employee a Team, scegli la funzione di aggregazione
Max
e rinomina l’attributo in/max_active_assignments
. In questo modo, viene copiato in Team il numero di incarichi attivi del membro che ne ha più di tutti; - a questo punto, trascina l’attributo /full_name di Employee su Team, e imponi una condizione con l’espressione
Team.max_active_assignments = Employee.num_active_assignments
; aggrega conConcat distinct (comma separated)
e rinomina l’attributo in/busiest_member
. Grazie al confronto con /max_active_assignments, vengono recuperate le altre informazioni del membro del team che ha quell’esatto numero di incarichi attivi, di cui memorizziamo nome e il cognome; nel caso in cui ci siano più impiegati che condividono il valore massimo, allora /busiest_member conterrà tutti i loro nomi, separati da una virgola.
Cliccando su /is_active di Project_assignment, vengono evidenziate tutte le query che dipendono da questo attributo, incluse quelle basate su derivati di derivati.
Abilita le query nella vista applicativa #
Prima di procedere oltre dobbiamo fare un piccolo appunto. Di default, i Platform attributes come __id sono disabilitati, cioè non compaiono nella vista applicativa (avevamo accennato l’argomento qui); la conseguenza è che anche tutte le query definite a partire da questi attributi saranno inizialmente disabilitate.
Ciò può andarci bene per le query “intermedie”, come /num_active_assignments o /max_active_assignments, dato che non abbiamo interesse a mostrarle in alcuna tabella; tuttavia ci sono query che vorremmo mostrare, come /num_members, /num_active_members e /busiest_member.
Per fare in modo che queste query compaiano nelle tabelle, spostati sull’Application Schema e riabilita solo questi attributi.
I selection filter #
Torniamo a ragionare sulle associazioni tra classi e consideriamo un aspetto che non avevamo trattato nella precedente lezione: per quanto sappiamo, è sufficiente che esista un’associazione tra due classi affinché gli oggetti di una siano associabili a quelli dell’altra. L’associabilità dipende dalla struttura della relazione, in termini di cardinalità e navigabilità dei ruoli; quindi, nelle relazioni in cui è possibile, due oggetti qualunque possono essere associati tra loro.
Prendiamo ora in considerazione tre scenari che il modello attualmente ci consente di rappresentare, ma che avrebbero poco senso nella realtà:
- un impiegato può indicare sé stesso come supervisore (gli basta selezionare il suo nome);
- un progetto può avere come direttore un impiegato qualsiasi, anche uno che non ha alcun incarico su di esso;
- un impiegato può avere più incarichi per lo stesso progetto che si sovrappongono temporalmente;
È chiaro che al momento gli strumenti che padroneggiamo non ci consentono di imporre vincoli per evitare queste situazioni. In questi scenari vogliamo infatti che l’associabilità tra impiegati, progetti e incarichi dipenda dal valore che essi assumono, e non solo dalla cardinalità e navigabilità delle relazioni le loro classi. Possiamo raggiungere questo obiettivo grazie ai Selection Filter.
Il Designer ci consente di definire, in modo analogo alla filter condition della query, una o più condizioni sui ruoli di un’associazione; tali condizioni hanno lo scopo di restringere l’insieme di oggetti della classe target che possono essere associati attraverso quel ruolo.
1. Aggiungi un filtro semplice #
Dal momento che il filtraggio di record rappresenta un partizionamento orizzontale dei dati, possiamo definire i Selection Filter solamente a livello applicativo; perciò, spostiamoci sull’Application Schema .
Cominciamo aggiungendo la prima regola: apri il Role menu di Employee.supervisor e seleziona Set selection filters...
per aprire il Role filters manager.
Clicca su Add
e digita nell’editor l’espressione Employee.__id != Employee.supervisor.__id
. Prima di confermare, puoi dare un nome simbolico alla regola: ad esempio, chiamala excludeSelf
e clicca su OK
per chiudere l’editor.
In questo modo abbiamo definito un filtro sul campo degli associabili per il ruolo supervisor nell’oggetto di Employee. Da questa lista ora non compare infatti il record relativo dell’impiegato aperto; quando gli __id coincidono, infatti, la condizione viene valutata FALSE
e quell’oggetto non sarà associabile.
Chiudi il Role filters manager e osserva il diagramma: su supervisor è comparso un pallino nero, a indicare la presenza di un selection filter per quel ruolo.
Come puoi notare, l’editor consente di definire condizioni multiple per un singolo ruolo. In questi casi il filtro valuterà sempre l’AND logico tra tutte le espressioni.
2. Combina filtri e attributi query #
Ora che abbiamo creato un filtro semplice, possiamo ragionare sugli altri due problemi:
Project → Employee #
Al momento un project può indicare un impiegato qualunque per il ruolo director; vogliamo invece che sia possibile sceglierlo solamente tra gli impiegati che hanno un incarico attivo per quel progetto.
Spostiamoci sul Database Schema . Per mostrare il nome del capo-progetto direttamente dalla tabella Project, creiamo una query a /full_name di Employee su questa classe: anche qui, rinomina il ruolo in director_
per rendere disponibile l’identificatore; dopodiché trascina /full_name passando per esso (Project.director_.full_name)
; questo percorso produce una query a un valore singolo, che chiamiamo /director
.
Un progetto deve conoscere quali sono gli impiegati che ci stanno lavorando: crea una query su Project trascinando su questa classe l’__id di Employee, stavolta scegliendo il percorso che passa per assignments (Project.assignments.employee.__id
); aggiungi la condizione Project_assignment.is_active
, aggrega con una Concat distinct (comma separated)
e rinomina l’attributo in /active_collaborators
. In questo modo memorizziamo, per ciascun progetto, una stringa contenente tutti gli __id degli impiegati che hanno un incarico attivo per quel progetto. Se uno di questi incarichi termina, l’__id dell’impiegato a esso relativo scompare dalla lista dei collaboratori attivi
Dall’Application Schema , aggiungi un filtro sul ruolo Project.director_ chiamandolo onlyActiveCollaborators
, e digitando l’espressione containsCS(Project.active_collaborators, Employee.__id)
. La funzione containsCS confronta due stringhe e verifica che la prima (una lista di valori separati da virgole) contenga la seconda.
In questo modo, quando da un oggetto di tipo project richiediamo la lista degli associabili per director, il risultato mostra un sottoinsieme contenente i soli impiegati il cui __id compare nella lista dei collaboratori attivi per quel progetto. Tutti gli altri oggetti sono stati infatti filtrati perché non soddisfano il selection filter.
È importante ricordarsi che i progetti inizialmente non hanno collaboratori attivi; pertanto, quando creiamo un nuovo progetto, la lista degli associabili di director è sempre vuota, e siamo dunque inizialmente costretti a creare un progetto senza direttore (completando l’associazione in un secondo momento). Tuttavia, sul ruolo director abbiamo imposto un vincolo di cardinalità Exactly one (1)
che, se non soddisfatto, non consente di confermare la creazione di alcun progetto! Per sbloccare la situazione dobbiamo necessariamente rilassare il vincolo: dal Database Schema modifica la cardinalità di _director__ in Zero or one
(01
).
Il filtro risultante appare come in figura:
Project_assignment → Project #
Al momento, un nuovo incarico di un impiegato può essere associato a un progetto qualsiasi, anche se esiste già un incarico associato a quel progetto nello stesso intervallo temporale (è un incarico attivo). Vogliamo quindi impedire a un impiegato di avere più incarichi contemporaneamente per lo stesso progetto, pur consentendogli di averne molteplici nel tempo.
Possiamo sfruttare la query /active_collaborators appena modellata per definire un filtro sul ruolo Project_assignment.project. Per prima cosa, dal Database Schema trascina l’__id di Employee su Project_assignment (passando per assignments) e rinomina la query in /employee_id
. Così facendo otteniamo visibilità dell’__id dell’oggetto whole direttamente dentro i suoi oggetti part. Poi, dall’Application Schema , aggiungi un filtro sul ruolo project chiamato onlyOneActiveAssignmentPerProject
, usando l’espressione !containsCS(Project.active_collaborators, Project_assignment.employee_id)
.
Il nostro obiettivo qui è permettere a un employee di associare un nuovo assignment ai progetti a cui non ha ancora lavorato, o a cui ha lavorato in precedenza, e nascondere invece i progetti per i quali è già un collaboratore attivo. Sappiamo che un filtro mostra sempre i record che soddisfano la sua condizione; in questo caso ci interessa però mostrare quelli che non la soddisfano, pertanto possiamo anteporre il prefisso !
(not) alla condizione in modo da negare il risultato calcolato da containsCS
e invertire il significato logico del filtro.
In questo modo, quando dall’oggetto di un nuovo assignment richiediamo la lista degli associabili del ruolo project, la vista confronta l’__id dell’employee “padre” dell’assignment che stiamo associando con la lista dei collaboratori attivi di ciascun progetto, e mostra tutti i progetti in cui tale __id non compare.
Completa il modello dei dati #
Ora che sappiamo usare query e filtri, possiamo procedere nel modellare i requisiti rimanenti per la nostra applicazione.
Prima di aggiungere ulteriori elementi, potresti voler mettere ordine nel diagramma. Ecco alcune azioni che puoi compiere:
- spostare una classe trascinandola per il suo header;
- selezionare più classi: tenendo premuto Ctrl e cliccando sui loro header, oppure trascinando un rettangolo di selezione su di esse. Puoi spostare il gruppo cliccando sull’intestazione di uno qualsiasi degli elementi;
- selezionare tutte le classi del diagramma premendo Ctrl+A;
- allineare automaticamente un gruppo di classi selezionate, in verticale oppure in orizzontale aprendo il Class menu di una di esse e cliccando su
Align classes
; - aggiungere una piega su una relazione cliccando su un punto qualunque lungo essa, e successivamente trascinare i ruoli e le pieghe per darle la forma desiderata;
- rimuovere una piega facendo click destro su di essa e selezionando
Remove bend
, oppure puoi rimuoverle tutte selezionandoRemove all bends
dal suo Relation menu.
1. Modella la gestione delle attività #
Dobbiamo implementare la gestione dei costi dello staff, cioè gli impiegati che collaborano a un progetto. Sappiamo finora che ciascun impiegato ha un costo orario (rappresentato da hourly_cost) ed è associato a un progetto tramite un incarico, che individua una finestra temporale tra due date.
Una prima strategia potrebbe consistere nel creare una query aggregata su Project a partire da Employee basandosi in qualche modo sulla durata dell’incarico. Tuttavia, dai requisiti, sappiamo di dover associare agli impiegati le attività che essi compiono relativamente ai progetti per cui hanno un incarico. Pertanto, dobbiamo suddividere un incarico (un periodo di più giorni) in entità più piccole che tengano traccia delle attività svolte nell’ambito di un singolo giorno di lavoro:
- Spostati nel Database Schema e crea la classe
Activity
come in figura: aggiungi una data, due interi per indicare ore e minuti e un text per annotare la descrizione dell’attività. date è l’object title ed è required, insieme a ore e minuti.
- Supponiamo esistano diverse tipologie di attività, come ad esempio assistenza tecnica, analisi, vendita o riunione, e che queste siano tutte note a priori. Possiamo modellare questo concetto utilizzando una classe Enum, la quale ammette come sue istanze un numero finito di oggetti con proprietà note e immutabili, identificati attraverso letterali (literal objects). Fai clic sul pulsanteper creare una nuova classe Enum e rinominala in
Activity_type
, quindi cliccaci sopra col destro e selezionaNew literal
per aggiungere un primo letterale da rinominare inTECHNICAL_ASSISTANCE
. Ripeti l’operazione per aggiungereANALYSIS
,SALES
eMEETING
, fino ad avere una classe come quella indicata qui a fianco. Noterai che le classi Enum vengono sempre create con un attributo di default chiamatoname
, valorizzato con uno dei letterali definiti per ciascuna delle sue istanze.
- Aggiungi poi un’associazione unidirezionale da Activity ad Activity_type, rinomina i ruoli in
activities
etype_
e imposta la cardinalità di quest’ultimo aExactly one (1)
. Infine, trascina name su Activity e rinomina la query in/type
.
- Associamo Activity a Project_assignment: un’attività fa riferimento a un incarico preesistente, mentre un incarico consiste di diverse attività svolte in più giorni; pertanto, aggiungi un’associazione unidirezionale da Activity a Project_assignment, rinomina i ruoli in
activities
eassignment
e imposta la cardinalità di quest’ultimo aExactly one (1)
.
Ora che Employee è collegata ad Activity, dobbiamo calcolare il costo di ciascuna attività in base al costo orario e al tempo totale trascorso dall’impiegato su di essa:
- Prima di tutto, aggreghiamo ore e minuti: crea una math su Activity digitando l’espressione
hours + (minutes/60)
e chiamala/actual_hours
. - A questo punto, trascina hourly_cost di Employee su Activity, scegli il path
Activity.assignment._employee.hourly_cost
e rinomina l’attributo in/employee_hourly_cost
: avremo una query a un valore singolo, perché ciascuna activity fa capo a un singolo project_assignment, che a sua volta fa capo a un singolo employee. - Adesso puoi usare l’espressione
actual_hours * employee_hourly_cost
per definire, su Activity, una math chiamata/cost
che calcola il costo della singola attività.
Dal momento che ciascun incarico è riferito a un solo progetto, anche le sue attività lo sono. Perciò possiamo calcolare il costo complessivo delle risorse umane per un progetto sommando i costi di tutte le attività a esso associate. Trascina /cost da Activity a Project, scegli il path Project.assignments.activities.costs
, aggrega con Sum
e rinomina l’attributo in /total_staff_cost
. Come puoi notare, questa query a molti è una somma a due livelli, che coinvolge tutti gli incarichi relativi a un progetto e tutte le attività relative a ciascuno di questi incarichi, da cui estrae i singoli costi.
Abbiamo soddisfatto il primo “obiettivo” per Project, cioè quello di avere un indicatore aggregato per il costo dello staff. Puoi vedere il risultato in figura:
Prima di procedere oltre, aggiungiamo alcune query per arricchire la classe Project_assignment:
- trascina Employee.full_name su Project_assignment passando per _employee__ e rinomina la query in
/employee
; - trascina
Project.name
su Project_assignment passando per _project__ e rinomina la query in/project
.
In questo modo, quando associamo delle attività, abbiamo una chiara rappresentazione degli incarichi, in quanto vengono mostrati il nome dell’impiegato e il progetto a cui gli incarichi si riferiscono; quest’ultimo dettaglio è utile anche quando esaminiamo gli incarichi dalla form di un impiegato.
2. Modella la gestione della contabilità in entrata #
Prendiamo in considerazione la contabilità dei progetti, iniziando dalle entrate. Ci aspettiamo di emettere una fattura per il committente di un progetto (Customer); a sua volta il committente provvede a saldare il conto, suddividendolo eventualmente in più pagamenti.
Modelliamo una classe che rappresenti le nostre fatture.
- Spostati sul diagramma Accounting, crea la classe
Invoice
con gli attributi in figura: ogni fattura ha un numero, una data e una somma da pagare (espressa con un numero reale). Restringi il dominio di amount ai soli reali positivi scegliendo un intervallo compreso tra0
eunlimited
, imposta a required tutti e tre gli attributi, e crea un object title composto che concateni number e date usando l’espressioneconcat(number," [",date,"]")
. Infine, imponi un vincolo di unicità sulla coppia number-date.
- Associamo le fatture ai progetti: aggiungi un’associazione unidirezionale da Invoice a Project, rinomina il primo ruolo in
invoices
e imposta la cardinalità del secondo aExactly one (1)
.
- Creiamo due query per mostrare, all’interno della fattura, il nome del progetto e il nome dell’eventuale cliente: trascina entrambi i name rispettivamente da Project e Customer, e rinominali in
/project
e/customer
.
- Imponiamo una semplice regola sul ruolo _Invoice.project__ così da permette di associare una fattura a un progetto solo se quest’ultimo non è ancora concluso. Spostati nell’Application Schema e crea il filtro
activeProjectsOnly
con l’espressione!Project.completed
.
Come abbiamo detto, vogliamo tenere traccia del pagamento delle fatture, che può avvenire a più riprese per un singolo “conto”.
- Crea la classe
Payment
come in figura: puoi copiare e incollare gli attributi date e amount da Invoice selezionandoli (tenendo premuto Ctrl), scegliendo la voceCopy
dell’Attribute menu di uno dei due e cliccando suPaste
dal Class menu di Payment. Spunta date come object title, e rinomina amount in payment_amount.
- Aggiungi un’associazione unidirezionale da Payment a Invoice, rinomina il primo ruolo in
payments
e imposta la cardinalità del secondo aExactly one (1)
. Dopodiché, trascina /title di Invoice su Payment e rinominalo in/invoice
.
Possiamo registrare più pagamenti per una fattura fino a estinguere la somma da pagare; pertanto, quando registriamo un pagamento per una fattura, dobbiamo tener conto dell’ammontare già pagato e di quello rimanente, e impedire all’utente di inserire un importo che ecceda l’ammontare rimanente. Procediamo come segue:
- sommiamo l’ammontare dei pagamenti ricevuti: trascina payment_amount su Invoice, aggrega con
Sum
e rinomina la query in/amount_received
; - contiamo l’ammontare rimanente: crea un attributo math su Invoice con l’espressione
amount - amount_received
e rinominalo in/amount_remaining
; - trascina /amount_remaining da Invoice a Payment e chiamalo
/invoice_amount_remaining
.
Spostati sull’Application Schema e definisci un Class warning su Payment chiamato amountExceeded
con l’espressione invoice_amount_remaining < 0
, che viene calcolata durante una SaveNew
e una SaveExisting
; digita il messaggio “The declared amount exceeds the remaining amount to pay.
” e rendi il warning bloccante.
Impediamo di associare nuovi pagamenti a fatture che sono state estinte, ovvero il cui ammontare rimanente è zero:
- sul Database Schema , crea un attributo math su Invoice con l’espressione
amount_remaining = 0
e rinominalo in/is_fully_paid
; - dall’ Application Schema , crea un filtro sul ruolo Payment.invoice chiamato
notPaidOnly
con la condizione negata!Invoice.is_fully_paid
.
Le due classi risultanti ora appaiono come in figura.
Finalmente possiamo calcolare le entrate per ciascun progetto, per il quale vogliamo conoscere sia il totale delle fatture emesse sia lo stato effettivo del pagamento, sotto forma di percentuale:
- trascina amount da Invoice a Project, aggrega con
Sum
e chiamalo/total_invoiced
; - trascina payment_amount da Payment a Project (passando ovviamente per Invoice), aggrega di nuovo con
Sum
e chiamalo/total_payments
. Dopodiché, crea un attributo math su Project digitando l’espressione(total_payments * 100) / total_invoiced
, e chiamalo/payment_percentage
.
Abbiamo soddisfatto il secondo obiettivo per Project, e ora disponiamo un resoconto anche del fatturato in entrata. Puoi vedere il risultato in figura:
3. Modella la gestione della contabilità in uscita #
L’ultimo fattore da considerare riguarda tutte le spese esterne che possiamo associare direttamente a un progetto, come quelle relative all’acquisto di materiali e risorse, al noleggio di strumenti, oppure ancora all’assunzione di stagisti e consulenti.
Immaginiamo quindi di voler registrare un certo numero di fatture passive – relative a un generico “fornitore” – e di associarle direttamente al progetto per cui sono state emesse. In questo modo potremo disporre, per ciascun progetto, anche di un dato relativo al totale delle spese vive.
Osserva Invoice: la classe rappresenta in realtà una fattura in uscita (o attiva): è infatti relativa a un cliente e ha dei pagamenti associati; non possiamo riutilizzarla nel nuovo contesto e dobbiamo invece introdurre una nuova classe. Consideriamo però una cosa: una generica fattura è pur sempre caratterizzata almeno da un numero, una data e un importo. Questi attributi sono già presenti su Invoice, perciò può essere una buona idea riutilizzare questi elementi.
Sfruttiamo quindi le composizioni a uno e definiamo una struttura comune a cui poi associamo due classi che rappresentano le due tipologie di fattura:
- rinomina l’attuale classe Invoice: fai doppio click sul suo header e digita
Outgoing_invoice
; crea poi una nuova classeInvoice
, che sarà la nostra struttura comune; aggiungi una composizione da Outgoing_invoice a Invoice, rinomina il ruolo part indetails
e imposta la sua cardinalità aExactly one (1)
; - sposta gli attributi number, date e amount sulla nuova classe in modo tale che compaiano nella form annidata “details”, sia per Outgoing_invoice che per la nuova classe che aggiungeremo tra poco: tenendo premuto Ctrl, seleziona i tre attributi, trascinali su Invoice e clicca su
Move here
.
Come puoi notare dal warning, questo spostamento non è gratuito, dato che gli attributi coinvolti sono sorgenti di alcune math e query. Niente paura, possiamo risolvere questi problemi con facilità!
Clicca su OK
per confermare lo spostamento; procediamo poi come segue:
- Per prima cosa abbiamo bisogno di un nuovo object title per le due classi: elimina /title da Outgoing_invoice e crea un nuovo object title composto su Invoice, concatenando number e date come prima, con l’espressione
concat(number," [",date,"]")
. Trascina poi /title da Invoice a Outgoing_invoice, selezionaLink here
, rinomina la query in/title
e spuntala come object title. - Aggiungiamo informazioni a Invoice, memorizzando anche una descrizione della fattura: aggiungi un nuovo attributo
description
: text
. - La math /amount_remaining richiede che su Outgoing_invoice vi sia un di un attributo di nome amount: trascina amount da Invoice a Outgoing_invoice, anche qui seleziona
Link here
e rinomina la query in/amount
. Allo stesso modo, rendiamo di nuovo visibili i dettagli della fattura aggiungendo le query per/number
e/date
e per il nuovo attributo/description
. - A questo punto dovresti non vedere alcun warning () sulle tre classi.
Ora possiamo creare una nuova classe Incoming_invoice
, collegando anch’essa a Invoice con una composizione a uno come abbiamo fatto sopra. Anche qui, rinomina il ruolo della classe part in details
e trascina gli attributi di Invoice dentro Incoming_invoice, spuntando la query /title come object title.
Aggiungiamo una classe per rappresentare un generico fornitore a cui associare questo tipo di fatture. Crea una classe Supplier
con un solo attributo name (che è anche l’object title) che indica la ragione sociale.
Collega direttamente Incoming_invoice a Supplier con un’associazione unidirezionale, rinomina il primo ruolo in invoices
e imposta la cardinalità del secondo a Exactly one (1)
. Poi, trascina name su Incoming_invoice e rinominalo in /supplier
.
Colleghiamo Incoming_invoice con Project con un’associazione a uno, allo stesso modo di Outgoing_invoice. A tal proposito, rinomina il ruolo di Outgoing_invoice in outgoing_invoices
, e chiama incoming_invoices
quello di Incoming_invoice. Anche qui, trascina name da Project a Incoming_invoice e rinominalo in /project
.
Definiamo un filtro activeProjectsOnly
sul ruolo Incoming_invoice.project, analogo a quello già presente su Outgoing_invoice.project.
Infine, colleghiamo Supplier a Project con un’associazione bidirezionale molti-a-molti, chiamando i rispettivi ruoli suppliers
e projects
.
Supponendo di poter associare più fornitori a più progetti, dobbiamo fare in modo di associare una fattura in entrata solamente ai fornitori registrati per quel progetto. A tale scopo introduciamo un nuovo strumento: il Selection path editor. Dall’Application Schema, apri il Role menu di Incoming_invoice.supplier e seleziona Set selection path...
; apparirà un menu simile al Query expression path selector visto in precedenza. Da qui, seleziona la prima voce Incoming_invoice.project.suppliers.Supplier
per chiudere il menu e far comparire anche qui un pallino sul ruolo.
Ciò che abbiamo appena fatto è stato restringere l’insieme dei fornitori associabili a una fattura in entrata ai soli fornitori associati al progetto a cui tale fattura è associata. In pratica, quando inseriamo una nuova incoming_invoice, inizialmente la lista degli associabili per supplier è vuota, e si popola solo dopo che abbiamo associato un progetto nel ruolo project, permettendoci di scegliere a quale fornitore fare riferimento.
Un Selection path è quindi una modo rapido per dichiarare un filtro su un ruolo in base a un percorso di relazioni; in questo caso abbiamo potuto utilizzarlo perché non dovevamo basare il filtro su condizioni particolari, a differenza di quanto abbiamo fatto per i filtri combinati definiti in precedenza.
Ora possiamo calcolare le spese di ciascun progetto: trascina amount da Invoice a Project, facendo attenzione a scegliere il path Project.incoming_invoices.details.amount
in modo da considerare solamente l’ammontare delle fatture passive. Aggrega con Sum
e rinomina la query in /total_expenses
. A tal proposito, modifichiamo l’attributo puntato da /total_invoiced in modo che anch’esso faccia riferimento a Invoice.amount: seleziona Edit expression...
dal suo Attribute menu e modifica il path in outgoing_invoice.details.amount
.
I percorsi delle due query appaiono come in figura:
4. Calcola il bilancio totale #
Abbiamo soddisfatto il terzo e ultimo “obiettivo” per Project, e possiamo finalmente calcolare il bilancio totale: crea un attributo math su Project con l’espressione total_invoiced - total_expenses - total_staff_cost
e chiamalo /total_balance
.
Ora il nostro diagramma Accounting è completo e appare come in figura:
Per avere una visione d’insieme su una classe centrale come Project possiamo creare un diagramma con tutte le classi del modello: per farlo rapidamente, fai click destro su uno dei due diagrammi e seleziona Clone
; dopodiché, mostra tutte le classi nascoste, trascinandole dal tab Classes oppure dalle relazioni nascoste. Rinomina il nuovo diagramma in Administration
.
Il nostro modello dei dati è completo, ed è rappresentato nei tre seguenti diagrammi:
Aggiorna e controlla l’applicazione #
Come di consueto, controlliamo che tutto funzioni correttamente nell’applicazione. Salva il modello e chiudi il Designer, dopodiché avvia la Cloudlet Workforce lasciando che Livebase risolva tutte le issue di allineamento del database (tutte di tipo low severity).
Accedi al client GraphiQL sulla vista Application e naviga la documentazione dello schema: sono comparse ulteriori servizi per le nuove classi, fatta eccezione per la classe Enum Activity_type
le cui istanze sono definite in fase di modellazione e non possono essere visionate o modificate direttamente. Possiamo però associare una di queste istanze in fase di creazione di una nuova Activity, come vedremo più avanti.
Tutti gli attributi query che abbiamo definito e abilitato per questa vista applicativa compaiono inoltre nello schema generato. Puoi verificare questo, ad esempio, per le strutture dati di Projects, Teams e Active Invoices.
Verifichiamo il corretto funzionamento dei selection filter e delle query che abbiamo definito, nonché della classe Enum:
- usa il servizio
Project___previewCreate
richiedendo il campodirector____associables
e verifica che non ci siano impiegati associabili come director; - crea un incarico attivo per un impiegato e assegnalo a un progetto; verifica che l’impiegato creato abbia l’attributo derivato Is active valorizzato a vero;
- usa il servizio
Activity___previewCreate
per verificare i possibili valori associabili per l’attributo activity_type, e la risposta ritornata rifletta i letterali che abbiamo definito nel Designer; - imposta un progetto come completed, poi crea una fattura di qualsiasi tipo; verifica che quel progetto non sia associabile;
- crea una fattura attiva, poi crea diversi pagamenti da associare a essa; verifica che amount remaining indichi correttamente la somma rimanente da pagare.
Conclusioni #
Con questa lezione abbiamo esaurito gli argomenti di modellazione relativi al Database Schema. Inoltre, abbiamo tradotto in codice gran parte dei requisiti descritti nello scenario, preparando il terreno per lo step finale del corso: il partizionamento dell’accesso ai dati.
Clicca sul bottone per scaricare il modello che abbiamo realizzato in questa lezione:
TutorialEngine_queries_filters.xml
Nella prossima lezione… #
Definiremo Application Schema e Profile Schema multipli per partizionare il modello in base ai diversi profili utente descritti nei requisiti: Definisci viste applicative e profili utente.