Backbone.js e PhoneGap Storage

Una soluzione più modulare ed efficiente per la ridefinizione del metodo sync() di Backbone.js per l’uso della Storage API di PhoneGap.

Backbone

La tecnologia web (HTML5, CSS3, JavaScript) permette lo sviluppo di applicazioni mobili, le quali possono essere sia web app che hybrid app; quest’ultime, in modo particolare, sono applicazioni in parte native in parte web e, come quelle native , presentano il vantaggio di poter accedere alle molte funzionalità offerte dai moderni device mobili. Il panorama tecnologico attuale presenta un’ampia gamma di framework che permettono la realizzazione di hybrid app; in questo articolo si è scelto di usare PhoneGap.

Lo sviluppo di app complesse richiede che il codice realizzato sia facilmente testabile e manutenibile quindi, nonostante l’impiego della tecnologia web, bisogna evitare ciò che accade nella maggior parte delle applicazioni web dove la logica applicativa è immersa in un groviglio di callback e codice HTML. Per questo si dovrebbe sempre prendere in considerazione l’impiego di framework o librerie JavaScript, quali Backbone.js, che permettano lo sviluppo seguendo il pattern MVC (anche se nel caso specifico sarebbe più giusto parlare di pattern MV*).

Per coloro che volessero approfondire le conoscenze di Backbone.js rimando ai seguenti link:

Documentazione della API offerta da Backbone.js: http://backbonejs.org/

Riferimenti bibliografici: Developing Backbone.js Applications

PhoneGap, d’altra parte, è un framework JavaScript il cui scopo è quello di permettere lo sviluppo di app, attraverso la tecnologia web, rispettando lo standard Packaged Web Apps del W3C e agevolando l’accesso alle funzionalità dei dispositivi mobili. In questo articolo si avrà a che fare con la  Storage API, in cui, vista la sua natura asincrona, la gestione dei risultati di una determinata operazione è demandata a una callback.

Per coloro che volessero approfondire le conoscenze di PhoneGap rimando ai seguenti link:

Documentazione del Framework PhoneGap: http://docs.phonegap.com/

Riferimenti bibliografici: Apache Cordova 3 Programming

Backbone.js attraverso models, collections, views, routers e gestione dichiarativa degli eventi permette di creare applicazioni web altamente modulari, consentendo la connessione ad API rispettando l’interfaccia RESTful JSON.

Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions,views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.

Nello specifico, in un’app per dispositivi mobili le informazioni vanno salvate in un database locale, quindi, per utilizzare le API offerte da PhoneGap, bisogna ridefinire il metodo sync() di ogni model o collection. Siccome un’applicazione potrebbe essere costituita da molte entità, il precedente approccio sarebbe totalmente controproducente e soprattutto violerebbe il principio DRY(Don’t Reapeat Yourself). Una soluzione più modulare ed efficiente consiste nel creare un adapter per il metodo sync() che possa essere impiegato in qualsiasi model o collection.

L’overriding del metodo Backbone.sync() è un’operazione abbastanza delicata e le informazioni su come eseguirlo correttamente sono reperibili in rete o nel sopracitato libro Developing Backbone.js Applications alla pagina 68, inoltre si può sempre pensare di prendere spunto dalle implementazioni di altri adapter quali:

Seguendo i principi sin qui elencati si è realizzato un adapter denominato backbone.cordova.storage scaricabile al seguente link:

https://github.com/naciostechnologies/backbone.cordova.storage 

Di seguito si discuterà dell’impiego di backbone.cordova.storage all’interno di un’app ibrida.

Installazione:

In un’app che non consente il caricamento dinamico degli script bisogna importare le seguenti dipendenze come in una classica web application:

1
<script src="path/cordova.js" type="text/JavaScript"></script><script src="path/jQuery.js" type="text/JavaScript"></script><script src="&quot;path/underscore.js" type="text/JavaScript"></script><script src="path/backbone.js" type="text/JavaScript"></script>

