“Job Design Pattern e best practice Talend” – Parte 4

“Job Design Pattern e best practice Talend” – Parte 4

  • Dale Anderson
    Dale Anderson is a Customer Success Architect at Talend. Over a 30 year career, Mr. Anderson has gained extensive experience in a range of disciplines including systems architecture, software development, quality assurance, and product management and honed his skills in database design, modeling, and implementation, as well as data warehousing and business intelligence. A keen advocate for an appropriate temperance of the software workflow process, he has applied his innovations to the often overlooked lifecycle of a database. The Database Development Lifecycle Mr. Anderson has developed incorporates data modeling best practices to address the complexities involved in modeling adaptations, multi-environment fulfilments, fresh installations, schema upgrades, and data migrations for any database implementation.

 

Il nostro viaggio di scoperta dei Job Design Pattern e delle best practice Talend si sta facendo sempre più appassionante. Il mio umile intento di fornire contenuti utili si è sviluppato ben oltre le mie aspettative. Il costante successo riscosso dai precedenti blog di questa serie (leggete la Parte 1, Parte 2 e Parte 3, se ancora non lo avete fatto), così come dalle presentazioni in occasione dei Technical Boot Camp (grazie a tutti coloro che hanno partecipato) e dal materiale presentato direttamente ai clienti, ha indotto il team Talend a richiedere una trasformazione. Abbiamo in programma di creare diversi webinar incentrati su questa serie di blog e di renderli disponibili nell'immediato futuro. Portate, tuttavia, un po' di pazienza, in quanto sarà necessario del tempo, oltre che la coordinazione di diverse risorse, ma mi auguro di vedere i primi di questi webinar già disponibili all'inizio del 2017. Sono entusiasta di questa iniziativa e ovviamente mi auguro che continuate a dimostrare lo stesso interesse.

Come promesso, però, ora è giunto il momento di addentrarci in alcune best practice aggiuntive per l'elaborazione di Job Design Pattern con Talend. Per prima cosa, lasciatemi ricordare un semplice dato di fatto che, purtroppo, spesso viene ignorato. Talend è un generatore di codice java, di conseguenza, la creazione di linee guida per gli sviluppatori non può che rafforzare e semplificare il codice java generato tramite i Job Design Pattern. Sembrerà ovvio, e forse lo è, ma la creazione di job ben progettati, in grado di generare codice java pulito, applicando questi concetti è il modo migliore per ottenere ottimi risultati. Personalmente, definisco questi job "progetti basati sul successo".

Progetti Talend basati sul successo

Creare job Talend può essere molto semplice; ma anche altrettanto complesso. Il segreto di un'implementazione di successo consiste nell'adottare e adattare le buone abitudini e la disciplina necessari.

Sin dai "Precetti fondamentali" discussi all'inizio di questa serie di blog fino a ora, il mio obiettivo è sempre stato quello di promuovere una discussione aperta su queste best practice, allo scopo di elaborare utili ed efficaci Job Design Pattern da utilizzare con Talend. La maggior parte dei casi d'uso trarrà beneficio dalla progettazione di moduli job di piccole dimensioni e da un'orchestrazione padre/figlio e, se i progetti contengono una quantità significativa di codice riutilizzabile, la buona riuscita del progetto potrà essere accelerata. Scegliete, ovviamente, un vostro percorso, ma vi chiedo almeno una cosa: siate coerenti!

Ciclo vitale di sviluppo del database – DDLC

Attenzione! Forse non è solo una questione di progettazione del job; cosa mi dite dei dati? Elaboriamo dei dati, giusto? La maggior parte dei dati risiede in un database. Vi chiedo quindi, sono necessarie delle best practice anche per i database? La domanda è ovviamente retorica. I data model (schemi) cambiano nel tempo, di conseguenza, anche i progetti dei database seguono necessariamente un ciclo vitale. Ha perfettamente senso.

I database si evolvono e noi sviluppatori dobbiamo fare i conti con questo dato di fatto. Abbiamo adottato processi SDLC, quindi non dovrebbe essere così difficile accettare l'esistenza anche di un ciclo vitale di sviluppo del database. Per me, in realtà, è abbastanza semplice. In qualsiasi ambiente (DEV/TEST/PROD), un database deve supportare:

  • Un'INSTALLAZIONE inizialebasata sulla versione corrente dello schema
  • L'applicazione di un UPGRADErilascio/creazione/alterazione di oggetti dB per l'aggiornamento di una versione alla successiva
  • La MIGRAZIONE dei datiquando un upgrade ha conseguenze "devastanti" (come la separazione di tabelle)

