Cómo emplear bases de datos SQLite internas y externas en Android con Android Studio

- Andrés Cruz

In english
Cómo emplear bases de datos SQLite internas y externas en Android con Android Studio

Las base de datos son una herramienta primordial para mantener los datos del usuario y en general de la aplicación en buen resguardo, organizada y accesible en todo momento; este último punto es fundamental y es el de poder acceder a la data guardada en todo momento, en tiempos modernos hemos visto como todas las aplicaciones hoy en día se conectan a una API externa, servidor, en esencia un agente externo para traer datos y presentarlos al usuario de manera organizada; por supuesto, para esto es necesario tener una conexión a Internet, o al menos en Intranet; si se pierde esta conexión entonces la aplicación quedaría inutilizable ya que no podría traer la información y presentarla al usuario.

¿Qué es una base de datos en SQLite?

SQLite es un motor de base de datos ligero y abierto, cuyo principal atractivo es que podemos mantener datos de usuario o de la aplicación de manera sencilla, segura en un archivo muy pequeño y que se diferencia de otros sistemas como MySQL en que no requiere el soporte de un servidor, no requiere soporte de configuración de puertos y demás y todo, pero todo lo almacena en un único y sencillo archivo que es nuestra base de datos y es de código abierto por lo tanto, lo verás en múltiples aplicaciones que no son solo Android con Android Studio u otro IDEs similares.

Empleando base de datos SQLite para guardar datos de manera local

Aquí es donde entra en juego base de datos para guardar la información de manera local en Android; actualmente la base de datos que soporta Android se llama SQLite y en esencia es un pequeño archivo que contiene todo los datos de nuestra aplicación, los que nosotros registramos para ser más precisos; podemos realizar consultas como a una base relacional se tratase, porque es una base de datos hecha y derecha y en esta entrada hablaremos de cómo emplear esta base de datos en nuestras aplicaciones en Android.

Ejemplos de creación de una base de datos SQLite en Android

Lo primero que debemos hacer es lógicamente crear nuestra base de datos; para ello se emplea una clase Helper en la cual definimos la estructura de nuestra base de datos; las tablas, tipos de datos, y claves primarias son definidas en este Helper realizado en con Kotlin como lenguaje de programación:

class ListSQLiteHelper(
    context: Context, name: String,
    factory: SQLiteDatabase.CursorFactory?, version: Int
) :
    SQLiteOpenHelper(context, name, factory, version) {

    private val TABLE_NOTIFICATIONS =
        "CREATE TABLE notifications(id INTEGER PRIMARY KEY, notification_id INTEGER, created_at TEXT, proccess TEXT, text TEXT, operation TEXT, operation_id INT, user_id INT, avatar TEXT, user_name TEXT, user_surname TEXT, username TEXT, paid_id TEXT, product_id TEXT, singer TEXT, dedit_from TEXT, dedit_to TEXT, cost TEXT)"
    private val TABLE_OPERATIONS =
        "CREATE TABLE operations(id INTEGER PRIMARY KEY, operation_id INTEGER, province TEXT, location TEXT, province_id INTEGER, location_id INTEGER, place TEXT, created_at TEXT, proccess TEXT, day TEXT, user_id INT, user_name TEXT, user_surname TEXT, username TEXT, avatar TEXT, amount_to_paid TEXT, active TEXT)"

    override fun onCreate(db: SQLiteDatabase) {

        // se crear las tablas necesarias
        db.execSQL(TABLE_NOTIFICATIONS)
        db.execSQL(TABLE_OPERATIONS)
    }

    override fun onUpgrade(db: SQLiteDatabase, arg1: Int, arg2: Int) {

        // Se elimina la versión anterior de la bd
        db.execSQL("DROP TABLE IF EXISTS notifications")
        db.execSQL("DROP TABLE IF EXISTS operations")

        // se crear las tablas necesarias
        db.execSQL(TABLE_NOTIFICATIONS)
        db.execSQL(TABLE_OPERATIONS)
    }
}

En Java para crear nuestra base de datos empleamos la misma lógica; en este framento de código vemos que extendemos de la misma clase en cual tiene la misma definición pero con otra sintaxis:

public class ListSQLiteHelper extends SQLiteOpenHelper {

   final private String TABLE_PEDIDO = "CREATE TABLE pedido(pedido_id INTEGER PRIMARY KEY, creado DATETIME DEFAULT CURRENT_TIMESTAMP, procesado DATETIME, cliente_id INTEGER, establecimiento_id INTEGER, mesas_establecimiento_id INTEGER, token TEXT)";

   public ListSQLiteHelper(Context context, String name,
                           SQLiteDatabase.CursorFactory factory, int version) {
       super(context, name, factory, version);
   }

   @Override
   public void onCreate(SQLiteDatabase db) {

       // se crear las tablas necesarias
       db.execSQL(TABLE_PEDIDO);
   }

   @Override
   public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) {

       // Se elimina la versión anterior de la bd
       db.execSQL("DROP TABLE IF EXISTS pedido");

       // se crear las tablas necesarias
       db.execSQL(TABLE_PEDIDO);
   }
}

Como puedes ver son los mismos métodos y por lo tanto se aplica la misma lógica, el método onCreate para crear la base de datos y el método onUpgrade que se ejecuta cuando vamos a borrar la base de datos.

Para insertar un registro; en este caso es un método static, lo que significa que podemos instanciar el método sin necesidad de crear una nueva instancia cada vez que queramos emplear el método, en la forma Kotlin, empleamos un companion object que nos ofrece un comportamiento similar a los static de Java:

public static Pedido getById(int pedido_id, Context context) {

   Pedido pedido = NULL;

   ListSQLiteHelper taskSQL = new ListSQLiteHelper(context, TABLE_NAME,
           NULL, 1);

   SQLiteDatabase db = taskSQL.getReadableDatabase();
   Cursor cursor = db.rawQuery("SELECT " + selectData + " WHERE "
           + ID_NAME + " = " + pedido_id, NULL);

   if (cursor.moveToFirst()) {
       pedido = new Pedido();
       pedido.setPedido_id(cursor.getInt(0));
       try {
           if (cursor.getString(1) != NULL)
               pedido.setCreado(dateFormat.parse(cursor.getString(1)));
       } catch (ParseException e) {
           e.printStackTrace();
       }
       try {
           if (cursor.getString(2) != NULL)
               pedido.setProcesado(dateFormat.parse(cursor.getString(2)));
       } catch (ParseException e) {
           e.printStackTrace();
       }
       pedido.setCliente_id(cursor.getString(3));
       pedido.setEstablecimiento_id(cursor.getInt(4));
       pedido.setMesas_establecimiento_id(cursor.getInt(5));
       pedido.setToken(cursor.getString(6));
   }

   cursor.close();
   db.close();
   return pedido;
}

Desde este archivo Helper tendremos todas las tablas de nuestra aplicación que para este ejemplo son de dos tablas; como vemos la primer sección se encarga de definir la estructura de la tabla, cosas como cantidad de columnas, tipos de datos y la llave o clave primaria, esto se crea en el método onCreate que ejecuta cuando la base datos va a ser creada; es decir, solo se ejecuta una vez (a menos que por algún motivo necesitas borrar la base de datos desde la app Android que estés desarrollando); luego tenemos el método onUpgrade que se encarga de realizar las consultas para actualizar nuestra base de datos, ejecutar las sentencias etc; en este caso simplemente borramos las tablas.

Definiendo la capa de acceso a los datos (DAO)

Ya con nuestra base de datos creada, lo siguiente que tenemos que hacer es definir una capa que en la práctica es otro archivo que permita emplear la base de datos, es decir, todas nuestras consultas, actualización de registros, inserción y borrado de los mismos se hace mediante un archivo que para este caso sería el de Notifications Esta capa nos permite abstraernos de todo las consultas SQL que debemos realizar; en la práctica viene siendo la forma en la que podemos acceder a los datos, claro que podemos aplicar todas estas reglas desde nuestra actividad, fragment, adapter u otro componente, pero es buena idea centralizar todo lo que podamos centralizar; y este es un ejemplo de algunas consultas que puedes realizar; en general es lo típico, consultas para eliminar, obtener registro(s), insertar y actualizar; lo típico que hacemos en otros sistemas web, pero en Android:

class NotificationDao {

   companion object {

       val TABLE_NAME = "notifications"
       val ID = "notification_id"
       val TEXT = "text"
       val CREATED_AT = "created_at"
       val OPERATION = "operation"
       val OPERATION_ID = "operation_id"
       val USERNAME = "username"
       val USER_NAME = "user_name"
       val USER_SURNAME = "user_surname"
       val USER_ID = "user_id"
       val AVATAR = "avatar"
       val PROCCESS = "proccess"
       var PAID_ID = "paid_id"
       var PRODUCT_ID = "product_id"
       var COST = "cost"
       var SINGER = "singer"
       var FROM = "dedit_from"
       var TO = "dedit_to"

       internal var dateFormat = SimpleDateFormat(
           "yyyy-MM-dd HH:mm:ss", Locale.getDefault()
       )

       private val selectData = (ID + ", " + TEXT + ", "
               + CREATED_AT + ", " + PROCCESS + ", " + USERNAME + ", " + OPERATION + ", " + OPERATION_ID + ", " + USER_ID + ", " + AVATAR + ", "
               + USER_NAME + ", " + USER_SURNAME + ", " + PAID_ID + ", " + PRODUCT_ID + ", " + COST + ", " + SINGER + ", " + FROM + ", " + TO + " FROM " + TABLE_NAME)

       fun getAllByOperationId(context: Context, id: Long): ArrayList<NotificationUser> {

           var notification: NotificationUser
           val notifications = ArrayList<NotificationUser>()

           val taskSQL = ListSQLiteHelper(
               context, TABLE_NAME,
               NULL, 1
           )

           // traigo la date de BD
           val db = taskSQL.getReadableDatabase()
           val cursor = db.rawQuery("SELECT $selectData WHERE $OPERATION_ID = $id", NULL)

           // guardo la data en un ArrayList
           if (cursor.moveToFirst()) {
               do {
                   var i = 0
                   notification = packOperation(cursor)
                   notifications.add(notification)
               } while (cursor.moveToNext())
           }

           cursor.close()
           db.close()
           return notifications
       }

       fun getAll(context: Context): ArrayList<NotificationUser> {

           var notification: NotificationUser
           val notifications = ArrayList<NotificationUser>()

           val taskSQL = ListSQLiteHelper(
               context, TABLE_NAME,
               NULL, 1
           )

           // traigo la date de BD
           val db = taskSQL.getReadableDatabase()
           val cursor = db.rawQuery("SELECT $selectData", NULL)

           // guardo la data en un ArrayList
           if (cursor.moveToFirst()) {
               do {
                   var i = 0
                   notification = packOperation(cursor)
                   notifications.add(notification)
               } while (cursor.moveToNext())
           }

           cursor.close()
           db.close()
           return notifications
       }


       fun getByID(context: Context, id: Long): NotificationUser? {

           var notification: NotificationUser? = NULL

           val taskSQL = ListSQLiteHelper(
               context, TABLE_NAME,
               NULL, 1
           )

           // traigo la date de BD
           val db = taskSQL.getReadableDatabase()
           val cursor = db.rawQuery("SELECT $selectData WHERE $ID = $id", NULL)

           // guardo la data en un ArrayList
           if (cursor.moveToFirst()) {
               notification = packOperation(cursor)
           }

           cursor.close()
           db.close()
           return notification
       }

       fun update(
           contentValues: ContentValues, id: Int,
           context: Context
       ): Long {

           // abrimos la base de datos 'tour' en modo escritura
           val sqlHelper = ListSQLiteHelper(
               context, NotificationDao.TABLE_NAME,
               NULL, 1
           )
           val db = sqlHelper.writableDatabase

           return db.update(
               NotificationDao.TABLE_NAME, contentValues, NotificationDao.ID + "=?",
               arrayOf(id.toString())
           ).toLong()

       }

       fun deleteAll(
           context: Context
       ) {

           // abrimos la base de datos 'tour' en modo escritura
           val sqlHelper = ListSQLiteHelper(
               context, NotificationDao.TABLE_NAME,
               NULL, 1
           )
           val db = sqlHelper.writableDatabase

           db.delete(
               NotificationDao.TABLE_NAME, "1",
               arrayOf()
           )
       }

       fun insert(contentValues: ContentValues, context: Context): Long {

           // abrimos la base de datos en modo escritura
           val sqlHelper = ListSQLiteHelper(context, TABLE_NAME, NULL, 1)
           val db = sqlHelper.writableDatabase

           return db.insert(TABLE_NAME, NULL, contentValues)
       }

       fun packOperation(cursor: Cursor): NotificationUser {
           var i = 0

           var notification = NotificationUser()
           notification.notification_id = cursor.getInt(i)
           notification.text = cursor.getString(++i)
           notification.created_at = cursor.getString(++i)
           notification.proccess = cursor.getString(++i)
           notification.username = cursor.getString(++i)
           notification.operation = cursor.getString(++i)
           notification.operation_id = cursor.getInt(++i)
           notification.user_id = cursor.getString(++i)
           notification.avatar = cursor.getString(++i)
           notification.user_name = cursor.getString(++i)
           notification.user_surname = cursor.getString(++i)
           notification.paid_id = cursor.getString(++i)
           notification.product_id = cursor.getString(++i)
           notification.cost = cursor.getString(++i)
           notification.singer = cursor.getString(++i)
           notification.dedit_from = cursor.getString(++i)
           notification.dedit_to = cursor.getString(++i)

           return notification
       }

       fun populateContentValue(notification: NotificationUser): ContentValues {

           var contentValues = ContentValues()
           contentValues.put(ID, notification.notification_id)
           contentValues.put(CREATED_AT, notification.created_at)
           contentValues.put(TEXT, notification.text)
           contentValues.put(USERNAME, notification.username)
           contentValues.put(OPERATION, notification.operation)
           contentValues.put(OPERATION_ID, notification.operation_id)
           contentValues.put(USER_NAME, notification.user_name)
           contentValues.put(USER_SURNAME, notification.user_surname)
           contentValues.put(USER_ID, notification.user_id)
           contentValues.put(AVATAR, notification.avatar)
           contentValues.put(PROCCESS, notification.proccess)
           contentValues.put(PAID_ID, notification.paid_id)
           contentValues.put(PRODUCT_ID, notification.product_id)
           contentValues.put(SINGER, notification.singer)
           contentValues.put(FROM, notification.dedit_from)
           contentValues.put(TO, notification.dedit_to)
           return contentValues
       }
   }
}

Analizando el código anterior: consultas en la base de datos SQLite: Cursores

En el caso de las consultas, como hacemos en los métodos getAll(), getById() y similares, empleamos los cursores en Android; los cursores son un apuntador a los valores obtenidos de una consulta, como puedes ver en el código anterior y simplemente nos vamos desplazando con el método moveToNext() que nos devuelve true si hay más datos y false en caso contrario.

Analizando el código anterior: insertar y actualizar en la base de datos SQLite: Cursor

El código es prácticamente autoexplicativo; en reglas generales accedemos a la base de datos sqlHelper = ListSQLiteHelper(context, TABLE_NAME, NULL, 1), indicamos que queremos escribir sobre la misma con sqlHelper.writableDatabase y luego aplicamos las consultas, insert, delete, etc; en el caso de los insert empleamos los ContentValues

Para realizar una inserción o actualización de registros tenemos que emplear los ContentValues; este tipo de objeto permite definir pares de valores, como los Pair en Kotlin, pero es empleado en este escenario para definir la columna en base de datos que queremos afectar y el valor del mismo; se emplean los ContentValues tanto para la inserción como la actualización como podemos ver en los siguiente código:

Insertar un registro en una base de datos en SQLite:

var id = NotificationDao.insert(
   NotificationDao.populateContentValue(notification),
   this@NotificationActivity
)

Actualizar un registro en una base de datos en SQLite:

var contentValues = ContentValues()
contentValues.put(NotificationDao.AVATAR, noti.avatar)
contentValues.put(NotificationDao.PROCCESS, noti.proccess)
contentValues.put(NotificationDao.COST, noti.cost)
contentValues.put(NotificationDao.PAID_ID, noti.paid_id)
contentValues.put(NotificationDao.PRODUCT_ID, noti.product_id)
NotificationDao.update(contentValues, noti.notification_id, this@NotificationActivity)

Y eso es todo con la capa de acceso de nuestra base de datos; en general toda las consultas deberían están en esta capa, y las actividades y otros componentes solo deben llamar a esta capa, por eso la mejor idea que puedes aplicar es crear estas clases como Singleton, cosa que hacemos en Java aplicando los static y en Kotlin con los companion object.

Empleando una base de datos SQLite externa en Android

En esta sección explicaremos cómo emplear una base de datos externa SQLite, como bien sabrás, SQLite no es una invención en Android, es una tecnología que puedes emplear libremente en otro tipos de aplicaciones incluida en nuestras aplicaciones web y como ya mencionamos antes, una base de datos SQLite consiste en un simple archivo, entonces, podemos agarrar este archivo que es producto de otra aplicación y emplearla en Android 100% de manera segura e igual de práctica y eso es lo que veremos en esta otra sección.

Como hemos mencionado SQLite es el medio principal por excelencia de persistencia en Android; generalmente cuando trabajamos con Android, pensamos que la base de datos tiene que estar interna a la misma aplicación Android y esto es debido al cómo está compuesta una app Android que en esencia es todo integrado, pero no tiene que ser así, una de las cosas muy interesantes y que identifica a SQLite es su portabilidad, es decir, una base de datos SQLite es muy pequeño y consta de un único archivo que contiene toda nuestra base de datos junto con su estructura y por lo tanto la podemos exportar (copiar y pegar) en nuestro proyecto y conectarnos a la misma.

SQLite no solo en Android ni en Android Studio

Existen muchas aplicaciones que emplean bases de datos en SQLite desde el mismo PHP hasta aplicaciones del lado del cliente con JavaScript (Primeros pasos con SQLite en JavaScript: los métodos openDatabase, executeSql y executeSql), por lo tanto es muy probable que en algún momento decidimos exportar toda esta estructura desde una aplicación web u otro tipo a una aplicación Android creada en Android Studio u otra tecnología.

Empleando nuestra base de datos SQLite en Android con Android Studio

Si decidimos importar una base de SQLite de otra aplicación (ya sea Android o no) en un principio podríamos pensar que es necesario realizar algún proceso complejo de leer el archivo, crear la base de datos dentro de la aplicación Android y leer tabla por tabla, registro por registro y copiarlo dentro de una base de datos en nuestra aplicación Android, esto no es necesario, simplemente basta con copiar el archivo de nuestra base de datos SQLite en una ubicación en específico en nuestro IDE Android Studio:

Base de datos local android

La base de datos la debemos copiar en el directorio assets/databases de nuestro proyecto.

Ahora creamos una clase como la siguiente:

public class ListSQLiteHelper extends SQLiteAssetHelper {

    private static final String DATABASE_NAME = "main";
    private static final int DATABASE_VERSION = 1;

    public ListSQLiteHelper(Context context, String name,
                            SQLiteDatabase.CursorFactory factory, int version) {
        super(context, DATABASE_NAME, context.getExternalFilesDir(NULL).getAbsolutePath(), NULL, DATABASE_VERSION);
        //super(context, name, factory, version);
    }
}

Cómo puede darse cuenta empleamos el método SQLiteAssetHelper en vez del método SQLiteOpenHelper que no necesita que sobreescribimos los métodos onCreate(SQLiteDatabase db) y onUpgrade(SQLiteDatabase db, int arg1, int arg2) que son empleados para la creación y actualización de la base de datos que aplicaría en caso de ser una base de datos generada desde el mismo Android.

La variable DATABASE_NAME contiene el nombre de nuestra base de datos.

Con esto tenemos lista la conexión a nuestra base de datos y podemos realizar las consultas sobre la misma como si fuera una base de datos integrada de manera nativa por Android.

Puedes consultar la documentación oficial en el siguiente enlace: Android SQLiteAssetHelper.

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy

Acepto recibir anuncios de interes sobre este Blog.