Introducción a las Bases de Datos Embebidas en Android (SQLite) - dgonzalez870/GUIAS GitHub Wiki

Bases de datos en Android.

Android Provee soporte para bases de datos SQLite. La manera recomendada para crear y acceder a las bases de datos es através de una subclase de SQLiteOpenHelper.


Nota: Las bases de datos son privadas para la aplicación y solo son accedidas por todas las clases dentro de la aplicación en la que es creada.1


La clase SQLiteOpenHelper provee dos métodos abstractos que deben ser sobreescritos:

  1. onCreate: En este método deben incluirse las operaciones de creación de tablas y carga de datos iniciales en la base de datos.
  2. onUpgrade: En este método se incluyen las instrucciones de creación de tablas, modificación de tablas ó eliminación de tablas en una base de datos existente.

Los pasos necesarios para crear, acceder y actualizar una base de datos SQLite en Android se resumen a continuación:

  1. Crear una subclase de SQLiteOpenHelper.
  2. Implementar los métodos onCreate y onUpgrade.
  3. Implementar un constructor que haga referencia al constructor de la clase madre (super).
  4. Crear atributos String públicos estáticos con el nombre de la base de datos y las tablas.
  5. Crear atributos String con instrucciones SQL para crear base de datos y tablas.

Ejemplo

En este ejemplo se utiliza el elemento Chronometer definido en Android, los registros del cronómetro serán almacenados en una base de datos SQLite.

Funcionamiento Requerido:

  1. Debe tener un cronómetro que presente en pantalla el tiempo transcurrido.
  2. Debe tener una lista que presente en pantalla los registros anteriores del cronómetro.
  3. Debe incluir un botón inicie el cronómetro y lo detenga, este botón debe presentar el texto Iniciar cuando el cronómetro está detenido y Detener cuando el cronómetro ha sido iniciado.
  4. Cada vez que el cronómetro sea detenido, el registro de tiempo debe ser almacenado en una base de datos SQLite.
  5. Cuando se inicie la aplicación se debe buscar los registros en la base de datos, si existiera alguno, deben ser mostrados el la lista de pantalla.

En Android Studio ó Eclipse cree un nuevo proyecto llamado Cronometro (se ha omitido el acento intencionalmente) con la siguiente configuración:

  • Package name: com.prueba.cronometro
  • Minimun SDK: API 10: Android 2.3.3
  • En el diálogo Ad an Activity to mobile seleccionar Blank Activity
  • Activity Name: CronometroActivity.
  • Layout Name: layout_cronometro.

1. Creación del layout de la aplicación

Cambiar la raíz del layout de RelativeLayout a LinearLayout con orientación vertical como se observa en el siguiente segmento código.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:orientation="vertical"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".CronometroActivity">

    <TextView android:text="@string/hello_world" android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>
  1. Cambiar el valor del String hello_world a Cronómetro.
  2. Cambiar los siguientes atributos del TextView:
  • android:layout_width="match_parent".
  • android:layout_height="wrap_content".
  • android:textSize="20sp"
  • android:gravity="center".
  1. Añadir al layout un elemento del tipo Chronometer con los siguientes atributos:
  • android:layout_width="match_parent"
  • android:layout_height="0dp"
  • android:textSize="40sp"
  • android:gravity="center"
  • android:layout_weight="2"
  1. Añadir al layout un elemento del tipo ListView con los siguientes atributos:
  • android:id="@+id/listaRegistros"
  • android:layout_width="match_parent"
  • android:layout_height="0dp"
  • android:background="#FFFFFF"
  • android:layout_weight="4"
  1. Añadir al layotu un botón con los siguientes atributos:
  • android:id="@+id/btn_cronometro"
  • android:layout_width="match_parent"
  • android:layout_height="0dp"
  • android:layout_weight="1"
  • android:onClick="controlCronometro"
  • android:text="Iniciar"

El atributo oClick del botón hace referencia al método que controla el funcionamiento del cronómetro en la clase CronometroActivity.

El código completo del layout es el siguiente:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
   android:orientation="vertical"
   android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
   android:paddingRight="@dimen/activity_horizontal_margin"
   android:paddingTop="@dimen/activity_vertical_margin"
   android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".CronometroActivity">

   <TextView
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:textSize="20sp"
       android:text="@string/hello_world"
       android:gravity="center" />

   <Chronometer
       android:id="@+id/chronometer1"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:textSize="40sp"
       android:gravity="center"
       android:layout_weight="2"
       android:text="Chronometer" />
   <ListView
       android:id="@+id/listaRegistros"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:background="#FFFFFF"
       android:layout_weight="4" >
   </ListView>

   <Button
       android:id="@+id/btn_cronometro"
       android:layout_width="match_parent"
       android:layout_height="0dp"
       android:layout_weight="1"
       android:onClick="controlCronometro"
       android:text="Iniciar" />

</LinearLayout>

###2. Modificación del Activity.

La plantilla Blank Activity, crea una subclase de ActionBarActivity, esta clase ha sido declarada como obsoleta (deprecated) y se recomienda utilizar en su lugar AppComptActivity, la primera modificación es esa, el código en este punto debe ser el siguiente:

package com.prueba.cronometro;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;


public class CronometroActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_cronometro);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_cronometro, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

Para cumplir con el funcionamiento requerido, se crean instancias de los elementos Button, ListView y Chronometer del layout declarados en el archivo XML utilizando el método del Activity findViewById(R.layout.layout_cronometro), además debe hacerse un cast para convertir al tipo específico de elemento.

Los datos en la lista de pantalla se cargan a través de un ArrayAdapter, el ArrayAdapter mapea los elementos de cualquier implementación de java.util.List en los elementos individuales de la lista que se presentarán en pantalla con los atributos descritos en el layout layout_item_lista.

Se implementa el método controlCronometro pasando como parámetro un elemento View, con este parámetro se le dice al sistema que el método está asociado a una vista. En el método controlCronometro se inicia y se detiene el cronómetro, se cambia el texto al botón y se guardan los registros en la base de datos.

    public void controlCronometro(View v){
        if(!iniciado){
            cronometro.setBase(SystemClock.elapsedRealtime());
            cronometro.start();
            iniciado=true;
            btn_iniciar.setText("Detener");
        }else{
            cronometro.stop();
            iniciado=false;
            btn_iniciar.setText("Iniciar");
            String registroActual=cronometro.getText().toString();
            adapter.add(registroActual);
            //Iniica el almacenamiento en la base de datos.
            BDHelper helper=new BDHelper(this, BDHelper.NOMBRE_BD, null, 1);
            SQLiteDatabase bd=helper.getWritableDatabase();
            ContentValues values=new ContentValues();
            values.put("INTERVALO", registroActual);
            if(bd.insert(BDHelper.NOMBRE_TABLA_REG, null, values)==-1){
                Toast.makeText(getApplicationContext(), "Error al insertar datos", Toast.LENGTH_LONG).show();
            }
            bd.close();
        }
    }

Los pasos para escribir en la base de datos se describen a continuación:

  1. Obtener una instancia de una subclase de SQLiteOpenHelper, en este caso BDHelper BDHelper helper=new BDHelper(this, BDHelper.NOMBRE_BD, null, 1);los parámetros son el context, el nombre de la base de datos y la versión.
  2. Obtener una instancia de una base de datos SQLite SQLiteDatabase bd=helper.getWritableDatabase();
  3. Obtener una instancia de la clase ContentValues, ContentValues values=new ContentValues(); esta representa los datos que serán guardados en la base de datos.
  4. Cargar los datos en la instancia de ContentValues creada en el paso anterior values.put("INTERVALO", registroActual);, los datos se cargan en la forma clave-valor donde la clave representa el nombre de la columna en la base de datos.
  5. Insertar los datos en la tabla haciendo uso de la instrucción insert bd.insert(BDHelper.NOMBRE_TABLA_REG, null, values), esta instrucción retorna -1 si ocurre un error en la inserción de datos.
  6. Cerrar la base de datos bd.close();.

Los pasos para leer los registros de la base de datos se describen a continuación:

  1. Obtener una instancia de una subclase de SQLiteOpenHelper, en este caso BDHelper BDHelper helper=new BDHelper(this, BDHelper.NOMBRE_BD, null, 1);los parámetros son el context, el nombre de la base de datos y la versión.
  2. Obtener una instancia de una base de datos SQLite SQLiteDatabase bd=helper.getReadbleDatabase();
  3. Realizar una búsqueda (query) en la base de datos, en este punto hay dos formas de hacerlo:
  4. Utilizando el método query
    • Crear un array del tipo String con el nombre de las columnas a ser retornadas en la búsqueda: String columnas[]=new String[]{"ID","INTERVALO"};
    • En el caso de una búsqueda filtrada, donde sea necesario utilizar una clausula WHERE, por ejemplo si en la base de datos del cronómetro se quiere leer los registros cuyo campo ID es menor a 10 y mayor a 4, la sentencia SQL sería SELECT ID, INTERVALO FROM REGISTROS WHERE ID < 10 AND ID >4, adaptando esta sentencia a Android, se debe crear un String en el que se omita el propio WHERE, quedaría String where="ID < ? AND ID > ?", el signo de interrogación deja un espacio para los parámetros que deben ser definidos en otro array según el orden en el que aparecen String[] selectionArgs=new String[]{"10","4"};
    • Obtener un elemento de la clase Cursor a través de la sentencia query con los parámetros definido anteriormente: Cursor cursor=bd.query(BDHelper.NOMBRE_TABLA_REG, columnas, where,selectionArgs , null, null, null);
    • Leer los datos del cursor utilizando los métodos cursor.moveToFirst() para verificar que existe algún registro según el query solicitado y cursor.moveToNext(); para iterar sobre cada uno de los registros en el cursor.
    • Cerrar el cursor cursor.close();
  5. Utilizando el método rawQuery
    • Crear un String con una sentencia SQL, por ejemplo String buqueda="SELECT ID, INTERVALO FROM REGISTROS WHERE ID < ? AND ID > ?", al igual que en la instrucción query definida en la parte anterior, los signos de interrogación dehan espacio para los parámetros definidos en el array de tipo String con los valores específicos.
  6. Cerrar la base de datos bd.close();.

Nota: Existen otros métodos query y rawQuery con parámetros distintos a los aquí mencionados, para mayor información refiérase a la documentación.


Siguiendo los pasos descritos anteriormente se implementa el método buscarRegistros

	public void buscarRegistros(){
		BDHelper helper=new BDHelper(getApplicationContext(), BDHelper.NOMBRE_BD, null, BDHelper.VERSION);
		SQLiteDatabase bd=helper.getReadableDatabase();
		//Array con el nombre de las columnas a ser retornadas
		String columnas[]=new String[]{"ID","INTERVALO"};
		//Sentencia SQL WHERE sin el ´WHERE´
		String where="ID < ? AND ID > ?";
		//Array con los valores que sustituyen a los signos de interrogación en el String anterior
		String selectionArgs[]=new String[]{"10","4"};
		//Instrucción que realiza la búsqueda en la base de datos
		Cursor cursor=bd.query(BDHelper.NOMBRE_TABLA_REG, columnas, where,selectionArgs , null, null, null);
		//Retorna true si hay algún dato en el cursor
		if(cursor.moveToFirst()){
			do{
				registros.add(cursor.getString(cursor.getColumnIndex("INTERVALO")));
			}while(cursor.moveToNext());
		}
		cursor.close();
		bd.close();		
	}

Para cumplir con el punto 5 de los requerimientos de funcionamiento debe ejecutarse el método buscarRegistros aquí definido en el método onCreate del Activity.


Nota: Una buena práctica es ejecutar la búsquedas en bases de datos dentro de un AsyncTask


El código completo de CronometroActivity se presenta a continuación:

package com.prueba.cronometro;

import android.app.ProgressDialog;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Chronometer;
import android.widget.ListView;
import android.widget.Toast;

import java.util.Vector;


public class CronometroActivity extends AppCompatActivity {

    private Chronometer cronometro;
    private Button btn_iniciar;
    private ListView listaRegistros;
    private ArrayAdapter<String> adapter;
    //Diálogo de progreso que se presenta durante las búsquedas en la base de datos
    private ProgressDialog progreso;
    //Indica si el cronómetro ha sido iniciado
    private boolean iniciado=false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_cronometro);
        cronometro=(Chronometer)findViewById(R.id.chronometer1);
        btn_iniciar=(Button)findViewById(R.id.btn_cronometro);
        adapter=new ArrayAdapter<String>(this, R.layout.layout_item_lista,new Vector<String>());
        listaRegistros=(ListView) findViewById(R.id.listaRegistros);
        listaRegistros.setAdapter(adapter);
        progreso=new ProgressDialog(this);
        progreso.setMessage("Realizando b\u00fasqueda");
        progreso.setTitle("PROGRESO");
        //Inicia la búsqueda de registros guardados en la base de datos en un AsyncTask
        TareaQuery tarea=new TareaQuery();
        tarea.execute("");
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_cronometro, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    public void controlCronometro(View v){
        if(!iniciado){
            //reinicia la base del cronómetro para ce inicie de cero
            cronometro.setBase(SystemClock.elapsedRealtime());
            cronometro.start();
            iniciado=true;
            btn_iniciar.setText("Detener");
        }else{
            cronometro.stop();
            iniciado=false;
            btn_iniciar.setText("Iniciar");
            String registroActual=cronometro.getText().toString();
            adapter.add(registroActual);
            //Iniica el almacenamiento en la base de datos.
            BDHelper helper=new BDHelper(this, BDHelper.NOMBRE_BD, null, 1);
            SQLiteDatabase bd=helper.getWritableDatabase();
            ContentValues values=new ContentValues();
            values.put("INTERVALO", registroActual);
            if(bd.insert(BDHelper.NOMBRE_TABLA_REG, null, values)==-1){
                Toast.makeText(getApplicationContext(), "Error al insertar datos", Toast.LENGTH_LONG).show();
            }
            bd.close();
        }
    }

    public String[] buscarRegistros(){
        String[] registrosEncontrados=null;
        BDHelper helper=new BDHelper(getApplicationContext(), BDHelper.NOMBRE_BD, null, 1);
        SQLiteDatabase bd=helper.getReadableDatabase();
        //Array con el nombre de las columnas a ser retornadas
        String columnas[]=new String[]{"ID","INTERVALO"};
        //Sentencia SQL WHERE sin el ´WHERE´
        String where="ID < ? AND ID > ?";
        //Array con los valores que sustituyen a los signos de interrogación en el String anterior
        String selectionArgs[]=new String[]{"10","4"};
        //Instrucción que realiza la búsqueda en la base de datos
        Cursor cursor=bd.query(BDHelper.NOMBRE_TABLA_REG, columnas, where,selectionArgs , null, null, null);
        //Retorna true si hay algún dato en el cursor
        if(cursor.moveToFirst()){
            registrosEncontrados=new String[cursor.getCount()];
            do{
                registrosEncontrados[cursor.getPosition()]=cursor.getString(cursor.getColumnIndex("INTERVALO"));
               //adapter.add(cursor.getString(cursor.getColumnIndex("INTERVALO")));
            }while(cursor.moveToNext());
        }
        cursor.close();
        bd.close();
        return registrosEncontrados;
    }

    //AsyncTask en el que se realiza la búsqueda de registros
    public class TareaQuery extends AsyncTask<String, Integer, String[]> {

        @Override
        protected String[] doInBackground(String... params) {
            return buscarRegistros();
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progreso.show();
        }

        @Override
        protected void onPostExecute(String[] result) {
            super.onPostExecute(result);
            //adapter.addAll(result); //aplica a versiones API>10
            for(String resultado:result)
                adapter.add(resultado);
            progreso.dismiss();
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
        }
    }


}

###3. Configuración de la base de datos SQLite.

Crear una subclase de SQLiteOpenHelper con el nombre BDHelper en el paquete com.prueba.cronometro, implementar los métodos onCreate y onUpgrade e implementar un constructor que haga referencia al constructor de la clase madre (super). En este punto el código es el siguiente:

package com.prueba.cronometro;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class BDHelper extends SQLiteOpenHelper{

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

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {

    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
}

El siguiente paso es crear atributos String que servirán de instrucciones para crear las tablas y acceder a ellas.

	public static final String NOMBRE_BD="registros";
	private final String CREAR_TABLA_REGISTROS="CREATE TABLE REGISTROS(ID INTEGER PRIMARY KEY AUTOINCREMENT, INTERVALO TEXT)";
	private final String CREAR_TABLA_USUARIOS="CREATE TABLE USUARIOS(ID INTEGER PRIMARY KEY AUTOINCREMENT, NOMBRE TEXT)";
	public static final String NOMBRE_TABLA_REG="REGISTROS";

Nota: Las variables CREAR_TABLA_XX guardan como valores sentencias SQL para la creación de tablas con la siguiente sintaxis:

CREATE TABLE NOMBRE_TABLA(NOMBRE_COLUMNA1 TIPO_DE_DATO1, NOMBRE_COLUMNA1 TIPO_DE_DATO1...)

En el método onCreate añadir la siguiente instrucción: sqLiteDatabase.execSQL(CREAR_TABLA_REGISTROS);, onCreate pasa como parámetro una instancia de SQLiteDatabase que representa la base de datos SQLite que ha sido creada, con esta instrucción se crea la tabla de registros en la base de datos. El método onCreate se ejecuta solo cuando se crea la base de datos, esto ocurre con la invocación del método getWritableDatabase ó getReadableDatabase, desde una instancia de BDHelper esto se explicará en lineas siguientes.

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL(CREAR_TABLA_REGISTROS);
    }


##Referencias

  1. Opciones de Almacenamiento (Bases de Datos)

⚠️ **GitHub.com Fallback** ⚠️