Integración de SQLite en Aplicaciones IONIC con Capacitor

Hola a todos, hoy os voy a explicar como podemos conectar sqlite con IONIC.

En IONIC podemos usar sqlite que nos permite hacer bases de datos locales, esto en móviles es muy útil, ya que nos permite gestionar de forma fácil y local datos de forma rápida, algo que en un móvil es muy necesario.

Crear proyecto IONIC

Primero, debemos crear nuestro proyecto IONIC, tenemos un tutorial aquí:

IONIC 7 – Guía sobre como crear tu primera aplicación

Instalar sqlite en IONIC

Después, necesitamos instalar la dependencia sqlite oficial de IONIC, este es el repositorio oficial.

$ npm install --save @capacitor-community/sqlite

Te recomiendo instalar @capacitor/preferences y @capacitor/device

Esto no es necesario pero en el fichero capacitor.config.json podemos personalizar la base de datos sqlite:

import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.jeep.app.ionic7.angular.sqlite',
  appName: 'ionic7-angular-sqlite-starter',
  webDir: 'www',
  server: {
    androidScheme: 'https'
  },
  plugins: {
    CapacitorSQLite: {
      iosDatabaseLocation: 'Library/CapacitorDatabase',
      iosIsEncryption: true,
      iosKeychainPrefix: 'angular-sqlite-app-starter',
      iosBiometric: {
        biometricAuth: false,
        biometricTitle : "Biometric login for capacitor sqlite"
      },
      androidIsEncryption: true,
      androidBiometric: {
        biometricAuth : false,
        biometricTitle : "Biometric login for capacitor sqlite",
        biometricSubTitle : "Log in using your biometric"
      },
      electronIsEncryption: true,
      electronWindowsLocation: "C:\\ProgramData\\CapacitorDatabases",
      electronMacLocation: "/Volumes/Development_Lacie/Development/Databases",
      electronLinuxLocation: "Databases"
    }
  }
};
export default config;

Ejecutar sql en el navegador

Antes de seguir, para probar en web debemos hacer algunas cosas más, ya que sqlite no se puede ejecutar en un navegador pero si lo podemos adaptar con indexDB.

$ npm install sql.js

Vamos a node_modules/sql.js/dist y copiamos el fichero sql-wasm.wasm a nuestra carpeta assets. Esto es necesario, ya que será quien ejecutará las peticiones sql que hagamos en web.

IMPORTANTE: sino lo copias, más adelante te saldrá un error indicando que lo necesitas.

Preparando sqlite en web con jeep-sqlite

A continuación, debemos instalar jeep-sqlite, que es un componente que nos permite utilizar sqlite en el navegador, solo debe ser ejecutado cuando estemos en modo web.

$ npm install @jeep-sqlite

Después, vamos al app.module y añadimos las siguientes lineas antes de ngModule


import { defineCustomElements as jeepSqlite } from 'jeep-sqlite/loader'

jeepSqlite(window)

Y en el modulo debemos añadir la propiedad schemas:

 schemas: [
   CUSTOM_ELEMENTS_SCHEMA
 ]

Ese CUSTOM_ELEMENTS_SCHEMA viene de @angular/core

Asi se quedaria:

import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { HttpClientModule } from '@angular/common/http';

import { defineCustomElements as jeepSqlite } from 'jeep-sqlite/loader'

jeepSqlite(window)

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, HttpClientModule],
  providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
  bootstrap: [AppComponent],
  schemas: [
    CUSTOM_ELEMENTS_SCHEMA
  ]
})
export class AppModule {}

Posteriormente, en el app.component.html, colocaremos el componente jeep-sqlite. Crearemos un par de atributos (load y isWeb).


  
  <!-- Solo en web -->
  

Seguidamente, en el app.component.ts, necesitaremos indicar cuando estamos en modo web.

Esto lo haremos cuando nuestro dispositivo este listo y podamos acceder a su información usando el plugin Device de Capacitor.

import { Component } from '@angular/core';
import { Device } from '@capacitor/device';
import { Platform } from '@ionic/angular';
import { SqliteService } from './services/sqlite.service';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {

  public isWeb: boolean;
  public load: boolean;

  constructor(
    private platform: Platform,
    private sqlite: SqliteService) {
    this.isWeb = false;
    this.load = false;
    this.initApp();
  }

  initApp(){

    this.platform.ready().then( async () => {

      // Comprobamos si estamos en web
      const info = await Device.getInfo();
      this.isWeb = info.platform == 'web';

    })

  }
}

Servicio SQLITE

Ahora, nos crearemos un servicio llamado llamado sqlite.service.ts.

En este servicio, comprobaremos si la base de datos esta creada o si debemos crear la base de datos, indicando desde app.component que la base de datos esta preparada.

Tendremos 4 atributos:

  • dbReady: Observable uqe nos indicará si la base de datos esta preparada.
  • isWeb: Indica si estamos en modo web o no. Cuando es web debemos almacenarlo de forma diferente.
  • isIOS: Indica si estamos usando un IOS o no. Servirá cuando hagamos el CRUD.
  • dbName: Indica el nombre de la base de datos que estamos usando.

Tendremos varios métodos:

  • init: según la plataforma usada, realizaremos una acción u otra.
  • setupDatabase: comprobamos si debemos crear la base de datos o recuperarla.
  • downloadDatabase: en el caso de que tengamos que sea la primera vez que tengamos que crear la base de datos, la descargaremos del fichero assets/db.json, este fichero contiene la información de la base de datos.
  • getDbName: Obtiene el nombre de la base de datos que estamos usando.
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SqliteService {

  // Atributos

  // Observable para comprobar si la base de datos esta lista
  public dbReady: BehaviorSubject;
  // Indica si estamos en web
  public isWeb: boolean;
  // Indica si estamos en IOS
  public isIOS: boolean;
  // Nombre de la base de datos
  public dbName: string;

  constructor(
    private http: HttpClient
  ) {
    this.dbReady = new BehaviorSubject(false);
    this.isWeb = false;
    this.isIOS = false;
    this.dbName = '';
  }

}

Ejemplo del contenido de db.json

{
    "database": "languages.db",
    "version": 1,
    "encrypted": false,
    "mode": "full",
    "tables": [
        {
            "name": "languages",
            "schema": [
                {
                    "column": "name",
                    "value": "TEXT NOT NULL PRIMARY KEY"
                }
            ]
        }
    ]
}

Método init del servicio SQLITE

Obtenemos la info del dispositivo y una variable sqlite para almacenar CapacitorSQLite, esto se debe a que tenemos que llamar algunos métodos que no están «oficialmente», esto es probable que lo cambien en un futuro no muy lejano. Al final, comprobaremos si la base de datos se ha iniciado o no. Según el tipo de plataforma, realizaremos diferentes acciones:

    • android: pedimos permisos al usuario.
    • web: en modo web, necesitamos iniciar la webstore.
    • IOS: ponemos el atributo isIOS a true, lo usaremos al hacer el CRUD.
async init() {

  const info = await Device.getInfo();
  // CapacitorSQLite no tiene disponible el metodo requestPermissions pero si existe y es llamable
  const sqlite = CapacitorSQLite as any;

  // Si estamos en android, pedimos permiso
  if (info.platform == 'android') {
    try {
      await sqlite.requestPermissions();
    } catch (error) {
      console.error("Esta app necesita permisos para funcionar")
    }
    // Si estamos en web, iniciamos la web store
  } else if (info.platform == 'web') {
    this.isWeb = true;
    await sqlite.initWebStore();
  } else if (info.platform == 'ios') {
    this.isIOS = true;
  }

  // Arrancamos la base de datos
  this.setupDatabase();

}

Método setupDatabase del servicio SQLITE

Comprobamos si hemos creado la base de datos, comprobando el valor de la clave first_setup_key, si no se ha iniciado la base de datos la crearemos, sino simplemente creamos la conexión, la abrimos e indicamos que la base de datos esta lista.

async setupDatabase() {

  // Obtenemos si ya hemos creado la base de datos
  const dbSetup = await Preferences.get({ key: 'first_setup_key' })

  // Sino la hemos creado, descargamos y creamos la base de datos
  if (!dbSetup.value) {
    this.downloadDatabase();
  } else {
    // Nos volvemos a conectar
    this.dbName = await this.getDbName();
    await CapacitorSQLite.createConnection({ database: this.dbName });
    await CapacitorSQLite.open({ database: this.dbName })
    this.dbReady.next(true);
  }

}

Método downloadDatabase del servicio SQLITE

Realizaremos el siguiente proceso:

  1. Obtenemos el fichero db.json.,
  2. Comprobamos si es valido.
  3. Si es valido lo importamos.
  4. Creamos la conexión.
  5. Abrimos la conexión.
  6. Indicamos en el valor de la clave first_setup_key a 1 para la próxima vez que iniciemos la app.
  7. Guardamos el nombre de la base de datos en el valor de la clave dbname.
  8. Indicamos que la base de datos esta lista.
downloadDatabase() {

  // Obtenemos el fichero assets/db/db.json
  this.http.get('assets/db/db.json').subscribe(async (jsonExport: JsonSQLite) => {


    const jsonstring = JSON.stringify(jsonExport);
    // Validamos el objeto
    const isValid = await CapacitorSQLite.isJsonValid({ jsonstring });

    // Si es valido
    if (isValid.result) {

      // Obtengo el nombre de la base de datos
      this.dbName = jsonExport.database;
      // Lo importo a la base de datos
      await CapacitorSQLite.importFromJson({ jsonstring });
      // Creo y abro una conexion a sqlite
      await CapacitorSQLite.createConnection({ database: this.dbName });
      await CapacitorSQLite.open({ database: this.dbName })

      // Marco que ya hemos descargado la base de datos
      await Preferences.set({ key: 'first_setup_key', value: '1' })
      // Guardo el nombre de la base de datos
      await Preferences.set({ key: 'dbname', value: this.dbName })

      // Indico que la base de datos esta lista
      this.dbReady.next(true);

    }

  })

}

Método getDbName del servicio SQLITE

Obtenemos el nombre de la base de datos desde la clave dbname.

async getDbName() {

  if (!this.dbName) {
    const dbname = await Preferences.get({ key: 'dbname' })
    if (dbname.value) {
      this.dbName = dbname.value
    }
  }
  return this.dbName;
}

Ejemplo servicio completo

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { CapacitorSQLite } from '@capacitor-community/sqlite';
import { Device } from '@capacitor/device';
import { Preferences } from '@capacitor/preferences';
import { JsonSQLite } from 'jeep-sqlite/dist/types/interfaces/interfaces';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SqliteService {

  // Atributos

  // Observable para comprobar si la base de datos esta lista
  public dbReady: BehaviorSubject;
  // Indica si estamos en web
  public isWeb: boolean;
  // Nombre de la base de datos
  public dbName: string;

  constructor(
    private http: HttpClient
  ) {
    this.dbReady = new BehaviorSubject(false);
    this.isWeb = false;
    this.dbName = '';
  }

  async init() {

    const info = await Device.getInfo();
    // CapacitorSQLite no tiene disponible el metodo requestPermissions pero si existe y es llamable
    const sqlite = CapacitorSQLite as any;

    // Si estamos en android, pedimos permiso
    if (info.platform == 'android') {
      try {
        await sqlite.requestPermissions();
      } catch (error) {
        console.error("Esta app necesita permisos para funcionar")
      }
      // Si estamos en web, iniciamos la web store
    } else if (info.platform == 'web') {
      this.isWeb = true;
      await sqlite.initWebStore();
    } else if (info.platform == 'ios') {
      this.isIOS = true;
    }

    // Arrancamos la base de datos
    this.setupDatabase();

  }

  async setupDatabase() {

    // Obtenemos si ya hemos creado la base de datos
    const dbSetup = await Preferences.get({ key: 'first_setup_key' })

    // Sino la hemos creado, descargamos y creamos la base de datos
    if (!dbSetup.value) {
      this.downloadDatabase();
    } else {
      // Nos volvemos a conectar
      this.dbName = await this.getDbName();
      await CapacitorSQLite.createConnection({ database: this.dbName });
      await CapacitorSQLite.open({ database: this.dbName })
      this.dbReady.next(true);
    }


  }

  downloadDatabase() {

    // Obtenemos el fichero assets/db/db.json
    this.http.get('assets/db/db.json').subscribe(async (jsonExport: JsonSQLite) => {


      const jsonstring = JSON.stringify(jsonExport);
      // Validamos el objeto
      const isValid = await CapacitorSQLite.isJsonValid({ jsonstring });

      // Si es valido
      if (isValid.result) {

        // Obtengo el nombre de la base de datos
        this.dbName = jsonExport.database;
        // Lo importo a la base de datos
        await CapacitorSQLite.importFromJson({ jsonstring });
        // Creo y abro una conexion a sqlite
        await CapacitorSQLite.createConnection({ database: this.dbName });
        await CapacitorSQLite.open({ database: this.dbName })

        // Marco que ya hemos descargado la base de datos
        await Preferences.set({ key: 'first_setup_key', value: '1' })
        // Guardo el nombre de la base de datos
        await Preferences.set({ key: 'dbname', value: this.dbName })

        // Indico que la base de datos esta lista
        this.dbReady.next(true);

      }

    })

  }

  async getDbName() {

    if (!this.dbName) {
      const dbname = await Preferences.get({ key: 'dbname' })
      if (dbname.value) {
        this.dbName = dbname.value
      }
    }
    return this.dbName;
  }

}

Volvemos al app.component.ts y estaremos pendiente de cuando esta la base de datos preparada.

import { Component } from '@angular/core';
import { Device } from '@capacitor/device';
import { Platform } from '@ionic/angular';
import { SqliteService } from './services/sqlite.service';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {

  public isWeb: boolean;
  public load: boolean;

  constructor(
    private platform: Platform,
    private sqlite: SqliteService) {
    this.isWeb = false;
    this.load = false;
    this.initApp();
  }

  initApp(){

    this.platform.ready().then( async () => {

      // Comprobamos si estamos en web
      const info = await Device.getInfo();
      this.isWeb = info.platform == 'web';

      // Iniciamos la base de datos
      this.sqlite.init();
      // Esperamos a que la base de datos este lista
      this.sqlite.dbReady.subscribe(load => {
        this.load = load;
      })
    })

  }
}

Aquí os dejo el repositorio de Github.

Os dejo un video donde explicamos como conectar sqlite en IONIC, hasta el minuto 21:36.

Espero que os sea de ayuda. Si tenéis dudas, preguntad. Estamos para ayudarte.

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *