05-SD Offline Apps - 01 - SP
05-SD Offline Apps - 01 - SP
22228
Hasta ahora hemos asumido que la aplicación móvil debía estar conectada siempre al servidor
web para poder funcionar, accediendo a los servicios REST y mediante éstos a la base de datos
que está en el server.
Sin embargo, GeneXus nos permite crear aplicaciones móviles que puedan trabajar en forma
parcialmente conectada o incluso totalmente desconectada del servidor web.
En su lógica interna, este Work With llama al servidor para que éste le devuelva los datos a
cargar en la parte fija del WorkWith a través de un servicio Rest, y luego le pide al servidor que
ejecute otro servicio Rest, que devuelve los datos del grid.
Con los datos devueltos en dos responses, se arma la pantalla (por un lado la parte fija, y por
otro el grid).
En esta arquitectura de una aplicación online, los servicios Rest, los data providers que acceden
a los datos y la base de datos están en el servidor, por lo que solamente pueden accederse a los
mismos si hay conexión.
Veamos ahora el caso de aplicaciones parcialmente conectadas.
Puede requerirse que haya parte de la aplicación que se siga ejecutando cuando se encuentra
desconectada de internet, mientras que otra parte de la misma necesariamente debe tener
conexión para poder funcionar.
Es el caso de nuestra aplicación Event Day para un evento, ya que vamos a querer que el
usuario pueda seguir viendo toda la agenda de conferencias, y toda la información relacionada,
incluso cuando pierde la conexión.
Luego, cuando ésta se reestablece, automáticamente la aplicación actualizará sus datos (que
estarán en una base de datos local en el dispositivo), sincronizándose con el server para recibir
datos (Receive). Como el usuario podrá haber marcado algunas conferencias como favoritas,
también enviará esa información al server al sincronizarse (Send), actualizando la base de datos
central.
Sin embargo, habrá tareas que requerirán necesariamente el acceso al servidor web, ya sea por
su sensibilidad (tareas que deben ser validadas en la base de datos centralizada), como por lo
rápido que cambian los datos. Estas tareas deberán ejecutarse online.
En nuestro caso, el login deberá ser con conexión, y el panel que muestra los tweets es deseable
que también lo sea.
Por tanto, podremos elegir qué objetos de la aplicación pueden ejecutarse offline y cuáles no.
O inclusive como caso particular puede que no nos interese en absoluto que se produzca la
sincronización, sería el caso de una aplicación en la que deseamos independizarnos
absolutamente de los datos en el server. Todo se manejaría en el dispositivo, ya que la aplicación
en el dispositivo perderá todo contacto con el servidor.
Y otro caso particular es cuando queremos que el dispositivo pueda recibir los cambios
efectuados en la base de datos centralizada, pero nunca enviar sus propios cambios, que
quedarán en su base de datos local.
Estas aplicaciones que pueden trabajar sin estar conectadas, las llamamos aplicaciones Offline.
Empecemos por analizar el caso de las aplicaciones desconectadas, en el que queremos que
todos los datos manejados por la aplicación móvil sean accesibles incluso cuando no hay
conexión. Al final abordaremos el caso mixto, en el que una parte de los datos deben seguir
siendo manejados exclusivamente en forma centralizada.
En este caso, toda la estructura de la base de datos centralizada en el servidor que es manejada
por la aplicación móvil, es espejada en el dispositivo. Es decir, se creará en éste una base de
datos local, SQLite con esas mismas tablas.
De aquí en más, ya sea que haya conexión, o no la haya, la aplicación siempre trabajará sobre la
base de datos local.
La aplicación en el dispositivo no accederá al servidor más que para sincronizar los datos de
ambas bases de datos.
Toda la capa de servicios que se encontraba en el server web, que contenía los data providers
para recuperar los datos y los business components para actualizar los datos de las tablas,
estarán ahora en el dispositivo; implementados en el lenguaje de la plataforma, accediendo a la
base de datos local, y compilados en el binario.
De esta manera todas las operaciones de CRUD serán siempre sobre la base de datos local y
nunca sobre la base de datos de server. El único contacto de la aplicación con el server será
para la sincronización.
Cuando se recupera la conectividad hay que sincronizar las bases de datos, es decir enviar y
recibir los cambios desde y hacia el dispositivo.
Siempre la sincronización será iniciada desde el dispositivo, puesto que el servidor no puede
saber cuándo el primero obtuvo conexión.
La información almacenada localmente puede sincronizarse con los datos que se encuentran en
el servidor (si es que así se desea; recordemos que también se puede querer nunca sincronizar o
sincronizar a pedido).
El proceso de enviar los datos que cambiaron en el dispositivo hacia el server se denomina:
Send
También los datos del servidor que cambiaron se envían al dispositivo para ser actualizados —
cada cierto tiempo o a demanda—.
El proceso de enviar los datos que cambiaron en el servidor, hacia el dispositivo se denomina:
Receive.
Cuando el dispositivo inicia el Send (que puede ser iniciado al momento de recuperar la
conexión, en forma manual a través del método Syncronization.Send o nunca): debe haber
armado una lista ordenada de las operaciones de insert, update y delete que fueron realizadas
desde la última sincronización. Es decir, aquellas operaciones que están pendientes.
Esa lista se le envía al proceso del lado del server. Este último debe recorrer ordenadamente esa
lista, y ejecutar la operación correspondiente sobre la base de datos… devolviendo al proceso del
lado del cliente el resultado.
Recordemos que ya sea que haya conexión, o que no la haya, en el cliente siempre se trabajará
sobre la base de datos local.
Todas las modificaciones realizadas sobre la base de datos local, son guardadas como “Eventos
de sincronización” en una tabla llamada GXPendingEvents
Esta tabla almacena ordenadamente todas las operaciones que se realizaron con Business
Components. Se almacena el nombre del BC donde se realizó la operación, el JSON del BC con
los datos del evento, el tipo de operación que se realizó (alta, baja o modificación) y el estado de
la misma.
Cada vez que el dispositivo ejecuta un business component, se almacena el evento y queda en
estado “pendiente de sincronización”.
Cuando se inicia el Send, el cliente traduce la lista de todos los eventos con estado “Pending” en
un SDT y lo envía al server. En el server está programado el procedimiento
GXOfflineEventReplicator que lee el SDT y realiza las tareas de CRUD respetando el orden de
las operaciones que vienen en el JSON del SDT.
La sincronización que permite al dispositivo recibir datos del server puede hacerse con una
granularidad: por Tabla o por Fila. Cuando la granularidad es By Table se lleva al dispositivo
todas las tablas que fueron modificadas desde la última sincronización. Cuando es By Row, se
llevan al dispositivo solamente aquellos registros que cambiaron de cada tabla, desde la última
sincronización.
La sincronización “by table” es útil en escenarios donde la cantidad de registros es poca, o
cambia con mucha frecuencia, ya que en este último caso es necesario llevar casi todo en cada
sincronización. Tiene la ventaja por sobre la sincronización “by row” de que el procesamiento que
requiere del lado del servidor es mucho menor.
Para determinar qué tablas fueron modificadas y por lo tanto deben enviarse al dispositivo, se
utiliza un hash que “identifica” el juego de datos de cada tabla.
Cuando un cliente pide sincronizar, envía al servidor los hashes de cada tabla, los cuales fueron
enviados por el servidor en la sincronización anterior. Para cada tabla, el servidor calcula un
nuevo hash con los datos actuales (luego de aplicar los filtros que aplican a ese dispositivo), y
solo envía datos de la tabla, cuando el nuevo hash es diferente del anterior, es decir, solamente
se envían datos de tablas que sufrieron modificaciones. Si la tabla no tuvo cambios desde la
última sincronización, entonces para esa tabla no se hace nada. Los mensajes se envían en
formato JSON.
En la primera sincronización, no hay datos en la base de datos local, por lo que se llevan todos
los datos de todas las tablas. (Observemos que esto no significa enviar todos los datos que están
en el server, porque los datos pueden definirse filtros en el objeto OfflineDatabase). Además de
guardarse los datos en la tablas, se guardan los hashes de cada tabla.
En las siguientes sincronizaciones, el dispositivo envía los hashes recibidos y el server envía
solamente los datos de las tablas que cambiaron. Cuando se reciben, se borra todo el contenido
de cada tabla y se reemplaza por el contenido recibido.
La sincronización “by row” solamente lleva al dispositivo aquellos registros que cambiaron desde
la última sincronización, por lo que primero se determina si la tabla cambió o no.
La forma de determinar cuáles tablas fueron modificadas, es exactamente la misma que se utiliza
en la sincronización “By Table”.
Una vez que se sabe que una tabla fue modificada, para saber cuáles fueron los registros
modificados, se calcula el hash de cada registro y se comparan con el hash correspondiente
almacenado previamente. Luego se envía al dispositivo, si la tabla fue modificada su nuevo hash
(igual que en la sicronización “By Table”) y tres listas: una para los registros nuevos, otra para los
modificados y otra para los eliminados.
Al llegar el JSON, se guarda el hash de cada tabla y sus listas se procesan en orden. Para los
registros nuevos, se hace un INSERT en la base de datos, y si falla por clave duplicada se hace
un UPDATE. Para los registros modificados se hace un UPDATE, y si no existe el registro se
hace el INSERT de los mismos.
Para los registros eliminados se hace un DELETE.
Un problema que la sincronización resuelve de manera automática, es el conflicto por
numeración de claves autonumeradas. Por autonumeradas se entiende tanto las claves que son
Autonumber, como las claves que se numeran mediante el uso de un Procedure o alguna regla
en la transacción.
Tomemos por ejemplo el caso del ejemplo de EventDay, que tenemos la transacción de Country
y la transacción de Speaker, en las que ambas claves primarias son autonumeradas.
Además, el atributo CountryId es clave foránea de la transacción Speaker.
Todos los datos quedan guardados en la base de datos local de cada dispositivo.
Una vez que se obtiene la conexión, uno de los dispositivos se sincroniza con el server, enviando
toda las operaciones que realizó a través de Business Components sobre la base de datos local.
Cuando el segundo dispositivo intenta sincronizar, se produce un conflicto porque se repite la
clave del país que está almacenado en el servidor.
Por último, actualiza los datos en el dispositivo. Para poder aplicar los cambios de clave en el
mismo, el servidor incluye en la respuesta que envía, la correspondencia entre los valores
enviados por el dispositivo y los valores con los que quedaron los datos en la base de datos del
servidor. Con esta información, el dispositivo actualiza las claves en las tablas locales, de forma
que queden coherentes con el servidor.
Los únicos conflictos de sincronización que se resuelven automáticamente son los de claves
autonumeradas. En cualquier otro conflicto se sustituye los datos del dispositivo con los datos del
servidor.
http://wiki.genexus.com/commwiki/servlet/wiki?23543
Una forma de minimizar los casos de conflictos por usar la misma clave, es utilizar un
identificador único.
En GeneXus disponemos del tipo de datos GUID para asignar a un identificador. Mediante su
propiedad Autogenerate GUID, podemos hacer que se genere automáticamente.
También disponemos de métodos y funciones para operar con este tipo de datos.
Esta API no se encuentra dentro del folder SmartDevicesAPI sino que es parte de la gramática.
Cuenta con los métodos Send y Receive para la sincronización, ServerStatus para determinar el
estado del server y ResetOfflineDatabase que retorna la base de datos local a su estado inicial,
ya sea haciendo un Create Database para vaciar las tablas o cargando una base datos pre-
cargada.
En el caso de la sincronización manual, por ejemplo, se puede crear un Panel for Smart Devices,
donde se programa el send y el receive, como vemos en la imagen.
Hasta ahora vimos qué son las aplicaciones conectadas, parcialmente conectadas y offline.
¿Pero cómo hacemos en GeneXus para construir una aplicación Offline?
Si queremos que una aplicación se ejecute en forma Offline, debemos asignar la propiedad
Connectivity Support de su objeto main, en el valor Offline. Este valor habilita la generación de la
aplicación en forma offline y se generá el código nativo necesario para que la aplicación pueda
ser ejecutada sin invocar a los servicios REST del servidor web.
Así, como el dashboard EventDay de nuestra aplicación es nuestro objeto main, modificamos su
propiedad Connectivity support pasándola a Offline.
La propiedad Connectivity Support está también a nivel de objetos que no son main. Su valor
puede ser Online, Offline o Inherit.
Todas las tablas que tengan la propiedad Connectivity support = Inherit, utilizadas en objetos SD
invocados desde el Main (directa o indirectamente), se van a crear en la base de datos local del
dispositivo. A no ser que se les configure que sean Online, en cuyo caso no se creará la tabla en
la base de datos local porque se accederá a la tabla del servidor a través de los servicios REST.
Si tenemos este árbol de invocaciones, entonces las tablas que se van a llevar al dispositivo son
todas las de los objetos Offline y los que heredan la conectividad offline del que los invocó.
Este objeto es el encargado de determinar cuándo se produce la sincronización, cuáles son las
tablas que se crearán en la BD local y también cuáles son los datos que se llevan a las mismas
cuando se sincronizan con las tablas del server.
Se ejecuta en el Server, antes de cada envío de datos al cliente, para sincronización (únicamente
en el Receive).
Este evento está pensado para inicialización de variables y algún otro procesamiento que se
debe hacer antes de la sincronización de tablas.
El uso de las Conditions es, al igual que en cualquier otro objeto GeneXus, para definir filtros
sobre los datos y solamente se utilizan para el Receive.
Los filtros se aplican a las tablas del server, para saber qué datos se llevan al dispositivo. Esto
implica que los datos en la base de datos local pueden ser un subconjunto de los datos de la
base de datos del server y como caso particular, que no hayan registros de una tabla específica,
en el dispositivo.
Son independientes y se aplican a la tabla extendida, por lo que GeneXus tiene la inteligencia de
determinar sobre qué tablas se aplican. En el listado de navegación se pueden ver las
condiciones que se aplican a cada tabla.
Para las expresiones de las condiciones se pueden utilizar variables predefinidas o definidas por
el desarrollador, en cuyo caso deben asignarse en el evento Start.
En este objeto es donde se configuran las propiedades que vimos para el Send y el Receive.
Para finalizar algunas puntualizaciones….
Si se tiene una aplicación Offline que usa GAM, hay que tener en cuenta que las credenciales
siempre estarán en el server, por lo tanto el login sólo se puede hacer estando Online.