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.     Remplaza en la clase LugaresBD el método elemento() con el siguiente código. Su finalidad es buscar el lugar correspondiente a un id y devolverlo.

@Override public Lugar elemento(int id) {
   Cursor cursor = getReadableDatabase().rawQuery(
                           "SELECT * FROM lugares WHERE _id = "+id, null);
   try {
      if (cursor.moveToNext()) 
         return extraeLugar(cursor);
      else 
         throw new SQLException("Error al acceder al elemento _id = "+id);
   } catch (Exception e) {
      throw e;
   } finally {
      if (cursor!=null) cursor.close();
   }
}  
override fun elemento(id: Int): Lugar {
   val cursor = readableDatabase.rawQuery(
                           "SELECT * FROM lugares WHERE _id = $id", null)
   try {
      if (cursor.moveToNext())
         return extraeLugar(cursor)
      else
         throw SQLException("Error al acceder al elemento _id = $id")
   } catch (e:Exception) {
      throw e
   } finally {
      cursor?.close()
   }
} 

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() realiza una consulta en la tabla lugares usando el comando SQL SELECT * FROM lugares WHERE _id =… . Este comando podría interpretarse como, selecciona todos los campos de la tabla lugares, para el registro con el id indicado. El resultado de una consulta es un Cursor con el registro, si es encontrado, o en caso contrario, un Cursor vacío.

En la siguiente línea llamamos 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. Si no lo encontramos lanzamos una excepción.

Es importante cerrar lo antes posibles el consumo de memoria. Lo hacemos en la sección finally para asegurarnos que se realiza siempre, haya habido una excepción o no.

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 actualiza()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) {
   getWritableDatabase().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);
} 
override fun actualiza(id:Int, lugar:Lugar) = with(lugar) {
   writableDatabase.execSQL("UPDATE lugares SET " +
      "nombre = '$nombre', direccion = '$direccion', " +
      "longitud = ${posicion.longitud}, latitud = ${posicion.latitud}, " +
      "tipo = ${tipoLugar.ordinal}, foto ='$foto', telefono =$telefono, "+
      "url = '$url', comentario = '$comentarios', fecha = $fecha, " +
      "valoracion = $valoracion  WHERE _id = $id")
} 

2.     En  la clase EdicionLugarActivity, en el método onOptionsItemSelected() añade el código subrayado y elimina el tachado:

int _id = lugares.getAdaptador().idPosicion(pos);
usoLugar.guardar(pos _id, lugar); 
val _id = lugares.adaptador.idPosicion(pos)
usoLugar.guardar(pos _id, nuevoLugar) 

La variable pos 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 no parecen en VistaLugarActivity ni en MainActivity. Realmente sí que se han almacenado, el problema está en que el adaptador no se ha actualizado. Para verificar que los cambios sí que se han almacenado puedes cambiar el criterio de ordenación, para que se actualice el adaptador.

4.   Para resolver el refresco de MainActivity has de sobrescribir el método actuliza() en LugaresDBAdapter.

@Override public void actualiza(int id, Lugar lugar) {
   super.actualiza(id,lugar);
   adaptador.setCursor(extraeCursor());
   adaptador.notifyDataSetChanged();
}
override fun actualiza(id:Int, lugar:Lugar) {
   super.actualiza(id,lugar)
   adaptador.setCursor(extraeCursor())
   adaptador.notifyDataSetChanged()
} 

Empezamos llamando al super para que el lugar se actualice en la base de datos según se indica en LugaresBD. Luego creamos un nuevo cursor, dado que al cambiar el lugar es posible que cambie el orden de los elementos seleccionados. Finalmente notificamos al adaptador que los datos han cambiado que vuelva a pintar las vistas correspondientes del RecyclerView.

5.    Ejecuta de nuevo la aplicación. Tras editar un lugar, los cambios se reflejan en <MainActivity pero no en VistaLugarActivity.

6.    Para resolver este problema has de añadir las siguientes líneas en VistaLugarActivity

private int _id = -1;

@Override public void onCreate(…) {
   …
   pos = extras.getInt("pos", 0);
   _id = lugares.getAdaptador().idPosicion(pos);
   …
@Override public void onActivityResult(…) {
   if (requestCode == RESULTADO_EDITAR) {
      lugar = lugares.elemento(_id);
      pos = lugares.getAdaptador().posicionId(_id);
      actualizaVistas();
   } 
private var _id: Int = -1

override fun onCreate(…) {
   …
   pos = intent.extras?.getInt("pos", 0) ?: 0
   _id = lugares.adaptador.idPosicion(pos)
   …
override fun onActivityResult(…) {
   if (requestCode == RESULTADO_EDITAR) {
      lugar = lugares.elemento(_id)
      pos = lugares.adaptador.posicionId(_id)
      actualizaVistas()
   }… 

Necesitamos actualizar la variable lugar dado que esta acaba de ser modificada. Extraerla según su posición en el listado es potencialmente peligroso, dado que esta posición puede cambiar dinámicamente. Por ejemplo, si ordenamos los lugares por orden alfabético y modificamos su inicial, posiblemente cambie su posición. Por el contrario, el _id de un lugar nunca puede cambiar. Hemos obtenido el _id al crear la actividad. Tras la edición del lugar, con este _id, obtenemos los nuevos valores para lugar y buscamos la nueva posición a partir de _id.

7.    Para hacer la última acción añade en AdaptadorLugaresBD la siguiente función:

public int posicionId(int id) {
    int pos = 0;
    while (pos<getItemCount() && idPosicion(pos)!=id) pos++;
    if (pos >= getItemCount()) return -1;
    else                       return pos;
} 
fun posicionId(id: Int): Int {
   var pos = 0
   while (pos < itemCount && idPosicion(pos) != id) pos++
   return if (pos >= itemCount) -1
          else                   pos
}  

Como ves se recorren todos los elementos del adaptador hasta encontrar uno con el mismo id. Si no es encontrado devolvemos -1.

8.    Ejecuta la aplicación y verifica el nuevo funcionamiento.


Ejercicio: Modificación valoración y fotografía de un lugar

1.    Algunos de los campos de un lugar no se modifica 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 actualizaVistas() de VistaLugarActivity el código subrayado.

valoracion.setOnRatingBarChangeListener(null);
valoracion.setRating(lugar.getValoracion());
valoracion.setOnRatingBarChangeListener(
        new RatingBar.OnRatingBarChangeListener() {
           @Override
           public void onRatingChanged(RatingBar ratingBar,
                                       float valor, boolean fromUser) {
              lugar.setValoracion(valor);
              pos = lugares.actualizaPosLugar(pos, lugar);
           }
        }); 
valoracion.setOnRatingBarChangeListener { _, _, _ -> }
valoracion.setRating(lugar.valoracion)
valoracion.setOnRatingBarChangeListener { _, valor, _ ->
   lugar.valoracion = valor
   pos = lugares.actualizaPosLugar(pos, lugar)
} 

Cuando el usuario cambie la valoración de un lugar se llamará a onRatingChanged()donde actualizamos la valoración y llamamos a actualizaLugares(). Esta función llamará a actualizaVistas(), donde cambiamos raingBar, lo que provocará una llamada al escuchador, y así sucesivamente entrando en bucle. Para evitarlo antes de cambiar el valor desactivamos el escuchador. Si tenemos seleccionada la ordenación por valoración, al cambiarla puede cambiar la posición del lugar en la lista. Por si ha cambiado, volvemos a obtener la variable pos.

2.     Añade la siguiente función a LugaresBDAdapter:

public int actualizaPosLugar(int pos, Lugar lugar) {
   int id = adaptador.idPosicion(pos);
   actualiza(id, lugar);
   return adaptador.posicionId(id); //devolvemos la nueva posición
} 
fun actualizaPosLugar(pos: Int, lugar: Lugar): Int {
   val id = adaptador.idPosicion(pos)
   actualiza(id, lugar)
   return adaptador.posicionId(id)  //devolvemos la nueva posición
}

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. Con el id, ya podemos actualizar la base de datos. La función devuelve la nueva posición que el lugar tiene en el adaptador.

3.     Para que los cambios en las fotografías se actualicen también has obtener el lugar de forma adecuada y llamar a actualizaPosLugar():

public void ponerFoto(int pos, String uri, ImageView imageView) {
   Lugar lugar = lugares.elemento adaptador.lugarPosicion(pos);
   lugar.setFoto(uri);
   visualizarFoto(lugar, imageView);
   lugares.actualizaPosLugar(pos, lugar);
} 
fun ponerFoto(pos: Int, uri: String?, imageView: ImageView) {
   val lugar = lugares.elemento adaptador.lugarPosicion(pos)
   lugar.foto = uri ?: ""
   visualizarFoto(lugar, imageView)
   lugares.actualizaPosLugar(pos, lugar)
} 

4.     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();
   getWritableDatabase().execSQL("INSERT INTO lugares (nombre, " +
       "direccion, longitud, latitud, tipo, foto, telefono, url, " +
       "comentario, fecha, valoracion) VALUES ('', '',  " +
       lugar.getPosicion().getLongitud() + ","+
       lugar.getPosicion().getLatitud() + ", "+ lugar.getTipo().ordinal()+
       ", '', 0, '', '', " + lugar.getFecha() + ", 0)");
    Cursor c = getReadableDatabase().rawQuery(
       "SELECT _id FROM lugares WHERE fecha = " + lugar.getFecha(), null);
    if (c.moveToNext()) _id = c.getInt(0);
    c.close();
    return _id;
}
   override fun nuevo():Int {
   var _id = -1
   val lugar = Lugar(nombre = "")
   writableDatabase.execSQL("INSERT INTO lugares (nombre, direccion, " +
       "longitud, latitud, tipo, foto, telefono, url, comentario, " +
       "fecha, valoracion) VALUES ('', '', ${lugar.posicion.longitud}, " + 
       "${lugar.posicion.latitud}, ${lugar.tipoLugar.ordinal}, '', 0, " +
       "'', '', ${lugar.fecha},0 )")
   val c = readableDatabase.rawQuery((
       "SELECT _id FROM lugares WHERE fecha = " + lugar.fecha), null)
   if (c.moveToNext()) _id = c.getInt(0)
   c.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();
      usoLugar.nuevo();
   }
});
fab.setOnClickListener { view ->
   Snackbar.make(view, "Replace with your own action",Snackbar.LENGTH_LONG)
      .setAction("Action", null).show()
   usoLugar.nuevo()
} 

4.  Añade el siguiente caso de uso en CasoUsoLugar:

   public void nuevo() {
   int id = lugares.nuevo();
   GeoPunto posicion = ((Aplicacion) actividad.getApplication())
           .posicionActual;
   if (!posicion.equals(GeoPunto.SIN_POSICION)) {
      Lugar lugar = lugares.elemento(id);
      lugar.setPosicion(posicion);
      lugares.actualiza(id, lugar);
   }
   Intent i = new Intent(actividad, EdicionLugarActivity.class);
   i.putExtra("_id", id);
   actividad.startActivity(i);
}
fun nuevo() {
   val _id = lugares.nuevo()
   val posicion = (actividad.application as Aplicacion).posicionActual
   if (posicion != GeoPunto.SIN_POSICION) {
      val lugar = lugares.elemento(_id)
      lugar.posicion = posicion
      lugares.actualiza(_id, lugar)
   }
   val i = Intent(actividad, EdicionLugarActivity::class.java)
   i.putExtra("_id", _id)
   actividad.startActivity(i)
}  

Comenzamos creando un nuevo lugar en la base e datos cuyo identificaor va a ser _id. La siguiente línea obtiene la posición actual. Si el dispositivo está localizado, obtenemos el lugar recién creado, cambiamos su posición y lo volvemos a guardar. A continuación vamos a lanzar la actividad EdicionLugarActivity para que el usuario rellene los datos del lugar. Hasta ahora hemos utilizado el extra "pos"“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

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

private int _id;

@Override protected void onCreate(Bundle savedInstanceState) {
   …
   Bundle extras = getIntent().getExtras();
   pos = extras.getInt("pos", -1) ;
   _id = extras.getInt("_id", -1);
   if (_id!=-1) lugar = lugares.elemento(_id);
   else         lugar = lugares.elementoPos(pos);
   actualizaVistas();
} 
var _id = -1

override fun onCreate(savedInstanceState: Bundle?) {
   …
   pos = intent.extras?.getInt("pos", -1) ?: -1
   _id = intent.extras?.getInt("_id", -1) ?: -1
   lugar = if (_id !== -1) lugares.elemento(_id)
           else            lugares.elementoPos(pos)
   actualizaVistas()
} 

Esta actividad va a poder ser llamada de dos formas alternativas: usando el extra “pos”“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 “pos” e _id. Aunque solo una se va a inicializar y la otra valdrá -1.  

6.     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 inicializada. Añade el código subrayado en este método:

case R.id.accion_guardar:	
   …
   if (_id==-1) int _id = lugares.getAdaptador().idPosicion(pos);
   usoLugar.guardar(_id, lugar);
   finish();
   return true; 
R.id.accion_guardar -> {
   …
   if (_id==-1) val _id = lugares.adaptador.idPosicion(pos)
   usoLugar.guardar(_id, nuevoLugar)
   finish()
   return true
} 

El if es añadido dado que si nos han pasado el identificador _id ya no tiene sentido obtenerlo a partir de la posición.

7.     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) {
   getWritableDatabase().execSQL("DELETE FROM lugares WHERE _id = " + id);
} 
override fun borrar(id: Int) {
   writableDatabase.execSQL("DELETE FROM lugares WHERE _id = $id")
} 

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

case R.id.accion_borrar:
    usoLugar.borrarPos(pos);
    return true;
R.id.accion_borrar -> {
   usoLugar.borrarPos(pos)
   return true
} 

3.     En CasosUsoLugar,añade la función:

public void borrarPos(int pos) {
   int id = lugares.getAdaptador().idPosicion(pos);
   borrar(id);
}
fun borrarPos(pos: Int) {
   val id = lugares.adaptador.idPosicion(pos)
   borrar(id)
} 

Tiene la misma finalidad que la función borrar(), pero indicando la posición.

4.     Dentro de borrar(), añade las dos líneas subrayadas para actualizar el cursor y notificar al adaptador que los datos han cambiado:

lugares.borrar(id);
lugares.getAdaptador().setCursor(lugares.extraeCursor());
lugares.getAdaptador().notifyDataSetChanged();
actividad.finish();
lugares.borrar(id)
lugares.adaptador.cursor = lugares.extraeCursor()
lugares.adaptador.notifyDataSetChanged()
actividad.finish() 

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


  Ejercicio: 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, puedes verificar que esta opción funciona mal. Los datos introducidos no se guardarán; sin embargo, se creará un nuevo lugar con todos sus datos en blanco. Para verificarlo has de salir de la aplicación para que se recargue el adaptador.

Para evitar este comportamiento, borra el elemento nuevo 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 y pos.

Solución:

Añade en el método onOptionsItemSelected() en la opción accion_cancelar:

if (_id!=-1) usoLugares.borrar(_id) 
Preguntas de repaso:  SQLite II