Comprendere il ciclo vitale del database e il suo impatto sulla progettazione dei job diventa essenziale. Eseguire un controllo delle versioni del modello di database è di importanza critica. Seguite una procedura di progettazione predefinita. È possibile usare diagrammi grafici per illustrare i progetti. Potete creare un "dizionario dei dati" o un "glossario" e tracciare la provenienza delle variazioni storiche. Presto scriverò un blog separato anche su questo argomento. State all'erta. Nel frattempo, considerate i seguenti processi durante la creazione di modelli di database. Si tratta di un lavoro che richiede disciplina, ma che dà i suoi frutti!

ddlc_dbdesignflow

 

Altre best practice per la progettazione di job

Ok. Eccovi altri Job Design Pattern e best practice per il vostro immediato uso e consumo. Con questa nuove best practice inizieremo ad addentrarci in funzionalità Talend che forse non conoscete ancora o che usate poco frequentemente. Mi auguro che vi saranno utili.

8 ulteriori best practice:

Ricerche tMap

Come molti di voi già sapranno, il componente essenziale tMap è ampiamente utilizzato nei job Talend. Ciò è dovuto alle sue straordinarie capacità di trasformazione.


L'impiego più comune del componente tMap consiste nella mappatura di uno schema di flusso di dati da una sorgente di input a un output di destinazione. Semplice, no? Certo che sì! Sappiamo anche che possiamo incorporare più flussi di dati di schemi sorgente e di destinazione, che offrono complesse opportunità per unire e/o suddividere tali flussi di dati in base alle necessità, magari incorporando espressioni di trasformazione che controllano che cosa viene disperso a valle e come i dati in arrivo vengono dispersi a valle. Le espressioni contenute in un componente tMap possono sussistere negli schemi sorgente o di destinazione. Esse possono inoltre essere applicate utilizzando variabili definite all'interno del componente tMap. Per maggiori informazioni su questo argomento, vi rimando alla Guida di riferimento ai componenti Talend. Ricordate: maggiori poteri comportano anche maggiori responsabilità!

tmaplookup1Un altro interessante impiego del componente tMap consiste nell'incorporazione di ricerche che si uniscono al flusso di dati sorgente. Se non esiste un limite fisico al numero di ricerche che è possibile applicare a un componente tMap, o riguardo a cosa è compreso nei dati di ricerca, vi sono una serie di aspetti pratici da tenere in considerazione.

Osserviamo questo semplice esempio: vi sono due generatori di riga, uno è rappresentato dalla sorgente, l'altro dalla ricerca. Al runtime, la generazione dei dati di ricerca avviene per prima, quindi vengono elaborati i dati sorgente.

Dal momento che l'impostazione di unione dei dati di ricerca è configurata su "Load Once" (Carica una sola volta), tutti i relativi record vengono caricati nella memoria, quindi elaborati rispetto al set di dati risultanti della sorgente. Questo comportamento predefinito garantisce unioni dalle elevate prestazioni e può essere piuttosto efficace.

tmaplookup2

Tuttavia, si può chiaramente immaginare come il caricamento di milioni di righe di ricerca o di decine di colonne, comporti l'impiego di considerevoli quantità di memoria. Probabilmente è così. E se fossero richieste ricerche multiple, con milioni di righe ciascuna? Quanta memoria sarebbe necessaria? Considerate attentamente le vostre ricerche quando sono coinvolti molti record o centinaia di colonne.

tmaplookup3Analizziamo ora un possibile compromesso: memoria rispetto a prestazioni. Sono disponibili tre modelli di ricerca:

  • Load Once (Carica una sola volta) legge tutti i record ammissibili nella memoria
  • Reload at each Row (Ricarica a ogni riga) legge la riga ammissibile solo per ciascun record sorgente
  • Reload at each Row (cache) (Ricarica a ogni riga (cache)) legge la riga ammissibile per ciascun record sorgente e la memorizza nella cache

Il caricamento in memoria dei dati di ricerca per essere uniti alla sorgente può essere piuttosto rapido. Tuttavia, se capacità di memoria limitate impediscono il caricamento di enormi quantità di dati di ricerca, o se semplicemente non desiderate caricare TUTTI i dati di ricerca in quanto non richiesti dal caso d'uso, utilizzate il modello "Reload at each Row" (Ricarica a ogni riga). Per riuscire nell'intento, è però necessario che conosciate un trucco.

Per prima cosa, all'interno del componente tMap, modificate il modello di ricerca in "Reload at each Row" (Ricarica a ogni riga). Come potrete notare, l'area sottostante si espande per consentire l'inserimento delle "chiavi" da utilizzare nella ricerca. Aggiungete le chiavi, che effettivamente definiscono le variabili globali disponibili al di fuori del componente tMap.

tmaplookup4

Per il componente di ricerca, utilizzate la funzione (datatype)globalMap.get("key") nella clausola "WHERE" della sintassi SQL, per applicare il valore della chiave salvato definito nel componente tMap del dataset di ricerca. Questo passaggio completa il recupero della ricerca per ciascun record elaborato dalla sorgente.

tmaplookup5

Ecco fatto; in qualsiasi modo si scelga di procedere, la ricerca risulterà efficace.

Variabili globali

Vi sono diversi aspetti da considerare relativamente alla definizione e all'uso di quelle che vengono denominate "variabili globali". Gli sviluppatori ne creano e usano di continuo nei job Talend e noi le chiamiamo "variabili di contesto". Talvolta sono "integrate" (locali rispetto a un job), mentre altre volte si trovano nel "repository dei progetti" sotto forma di gruppi di contesto, per poter essere riutilizzate in più job.

In entrambi i casi, si tratta sempre di "variabili globali" il cui valore è
determinato in fase di runtime e disponibile per l'uso ovunque all'interno del job che le definisce. Sapete di usarne una ogni volta che un oggetto context.varname viene incorporato in un componente, un'espressione o un trigger. Ricordate di collocare le variabili di uso comune in un "progetto di riferimento" per ottimizzarne l'accesso dai vari progetti.


Talend offre inoltre i componenti tSetGlobalVar e tGlobalVarLoad che possono definire, memorizzare e usare "variabili globali" al runtime. Il componente tSetGlobalVar memorizza una coppia chiave-valore all'interno dei job, operazione analoga all'uso di una "variabile di contesto", ma che garantisce maggiore controllo (ad esempio nella gestione degli errori). Osservate il mio esempio in cui un unico valore MAX(date) viene recuperato e quindi utilizzato in una successiva query SQL per filtrare un secondo recupero di record.

tsetglobalvar1

Per accedere alla variabile globale, utilizzate la funzione (datatype)globalMap.get("key") nella clausola "WHERE" della sintassi SQL. Vi consiglio di acquisire familiarità con questa funzione, in quanto è molto probabile che la utilizzerete parecchio una volta che ne avrete sperimentato l'efficacia.tglobalvarload1

Il componente tGlobalVarLoad offre funzionalità simili per job che coinvolgono big data e in cui il componente tSetGlobalVar non è disponibile. Osservate il mio esempio dove un valore aggregato viene calcolato, quindi utilizzato in una successiva lettura per qualificare i record da restituire.tglobalvarload2

L'argomento non è ancora esaurito. Nascoste in maniera visibile troviamo una serie di "variabili globali di sistema"; esse sono disponibili all'interno di job i cui valori sono determinati dai componenti stessi. Abbiamo già parlato di una di queste variabili quando abbiamo affrontato la best practice di gestione degli errori, nella Parte 1 di questa serie di blog: CHILD_RETURN_CODE e ERROR_MESSAGE. Queste variabili globali di sistema sono generalmente disponibili per l'uso immediatamente dopo l'esecuzione di un componente che ne definisce il valore. In base al tipo di componente, sono disponibili diverse variabili di sistema. Ecco un elenco parziale:

  • ERROR_MESSAGE / DIE_MESSAGE / WARN_MESSAGE
  • CHILD_RETURN_CODE / DIE_CODE / WARN_CODE / CHILD_EXCEPTION_STACK
  • NB_LINE / NB_LINE_OK / NB_LINE_REJECT
  • NB_LINE_UPDATED / NB_LINE_INSERTED / NB_LINE_DELETED
  • global.projectName / global.jobName (queste sono a livello di sistema e il loro uso è ovvio)

Caricamento dei contesti