importazione dell’adapter:

1
<script src="path/backbone.cordova.storage.js" type="text/JavaScript"></script>

In un’app che impiega il caricamento dinamico degli script attraverso RequireJS occorre impostare i seguenti parametri nel file di configurazione:

1
2
3
4
5
6
7
8
9
10
require.config({
    baseUrl: 'js',
        paths: {
            cordova: 'libs/cordova/cordova',
 	    jquery: 'libs/jquery/jquery',
 	    underscore: 'libs/underscore/underscore',
 	    backbone: 'libs/backbone/backbone',
            SQLiteCordovaStorage: 'libs/backbone/backbone.cordova.storage'	
 	}
 });

Inizializzazione di Model o Collection che usano l’adapter backbone.cordova.storage:

La prima cosa da fare è ottenere un riferimento all’oggetto Database attraverso la chiamata del metodo openDtatabase() dell’oggetto window:

1
2
3
var database = window.openDatabase('Database name', 'Database version', 'Database display name', Database size);
//Es.
var database = window.openDatabase('it.nacios.app.DBTest', '1.0', 'TestDB', 1000000);

Adesso sarà possibile creare classi che estendono Backbone.Model e Backbone.Collection e istanziare oggetti di tali classi, in modo che usino l’adapter backbone.cordova.storage. Per fare ciò, all’interno di tali classi, bisogna prevedere un campo denominato SQLiteStore al quale si deve associare la creazione di un oggetto SQLiteCordovaStorage, al cui costruttore vanno passati i seguenti parametri:

  • Istanza attiva dell’oggetto Database
  • Nome della tabella del database locale di cui si vuole eseguire il binding

Esempio per Backbone.Model:

1
2
3
4
var ClassObj = Backbone.Model.extend({
    SQLiteStore: new SQLiteCordovaStorage(Database object, 'Table name')
    //altri metodi e campi
});

Esempio per Backbone.Collection:

1
2
3
4
5
var ClassColl = Backbone.Collection.extend({
    model: ClassObj,
    SQLiteStore: new SQLiteCordovaStorage(Database object, 'Table name')
    //altri metodi e campi
});

Adesso sarà possibile usare tutti i metodi messi a disposizione dall’API di Backbone.js sia su model che su collection; infatti, sarà responsabilità dell’adapter garantire la sincronizzazione dei dati con il database locale sfruttando al suo interno la Storage API di PhoneGap.

Particolare attenzione deve essere prestata al metodo fetch() responsabile di reperire le infomazioni del database e di popolare model e collection con gli opportuni valori. Tale metodo è stato arricchito in modo da prevedere tra le options (opzioni) un campo filters attraverso cui si potrà personalizzare la query che verrà eseguita sul database sottostante.

Nel caso di un model, è stata prevista la ricerca di una singola entità attraverso un indice, che in un oggetto backbone coincide con l’idAttribute e che di default, se non diversamente indicato, corrisponde al valore del campo ‘id’. Per chiarire il tutto si può far riferimento al seguente esempio:

1
2
3
4
5
6
7
8
9
10
11
12
//database rappresenta un'istanza dell'oggetto Database
//users è il nome della tabella di cui si vuole eseguire il binding
var User = Backbone.Model.extend({
    idAttribute: 'idutente',
    SQLiteStore: new SQLiteStorage(database, 'users')
    //altri campi e metodi
});
 
var myUser = new User();
//il metodo fetch richiamerà la query per la ricerca dell'utente 
//che ha il campo idutente uaguale a 3
myUser.fetch({filters:{idutente:3}});

In sostanza per eseguire la ricerca di un determinato model bisogna rispettare il seguente pattern:

1
modelInstance.fetch({filters:{key:value}});

dove come opzioni del metodo fetch() bisogna passare il campo filters, il quale è un oggetto del tipo key-value. La key rappresenta il nome del campo in base a cui i models sono indicizzati, mentre value ne rappresenta il valore.

