Nell’articolo precedente abbiamo visto da vicino cos’è Couchbase Lite e come lo possiamo sfruttare all’interno della nostra app per le principali operazioni CRUD. Oggi andremo a conoscere quali potenzialità offre il suo motore di Query approfondendo quali strumenti offre e come posso sfruttare al massimo le Live Query.

Le applicazioni mobile moderne richiedono soluzioni moderne ai problemi comuni. Oggi è davvero raro trovare un’app mobile che non comunichi con il “mondo esterno”. Nella maggior parte dei casi, i dati vengono recuperati da API remote, utilizzando semplici chiamate HTTP REST. Ma un’app può fare ancora di più. Può richiedere dati da periferiche Bluetooth, servizi di localizzazione e così via.

Video introduzione ai contenuti dell’articolo raccontata da Matteo Sist e Damiano Giusti

Tutti questi casi hanno un requisito comune per consentire il corretto funzionamento dell’applicazione: una connessione funzionante verso una fonte definita. In effetti, è piuttosto difficile pensare a una chiamata HTTP senza in realtà avere un provider cloud raggiungibile o un server locale. Giusto?

Dati disponibili anche offline

In questo post parleremo dell’importanza della disponibilità dei dati. Per la maggior parte delle applicazioni mobili possiamo affermare che è fondamentale fornire dati offline all’utente finale.

Come sviluppatore di app mobile, dovrai affrontare questo scenario. Non c’è via d’uscita. Se hai applicato i principi comuni di ingegneria software, ti ritroverai a disporre del codice che esegue il recupero dei dati e la loro manipolazione di base in un livello isolato dell’applicazione. Ma se non sei stato così saggio nel progettare la tua architettura di base (per ragioni assolutamente giustificabili, vero?), i concetti che presenterò potrebbero aiutarti in ugual modo.

Ok, quindi?

Hai bisogno di una cache. Una soluzione ottimale che può aiutarti (anche) a memorizzare dati in cache è Couchbase Lite. Ti mostrerò due casi d’uso, quindi a te la scelta su quale soluzione può soddisfare maggiormente le tue esigenze!

Un attimo, cos’è Couchbase Lite?

Semplice. È un database NoSQL che funziona molto bene con le apps. È in circolazione dal 2014 in versione “Lite” ovvero dedicata al mondo mobile. Fondamentalmente, ti consente di archiviare i dati come documenti JSON, con elementi chiave-valore. Anche se si tratta di uno store NoSQL, le query sono sorprendentemente veloci, grazie al suo potente linguaggio di query N1QL che offre una sintassi più vicina al SQL standard per agevolare nell’accesso alle informazioni che stiamo cercando.

#1 – Semplice soluzione di caching

Couchbase funziona molto bene come livello di memorizzazione in cache. Grazie alla sua velocità offre un rapido accesso ai dati memorizzati nella cache, anche per le scritture.

Esempio: Couchbase Lite come caching locale

Facciamo un esempio comune: supponiamo di dover memorizzare in cache i risultati di una chiamata API, poiché tali dati sono statici e non cambiano frequentemente. Per prima cosa controlliamo se i dati richiesti sono già presenti in cache. In tal caso, li restituiremo direttamente, altrimenti eseguiremo una chiamata API e memorizzeremo il risultato nella cache.

La nostra classe repository apparirà come riportato qui sotto:

public class MyDataRepository {
  private final Cache cache;
  private final HttpClient HttpClient;
 
  public MyDataRepository(Cache cache, HttpClient httpClient) {
    this.cache = cache;
    this.httpClient = httpClient;
  }
 
  public Cancelable getData(String identifier, Function<Data> callback) {
    final Data cachedData = cache.get(identifier);
    if (cachedData == null) {
      return httpClient.getData(identifier, (data) -> { 
        cache.set(identifier, data);
        callback.call(data);
      });
    } else {
      callback.call(cachedData);
      return Cancelable.empty();
    }
  }
}

Guardando questo capolavoro, assumiamo che Cancelable sia un oggetto che può essere usato per cancellare i task in esecuzione, come ad esempio la chiamata API che facciamo nello snippet qui sopra. Assumiamo anche Cancelable.empty() come un Cancelable che non fa nulla. 

