[Xamarin.Forms] Uso de SQLite, múltiples tablas, relaciones y operaciones en cascada

Introducción

El trabajo con datos en dispositivos móviles se ha convertido ya en algo común y habitual en el desarrollo de aplicaciones. Existe una gran variedad de tipos de datos y formas de almacenamiento:

  • Archivos de texto. Texto plano o html cacheado en el espacio de almacenamiento aislado de la aplicación.
  • Imágenes. En el espacio de almacenamiento aislado de la aplicación o almacenadas en directorios conocidos del sistema.
  • Archivos serializados. Archivos XML o Json con objetos serializados.
  • Bases de datos. Cuando se requieren datos estructurados, obtener información más compleja con consultas avanzadas entre otro tipo de necesidades, la posibilidad de las bases de datos es la elección idónea.

Las ventajas de utilizar una base de datos son múltiples:

  • Almacenamiento estructurado con eficacia alta.
  • Posibilidad de utilizar consultas y aplicar filtros.
  • Posibilidad de reutilizar conocimientos de base de datos en la gestión de datos en nuestras aplicaciones móviles.

Introducción a SQLite

SQLite es un motor de base de datos Open Source utilizado en todas las plataformas móviles y adoptado tanto por Apple como Google como Microsoft. El uso de SQLite en aplicaciones móviles es una gran opción ya que:

  • La base de datos es pequeña y fácil de portar.
  • La base de datos se concentra en un pequeño archivo.
  • Implementa la mayor parte del estándar SQL92.

Arrancamos el proyecto

Comenzamos creando una aplicación Xamarin.Forms utilizando una librería NET Standard:

Nueva aplicación Xamarin.Forms usando Net Standard

Tras crear la aplicación, añadimos las carpetas básicas para aplicar el patrón MVVM además del paquete NuGet de Autofac para la gestión del contenedor de dependencias.

Estructura del proyecto

Con el proyecto y estructura base creada, vamos a añadir SQLite al proyecto. Para ello, vamos a usar NuGet. Vamos a añadir en cada proyecto de la solución la última versión disponible del paquete utilizando NuGet. El paquete a utilizar es sql-net-pcl, implementación Open Source con soporte a .NET.

sqlite-net-pcl

Tras añadir la referencia vamos a crear una interfaz que defina como obtener la conexión con la base de datos y abstraer la funcionalidad específica de cada plataforma. Trabajando con SQLite, el único trabajo específico a implementar en cada plataforma es determinar la ruta a la base de datos.

public interface IPathService
{
     string GetDatabasePath();
}

En Android, la implementación de IPathService nos permite obtener la ruta a la base de datos.

[assembly: Dependency(typeof(PathService))]
namespace TodoSqlite.Droid.Services.Sqlite
{
     public class PathService : IPathService
     {
          public string GetDatabasePath()
          {
               string path = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
               return Path.Combine(path, AppSettings.DatabaseName);
          }
     }
}

NOTA: Utilizamos el atributo assembly:Dependency para poder realizar la resolución de la implementación con DependencyService.

En iOS:

[assembly: Dependency(typeof(PathService))]
namespace TodoSqlite.iOS.Services.Sqlite
{
     public class PathService : IPathService
     {
          public string GetDatabasePath()
          {
                string docFolder = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
                string libFolder = Path.Combine(docFolder, "..", "Library", "Databases");

                if (!Directory.Exists(libFolder))
                {
                     Directory.CreateDirectory(libFolder);
                }

                return Path.Combine(libFolder, AppSettings.DatabaseName);
          }
     }
}

El archivo de la base de datos lo situamos dentro de la carpeta Library dentro del espacio de almacenamiento de la aplicación.

Y en UWP:

[assembly: Dependency(typeof(PathService))]
namespace TodoSqlite.UWP.Services.Sqlite
{
     public class PathService : IPathService
     {
          public string GetDatabasePath()
          {
               return Path.Combine(ApplicationData.Current.LocalFolder.Path, AppSettings.DatabaseName);
          }
     }
}

Todo listo para comenzar!

La definición de modelos

En nuestra aplicación, trabajaremos con elementos del listado ToDo, una única entidad sencilla.

public class TodoItem
{
     [PrimaryKey, AutoIncrement]
     public int Id { get; set; }
     public string Name { get; set; }
     public string Notes { get; set; }
     public bool Done { get; set; }
}

La gestión de campos especiales o relacionados las gestionamos mediante el uso de etiquetas. En nuestro ejemplo establecemos el campo Id como clave primaria gracias a la etiqueta PrimaryKey y además que autoincremente con el uso de AutoIncrement.

Trabajando con SQLite

Para trabajar con la base de datos utilizaremos DependencyService para obtener la implementación de IPathService y obtener la ruta a la base de datos en cada plataforma.

var databasePath = DependencyService.Get<IPathService>().GetDatabasePath();

Con la ruta de la base de datos, creamos una conexión:

var sqlCon = new SQLiteAsyncConnection(databasePath);

Comenzamos creando la tabla necesaria en la base de datos.

await _sqlCon.CreateTableAsync<TodoItem>().ConfigureAwait(false);

Continuamos con las operaciones básicas de CRUD. Para obtener la información almacenada en una tabla podemos acceder a la tabla y obtener el listado utilizando el método ToListAsync.

public async Task<IList<TodoItem>> GetAll()
{
     var items = new List<TodoItem>();
     using (await Mutex.LockAsync().ConfigureAwait(false))
     {
          items = await _sqlCon.Table<TodoItem>().ToListAsync().ConfigureAwait(false);
     }

     return items;
}

NOTA: Podemos realizar consultar SQL utilizando el método QueryAync.

A la hora de insertar, verificamos si estamos ante un registro existente o no, para realizar el registro de un nuevo elemento o actualizar uno existente con los métodos InsertAsync o UpdateAsync respectivamente.

public async Task Insert(TodoItem item)
{
      using (await Mutex.LockAsync().ConfigureAwait(false))
      {
           var existingTodoItem = await _sqlCon.Table<TodoItem>()
           .Where(x => x.Id == item.Id)
           .FirstOrDefaultAsync();

          if (existingTodoItem == null)
          {
               await _sqlCon.InsertAsync(item).ConfigureAwait(false);
          }
          else
          {
               item.Id = existingTodoItem.Id;
               await _sqlCon.UpdateAsync(item).ConfigureAwait(false);
          }
     }
}

Eliminar es una acción sencilla realizada con el método DeleteAsync.

await _sqlCon.DeleteAsync(item);

El resultado del ejemplo:

Tenéis el código fuente disponible e GitHub:

Ver GitHub

Múltiples tablas

Con lo visto hasta aquí, tenemos suficiente para gestionar una base de datos local en Android, iOS y Windows. Sin embargo, ¿cómo hacemos relaciones entre diferentes tablas?, ¿y operaciones en cascada que afecten a múltiples tablas a la vez?.

Con sqlite-net-pcl, podemos trabajar con diferentes tablas y realizar relaciones. Veamos un ejemplo:

public class Stock 
{ 
     [PrimaryKey, AutoIncrement] 
     public int Id { get; set; } 
     public string Symbol { get; set; } 
} 
 
public class Valuation 
{ 
     [PrimaryKey, AutoIncrement] 
     public int Id { get; set; } 
     [Indexed] 
     public int StockId { get; set; } 
     public DateTime Time { get; set; } 
     public decimal Price { get; set; } 
}

Es importante resaltar el uso de atributo Indexed. Estamos asociando las tablas Valuation y Stock.

Creamos ambas tablas utilizando el método CreateTable por cada tabla:

db.CreateTable<Stock>();		
db.CreateTable<Valuation>();

Y obtenemos la información relacionada con ambas tablas gracias al uso de queries:

return db.Query<Valuation> ("select * from Valuation where StockId = ?", stock.Id);

De esta forma obtendríamos los registros de la tabla Valuation dado un SotckId específico.

Esta es una forma simple de trabajar. Sin embargo, existe un wrapper de SQLite.NET llamado SQLite-Net Extensions que amplía funcionalidades como permitir gestionar relaciones (uno a uno, uno a varios, varios a uno y varios a varios), operaciones en cascada además de otras opciones interesantes de forma muy sencilla.

Vamos a regresar a nuestra aplicación de tareas. Para aprender como gestionar más de una tabla, vamos a añadir una segunda tabla de pasos en las tareas. De modo que, una tarea tendrá N pasos.

Veamos la creación de las tablas. Creamos una segunda tabla (recuerda, una clase) llamada Step:

[Table("Steps")]
public class Step
{
     [PrimaryKey, AutoIncrement]
     public int Id { get; set; }

     public string Name { get; set; }

     [ForeignKey(typeof(TodoItem))]
     public int WorkerId { get; set; }
}

Al crear directamente la definición, vamos a especificar relaciones entre tablas. En este caso, destacamos una propiedad que será la ForeignKey de la tabla TodoItem.

[Table("TodoItems")]
public class TodoItem
{
     [PrimaryKey, AutoIncrement]
     public int Id { get; set; }
     public string Name { get; set; }
     public string Notes { get; set; }
     public bool Done { get; set; }
     [OneToMany(CascadeOperations = CascadeOperation.CascadeInsert)]
     public List<Step> Steps { get; set; }
}

Gracias a las extensiones aportadas, podemos definir directamente la relación entre las tablas TodoItems y Steps. En este caso será un OneToMany. Por defecto, las operaciones no son recursivas. Podemos modificar este compartamiento estableciendo operaciones en cascada. En nuestro caso, usaremos CascadeOperations para establecer el insertar registros en cascada (al registrar un nuevo TodoItem, registraremos también sus Steps).

Para crear ambas tablas, al igual que con sqlite-net-pcl, utilizamos el método CreateTableAsync:

await _sqlCon.CreateTableAsync<Step>(CreateFlags.None).ConfigureAwait(false);
await _sqlCon.CreateTableAsync<TodoItem>(CreateFlags.None).ConfigureAwait(false);

Operaciones en cascada

A la hora de insertar registros, contamos con diferentes métodos en las extensiones de SQLite:

  • InsertWithChildren
  • InserOrReplaceWithChildren
  • InserAllWithChildren
  • InsertOrReplaceAllWithChildren

Su uso dependerá de si vamos a insertar o reemplazar y del número de registros.

NOTA: De cada método existe una version asíncrona.

await _sqlCon.InsertWithChildrenAsync(item, recursive: true).ConfigureAwait(false);

También existen otras opciones interesantes como:

  • Actualizar en cascada.
  • Eliminar en cascada.
  • Propiedades sólo de lectura.
  • Diferentes tipos de relaciones y relaciones inversas.
  • Etc.

Tenéis el código fuente disponible e GitHub:

Ver GitHub

Recordad que podéis dejar cualquier comentario, sugerencia o duda en los comentarios.

Más información

Anuncios

[Xamarin] Comparativa de SQLite y Realm

Sword-01-WFIntroducción

Tras la llegada de Realm a Xamarin con la promesa de una opción de base de datos sencilla y sobretodo con mejor rendimiento que el resto de opciones (incluida SQLite, la opción más usada y extendida), la duda es obvia…¿cuál usar?. En este artículo vamos a realizar unas pruebas básicas con las operaciones de CRUD para medir en tiempos el rendimiento exacto de cada opción. Además verificaremos otros aspectos como la facilidad de uso, el nivel de documentación y otros aspectos.

¿Os apuntáis?

Realm

Realm es una base de datos gratuita pensada para aplicaciones móviles, tabletas o wearables siendo una alternativa interesante a SQLite. Llega con el gran objetivo en mente de conseguir un alto rendimiento manteniendo una alta facilidad de uso.

SQLite

SQLite es un motor de base de datos Open Source utilizado en todas las plataformas móviles y adoptado tanto por Apple como Google como Microsoft. El uso de SQLite en aplicaciones móviles es una gran opción ya que:

  • La base de datos es pequeña y fácil de portar.
  • La base de datos se concentra en un pequeño archivo.
  • Implementa la mayor parte del estándar SQL92.

La aplicación para realizar pruebas

En nuestra aplicación necesitamos una única vista donde contaremos con botones para realizar las acciones básicas del CRUD junto a un área donde mostrar los resultados.

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 
             x:Class="SqliteVsRealm.Views.MainView"
             Title="SQLite VS Realm">
  <ContentPage.Content>
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="20"/>
        <RowDefinition Height="50"/>
        <RowDefinition Height="50"/>
        <RowDefinition Height="50"/>
        <RowDefinition Height="20"/>
        <RowDefinition Height="*"/>
      </Grid.RowDefinitions>
      <Button Grid.Row="1"
              Text="Insert 1000 Items"
              Command="{Binding InsertCommand}"/>
      <Button Grid.Row="2"
              Text="Query 1000 Items"
              Command="{Binding QueryCommand}"/>
      <Button Grid.Row="3"
              Text="Delete 1000 Items"
              Command="{Binding DeleteCommand}"/>
      <Editor Grid.Row="5"
              Text="{Binding Log}"
              FontSize="10"/>
    </Grid>
  </ContentPage.Content>
</ContentPage>

El resultado visual:

Nuestra aplicación para realizar pruebas

Nuestra aplicación para realizar pruebas

Enlazamos la View con la ViewModel estableciendo una instancia de la ViewModel a la propiedad BindingContext de la página.

BindingContext = App.Locator.MainViewModel;

En cuanto a cada botón, cada uno de ellos estará enlazado a un comando:

private ICommand _insertCommand;
private ICommand _queryCommand;
private ICommand _deleteCommand;

public ICommand InsertCommand
{
     get { return _insertCommand = _insertCommand ?? new DelegateCommand(InsertCommandExecute); }
}

public ICommand QueryCommand
{
     get { return _queryCommand = _queryCommand ?? new DelegateCommand(QueryCommandExecute); }
}

public ICommand DeleteCommand
{
     get { return _deleteCommand = _deleteCommand ?? new DelegateCommand(DeleteCommandExecute); }
}

private async void InsertCommandExecute()
{

}

private async void QueryCommandExecute()
{

}

private async void DeleteCommandExecute()
{

}

Sencillo, ¿cierto?.

La comparativa

A continuación, vamos a utilizar la aplicación creada para medir rendimiento de ambas opciones además de valorar otros aspectos de peso a la hora de determinar el uso de una u otra opción. Vamos a valorar aspectos como:

  • Facilidad de uso
  • Rendimiento
  • Documentación
  • Mantenimiento

Facilidad de uso

En el arranque de uso de SQLite y Realm tenemos que utilizar paquetes NuGet. En el caso de Realm todo viene empaquetado en un único paquete mientras que en el caso de SQLite utilizaremos varios paquetes (SQLite.Net-PCL, SQLite.Net.Core-PCL y SQLite.Net.Async-PCL).

Tras añadir los paquetes debemos realizar la configuración básica. En el caso de Realm es realmente reducida mientras que en SQLite si que necesitamos un poco de trabajo para especificar la ruta a la base de datos en cada plataforma para poder establecer la conexión.

public interface ISQLite
{
     SQLiteAsyncConnection GetConnection();
}

La implementación de ISQLite nos permite establecer la conexión con la base de datos.

[assembly: Dependency(typeof(SQLiteClient))]
namespace TodoSqlite.Droid.Services
{
    public class SQLiteClient : ISQLite
    {
        public SQLiteAsyncConnection GetConnection()
        {
            var sqliteFilename = "Todo.db3";
            var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);

            var path = Path.Combine(documentsPath, sqliteFilename);

            var platform = new SQLitePlatformAndroid();

            var connectionWithLock = new SQLiteConnectionWithLock(
                                         platform,
                                         new SQLiteConnectionString(path, true));

            var connection = new SQLiteAsyncConnection(() => connectionWithLock);

            return connection;
        }
    }
}

Por último, el trabajo con cada base de datos es realmente similar. Tenemos métodos para cada acción básica, CRUD y tareas de gestión y mantenimiento directamente en C#. Veamos como obtener datos de una tabla.

En Realm:

public IList<TodoItem> GetAll()
{
     var result = _realm.All<TodoItem>().ToList();

     return result;
}

En SQLite:

public async Task<IList<TodoItem>> GetAll()
{
     var items = new List<TodoItem>();
     using (await Mutex.LockAsync().ConfigureAwait(false))
     {
          items = await _sqlCon.Table<TodoItem>().ToListAsync().ConfigureAwait(false);
     }

     return items;
}

El número de líneas y opciones en este caso estan realmente parejas. Sin embargo, la preparación inicial es practicamente nula en el caso de Realm.

Ganador: Realm

Rendimiento

Llegamos a uno de los puntos fuertes o generalmente de mayor preocupación. Desde Realm se habla del rendimiento como una de sus principales bazas, pero…¿es realmente más rápido?, ¿cuánto más?.

A continuación, vamos a centrarnos en la lógica de cada botón de nuestra interfaz. A la hora de insertar datos:

private async void InsertCommandExecute()
{
     var watch = System.Diagnostics.Stopwatch.StartNew();
     for (int i = 0; i < 1000; i++)
     {
          await _sqliteService.Insert(new SqliteTodoItem
          {
               Id = i + 1,
               Name = string.Format("Name {0}", i + 1),
               Notes = string.Format("Notes {0}", i + 1),
               Done = false
          });
     }
     watch.Stop();

     Log += $"SQLite: INSERT 1000 items in {watch.ElapsedMilliseconds} milliseconds\n";

     watch.Restart();
     for (int i = 0; i < 1000; i++)
     {
          _realmService.Insert(new RealmTodoItem
          {
               Id = i + 1,
               Name = string.Format("Name {0}", i + 1),
               Notes = string.Format("Notes {0}", i + 1),
               Done = false
          });
      }
      watch.Stop();

      Log += $"Realm: INSERT 1000 items in {watch.ElapsedMilliseconds} milliseconds\n";
}

Insertamos 1000 elementos en cada una de las base de datos con los siguientes resultados de media:

  • SQLite: 2400 milisegundos
  • Realm 1800 milisegundos

A la hora de obtener los 1000 registros almacenados:

private async void QueryCommandExecute()
{
     var watch = System.Diagnostics.Stopwatch.StartNew();
     var sqliteResult = await _sqliteService.GetAll();
     watch.Stop();

     Log += $"SQLite: QUERY 1000 items in {watch.ElapsedMilliseconds} milliseconds\n";

     watch.Restart();
     var realmResult = _realmService.GetAll();
     watch.Stop();

     Log += $"Realm: QUERY 1000 items in {watch.ElapsedMilliseconds} milliseconds\n";
}

Los tiempos de media son:

  • SQLite: 50 milisegundos
  • Realm: 2 milisegundos

Por último, llegamos al momenos de eliminar registros:

private async void DeleteCommandExecute()
{
     var sqliteResult = await _sqliteService.GetAll();
     var watch = System.Diagnostics.Stopwatch.StartNew();
     foreach (var item in sqliteResult)
     {
          await _sqliteService.Remove(item);
     }
     watch.Stop();

     Log += $"SQLite: DELETE 1000 items in {watch.ElapsedMilliseconds} milliseconds\n";

     var realmResult = _realmService.GetAll();
     watch.Restart();
     foreach (var item in realmResult)
     {
          _realmService.Remove(item);
     }
     watch.Stop();

     Log += $"Realm: DELETE 1000 items in {watch.ElapsedMilliseconds} milliseconds\n";
}

Los resultados son:

  • SQLite: 2300 milisegundos
  • Realm: 1300 milisegundos
Resultados rendimiento

Resultados rendimiento

Los tiempos en general en Realm son más reducidos que con SQLite, por lo que podemos decir que efectivamente el rendimiento en general es mejor. Optimizando (transacciones, etc) podemos mejorar ligeramente el rendimiento en SQLite dejando los resultados aún más cercanos. Realm hace mejor el trabajo sin nada especial adicional.

Ganador: Realm

Documentación

Realm ha creado una documentación específica para desarrolladores Xamarin bastante completa. Sin embargo, si comparamos con SQLite y debido fundamentalmente a la maduración y uso de cada uno de ellos, SQLite cuenta con mayor documentación. Xamarin cuenta con documentación y ejemplos utilizando SQLite sumado a la comunidad a nivel de artículos y ejemplos, hace que hoy por hoy, el acceso y aprendizaje de cada opción este mejor documentada en SQLite.

Ganador: SQLite

Mantenimiento

Ya hemos revisado aspectos importante relacionados con el arranque en cada uno de ellos, el acceso inicial y facilidad de uso. Sin embargo, creo bastante positivo el análisis del mantenimiento en cada caso en proyectos de peso.

El primer punto de impacto radica en la forma de trabajo con modelos de Realm. Dado que deben heredar de RealmObject y la creación debe realizarse utilizando Realm.CreateObject impidiendo la creación mediante un constructor simple. Igualmente en listas se utiliza RealmList en lugar de POCOs como hace SQLite que sencillamente añade ciertas etiquetas para saber como trabajar correctamente en cada caso. De cara a una correcta gestión de dependencias y testing podemos llegar a tener problemas con Realm necesitando una duplicidad de modelos con sencillos POCOs. Es posible aunque afecta sin duda al punto de facilidad de uso.

El segundo punto importante que puede afectar es la falta de soporte a Async en Realm. Damos por hecho que se añadirá soporte en futuras versiones de Realm, sin embargo, en la versión actual la falta de métodos asíncronos puede afectar en la gestión y mantenimiento de nuestro código.

Ganador: SQLite

Conclusiones

Llegados a este punto, cuatro evaluciones, dos con SQLite como vencedor, otras dos para Realm. ¿Qué usar?. La respuesta en este caso, como en la mayoría de casos, es depende. Realm se muestra como una interesante alternativa diría que perfectamente válida en pequeños proyectos con una cantidad baja de modelos. Su facilidad de arranque, uso y rendimiento lo hacen idóneo. Sin embargo, ante proyectos de peso con un gran mantenimiento la orientación al menos por ahora, seguiría decantándose por SQLite. El apoyo de los principales actores en el ambito móvil y la comunidad, hace que sea la opción más madura, sólida y documentada disponible.

Más información

[Xamarin] Utilizando SQLite

Database-WFIntroducción

El trabajo con datos en dispositivos móviles se ha convertido ya en algo común y habitual en el desarrollo de aplicaciones. Existe una gran variedad de tipos de datos y formas de almacenamiento:

  • Archivos de texto. Texto plano o html cacheado en el espacio de almacenamiento aislado de la aplicación.
  • Imágenes. En el espacio de almacenamiento aislado de la aplicación o almacenadas en directorios conocidos del sistema.
  • Archivos serializados. Archivos XML o Json con objetos serializados.
  • Bases de datos. Cuando se requieren datos estructurados, obtener información más compleja con consultas avanzadas entre otro tipo de necesidades, la posibilidad de las bases de datos es la elección idónea.

Las ventajas de utilizar una base de datos son múltiples:

  • Almacenamiento estructurado con eficacia alta.
  • Posibilidad de utilizar consultas y aplicar filtros.
  • Posibilidad de reutilizar conocimientos de base de datos en la gestión de datos en nuestras aplicaciones móviles.

Introducción a SQLite

SQLite es un motor de base de datos Open Source utilizado en todas las plataformas móviles y adoptado tanto por Apple como Google como Microsoft. El uso de SQLite en aplicaciones móviles es una gran opción ya que:

  • La base de datos es pequeña y fácil de portar.
  • La base de datos se concentra en un pequeño archivo.
  • Implementa la mayor parte del estándar SQL92.

Preparando el entorno

Comenzamos creando una aplicación Xamarin.Forms utilizando una librería portable (PCL):

Nueva aplicación Xamarin.Forms

Nueva aplicación Xamarin.Forms

Tras crear la aplicación, añadimos las carpetas básicas para aplicar el patrón MVVM además del paquete NuGet de Unity para la gestión del contenedor de dependencias.

Estructura del proyecto

Estructura del proyecto

Con el proyecto y estructura base creada, vamos a añadir SQLite al proyecto. Realm esta disponible en NuGet. Vamos a añadir en cada proyecto de la solución la última versión disponible del paquete utilizando NuGet. El paquete a utilizar es SQLite.Net PCL, implementación Open Source compatible con librerías portables (PCL) con soporte a .NET y Mono.

SQLite.Net PCL

SQLite.Net PCL

Tras añadir la referencia vamos a crear una interfaz que defina como obtener la conexión con la base de datos y abstraer la funcionalidad específica de cada plataforma. Trabajando con SQLite, el único trabajo específico a implementar en cada plataforma es determinar la ruta a la base de datos y establecer la conexión.

public interface ISQLite
{
     SQLiteAsyncConnection GetConnection();
}

En Android, la implementación de ISQLite nos permite establecer la conexión con la base de datos.

[assembly: Dependency(typeof(SQLiteClient))]
namespace TodoSqlite.Droid.Services
{
    public class SQLiteClient : ISQLite
    {
        public SQLiteAsyncConnection GetConnection()
        {
            var sqliteFilename = "Todo.db3";
            var documentsPath = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal);

            var path = Path.Combine(documentsPath, sqliteFilename);

            var platform = new SQLitePlatformAndroid();

            var connectionWithLock = new SQLiteConnectionWithLock(
                                         platform,
                                         new SQLiteConnectionString(path, true));

            var connection = new SQLiteAsyncConnection(() => connectionWithLock);

            return connection;
        }
    }
}

NOTA: Utilizamos el atributo assembly:Dependency para poder realizar la resolución de la implementación con DependencyService.

En iOS, la implementación de ISQLite nos permite establecer la conexión con la base de datos. El archivo de la base de datos lo situamos dentro de la carpeta Library dentro del espacio de almacenamiento de la aplicación.

[assembly: Dependency(typeof(SQLiteClient))]
namespace TodoSqlite.iOS.Services
{
    public class SQLiteClient : ISQLite
    {
        public SQLiteAsyncConnection GetConnection()
        {
            var sqliteFilename = "Todo.db3";
            var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
            var libraryPath = Path.Combine(documentsPath, "..", "Library");
            var path = Path.Combine(libraryPath, sqliteFilename);

            var platform = new SQLitePlatformIOS();

            var connectionWithLock = new SQLiteConnectionWithLock(
                                          platform,
                                          new SQLiteConnectionString(path, true));

            var connection = new SQLiteAsyncConnection(() => connectionWithLock);

            return connection;
        }
    }
}

Todo listo para comenzar!

La definición de modelos

En nuestra aplicación, trabajaremos con elementos del listado ToDo, una única entidad sencilla.

public class TodoItem
{
     [PrimaryKey, AutoIncrement]
     public int Id { get; set; }
     public string Name { get; set; }
     public string Notes { get; set; }
     public bool Done { get; set; }
}

La gestión de campos especiales o relacionados las gestionamos mediante el uso de etiquetas. En nuestro ejemplo establecemos el campo Id como clave primaria gracias a la etiqueta PrimaryKey y además que autoincremente con el uso de AutoIncrement.

La interfaz de usuario

En nuestra aplicación contaremos con dos vistas, un listado de tareas y una vista de detalles para crear, editar o eliminar una tarea específica.

Comenzamos definiendo la vista principal. Tendremos un listado de tareas:

<ListView 
    ItemsSource="{Binding Items}" 
    SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
          <ViewCell.View>
            <StackLayout 
              Padding="20,0,20,0"                       
              Orientation="Horizontal"       
              HorizontalOptions="FillAndExpand">
              <Label Text="{Binding Name}"
                     VerticalTextAlignment="Center"
                     HorizontalOptions="StartAndExpand" />
              <Image Source="check.png"
                     HorizontalOptions="End"
                     IsVisible="{Binding Done}"/>
            </StackLayout>
          </ViewCell.View>
        </ViewCell>
      </DataTemplate>
     </ListView.ItemTemplate>
</ListView>

A parte de definir como se visualizará cada elemento de la lista definiendo el DataTemplate establecemos la fuente de información, propiedad ItemsSource enlazada a propiedad de la ViewModel que obtendrá los datos de la base de datos.

Además del listado, debemos añadir en nuestra interfaz una forma de poder insertar nuevas tareas. Para ello, una de las opciones más habituales e idóneas es utilizar una Toolbar.

<ContentPage.ToolbarItems>
    <ToolbarItem Name="Add" 
                 Command="{Binding AddCommand}"  >
      <ToolbarItem.Icon>
        <OnPlatform x:TypeArguments="FileImageSource"
                    Android="plus"
                    WinPhone="Assets/add.png" />
      </ToolbarItem.Icon>
    </ToolbarItem>
</ContentPage.ToolbarItems>

Añadimos un ToolbarItem que permitirá añadir elementos.

La clase Device es muy importante en Xamarin.Forms ya que nos permite acceder a una serie de propiedades y métodos con el objetivo de personalizar la aplicación según dispositivo y plataforma. Además de permitirnos detectar el tipo de dispositivo, podemos detectar la plataforma gracias a la enumeración Device.OS o personalizar elementos de la interfaz gracias al método Device.OnPlatform entre otras opciones. En nuestro ejemplo, personalizamos el icono de añadir en base a la plataforma.

Nuestra interfaz:

Vista principal

Vista principal

Enlazamos la View con la ViewModel estableciendo una instancia de la ViewModel a la propiedad BindingContext de la página.

BindingContext = App.Locator.TodoItemViewModel;

En la ViewModel contaremos con una propiedad pública para definir el listado de tareas, además de la tarea seleccionada (utilizada para la navegación):

private ObservableCollection<TodoItem> _items;
private TodoItem _selectedItem;

public ObservableCollection<TodoItem> Items
{
     get { return _items; }
     set
     {
          _items = value;
          RaisePropertyChanged();
     }
}

public TodoItem SelectedItem
{
    get { return _selectedItem; }
    set
    {
          _selectedItem = value;
    }
}

Añadimos elementos con un comando disponible en la ViewModel.

private ICommand _addCommand;

public ICommand AddCommand
{
     get { return _addCommand = _addCommand ?? new DelegateCommand(AddCommandExecute); }
}

private void AddCommandExecute()
{

} 

Al pulsar y lanzar el comando, navegaremos a la vista de detalles.

_navigationService.NavigateTo<TodoItemViewModel>(_selectedItem);

Si creamos un nuevo elemento pasaremos como parámetro una nueva entidad de TodoItem, en caso de seleccionar una existente, pasaremos el seleccionado disponible en la propiedad SelectedItem.

Definimos la interfaz de la vista de detalles:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="TodoRealm.Views.TodoItemView"
             Title="{Binding Name}">
  <StackLayout 
    VerticalOptions="StartAndExpand" 
    Padding="20">
    <Label 
      Text="Name" />
    <Entry 
      Text="{Binding Name}"/>
    <Label 
      Text="Notes" />
    <Entry 
      Text="{Binding Notes}"/>
    <Label 
      Text="Done" />
    <Switch 
      x:Name="DoneEntry" 
      IsToggled="{Binding Done, Mode=TwoWay}"/>
    <Button 
      Text="Save"
      Command="{Binding SaveCommand}"/>
    <Button 
      Text="Delete"
      Command="{Binding DeleteCommand}"/>
    <Button 
      Text="Cancel"
      Command="{Binding CancelCommand}"/>
  </StackLayout>
</ContentPage>

Añadimos cajas de texto para poder editar toda la información de una tarea además de botones para poder guardar, borrar o cancelar y navegar atrás.

El resultado:

Detalle

Detalle

Para enlazar la información de un elemento seleccionado, debemos capturar la información enviada en la navegación. Creamos una propiedad pública para enlazar con la UI de la tarea:

private TodoItem _item;

public TodoItem Item
{
     get { return _item; }
     set { _item = value; }
}

¿Cómo capturamos el elemento seleccionado en la navegación?. Utilizamos el método OnAppearing para capturar el parámetro NavigationContext.

public override void OnAppearing(object navigationContext)
{
     var todoItem = navigationContext as TodoItem;

     if (todoItem != null)
     {
          Item = todoItem;
     }

     base.OnAppearing(navigationContext);
}

En cuanto a cada botón, cada uno de ellos estará enlazado a un comando:

private ICommand _saveCommand;
private ICommand _deleteCommand;
private ICommand _cancelCommand;

public ICommand SaveCommand
{
     get { return _saveCommand = _saveCommand ?? new DelegateCommand(SaveCommandExecute); }
}

public ICommand DeleteCommand
{
     get { return _deleteCommand = _deleteCommand ?? new DelegateCommand(DeleteCommandExecute); }
}

public ICommand CancelCommand
{
     get { return _cancelCommand = _cancelCommand ?? new DelegateCommand(CancelCommandExecute); }
}

private void SaveCommandExecute()
{

}

private void DeleteCommandExecute()
{

}

private void CancelCommandExecute()
{

}

Trabajando con SQLite

Para trabajar con la base de datos utilizaremos DependencyService para obtener la implementación de ISQLite y obtener una conexión.

private SQLiteAsyncConnection _sqlCon;
_sqlCon = DependencyService.Get<ISQLite>().GetConnection();

Para almacenar nuestras tareas, comenzamos creando la tabla necesaria en la base de datos.

public async void CreateDatabaseAsync()
{
     using (await Mutex.LockAsync().ConfigureAwait(false))
     {
          await _sqlCon.CreateTableAsync<TodoItem>().ConfigureAwait(false);
     }
}

Utilizamos el método CreateTableAsync<>() para crear la tabla.

Continuamos con las operaciones básicas de CRUD. Para obtener la información almacenada en una tabla podemos acceder a la tabla y obtener el listado utilizando el método ToListAsync.

public async Task<IList<TodoItem>> GetAll()
{
     var items = new List<TodoItem>();
     using (await Mutex.LockAsync().ConfigureAwait(false))
     {
          items = await _sqlCon.Table<TodoItem>().ToListAsync().ConfigureAwait(false);
     }

     return items;
}

NOTA: Podemos realizar consultar SQL utilizando el método QueryAync.

A la hora de insertar, verificamos si estamos ante un registro existente o no, para realizar el registro de un nuevo elemento o actualizar uno existente con los métodos InsertAsync o UpdateAsync respectivamente.

public async Task Insert(TodoItem item)
{
     using (await Mutex.LockAsync().ConfigureAwait(false))
     {
          var existingTodoItem = await _sqlCon.Table<TodoItem>()
                        .Where(x => x.Id == item.Id)
                        .FirstOrDefaultAsync();

          if (existingTodoItem == null)
          {
               await _sqlCon.InsertAsync(item).ConfigureAwait(false);
          }
          else
          {
               item.Id = existingTodoItem.Id;
               await _sqlCon.UpdateAsync(item).ConfigureAwait(false);
          }
     }
}

Eliminar es una acción sencilla realizada con el método DeleteAsync.

public async Task Remove(TodoItem item)
{
     await _sqlCon.DeleteAsync(item);
}

Tenéis el código fuente disponible e GitHub:

Ver GitHub

Recordad que podéis dejar cualquier comentario, sugerencia o duda en los comentarios.

Más información

WinRT. Utilizando SQLite.

Introducción

El SDK de WinRT al contrario que el de Windows Phone (nos facilitaba el trabajo con SQL Compact junto al uso de LINQ to SQL desde la actualización a Mango) no nos proporciona soporte directo para trabajar con base de datos locales. En la entrada actual vamos a centrarnos en como solucionar este problema utilizando SQLite para WinRT.

SQLite es un sistema de gestión de bases de datos relacional. A diferencia de los motores de base de datos convencionales con la arquitectura cliente-servidor, SQLite es independiente, ya que no se comunica con un motor de base de datos,sino que las librerías de SQLite pasan a integrar la aplicación.

Instalar el paquete SQLite para WinRT

Como siempre solemos hacer vamos a realizar un ejemplo lo más simple posible pero que nos sea válida para lograr nuestros objetivos. La plantilla selecciona para realizar el ejemplo lo más simple posible será “Blank Application”.

Lo primero que debemos hacer para poder trabajar con SQLite en nuestras aplicaciones para Windows 8 será instalar el paquete SQLite ya que por defecto SQLite no viene incluido en Windows 8. Para ello, nos dirigimos al menu Herramientas del Visual Studio. Elegimos la opción “Extensiones y actualizaciones…”:

A continuación, elegimos la opción “En línea” del menu situado a la izquierda y buscamos por la palabra “sqlite”. Seleccionaremos la opción “SQLite for Windows Runtime”:

Tras pulsar el botón “Descargar” obtendremos el paquete.

NOTA: Tras instalar el paquete para poder comenzar a utilizar debemos reiniciar Visual Studio.

Usar SQLite

Ya tenemos el paquete listo para usar. A continuación, debemos añadir la referencia a la librería en nuestro proyecto. Para ello, hacemos clic derecho sobre las referencias y seleccionamos “Añadir referencia”:

Nos aparecerá una ventana como la que puedes ver en la captura superior. Debemos elegir la opción Windows\Extensiones del menu de la izquierda. A continuación, debemos marca la opción “SQLite for Windows Runtime” junto a “Microsoft Visual C++ Runtime Package”.

Llegamos al primer gran inconveniente por ahora que tenemos al utilizar SQLite. Si pruebas a compilar en este punto verás un error como el siguiente:

¿Que ocurre?

Simple, debemos compilar el proyecto en X86, X64 o ARM para que funcione todo correctamente. Es decir, si queremos que nuestra aplicación funcione en cualquier arquitectura debemos generar un paquete por cada una de ellas.

Por último, para facilitarnos el trabajo, vamos a añadir a nuestro proyecto un pequeño Wrapper que utilizaremos para realizar llamadas a la API de SQLite llamado “sqlite-net”. Es totalmente compatible con WinRT, nos permite el uso de TPL (Task, async, await), etc.

Podemos obtener sqlite-net desde NuGet.

PM> Install-Package sqlite-net

Tras instalar sqlite-net se ños añadirán dos clases a nuestro proyecto:

  • SQLite.cs 
  • SQLiteAsync.cs

NOTA: Recomiendo echarle un vistazo al siguiente video creado por Tim Heuer donde se guia paso a paso en el proceso que acabamos de realizar (además recomiendo la visita a su blog, enormes artículos os esperan):

Ya tenemos lo necesario. Manos a la obra. Los primero que debemos hacer es preparar los datos que vamos a almacenar. En este ejemplo vamos a trabajar con libros. Por lo tanto, vamos a crear una clase Book con sus correspondientes propiedades públicadas acompañadas de distintos atrtibutos (todos definidos dentro del namespace SQLite) que nos permiten identificar al Primary Key, tipo del campo, etc:

public class Book
{
[SQLite.AutoIncrement, SQLite.PrimaryKey]
public int ID { get; set; }

[SQLite.MaxLength(256)]
public string Title { get; set; }

[SQLite.MaxLength(256)]
public string Author { get; set; }
}

Una vez preparado el tipo de información a almacenar debemos comenzar a crear nuestra base de datos SQLIte. Definimos la ruta y nombre de la base de datos (en nuestro ejemplo “books.sqlite”):

var dbPath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "books.sqlite");

Vamos a crear la tabla de libros (Book):

using (var db = new SQLite.SQLiteConnection(dbPath))
{
     db.CreateTable<Book>();
}

Muy sencillo, hemos pasado la ruta completa donde deseamos crear la base de datos al método SQLiteConnection para crear la conexión con la base de datos. Posteriormente para crear la tabla llamamos al método CreateTable.

NOTA: Hemos utilizado en nuestro ejemplo el método CreateTable oara crear la tabla. Sin embargo, también tenemos disponible el método equivalente asíncrono CreateTableAsync.  Es algo a destacar. SQLite para Windows 8 permite utilizar TPL (es decir, Task Parallel Library: async, await, etc).

Continuamos. Llegamos a un punto clave. ¿Cómo guardamos información en la tabla que acabamos de crear?

Fácil, vamos a utilizar el método Insert pasandole instancias de nuestra tabla Book:

db.RunInTransaction(() =>
{
     db.Insert(new Book() { Title = "Windows Phone 7.5 Mango - Desarrollo Silverlight", Author = "Josué Yeray Julián" });
     db.Insert(new Book() { Title = "Trabajando en equipo con Visual Studio ALM ", Author = "Bruno Capuano" });
});

Como podéis ver en nuestro ejemplo, no estamos limitados a una única insercción por transacción.

NOTA: Al igual que en casos anteriores. Tenemos el método equivalente asíncrono en este caso para insertar InsertAsync.

Continuamos. Podemos utilizar LINQ para por ejemplo realizar consultas como el número de elementos insertados o los valores del primer o último registro insertado:

int NumBooks1 = db.Table<Book>().Count();

Sin embargo, si en determinadas ocasiones echas en falta ejecutar una consulta SQL no tenemos limitación ninguna:

int NumBooks2 = db.ExecuteScalar<int>("SELECT COUNT(ID) FROM Book");

En ambos casos obtenemos lo mismo (en nuestro ejemplo un entero de valor 2).

El método completo utilizado en nuestro pequeño ejemplo quedaría:

private async void InitDB()
{
     var dbPath = Path.Combine(Windows.Storage.ApplicationData.Current.LocalFolder.Path, "books.sqlite");
     using (var db = new SQLite.SQLiteConnection(dbPath))
     {
          db.CreateTable<Book>();

          db.RunInTransaction(() =>
          {
               db.Insert(new Book() { Title = "Windows Phone 7.5 Mango - Desarrollo Silverlight", Author = "Josué Yeray Julián" });
               db.Insert(new Book() { Title = "Trabajando en equipo con Visual Studio ALM ", Author = "Bruno Capuano" });
          });

          int NumBooks1 = db.Table<Book>().Count();
          int NumBooks2 = db.ExecuteScalar<int>("SELECT COUNT(ID) FROM Book");

          MessageDialog message = new MessageDialog(string.Format("Número de libros: {0}", NumBooks1));
          await message.ShowAsync();
     }
}

Puedes descargar el ejemplo realizado:

Espero que lo visto en esta entrada os sea de utilidad. En próximas entradas profundizaremos más en el asunto. También analizaremos como utilizar SQLite en Windows Phone. Cualquier duda o sugerencia podéis plantearlas en los comentarios.

Conclusiones

SQLite para Windows 8 nos aporta una correcta implementación en un área importante en la que teníamos un enorme hueco. Nos porporciona una base de datos relacional local para nuestras aplicaciones. Es Open Source y multiplataforma. Esto nos da cierta ventaja en caso de querer portar nuestra aplicación a otras plataformas móviles. Implementa muy bien TPL facilitandonos mucho el acceso a datos de manera asíncrona sin interferir en la interfaz de usuario. Por contra, no todo iba a ser positivo. Ahora mismo necesitamos generar un paquete de nuestra aplicación por cada plataforma (X64, X86 y ARM). Una vez en la Windows Store no afecta a nuestros usuarios pero si tenemos que tenerlo en cuenta de cara al desarrollo.

Más información