I "gruppi di contesto" supportano una progettazione dei job altamente riutilizzabile, tuttavia, talvolta è necessaria ancora più flessibilità. Ad esempio, supponiamo che desideriate gestire esternamente i valori predefiniti delle variabili di contesto. Talvolta, ha più senso archiviarli all'interno di un file o di un database. Poter gestire questi valori esternamente può risultare piuttosto efficace e addirittura essere d'aiuto per supportare alcuni problemi di sicurezza. Ed è proprio qui che entra in gioco il componente tContextLoad.

tcontextload1

L'esempio sopra mostra una semplice progettazione del job che prevede l'inizializzazione delle variabili di contesto in fase di runtime. Il file esterno utilizzato per il caricamento contiene coppie di valori chiave delimitati da virgole che, a mano a mano che vengono letti, vanno a sostituire i valori correnti delle variabili di contesto definite per il job. In questo caso, vengono caricati i dettagli della connessione del database per assicurare la connessione desiderata. Come si può notare, è possibile controllare la gestione degli errori e, in effetti, questo è un altro punto in cui un job può uscire immediatamente in maniera programmatica: "Die on Error". Sono pochi i casi in cui ciò è possibile. Ovviamente, il componente tContextLoad può usare una query di database altrettanto facilmente e conosco diversi clienti che procedono esattamente in questo modo.

È disponibile un componente tContextDump corrispondente che scrive i valori attuali delle variabili di contesto all'interno di un file o database. Questo componente può risultare utile quando si creano progettazioni job altamente adattabili.

Uso degli schemi dinamici

Spesso mi viene chiesto come creare job in grado di utilizzare schemi dinamici. In realtà, si tratta di una domanda complessa in quanto esistono diversi casi d'uso in cui è prevista la gestione di schemi dinamici. Il caso d'uso più comune sembra essere quello in cui si devono trasferire i dati di diverse tabelle in un'altra serie di tabelle corrispondenti, magari collocate in un diverso sistema di database, ad esempio da Oracle a MS SQL Server. Creare un job per trasferire tali dati è semplice, tuttavia, ci rendiamo conto quasi subito che creare un job per ogni singola tabella non è così pratico. E se le tabelle fossero centinaia? Non è possibile creare un unico job in grado di gestire TUTTE le tabelle? Purtroppo, questo rimane un limite di Talend. Tuttavia, non è il caso di abbattersi, perché possiamo farlo con DUE job: uno per scaricare i dati e un altro per caricarli. Accettabile?

tsetdynamicschema1

Ecco il mio job di esempio. Vengono stabilite tre connessioni: le prime due per recuperare gli elenchi TABLE e COLUMN e la terza per recuperare i dati veri e propri. Tramite la semplice iterazione di ciascuna tabella, salvando le relative colonne, posso leggere e scrivere i dati in un file flat posizionale (processo DUMP o di scarico) utilizzando il componente tSetDynamicSchema. Un job simile eseguirà la stessa procedura con la sola differenza che la terza connessione leggerà il file posizionale e scriverà i dati nel data store di destinazione (processo LOAD o di caricamento).

In uno scenario come questo, è necessario che gli sviluppatori conoscano un po' il funzionamento interno del database host utilizzato. Nella maggior parte dei sistemi, come Oracle, MS SQL Server e MySQL sono presenti delle tabelle di sistema, spesso chiamate "schemi di informazioni", che contengono metadati di oggetti relativi a un database, incluse le relative tabelle e colonne. Ecco una query che consente di estrarre un elenco completo di tabelle/colonne dal database MySQL in TAC v6.1 (cosa ne pensate della mia formattazione di sintassi SQL preferita?):

screen-shot-2017-01-09-at-11-04-28-am

Assicuratevi di utilizzare credenziali di connessione che presentino autorizzazioni "SELECT" per questo database generalmente protetto.

Notate l'uso del componente tJavaFlex per iterare i nomi delle tabelle trovati. Salvo ogni "nome di tabella" e definisco un contrassegno "Control Break", quindi eseguo l'iterazione per ciascuna tabella rilevata e recupero il relativo elenco di colonne ordinato. Una volta regolati eventuali valori "null" nelle lunghezze di colonna, lo "Schema dinamico" salvato è completo. L'istruzione condizionale "IF" verifica il contrassegno "Control Break" quando il nome di tabella cambia e inizia il processo di dump della tabella corrente. Ecco fatto!