Quando si ha a che fare con le collections, il campo filters permette l’esecuzione di query in modo più dettagliato. Filters è  costituito da diversi campi a seconda del grado di personalizzazione della query che si intende realizzare. In definitiva si è data la possibilità di eseguire due tipi di interrogazioni:

  • Query personalizzata
  • Query realizzata attraverso le condizioni logiche offerte dall’adapter

Query personalizzata:

Quando si devono eseguire query complesse il campo filters è un oggetto di tipo key-value in cui la chiave sarà chiamata ‘query’, mentre il valore è la stringa che rappresenta la query che si intende creare; vediamolo nel seguente esempio:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//database rappresenta un'istanza dell'oggetto Database
//users è il nome della tabella di cui si vuole eseguire il binding
//User classe che estende Backbone.Model creata nell'esempio precedente
 
//Users classe che estende Backbone.Collection
var Users = Backbone.Collection.extend({
    model: User,
    SQLiteStore: new SQLiteStorage(database, 'users')
    //altri campi e metodi
});
 
var myUsers = new Users();
//Stringa rappresentante la query personalizzata
var sql = " SELECT * FROM users WHERE nazione = 'Italia' ";
myUsers.fetch({filters:{query:sql}});

In sostanza, per eseguire una query personalizzata per la ricerca di elementi da inserire in una collection bisogna seguire il seguente pattern:

1
collectionInstance.fetch({filters:{query:'SQL query'}});

Query realizzata attraverso le condizioni logiche offerte dall’adapter:

L’adapter mette a disposizione un insieme di opzioni che permettono di realizzare query più o meno complesse. Il campo filters adesso è un oggetto contenente altri tre elementi query, order e limit di cui solo query deve essere necessariamente presente.

Il campo query è un vettore contenente oggetti costituiti al massimo da quattro campi field ( nome del campo del database sul quale si vuole operare una valutazione), condition ( operatore di confronto), value ( valore effettivo rispetto al quale verrà eseguito il confronto), lcondition ( nel caso ci fossero ulteriori campi da confrontare deve essere espressa la condizione logica che permetta il corretto concatenamento delle diverse operazioni di confronto).

Struttura degli oggetti presenti nel vettore query:

1
2
3
4
5
6
{
  field: 'Nome del campo',
  condition: 'Condizione di confronto',
  value: 'Valore numerico o stringa per il confronto',
  lcondition: 'Condizione logica AND o OR (AND default)'
}

L’elemento order è un oggetto costituito da due elementi, fields e type, che rispettivamente rappresentano i campi in base ai quali verrà eseguito l’ordinamento e il tipo di ordinamento ASC (ascendente) o DESC (discendente). Nello specifico, il campo fields è un array contenente il nome dei campi in base al quale verrà eseguito l’ordinamento.

L’elemento limit è rappresentato da un siffatto oggetto:

1
2
3
4
{
  start: Indice dei risultati da cui partire,
  qty: Numero di elementi 
}

nel quale, il campo start indica l’indice da cui iniziare a prendere i risultati, mentre qty specifica la quantità di elementi da inserire nella collection.

Per una maggiore chiarezza si pensi di voler recuperare gli ultimi dieci utenti di nazionalità italiana con un’età massima di 30 anni e di ordinare i risultati per data di iscrizione e username. La chiamata al metodo fetch della collection Users, presentata nell’esempio precedente, sarà molto simile alla seguente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//Users è una classe che estende Backbone.Collection presentata
//nell'esempio precedente
var myUsers = new Users();
myUsers.fetch({
    filters:{
        limit:{ 
           start:0, 
           qty:10
        }, 
        order:{
           fields:['data','username'], 
           type:'DESC'
        },
        query:
        [
           {
              field:'nazione',
              condition:'=',
              value:'Italia',
              lcondition: 'AND'
           },
           {
             field:'eta', 
             condition:'&lt;=', 
             value:30
           }  
        ]
    }
});