Per prima cosa, vediamo come è composto l’oggetto Data:

public class Data {
  private String identifier;
  private String description;
 
  public Data(String identifier, String description) {
    this.identifier = identifier;
    this.description = description;
  }
 
  public String getIdentifier() { 
    return identifier; 
  }
  
  public String getDescription() { 
    return description; 
  }
}

È un semplice value object, senza setters, perché vogliamo renderlo immutabile dall’esterno. Diamo un’occhiata all’integrazione con Couchbase Lite. 

Indichiamo Cache come un’interfaccia definita come di seguito:

public interface Cache<T, K> {
  @Nullable T get(K identifier);
  void set(K identifier, @Nullable T value);
}

E possiamo implementarla usando l’SDK di Couchbase Lite:

public class CouchbaseLiteCache implements Cache<Data, String> {
 
  private Database database;
 
  CouchbaseLiteCache(Database database) {
    this.database = database;
  }
 
  @Override @Nullable public Data get(String identifier) {
    final Document document = database.getDocument(identifier);
    if (document == null) {
      return null;
    } else {
      return new Data(
        document.getString("identifier"),
        document.getString("description")
      );
    }
  }
 
  @Override public void set(String identifier, @Nullable Data value) {
    try {
      if (value == null) {
        database.purge(identifier);
      } else {
        database.save(
          new MutableDocument(identifier)
            .setString("identifier", value.getIdentifier())
            .setString("description", value.getDescription())
        );
      }
    } catch (CouchbaseLiteException e) {
      throw new CachingException(e);
    }
  }
 
  public static CouchbaseLiteCache create(final String databaseName) throws CouchbaseLiteException {
    return new CouchbaseLiteCache(new Database(databaseName, new DatabaseConfiguration()));
  }
}

L’implementazione è piuttosto semplice e ha lo scopo di memorizzare nella cache i dati di risposta alle chiamate HTTP per un successivo riutilizzo.

#2 – “Singola fonte di verità”

Un altro scenario riguarda l’adozione di Couchbase Lite come “singola fonte di verità” per il data layer della nostra app. Per trarre il massimo da questa soluzione, il mio suggerimento è di applicarla insieme ad un Observer pattern. Possiamo scegliere di implementare il pattern da zero oppure di adottare alcune librerie potenti e complete, come RxJava. Onestamente, consiglio vivamente quest’ultima opzione. Ma per le finalità di questo esempio, manteniamo un flusso callback-style. 

Abbiamo ancora la nostra classe repository, che funge da facciata per quello che concerne l’accesso ai dati. Ma in questo scenario, avremo la cache come fonte di dati principale, indipendentemente dal fatto che sia necessario chiamare l’API REST o meno. 

Couchbase Lite: le Live Query

Spieghiamo prima cosa ci offre Couchbase Lite:

Come altri database moderni, Couchbase Lite consente di ascoltare le modifiche che si verificano a livello di base dati. Quando eseguiamo una query sul database, possiamo eseguirne una di un particolare tipo, denominata Live Query.

Una Live Query consente di essere “osservata” tramite una callback agganciata ad essa. La callback verrà invocata ogni volta che una modifica del dataset subisce una variazione che influisce sul set di risultati della query specificata. Le modifiche supportate sono sostanzialmente tutte le operazioni CRUD che è possibile eseguire su un database. 

Detto questo, potresti aver iniziato a capire cosa implementeremo. Per creare un’unica fonte di verità in modalità “live”, ci sottoscriviamo agli aggiornamenti di una Live Query che definiamo. Questo “darà vita” ai nostri risultati. Quindi, chiamiamo la nostra HTTP REST api, memorizzando i risultati nel database. In questo modo, verrà attivato l’observer della Live Query e la nostra datasource emetterà un aggiornamento. 

Utilizziamo le Live Query con Couchbase Lite

Vediamo ora un esempio pratico.

Aggiungiamo un metodo alla nostra interfaccia Cache, permettendo alle implementazioni di emettere aggiornamenti:

public interface Cache<T, K> {
  @Nullable T get(K identifier);
  void set(K identifier, @Nullable T value);
  
  Cancelable getUpdating(K identifier, Function<Data> callback);
}

Poi implementiamolo usando la funzionalità della Live Query di Couchbase Lite.

public class CouchbaseLiteCache implements Cache<Data, String> {
  
  private Database database;
 
  CouchbaseLiteCache(Database database) {
    this.database = database;
  }
  
  // ...
 
  @Override public Cancelable getUpdating(String identifier, Function<Data> callback) {
    final Query query = QueryBuilder
      .select(all())
      .from(database(database))
      .where(string("identifier").equalTo(string(identifier)));
 
    final ListenerToken token = query.addChangeListener(queryChange -> {
      final Data data = getDataFromQueryChange(queryChange);
      if (data != null) callback.call(data);
    });
 
    return () -> query.removeChangeListener(token);
  }
 
  // ...
 
  private static Data getDataFromQueryChange(final QueryChange queryChange) {
    final Iterator<Result> iterator = queryChange.getResults().iterator();
    if (iterator.hasNext()) {
      final Result result = iterator.next();
      return new Data(
        result.getString("identifier"),
        result.getString("description")
      );
    } else {
      return null;
    }
  }
}

Prestiamo attenzione all’implementazione Cancelable. Quando si esegue una Live Query, l’SDK restituisce un oggetto ListenerToken. Tale token consente di annullare la sottoscrizione agli aggiornamenti della query. Tieni traccia dei tuoi ListenerToken, devi annullare la sottoscrizione a tutte le tue query quando non ti sono più utili, per evitare leaks! 

Inoltre, nota quanto è bella la API di query. Grazie anche agli static import di Java, il linguaggio di query diventa fluente, leggibile e type-safe. Ma questo è Java… Mi chiedo quanto si possa migliorarlo aggiungendo alcune funzioni di estensione di Kotlin e un po’ sintassi “infix” ?

Integriamo ora il tutto nel repository

A questo punto, possiamo integrare il nuovo metodo di cache nel nostro repository.

public class MyLiveRepository {
 private final Cache<Data, String> cache;
 private final HttpClient httpClient;
 
 public MyLiveRepository(Cache<Data, String> cache, HttpClient httpClient) {
   this.cache = cache;
   this.httpClient = httpClient;
 }
 
 public Cancelable getData(String identifier, Function<Data> callback) {
   final Cancelable cacheSubscription = cache.getUpdating(identifier, callback);
   final Cancelable restCall = httpClient.getData(identifier, (data) -> cache.set(identifier, data));
   return () -> {
     restCall.cancel();
     cacheSubscription.cancel();
   };
 }
}

ll metodo getData usa la callback fornita per osservare direttamente gli aggiornamenti della cache. Quindi, esegue la chiamata HTTP REST, aggiornando il dataset.
Da notare di nuovo l’utilizzo dell’oggetto Cancelable. Qui sono raggruppate le cancellazioni delle rimanenti chiamate e delle sottoscrizioni agli aggiornamenti della cache: quando  il risultante Cancelable viene disposto, dobbiamo ovviamente interrompere le operazioni in corso.

Fatto! Hai promosso Couchbase Lite a “fonte di verità” per i dati della tua applicazione.

Ricapitolando

Couchbase Lite è sicuramente una tecnologia interessante. Con questo articolo volevo mostrare come può aiutarti a ottenere soluzioni efficaci a problemi comuni. Naturalmente puoi scoprirne di più consultando la documentazione ufficiale. 

Couchbase non è solo un database locale: Couchbase Server e Couchbase Sync Gateway sono strumenti di punta offerti per la creazione di applicazioni web e mobile realtime. E con la Community Edition, puoi averli tutti gratuitamente. Sicuramente, Couchbase è un ecosistema che merita assolutamente di essere provato, per verificare se può adattarsi alle tue esigenze o alle esigenze della tua azienda.

Questo e molto altro ancora su Couchbase vi aspetta, rimanete sintonizzati. 

Grazie per la lettura!