Componenti SQL dinamici

Il codice dinamico è eccezionale! Talend consente di implementarlo in vari modi. Nella progettazione dei job precedente, ho seguito un approccio diretto per recuperare elenchi di tabelle e colonne da un database. Talend offre componenti specifici del sistema host in grado di fare la stessa cosa. Questi componenti, chiamati t{DB}TableList e t{DB}ColumnList (dove {DB} viene sostituito dal nome del componente host), forniscono l'accesso diretto ai metadati dello "schema di informazioni" senza necessità di conoscerlo. L'uso di questi componenti al posto dei processi DUMP/LOAD descritti precedentemente porta allo stesso risultato: quindi qual è realmente il loro scopo?

Non tutte le query SQL necessitano di recuperare o memorizzare dati. Talvolta sono necessarie altre operazioni di database. Per scoprire quali sono questi requisiti, è possibile elencare i componenti t{DB}Row e t{DB}SP. Il primo consente di eseguire praticamente qualsiasi query SQL che non restituisce un set di risultati, come "DROP TABLE". Il secondo permette di eseguire una "procedura memorizzata".

Ultimo, ma non meno importante, il componente t{DB}LastInsertId permette di recuperare l'ultimo "ID" inserito da un componente di output di un database; molto utile in determinate circostanze.

CDC

Un'altra domanda che mi viene rivolta spesso è: "Talend supporta il CDC (Change Data Capture)?" La risposta è SÌ, ovviamente. Tramite i meccanismi "pubblicazione/abbonamento" direttamente collegati al sistema database host coinvolto. È importante notare che non tutti i sistemi di database supportano il CDC. Di seguito troverete un elenco completo e aggiornato relativo al supporto CDC nei job Talend:

screen-shot-2017-01-09-at-11-15-36-am

Sono disponibili tre diverse modalità CDC, ovvero:

  • Trigger (predefinita) impiega i trigger host del database per tenere traccia di inserimenti, aggiornamenti ed eliminazioni
  • Redo/Archive Log modalità utilizzata solo con Oracle 11g e versioni precedenti
  • XStream modalità utilizzata solo con Oracle 12 e OCI

Dal momento che la modalità Trigger sarà quella che probabilmente utilizzerete, analizziamone l'architettura:

cdcprocessflow

Il Capitolo 11 della Guida per l'utente Talend offre una panoramica completa del processo CDC, della sua configurazione e utilizzo in Studio, così come insieme al sistema database host utilizzato. Anche se si tratta di un processo concettualmente semplice, esso prevede una lunga serie di passaggi. Vi consiglio di comprendere prima appieno i vostri requisiti, le modalità CDC e i parametri di progettazione job, quindi di documentarli accuratamente nelle vostre Linee guida di sviluppo.

Una volta definito, l'ambiente CDC offre un efficace meccanismo per mantenere sempre aggiornate le destinazioni a valle (in genere, un data warehouse). Utilizzate i componenti t{DB}CDC all'interno dei job Talend per estrarre i dati che sono stati modificati dall'ultima estrazione. Anche se CDC richiede tempo e diligenza per essere configurato e reso operativo, si tratta senza dubbio di una funzionalità estremamente utile.

Componenti personalizzati

Nonostante Talend oggi offra più di 1.000 componenti tra cui scegliere, sono ancora tante le ragioni per acquistarne di propri. Gli sviluppatori Talend spesso includono funzionalità speciali in un componente personalizzato. Alcuni hanno creato e messo in commercio i propri componenti, mentre altri li pubblicano con possibilità di accesso gratuito sul portale Talend Exchange recentemente modernizzato. Se un componente non è disponibile nella Palette (Tavolozza) Talend, provate a cercarlo qui, potreste trovare ciò che vi serve. Per accedere, è necessario un account "Talend Forge", ma probabilmente ne avrete già uno.

customcomponents1Per iniziare, assicuratevi che la directory in cui archiviare i componenti personalizzati sia configurata correttamente. Andate al menu "Preferences" (Preferenze) e selezionate una directory comune che tutti gli sviluppatori utilizzerebbero. Fate clic su "Apply" (Applica), quindi su "OK".