Più in generale, quando si vuole eseguire una query bisogna rispettare il seguente pattern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
collectionInstance.fetch({
    filters:{
        limit:{ 
           start:Indice di partenza, 
           qty:Quantità di risultati
        }, 
        order:{
           fields:['campo1','campo2'.....,'campoN'], 
           type:tipo di ordinamento (ASC o DESC) 
        },
        query:
        [
            {
               field:'campo1', 
               condition:'condizione per il confronto', 
               value:'Valore per il confronto',   
               lcondition: 'condizione logica'
            },
            {
               field:'campo2', 
               condition:'condizione per il confronto', 
               value:'Valore per il confronto',   
               lcondition: 'condizione logica'
            },
            .......
            {
               field:'campoN', 
               condition:'condizione per il confronto', 
               value:'Valore per il confronto',   
            }
        ]
    }
});

Si ricordi che limit e order sono campi opzionali dell’oggetto filters da dare in input alle opzioni del metodo fetch()  di una collection.  Nel caso in cui si avesse bisogno di recuperare tutti gli elementi di una collection si può semplicemente richiamare il metodo fetch senza alcuna opzione aggiuntiva:

1
2
//Ottenere tutti gli elementi di una collection
collectionInstance.fetch();

In ultimo si ricorda che le API sin qui proposte hanno una natura asincrona, ciò vuol dire che l’app continua il suo flusso di esecuzione senza aspettare i risultati dal database, garantendo ciò che va sotto il nome di non-blocking IO . La gestione dei risultati sarà eseguita tramite l’ascolto di un determinato evento o a mezzo di una callback. Per chi non è esperto di programmazione basata sugli eventi e di Backbone.js rimando al video introduttivo della seconda BackboneConference in cui Jeremy Ashkenas presenta le novità introdotte nella versione 1.1.0 e i più comuni pattern di programmazione, che hanno lo scopo di rendere  l’applicazione, sia essa web che mobile, più modulare e veloce.

4 commenti
  1. Derek Kahongo dice:

    Hi, I’ve download your plugin and want to use its in a data entry hybrid app. could you help me instancing its in my app for data entry form an how to retrieve data using complex sql querry? I use backbone, cordova, underscore, require js, handlebar

    Rispondi
    • Nacios Technologies dice:

      Hi Derek,
      if you use requirejs you need to import backbone.cordova.storage. After that following the cordova specifications https://cordova.apache.org/docs/en/latest/cordova/storage/storage.html#websql you have to create a new instance of your database
      //Es.
      var database = window.openDatabase(‘it.nacios.app.DBTest’, ‘1.0’, ‘TestDB’, 1000000);
      After that you can use this database as storage for Backbone model and collection, it’s very simple you only add a SQLiteStore properties in your model or collection and set this properties to new SQLiteStorage(database, ‘users’) where database is the instance of your database and users the table name of your database.
      You can execute query in two ways but the very simple is to call the method fetch and pass an object like this:
      //Es.
      modelInstance.fetch({filters:{query:’SQL query’}});
      The string SQL query is your SQL query.
      If you want in the next days i can create a repo on GitHub with a little example.
      Hope to be helpful.
      Nicola Del Gobbo

      Rispondi
      • Derek Kahongo dice:

        Thank you Nicola. I will try. I’m a newbie in cordova and It’s my first time to try to create a data entry apps with backbone and cordova. Hope that It’ll work well. Another question is to know whether I have to install websql plugin in my cordova project or not. I checked the adapter, I did not see where you call install websql in.
        Last one is to know if the connection to db of this plugin can be modified to stand as Sqlite connection (windows.Sqlite.opendatabase) or It’s need some change?

        Thanks.

        Rispondi

Lascia un Commento

Vuoi partecipare alla discussione?
Fornisci il tuo contributo!

Lascia un commento

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