Operaciones con bases de datos en Mis Lugares

 

 


En los apartados anteriores hemos aprendido a crear una base de datos y a realizar consultas en una tabla. En este apartado vamos a continuar aprendiendo las operaciones básicas cuando trabajamos con datos. Estas son: altas, bajas y modificaciones y consultas.

Ejercicio: Consulta de un elemento en Mis Lugares

1.     Reemplaza en la clase lugaresnBD en el método Melemento()con el siguiente código. Su finalidad es buscar el lugar correspondiente a un id y devolverlo.

@Override
public Lugar elemento(int id) {
   Lugar lugar = null;
   SQLiteDatabase bd = getReadableDatabase();
   Cursor cursor = bd.rawQuery("SELECT * FROM lugares WHERE _id = " + id, 
            null);
   if (cursor.moveToNext()) {
      lugar = extraeLugar(cursor);
   }
   cursor.close();
   bd.close();
   return lugar;
}

Comenzamos inicializando el valor del lugar a devolver a null, para que corresponda con el valor devuelto en caso de no encontrarse el id. Luego se obtiene el objeto bd llamando a getReadableDatabase(). Este objeto nos permitirá hacer consultas en la base de datos. Por medio del método rawQuery() se realiza una consulta en la tabla lugares usando el comando SQL SELECT * FROM lugares WHERE _id =…. Este comando podría interpretarse como “selecciona todas las columnas de la tabla lugares, para la fila con el id indicado”. El resultado de una consulta es un Cursor con la fila, si es encontrado, o en caso contrario, un Cursor vacío.

En la siguiente línea llamamos a cursor.moveToNext() para que el cursor pase a la siguiente fila encontrada. Como es la primera llamada estamos hablando del primer elemento. Devuelve  true  si lo encuentra y false si no. En caso de encontrarlo, llamamos a extraeLugar() para actualizar todos los atributos de lugar con los valores de la fila apuntada por el cursor. Es importante cerrar lo antes posibles los cursores y bases de datos por tener mucho consumo de memoria. El método termina devolviendo el lugar.

2.     Este método no es usado por la aplicación. Ha sido añadido por pertenecer a la interfaz Lugares.

Ejercicio: Modificación de un lugar

Si tratas de modificar cualquiera de los lugares, observarás que los cambios no tienen efecto. Para que la base de datos sea actualizada, realiza el siguiente ejercicio:

1.     Añade en la clase LugaresBD, en el método actualizaLugar(), el siguiente código. Su finalidad es reemplazar el lugar correspondiente al id indicado por un nuevo lugar.

@Override
public void actualiza(int id, Lugar lugar) {
    SQLiteDatabase bd = getWritableDatabase();
    bd.execSQL("UPDATE lugares SET nombre = '" + lugar.getNombre() +
            "', direccion = '" + lugar.getDireccion() +
            "', longitud = " + lugar.getPosicion().getLongitud() +
            " , latitud = " + lugar.getPosicion().getLatitud() +
            " , tipo = " + lugar.getTipo().ordinal() +
            " , foto = '" + lugar.getFoto() +
            "', telefono = " + lugar.getTelefono() +
            " , url = '" + lugar.getUrl() +
            "', comentario = '" + lugar.getComentario() +
            "', fecha = " + lugar.getFecha() +
            " , valoracion = " + lugar.getValoracion() +
            " WHERE _id = " + id);
    bd.close();
}

2.   En la clase EdicionLugarActivity, el método onOptionsItemSelected() reemplaza:

MainActivity.lugares.actualiza((int) id,lugar);

por:

int _id = MainActivity.adaptador.idPosicion((int) id);
MainActivity.lugares.actualiza(_id,lugar);

La variable id corresponde a un indicador de posición dentro de la lista. Para utilizar correctamente el método actualiza() de LugaresBD, hemos de obtener el id correspondiente a la primera columna de la tabla. Este cambio lo realiza el método idPosicion() de adaptador.

3.     Ejecuta la aplicación y trata de modificar algún lugar. Observa que, cuando realizas un cambio en un lugar, estos parecen que no se almacenan. Realmente sí que se han almacenado, el problema está en que la variable cursor no se ha actualizado. Para verificar que los cambios sí que se han almacenado, has de salir de la aplicación y volver a lanzarla.

4.     Para resolver este problema has de añadir la siguiente línea tras las que acabas de añadir:

MainActivity.adaptador.setCursor(MainActivity.lugares.extraeCursor());

5.    Ejecuta de nuevo la aplicación. Tras editar un lugar, los cambios se reflejan en VistaLugarActivity pero no en el RecyclerView de MainActivity. Has de salir de la aplicación y volver a lanzarla para que se actualice la lista.

6.     Para resolver este nuevo problema has de añadir la siguiente línea tras la que acabas de añadir:

MainActivity.adaptador.notifyItemChanged((int) id);

Estamos notificando al adaptador que ha sido cambiado el elemento de una determinada posición. Esto provocará una llamada al método onBindViewHolder() donde se refrescará la vista.

7.     Ejecuta de nuevo la aplicación. Edita un lugar y verifica que los cambios se reflejan en el RecyclerView.

8.     Algunos de los campos de un lugar no se modifican en la actividad EdicionLugarActivity, si no que se hacen directamente en VistaLugarActivity. En concreto la valoración, la fotografía y más adelante añadiremos fecha y hora. Cuando se modifiquen estos campos, también habrá que almacenarlos de forma permanente en la base de datos. Empezaremos por la valoración. Añade en el método onCreate() de VistaLugarActivity el código subrayado.

@Override
public void onRatingChanged(RatingBar ratingBar,
                                   float valor, boolean fromUser) {
    lugar.setValoracion(valor);
    actualizaLugar();
}

Cuando el usuario cambie la valoración de un lugar se llamará a onRatingChanged(). Tras cambiar el valor del objeto lugar, llamamos a actualizaLugar() para almacenar en la base de datos este objeto.

9.     Añade el siguiente método:

void actualizaLugar(){
    int _id = MainActivity.adaptador.idPosicion((int) id);
    MainActivity.lugares.actualiza(_id, lugar);
    MainActivity.adaptador.setCursor(MainActivity.lugares.extraeCursor());
    MainActivity.adaptador.notifyItemChanged((int) id);
}

Primero obtenemos en la variable _id el identificador del lugar. Para ello, vamos a usar la posición que el lugar ocupa en el listado almacenado en la variable id. Una vez obtenido _id, ya podemos actualizar la base de datos. Como hemos visto, siempre que cambie algún contenido es importante que el adaptador actualice el Cursor. Además, si queremos que la vista correspondiente sea creada de nuevo hemos de llamar a notifyItemChanged().  

10.     Para que los cambios en las fotografías se actualicen también has de hacer llamar a actualizaLugar(). En concreto en onActivityResult(), tras las dos apariciones de ponerFoto() y al final de eliminarFoto().

11.     Verifica que tanto los cambios de valoración como de fotografía se almacenan correctamente.

Ejercicio: Alta de un lugar

En este ejercicio aprenderemos a añadir nuevos registros a la base de datos.

1.     Reemplaza en la clase LugaresBD el método nuevo() por el siguiente. Su finalidad es crear un nuevo lugar en blanco y devolver el id del nuevo lugar.

@Override
public int nuevo() {
    int _id = -1;
    Lugar lugar = new Lugar();
    SQLiteDatabase bd = getWritableDatabase();
    bd.execSQL("INSERT INTO lugares (longitud, latitud, tipo, fecha) "+
            "VALUES ( " + lugar.getPosicion().getLongitud()+","+
            lugar.getPosicion().getLatitud()+", "+
            lugar.getTipo().ordinal()+", "+lugar.getFecha()+")");
    Cursor c = bd.rawQuery("SELECT _id FROM lugares WHERE fecha = " +
            lugar.getFecha(), null);
    if (c.moveToNext()){
        _id = c.getInt(0);
    }
    c.close();
    bd.close();
    return _id;
}

Comenzamos inicializando el valor del _id a devolver a -1. De esta manera, si hay algún problema este será el valor devuelto. Luego se crea un nuevo objeto Lugar. Si consultas el constructor de la clase, observarás que solo se inicializan posicion, tipo y fecha. El resto de los valores serán una cadena vacía para String y 0 para valores numéricos. Acto seguido, se crea una nueva fila con esta información. Los valores de texto y numéricos tampoco se indican, al inicializarse de la misma manera.

El método ha de devolver el _id del elemento añadido. Para conseguirlo se realiza una consulta buscando una fila con la misma fecha que acabamos de introducir.

2.     Para la acción de añadir vamos a utilizar el botón flotante que tenemos desde la primera versión de la aplicación. Abre el fichero res/layout/activity_main.xml y reemplaza el icono aplicado a este botón:

<android.support.design.widget.FloatingActionButton
    … 
    android:src="@android:drawable/ic_input_add"
    … />

3.     Abre la clase MainActivity y dentro de onCreate() comenta el código tachado y añade el subrayado, para que se ejecute al pulsar el botón flotante:

fab.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
      Snackbar.make(view,"Replace with your own action",Snackbar.LENGTH_LONG)
              .setAction("Action", null).show();
      long _id = lugares.nuevo();
      Intent i = new Intent(MainActivity.this,EdicionLugarActivity.class);
      i.putExtra("_id", _id);
      startActivity(i);
   }
});

Comenzamos creando un nuevo lugar en la base e datos cuyo identificaor va a ser _id. A continuación vamos a lanzar la actividad EdicionLugarActivity para que el usuario rellene los datos del lugar. Hasta ahora hemos utilizado el extra “id” para indicar la posición en la lista del objeto a editar. Pero ahora esto no es posible, dado que este nuevo lugar no ha sido añadido a la lista. Para resolver el problema vamos a crear un nuevo extra, “_id”, que usaremos para identificar el lugar a editar por medio de su campo _id.  

4.     En la clase EdicionLugarActivity añade el código subrayado:

private long _id;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.edicion_lugar);
    Bundle extras = getIntent().getExtras();
    id = extras.getLong("id", -1);
    _id = extras.getLong("_id", -1);
    if (_id!=-1) {
        lugar = MainActivity.lugares.elemento((int) _id);
    } else {
        lugar = MainActivity.adaptador.lugarPosicion((int) id);
    }
    …

Esta actividad va a poder ser llamada de dos formas alternativas: usando el extra “id” para indicar que el lugar a modificar ha de extraerse de una posición del adaptador; o usando “_id” en este caso el lugar será extraido de la base de datos usando su identificador. Observa como se han definido dos variables globales, id e _id. Aunque solo una se va a inicializar y la otra valdrá -1. 

5.     Cuando el usuario pulse la opción guardar se llamará al método onOptionsItemSelected(). Para almacenar la información tendremos que verificar cual de las dos variables ha sido inicialzada. Añade el código subrallado en este método:

case R.id.accion_guardar:
    …
    if (_id==-1) {
       int _id = MainActivity.adaptador.idPosicion((int) id);
    }
    MainActivity.lugares.actualiza((int) _id, lugar);
    MainActivity.adaptador.setCursor(MainActivity.lugares.extraeCursor());
    if (id!=-1) {
        MainActivity.adaptador.notifyItemChanged((int) id);
    } else {
        MainActivity.adaptador.notifyDataSetChanged();
    }
    finish();
    return true;

El primer if es añadido dado que si nos han pasado el identificador _id ya no tiene sentido obtenerlo a partir de la posición. El segundo if permite hacer la siguiente distinción: si nos han pasado una posición nodemos notificar al adaptador que obtenga la nueva vista de una determinada posición. En caso contrario, no conocemos la posición, por lo que tendremos que notificar al adaptador que todos los datos han cambiado.

6.     Verifica que los cambios introducidos funcionan correctamente.

Ejercicio: Baja de un lugar

En este ejercicio aprenderemos a eliminar filas de la base de datos.

1.    Reemplaza en la clase LugaresBD el método borrar() por el siguiente. Su finalidad es eliminar el lugar correspondiente al id indicado.

public void borrar(int id) {
    SQLiteDatabase bd = getWritableDatabase();
    bd.execSQL("DELETE FROM lugares WHERE _id = " + id );
    bd.close();
}

El código mostrado resulta sencillo de entender.

2.    Añade en la clase VistaLugarActivity, dentro del método onOptionsItemSelected(), el código subrayado:

case R.id.accion_borrar:
    int _id = MainActivity.adaptador.idPosicion((int) id);
    borrarLugar((int) _id);
    return true;

3.   Si has hecho la Práctica: Un cuadro de diálogo para confirmar el borrado, ya habrás creado un método similar al siguiente. En caso contrario, añádelo. Es importante que incluyas las dos líneas subrayadas para actualizar el cursor y notificar al adaptador que los datos han cambiado.

public void borrarLugar(final int id) {
   new AlertDialog.Builder(this)
     .setTitle("Borrado de lugar")
     .setMessage("¿Estás seguro que quieres eliminar este lugar?")
     .setPositiveButton("Confirmar",new DialogInterface.OnClickListener(){
        public void onClick(DialogInterface dialog, int whichButton) {
           MainActivity.lugares.borrar(id);
           MainActivity.adaptador.setCursor(
                                    MainActivity.lugares.extraeCursor());
           MainActivity.adaptador.notifyDataSetChanged();
           finish();
        }
     })
     .setNegativeButton("Cancelar", null)
     .show();
}

4.     Ejecuta la aplicación y trata de dar de baja algún lugar.

Práctica: Opción CANCELAR en el alta de un lugar

Si seleccionas la opción nuevo y en la actividad EdicionLugarActivity seleccionas la opción CANCELAR, posiblemente esta opción no funcione. Otra opción es que los datos introducidos no se guarden; sin embargo, se cree un nuevo lugar con todos sus datos en blanco. Para evitar este comportamiento, borra el elemento nuevo creado cuando se seleccione la opción CANCELAR. Pero este comportamiento ha de ser diferente cuando el usuario entró en la actividad EdicionLugarActivity para editar un lugar ya existente. Para diferenciar estas dos situaciones puedes utilizar los extras _id e  id.

Solución:

Añade dentro del método onOptionsItemSelected() de la actividad EdicionLugarActivity:

case R.id.accion_cancelar:
   if (_id!=-1) {
      MainActivity.lugares.borrar((int) _id);
   }
   finish();
   return true;

Preguntas de repaso: SQLite II