Individuate il link "Exchange" sulla barra dei menu che consente la selezione e installazione dei componenti. La prima volta che lo fate, selezionate "Always run in Background" (Esegui sempre in background), quindi fate clic sul pulsante "Run in Background" (Esegui in background) in quanto il caricamento dell'intero elenco di oggetti disponibili richiede diverso tempo. Da questo elenco, è possibile visualizzare/scaricare oggetti di interesse. Una volta eseguito il download del componente, fate clic su "Downloaded Extensions" (Estensioni scaricate) per installare i componenti in Studio. Al termine dell'installazione, il componente verrà visualizzato come installato e risulterà disponibile nella Palette (Tavolozza).

customcomponents2

Un componente e i file a esso associati, una volta installati, possono essere difficili da trovare. Cercate in due posti:

{talend}/studio/plugins/org.talend.designer.components.exchange{v}
{talend}/studio/plugins/org.talend.designer.components.localprovider{v}

Se volete creare autonomamente un componente personalizzato, passate alla vista "Component Designer" (Designer componenti) in Studio. La maggior parte dei componenti personalizzati impiega "JavaJet", ovvero l'estensione file per l'inserimento di codice java per l'IDE (Integrated Development Environment) di Eclipse. Per chi è alle prime armi, è disponibile un tutorial soddisfacente per la creazione di componenti personalizzati. Nonostante sia un po' datato (risale circa al 2013), illustra gli elementi di base che è necessario conoscere. Sono anche disponibili tutorial di terze parti (alcuni elencati nel tutorial stesso). Ecco un buon esempio: Talend by Example: Custom Components (Talend per esempi: componenti personalizzati). Potete anche effettuare ricerche in Google sulla creazione di componenti personalizzati.

API JobScript

Normalmente, utilizziamo la vista "Designer" per creare il nostro job Talend, che poi genera il codice java sottostante. Vi siete mai chiesti se sia possibile generare automaticamente un job Talend? Ebbene sì, esiste un modo! Aprite uno dei vostri job. Alla base della "tela" potete vedere tre schede: DESIGNER, CODE e JOBSCRIPT. Sembra interessante. Probabilmente avete già provato a fare clic sulla scheda CODE per ispezionare il codice java generato. Avete mai fatto clic sulla scheda JOBSCRIPT? Se lo avete fatto, vi siete resi conto di ciò che avevate davanti agli occhi? Immagino di no, per la maggior parte di voi. In questa scheda è riportato lo script che rappresenta il progetto del job. Guardate più attentamente la prossima volta. Riuscite a scorgere qualcosa di familiare relativamente alla progettazione del job? Sicuramente sì…

Che cosa? Lasciate che vi spieghi. Supponiamo che abbiate creato e gestito da qualche parte metadati relativi alla progettazione del vostro job e che li abbiate eseguiti mediante un motore di elaborazione (creato da voi), per generare JobScript adeguatamente formattato, magari aggiustando elementi chiave allo scopo di creare più permutazioni del job. Ora la storia si fa interessante.

jobscript1

Cercate nel "Project Repository" (Repository dei progetti), nella sezione CODE (Codice)>ROUTINES (Routine), la cartella "Job Scripts". Create un nuovo JobScript (ho chiamato il mio "test_JobScript"). Aprite uno dei vostri job, copiate il contenuto della scheda JobScript nel file del JobScript e salvate. Fate clic con il pulsante destro del mouse sul JobScript e selezionate "Generate Job" (Genera job). Ora guardate nella cartella "Job Designs" (Progetti job) e troverete un job nuovo di zecca. Provate a immaginare ciò che potete fare ora! Entusiasmante, vero?

Conclusione

Eccoci. Direi che è tutto. Non che le best practice relative alla creazione e alla gestione di progetti di job Talend si esauriscano qui, sono certo che ve ne siano molte altre. seo-small-business-competition-300x300Per il momento, lascio aperto l'argomento per un dialogo di più ampio respiro all'interno della community, con la convinzione che questa raccolta (32 in tutto) offra un vademecum completo e approfondito per la creazione di progetti Talend di successo.


Nel mio prossimo blog di questa serie, faremo un passo avanti e vedremo come applicare tutte queste best practice in un caso d'uso convenzionale. Tecnologia applicata, metodologie efficaci e soluzioni che consentono di ottenere risultati! La sintesi di dove e come usare queste best practice e Job Design Pattern. Alla prossima!

Partecipa alla discussione

0 Comments

Scrivi una risposta

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *