Persistencia de datos en Xamarin usando SQLite


Las aplicaciones moviles del dia de hoy, a pesar de ser diseñadas pensando en estar conectadas y es justamente esta conexion la que las hace tan utiles necesitan muchas veces funcionar de forma reducida sin conexion. Por otro lado el almacenamiento local tambien nos ayuda a reducir las llamadas a los servicios web y de esta forma reducir el consumo de datos a nuestros usuarios.

En Xamarin tenemos varias opciones de almacenamiento local, entre las bases de datos que estan disponibles tenemos SQLite.

Ventajas de usar SQLite

  • Facil de integrar con proyectos existentes de Xamarin.Android, Xamarin.iOS o Xamarin.Forms.

  • Este wrapper sobre SQLite es rapido y eficiente.

  • API simple para ejecutar operaciones CRUD y consultas de forma segura y obtener los resultados de las consultas de forma fuertemente tipada.

  • Trabaja con tu modelo de datos sin estar forzado a cambiar tus clases (A small reflection-driven ORM layer).

Como agregar SQLite a nuestra aplicacion en Xamarin

  1. Primero debemos instalar el paquete sqlite-net-pcl en todos el proyecto PCL (cross-platform) y en los proyectos especificos de plataforma (Xamarin.Android, Xamarin.iOS y UWP). Si no puedes instalar el paquete asegurate de que tu libreria compartida sea Profile 259 (.NET 4.5, Windows 8.0, Windows Phone Silverlight 8.0, Windows Phone 8.1).

  1. Ahora necesitamos definir una interfaz que abstraiga la creacion de la cadena de conexion, pues la ubicacion de la base de dato local varia por plataforma. Agregare una interfaz llamada ISQLitePlatform a mi proyecto PCL.

     using SQLite;
     namespace SQLiteDemo.Services
     {
         public interface ISQLitePlatform
         {
             SQLiteConnection GetConnection();
             SQLiteAsyncConnection GetConnectionAsync();
         }
     }
    
  2. Ahora implementaremos esta interfaz en cada proyecto especifico, comencemos con Android.

     using System.IO;
     using SQLite;
     using SQLiteDemo.Droid.Services;
     using SQLiteDemo.Services;
     [assembly: Xamarin.Forms.Dependency(typeof(AndroidSQLitePlatform))]
     namespace SQLiteDemo.Droid.Services
     {
         public class AndroidSQLitePlatform: ISQLitePlatform
         {
             private string GetPath()
             {
                 var dbName = "somostechies.db3";
                 var path = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), dbName);
                 return path;
             }
             public SQLiteConnection GetConnection()
             {
                 return new SQLiteConnection(GetPath());
             }
             public SQLiteAsyncConnection GetConnectionAsync()
             {
                 return new SQLiteAsyncConnection(GetPath());
             }
         }
     }
    
  3. Ahora en iOS...

     using System;
     using System.IO;
     using SQLite;
     using SQLiteDemo.iOS.Services;
     using SQLiteDemo.Services;
    
     [assembly: Xamarin.Forms.Dependency(typeof(iOSSQLitePlatform))]
     namespace SQLiteDemo.iOS.Services
     {
         public class iOSSQLitePlatform : ISQLitePlatform
         {
             private string GetPath()
             {
                 var dbName = "somostechies.db3";
                 string personalFolder =Environment.GetFolderPath(Environment.SpecialFolder.Personal);
         string libraryFolder =Path.Combine(personalFolder, "..", "Library");
                 var path = Path.Combine(libraryFolder, dbName);
                 return path;
             }
             public SQLiteConnection GetConnection()
             {
                 return new SQLiteConnection(GetPath());
             }
             public SQLiteAsyncConnection GetConnectionAsync()
             {
                 return new SQLiteAsyncConnection(GetPath());
             }
         }
     }
    
  4. En Universal Windows Platform...

     using System.IO;
     using Windows.Storage;
     using SQLite;
     using SQLiteDemo.Services;
     using SQLiteDemo.UWP.Services;
     [assembly: Xamarin.Forms.Dependency(typeof(WindowsSQLitePlatform))]
     namespace SQLiteDemo.UWP.Services
     {
         public class WindowsSQLitePlatform: ISQLitePlatform
         {
             private string GetPath()
             {
                 var dbName = "somostechies.db3";
                 var path = Path.Combine(ApplicationData.Current.LocalFolder.Path, dbName);
                 return path;
             }
             public SQLiteConnection GetConnection()
             {
                 return new SQLiteConnection(GetPath());
             }
             public SQLiteAsyncConnection GetConnectionAsync()
             {
                 return new SQLiteAsyncConnection(GetPath());
             }
         }
     }
    
  5. En este punto cabe resaltar que Xamarin.Forms posee un DependecyService que nos permite resolver componentes que existen en nuestros proyectos especificos de plataforma, resueltos a traves de nuestra interfaz que abstraimos y este atributo, mas detalle aqui.

  1. Ahora basta con resolver nuestro ISQLitePlatform y obtener una conexion para empezar a trabajar con SQLite.

      var platform = DependencyService.Get<ISQLitePlatform>();
      SQLiteConnection db = _platform.GetConnection();
    

Por ejemplo, podemos generar la tabla en nuestra base de datos llamando al metodo CreateTable:

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

Puedes insertar filas en la base de datos usando Insert. Si la tabla contiene una llave primaria auto-incremented, entonces el valor estara disponible despues del insert.

    var s = db.Insert(new Stock() {
	   Symbol = symbol
    });
    Debug.WriteLine("{0} == {1}", s.Symbol, s.Id);

Y hay metodos similares para Update y Delete.

La forma mas rapida de consultar por datos es usando el metodo Table. Este metodo puede tomar predicados WHERE y/o agregar sentencias ORDER BY:

    var query = db.Table<Stock>().Where(v => v.Symbol.StartsWith("A"));

    foreach (var stock in query)
       Debug.WriteLine("Stock: " + stock.Symbol);

Tambien podemos consutlar la base de datos a bajo nivel usando el metodo Query:

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

El parametro generico del metodo Query especifica el tipo de objeto a crear para cada fila. Esta puede ser una clase de tabla o cualquier otra clase cuyas propiedades publicas encajen con las columnas retornadas por la query. Por ejemplo:

    public class Val 
    {
       public decimal Money { get; set; }
       public DateTime Date { get; set; }
    }

    ...

    return db.Query<Val> ("select 'Price' as 'Money', 'Time' as 'Date' from Valuation where StockId = ?", stock.Id);

Asi mismo, podemos hacer updates a bajo nivel usando el metodo
You can perform low-level updates of the database using the Execute method.

  1. Ahora bien, podemos crear un repositorio como abstraccion para aplicaciones un poco mas compleja en las que deseen una mejor separacion de conceptos.

     using System.Collections.Generic;
     using System.Threading.Tasks;
     using Xamarin.Forms;
    
     namespace SQLiteDemo.Services
     {
         public class Repository<T> where T:class ,new()
         {
             private readonly ISQLitePlatform _platform;
             public Repository(ISQLitePlatform platform)
             {
                 _platform = platform;
                 var con = _platform.GetConnection();
                 con.CreateTable<T>();
                 con.Close();
             }
             public Repository()
             {
                 _platform = DependencyService.Get<ISQLitePlatform>();
                 var con = _platform.GetConnection();
                 con.CreateTable<T>();
                 con.Close();
             }
             public async Task<bool> AddItemAsync(T item)
             {
                 return (await _platform.GetConnectionAsync().InsertAsync(item)) > 0;
             }
             public async Task<bool> UpdateItemAsync(T item)
             {
                 return (await _platform.GetConnectionAsync().UpdateAsync(item)) > 0;
             }
             public async Task<bool> DeleteItemAsync(T item)
             {
                 return (await _platform.GetConnectionAsync().DeleteAsync(item)) > 0;
             }
             public async Task<IEnumerable<T>> GetItemsAsync()
             {
                 return await _platform.GetConnectionAsync().Table<T>().ToListAsync();
             }
         }
     }
    

Veamoslo en accion...

Y si desean el codigo usado en la demo pueden descargarlo de Github.

Capacidades del ORM incluido en SQLite PCL

El ORM incluido en el paquete de SQLite es capz de tomar una clase .NET y convertirla a una tabla. Esto lo logra examinando todas las propiedades publicas de tu clase y apoyandose en atributos que utilizamos para especificar detalles de las columnas.

Los siguientes atributos son usados:

  • PrimaryKey ,esta propiedad es la llave primaria de la tabla. Solo se soporta llaves primarias de una sola columna.

  • AutoIncrement , esta propiedad es automaticamente generada por la base de datos al momento de ser insertada, la propiedad debe ser un entero y estar marcada como PrimaryKey tambien.

  • Indexed , esta propiedad tendra un indice creado.

  • MaxLength If this property is a String then MaxLength is used to specify the varchar max size. The default max length is 140.

  • Ignore , esta propiedad no formara parte de la tabla.

Los siguientes tipos de datos son soportados en propiedades:

  • Integers son almacenados usando integer o bigint en SQL.

  • Boolean son almacenados como integers con el valor 1 designado como true mientras que todos los otros valores son false.

  • Enums son almacenaddos como integers usando el valor del enum.

  • Singles y Doubles punto flotantes son almacenados como float.

  • Strings son almacenados como varchars con un tamaño maximo especificado por el atributo MaxLength. Si el atributo MaxLength no es especificado el tamaño por defecto es 140.

  • DateTimes son almacenados datetime in SQL and estan sujetos a la precision ofrecida por SQLite.

  • Byte arrays byte[] son almacenados como columnas blob.

  • Guid son almacenados como columnas varchar(36).