A través de la sección Proponer una noticia, José María Tristán Martín, nos ha hecho llegar este interesantísimo artículo sobre manejo de datos a través de SQLite en Android. No os lo perdáis que es muy completo. Además desde aquí animamos a la gente a mandar sus artículos para que sean publicados.
A la hora de poder almacenar datos o consultar los mismos, Android, dispone de una serie de mecanismos, como son, la utilización de una base de datos relacional, en concreto SQLite, utilizar un sistema de preferencias, en el que se almacenarán pares de clave y valor y usar el sistema de ficheros. También dispone de una utilidad que nos permite compartir datos entre aplicaciones, los ContentProvider. A continuación vamos a analizar la forma de construir bases de datos así como la gestión de los datos.
Como ya hemos dicho, Android utiliza como base de datos SQLite. SQLite es una pequeña base de datos relacional y transaccional que nos permitirá realizar las tareas más habituales. No se trata de una base de datos cliente-servidor, sino que funciona directamente dentro de la aplicación, incorporándose como una parte más de la misma.
En el artículo veremos alguna de las caracterísitcas y sentencias de SQLite, pero no es nuestro propósito profundizar en ellas. Está disponible toda la documentación en la página de referencia de
SQLite.
Analizando rápidamente el API de Android para la utilización de SQLite, vemos, que nos encontramos con dos paquetes:
android.database se trata de un paquete genérico, no es específico de ninguna base de datos. Aquí tenemos interfaces tan importantes como Cursor (nos permitirá recuperar datos) y clases de ayuda como DatabaseUtils.
Para crear y gestionar SQLite tendremos que usar android.database.sqlite. Aquí encontraremos las clases necesarias para crear y actualizar la base de datos, para realizar querys, sentencias precompiladas, inserts, updates y deletes, la implementación de Cursor, etc.
Veamos las principales clases:
- SQLiteCursor: nos permite recuperar los datos, mediante una select o con el método query que ejecuta también una select. Podremos iterar sobre cada uno de los registros, columna a columna, como si se tratase de un resultset.
- SQLiteDatabase: expone métodos para gestionar los datos en una base de datos, como insertar, actualizar, eliminar, ejecutar sentencias sql, abrir y cerrar las conexiones, trabajar de forma transaccional.
- SQLiteOpenHelper: nos permite diseñar, crear, actualizar la base de datos.
- y gestionar la versión de la misma.
- SQLiteQueryBuilder: es un helper para crear sqls.
- SQLiteStatement: para trabajar con sentencias precompiladas.
Al contrario que con bases de datos como MySQL o SQL Server, en Android, no dispondremos de ninguna herramienta para construir la base de datos, sino que tendremos que hacerlo mediante comandos sql directamente en el código. Para crear la base de datos tendremos que sobreescribir la clase SQLiteOpenHelper. En nuestro ejemplo sobreescribiremos los métodos onCreate(SQLiteDatabase) y onUpgrade(SQLiteDatabase), pudiendo también sobreescribir onOpen(SQLiteDatabase), onDowngrade(), etc. Es fácil ver, que en onCreate vamos a insertar nuestros comandos de creación de las tablas, en onUpdate, introduciremos el código para los casos en que se produzca un cambio en la versión de la base de datos y por lo tanto haya que modificar alguna tabla. Finalmente, onOpen, lo utilizaremos para abrir una base de datos que ya está creada.
Para poder realizar un ejemplo, pensemos que queremos desarrollar una aplicación que va a permitirnos indicar qué contactos queremos que activen el sonido o la vibración del móvil en un día y una hora determinada. Es decir, queremos una aplicación que impida que alguien no deseado pueda molestarnos a ciertas horas. Necesitaremos tres tablas:
- eventos: cada evento nos indicará durante qué días y horas pueden sonar las llamadas de los contactos que indiquemos.
- contactos: contendrá todos nuestros contactos. En la aplicación podríamos llamar a un ContentProvider para acceder directamente a los contactos almacenados en el móvil, pero por simplificación de la aplicacición, almacenaremos a nivel de aplicación, los contactos.
- contactosDelEvento: contendrá simplemente la relación de qué contactos contiene cada evento.
Aunque tengamos que escribir algo más de código, vamos a utilizar el patrón DAO (Data Access Object), mediante el cual podremos encapsular la forma de acceder a la fuente de datos. De esta manera, si mañana, fuese necesario sustituir el acceso a SQLite por un acceso a ficheros no sería necesario modificar la aplicación.
Para construir nuestra aplicación necesitaremos varias capas. En primer lugar crearemos el modelo, utilizando clases POJOS. A continuación crearemos las clases que contendrán los métodos de acceso y tratamiento de las tablas de la base de datos y finalmente crearemos una clase que gestione el acceso a los daos.
Antes de comenzar con el patrón DAO, vamos a crear el esquema de la base de datos. Para ello, tenemos que sobreescribir la clase SQLiteOpenHelper. En este caso, como tenemos varias tablas, y para una mejor comprensión, vamos a crearnos una clase por cada una de las tablas, en la que expondremos la sentencias necesarias para crear y actualizar la base de datos.
La tabla evento, contendrá información sobre cuando ha de ejecutarse el evento y si está activo o no. Los campos de los días de la semana, se tratarán como booleanos, pudiendo estar varios activos.
public final class EventoTabla {
public static final String TABLE_NAME = "eventos";
public static class EventoColumnas implements BaseColumns {
public static final String NOMBRE = "nombre";
public static final String LUNES = "lunes";
public static final String MARTES = "martes";
public static final String MIERCOLES = "miercoles";
public static final String JUEVES = "jueves";
public static final String VIERNES = "viernes";
public static final String SABADO = "sabado";
public static final String DOMINGO = "domingo";
public static final String DE_HORA = "de_hora";
public static final String A_HORA = "a_hora";
public static final String DE_MINUTO = "de_minuto";
public static final String A_MINUTO = "a_Minuto";
public static final String ACTIVO = "activo";
}
public static void onCreate(SQLiteDatabase db){
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE " + EventoTabla.TABLE_NAME + " (");
sb.append(BaseColumns._ID + " INTEGER PRIMARY KEY, ");
sb.append(EventoColumnas.NOMBRE + " TEXT UNIQUE NOT NULL, ");
sb.append(EventoColumnas.LUNES + " INTEGER, ");
sb.append(EventoColumnas.MARTES + " INTEGER, ");
sb.append(EventoColumnas.MIERCOLES + " INTEGER, ");
sb.append(EventoColumnas.JUEVES + " INTEGER, ");
sb.append(EventoColumnas.VIERNES + " INTEGER, ");
sb.append(EventoColumnas.SABADO + " INTEGER, ");
sb.append(EventoColumnas.DOMINGO + " INTEGER, ");
sb.append(EventoColumnas.DE_HORA + " INTEGER NOT NULL, ");
sb.append(EventoColumnas.A_HORA + " INTEGER NOT NULL, ");
sb.append(EventoColumnas.DE_MINUTO + " INTEGER NOT NULL, ");
sb.append(EventoColumnas.A_MINUTO + " INTEGER NOT NULL, ");
sb.append(EventoColumnas.ACTIVO + " INTEGER ");
sb.append(");");
db.execSQL(sb.toString());
}
public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
db.execSQL("DROP TABLE IF EXISTS " + EventoTabla.TABLE_NAME);
EventoTabla.onCreate(db);
}
En primer lugar declaramos una constante con el nombre de la tabla. Vamos a utilizar una clase interna para declarar las constantes de cada columna. Esta clase implementa la interfaz
BaseColumns. Si nuestra aplicación necesitase compartir datos con otras aplicaciones, tendríamos que utilizar un ContentProvider. Uno de los requisitos de los ContentProvider es tener una columna _ID, como clave única. En este punto, hay que tener en cuenta, que SQLite sólo admite un campo como clave primaria. Por ello, utilizaremos una columna, autonumérica, como clave primaria. Esta columna va a ser nuestra BaseColums._ID. Para la creación del esquema de la tabla, utilizamos dos métodos, onCreate y onUpdate.
El método onCreate(SQLiteDabase) va a recibir el objeto SQLiteDatabase que contiene la conexión a la base de datos. Simplemente creamos una sentencia sql CREATE TABLA y ejecutamos el método execSQL(). Mediante este método podemos ejecutar cualquier sentencia sql que no sea un select o devuelva datos.
Una de las características de SQLite es que no se asigna un tipo de datos a la columna, sino que se asigna a cada valor. Es decir, aunque definamos una columna como entero, podemos insertar un string. Vamos a poder manejar cinco tipos de datos:
- NULL: el valor que va a contener la columna es NULL.
- INTEGER: pueden almacenarse enteros, de 1,2,3,4,6, o 8 bytes con signo.
- REAL: valores de punto flotante.
- TEXT: se trata de un string.
- BLOB: para valores BLOB( objetos binarios grandes) como imágenes, archivos multimedia, etc.
No existen los boolean, como tal. Estos se almacenan como INTEGER con el valor 0(falso) o 1(verdadero). Igual sucede con los campos fecha y hora. Estos se pueden almacenas como TEXT, REAL o INTEGER. En función del tipo elegido se almacenará en distintos formatos, por ejemplo, si es TEXT, el dato tendrá el formato "YYYY-MM-DD HH:MM:SS.SSS".
En onUpdate(SQLiteDatabase), borraremos la base de datos mendiente un DROP TABLE IF EXISTS. Como vemos, a continuación, lo que pretendemos hacer es eliminar la tabla para luego ejecutar el método onCreate y crear de esa forma la nueva estructura de la base de datos. Naturalmente, si la aplicación ya está en el market, no es buena idea borrar toda la tabla, junto con los datos, al menos que estos sean propios de la aplicación y vayamos a volver a cargarlos. Si deseamos hacer algún cambio en las tablas pero mantener sus registros también podríamos utilizar la sentencia ALTER TABLE.
La clase contactoTabla es igual que la de la tabla evento, conteniendo sólo los campos _id, nombre y teléfono.
Vamos a pararnos un momento en la tabla contactosDelEvento. Esta tabla, nos va a valer para poder relacionar ambas tablas, de esta forma, tendremos un diseño de base de datos normalizado, no teniendo la información de los contactos repetidos. En esta tabla los dos campos son claves foráneas del evento y del contacto.
public final class ContactoDelEventoTabla {
public static final String TABLE_NAME = "contactosDelEvento";
public static class ContactoDelEventoColumnas implements BaseColumns {
public static final String CONTACTO_ID = "contacto_id";
public static final String EVENTO_ID = "evento_id";
}
public static void onCreate(SQLiteDatabase db){
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE " + ContactoDelEventoTabla.TABLE_NAME + " (");
sb.append(ContactoDelEventoColumnas.EVENTO_ID + " INTEGER NOT NULL, ");
sb.append(ContactoDelEventoColumnas.CONTACTO_ID + " INTEGER NOT NULL, ");
sb.append("FOREIGN KEY(" + ContactoDelEventoColumnas.CONTACTO_ID + ")" +
" REFERENCES " + EventoTabla.TABLE_NAME + "(" +
BaseColumns._ID + ")");
sb.append("FOREIGN KEY(" + ContactoDelEventoColumnas.EVENTO_ID + ")" +
" REFERENCES " + ContactoDelEventoTabla.TABLE_NAME + "(" +
BaseColumns._ID + ")");
sb.append(");");
db.execSQL(sb.toString());
}
public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
db.execSQL("DROP TABLE IF EXISTS " +
ContactoDelEventoTabla.TABLE_NAME);
ContactoDelEventoTabla.onCreate(db);
}
}
Para declarar una clave foránea en SQLite, utilizamos la sentencia FOREIGN KEY.
Ahora ya podemos centrarnos en sobreescribir la clase SQLiteOpenHelper. Vamos a sobreescribir los métodos onCreate y onUpgrade. onCreate se ejecutará la primera vez que se ejecute la aplicación y onUpgrade cuando haya un incremento en el número de versión. Así, que en onCreate simplemente llamaremos a los métodos con las sentencias de creación de la tabla y en onUpgrade borraremos las tablas. Como nuestro método onUpgrade de cada tabla ya contiene la llamada al método onCreate no es necesario llamarle de nuevo.
Utilizaremos como modelos clases planas, para recoger los datos que pasemos a la base de datos o de esta a la clase modelo. Estas clases contendran simplemente los campos de cada tabla con sus respectivos getter y setters.
public class Evento{
private Long id;
private String nombre;
private boolean lunes;
private boolean martes;
private boolean miercoles;
private boolean jueves;
private boolean viernes;
private boolean sabado;
private boolean domingo;
private int deHora;
private int deMinuto;
private int aHora;
private int aMinuto;
private boolean activo;
private Set contactos;
public Evento(){
contactos = new LinkedHashSet();
}
...
No incluimos el código de los getters y setters.
Un evento contendrá varios contactos, por ello, incluimos el Set de contactos dentro de esta clase. El resto de la clase no tiene mucha más explicación. Igualmente haríamos con la clase Contactos.
El siguiente paso será crear los daos. En estas clases tendremos los métodos para seleccionar datos, insertarles, actualizarles o eliminarles. Todas estas opciones se pueden realizar de varias formas. Por ejemplo, para el insert podemos utilizar una sentencia precompilada o el método insert de la clase SQLiteDatabase. A la hora de ejecutar selects, podemos desarrollar la consulta entera o utilizar el método query que crea la select con los parámetros que le pasamos.
Veamos, como ejemplo la clase EventoDAO.
public class EventoDao implements DAO {
private static final String INSERT =
"insert into " + EventoTabla.TABLE_NAME
+ "(" + EventoColumnas.NOMBRE + ", " + EventoColumnas.LUNES + ", "
+ EventoColumnas.MARTES + ", " + EventoColumnas.MIERCOLES + ", "
+ EventoColumnas.JUEVES + ", " + EventoColumnas.VIERNES + ", "
+ EventoColumnas.SABADO + ", " + EventoColumnas.DOMINGO + ", "
+ EventoColumnas.DE_HORA + ", " + EventoColumnas.A_HORA + ", "
+ EventoColumnas.DE_MINUTO + ", " + EventoColumnas.A_MINUTO + ", "
+ EventoColumnas.ACTIVO + ")"
+ "values (?,?,?,?,?,?,?,?,?,?,?,?,?)";
private SQLiteDatabase db;
private SQLiteStatement insertStatement;
Para insertar los registros vamos a utilizar una sentencia precompilada. Estas sólo pueden devolver un valor, nunca múltiples registros, por lo cual, sólo es indicado para insertar, actualizar, eliminar, etc, pero no para selects. En estas acciones, el valor devuelto nos indicará el número de registros afectados, con lo cuál será posible ver si ha tenido éxito o no.
Declaramos el comando sql para el insert (INSERT INTO). Pasaremos los valores a insertar, como parámetros, estos ocuparán el lugar de las interrogaciones. Para ejecutar la sentencia precompilada utilizaremos el método compilaStatement de la clase SQLiteDatabase y recogeremos el valor en un objeto de tipo SQLiteStatement.
public EventoDao(SQLiteDatabase db){
this.db = db;
insertStatement = db.compileStatement(EventoDao.INSERT);
}
Para pasar los parámetros a la sentencia precompilada utilizaremos el método bindString, el cuál nos permite asignar un conjunto de claves y valor. La clave es el índice del parámetro empezando por el uno.
Finalmente, ejecutamos el insert con el executeInsert(), el cual nos devolverá el número de registros insertados.
public long save(Evento evento) {
insertStatement.clearBindings();
insertStatement.bindString(1, String.valueOf(evento.getNombre()));
insertStatement.bindString(2, String.valueOf(evento.isLunes()));
insertStatement.bindString(3, String.valueOf(evento.isMartes()));
insertStatement.bindString(4, String.valueOf(evento.isMiercoles()));
insertStatement.bindString(5, String.valueOf(evento.isJueves()));
insertStatement.bindString(6, String.valueOf(evento.isViernes()));
insertStatement.bindString(7, String.valueOf(evento.isSabado()));
insertStatement.bindString(8, String.valueOf(evento.isDomingo()));
insertStatement.bindLong(9, evento.getDeHora());
insertStatement.bindLong(10, evento.getaHora());
insertStatement.bindLong(11, evento.getDeMinuto());
insertStatement.bindLong(12, evento.getaMinuto());
insertStatement.bindString(13, String.valueOf(evento.isActivo()));
return insertStatement.executeInsert();
}
Como hemos comentado, es posible insertar registros de otra manera, mediante el uso del método insert de la clase SQLiteDatabase. En este caso, sólo hay que pasar un ContentValues, con el conjunto de claves y valor, nombre de la columna y el valor que queremos grabar.
public long save(Contacto contacto) {
final ContentValues values = new ContentValues();
values.put(ContactoColumnas.NOMBRE, contacto.getNombre());
values.put(ContactoColumnas.TELEFONO, contacto.getTelefono());
return db.insert(ContactoTabla.TABLE_NAME, null, values);
}
Al igual que el método insert, disponemos de un método específico para actualizar valores, update. Además de indicar la tabla que queremos actualizar y los valores a actualizar, debemos indicarle la condición para que sean actualizados los registros que deseamos. En un parámetro le indicaremos el where, "(BaseColumns._ID + " = ? ")" y en el siguiente los argumentos para el where mediante un array de strings.
public void update(Evento evento) {
final ContentValues values = new ContentValues();
values.put(EventoColumnas.NOMBRE, evento.getNombre());
values.put(EventoColumnas.LUNES, evento.isLunes());
values.put(EventoColumnas.MARTES, evento.isMartes());
values.put(EventoColumnas.MIERCOLES, evento.isMiercoles());
values.put(EventoColumnas.JUEVES, evento.isJueves());
values.put(EventoColumnas.VIERNES, evento.isViernes());
values.put(EventoColumnas.SABADO, evento.isSabado());
values.put(EventoColumnas.DOMINGO, evento.isDomingo());
values.put(EventoColumnas.DE_HORA, evento.getDeHora());
values.put(EventoColumnas.A_HORA, evento.getaHora());
values.put(EventoColumnas.DE_MINUTO, evento.getDeMinuto());
values.put(EventoColumnas.A_MINUTO, evento.getaMinuto());
values.put(EventoColumnas.ACTIVO, evento.isActivo());
db.update(EventoTabla.TABLE_NAME, values,
BaseColumns._ID + " = ? ", new String[]{
String.valueOf(evento.getId())});
}
Es fácil darnos cuenta, que para la última acción CRUD también dispondremos del método delete. Al igual que en el update, le indicaremos la condición que se va a aplicar para eliminar los registros.
public void delete(Evento evento) {
if (evento.getId()> 0 ){
db.delete(EventoTabla.TABLE_NAME,
BaseColumns._ID + " = ?" , new String[]{
String.valueOf(evento.getId())});
}
}
Queda por ver como podemos seleccionar registros. Vamos a ver dos formas de hacerlo.
En primer lugar, utilizando el método query de la clase SQLiteDatabase. Este método nos va a permitir una serie de parámetros mediante los cuales él va a componer la sql, de forma que no vamos a tener que escribir la consulta.
public Evento get(long id) {
Evento evento = null;
Cursor c = db.query(EventoTabla.TABLE_NAME,
new String[]{
BaseColumns._ID, EventoColumnas.NOMBRE,
EventoColumnas.LUNES, EventoColumnas.MARTES,
EventoColumnas.MIERCOLES, EventoColumnas.JUEVES,
EventoColumnas.VIERNES, EventoColumnas.SABADO,
EventoColumnas.DOMINGO,
EventoColumnas.DE_HORA, EventoColumnas.A_HORA,
EventoColumnas.DE_MINUTO, EventoColumnas.A_MINUTO,
EventoColumnas.ACTIVO},
BaseColumns._ID + " = ?", new String[]{String.valueOf(id)},
null, null, null, "1");
if (c.moveToFirst()){
evento = this.pasarDeCursorAEvento(c);
}
if (!c.isClosed()){
c.close();
}
return evento;
}
Vamos a ver, como mediante los parámetros que acepta el método query, tenemos todas las opciones que nos permitiría una select, como las sentencias group by, having, order by, etc.
public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
- table: el nombre de la tabla donde vamos a seleccionar los registros.
- columns: una lista con las columnas que debe devolver la select.
- selection: la condición where que deseamos aplicar. Si no pasamos ninguna condición nos devolvera todos los registros.
- selectionArgs: los argumentos a reemplazar en el where. Para ello, en el where utilizaremos el símbolo ?.
- groupBy: agrupará los valores por los campos indicados.
- having: ejecutará una condición de selección una vez agrupados los valores.
- orderBy: las columnas por las que se realizará la ordenación de los registros.
- limit: número máximo de registros que serán devueltos.
Para recoger los valores utilizaremos un cursor. El cursor tiene un comportamiento muy similar a un resulset. Como vemos, comprobamos que exista algún registro con el método moveToFirst().
moveToFirst().
if (c.moveToFirst()){
evento = this.pasarDeCursorAEvento(c);
}
Si se han encontrado valores, dispondremos de distintos métodos, en función del tipo de dato devuelto para poder asignarles a nuestro objeto evento. Les indicaremos el índice de la columna empezando por el cero.
private Evento pasarDeCursorAEvento(Cursor c) {
Evento evento = null;
if (c != null){
evento = new Evento();
evento.setId(c.getLong(0));
evento.setNombre(c.getString(1));
evento.setLunes(Boolean.getBoolean(c.getString(2)));
evento.setMartes(Boolean.getBoolean(c.getString(3)));
evento.setMiercoles(Boolean.getBoolean(c.getString(4)));
evento.setJueves(Boolean.getBoolean(c.getString(5)));
evento.setViernes(Boolean.getBoolean(c.getString(6)));
evento.setSabado(Boolean.getBoolean(c.getString(7)));
evento.setDomingo(Boolean.getBoolean(c.getString(8)));
evento.setDeHora(c.getInt(9));
evento.setaHora(c.getInt(10));
evento.setDeMinuto(c.getInt(11));
evento.setaMinuto(c.getInt(12));
evento.setActivo(Boolean.getBoolean(c.getString(13)));
}
return evento;
}
Si queremos desplazarnos por el cursor para trabajar con los siguientes registro utilizaremos moveToNext(). Finalmente, no debemos olvidarnos cerrar el cursor con el método close().
public List getAll() {
List list = new ArrayList();
Cursor c = db.query(EventoTabla.TABLE_NAME,
new String[]{
BaseColumns._ID, EventoColumnas.NOMBRE,
EventoColumnas.LUNES, EventoColumnas.MARTES,
EventoColumnas.MIERCOLES, EventoColumnas.JUEVES,
EventoColumnas.VIERNES, EventoColumnas.SABADO,
EventoColumnas.DOMINGO,
EventoColumnas.DE_HORA, EventoColumnas.A_HORA,
EventoColumnas.DE_MINUTO, EventoColumnas.A_MINUTO,
EventoColumnas.ACTIVO},
null, null,
EventoColumnas.NOMBRE,
null, null, null);
if (c.moveToFirst()){
do{
Evento evento = this.pasarDeCursorAEvento(c);
if (evento != null)
list.add(evento);
}while (c.moveToNext());
}
if (!c.isClosed()){
c.close();
}
return list;
}
La segunda forma que vamos a ver de ejecutar una sql es escribiendo directamente la consulta. Con el método rawQuery() se ejecuta esta query devolviéndonos igualmente los valores es un cursor.
public Evento find(String nombre){
long eventoId = 0L;
String sql = "select _id from " + EventoTabla.TABLE_NAME
+ " where upper(" + EventoColumnas.NOMBRE + ") = ? limit 1";
Cursor c = db.rawQuery(sql, new String[]{nombre.toUpperCase()});
if (c.moveToFirst()){
eventoId = c.getLong(0);
}
if (!c.isClosed()){
c.close();
}
return this.get(eventoId);
}
Este es un método muy útil si queremos ejecutar una select sobre varias tablas, o utilizar sentencias de agregado, como COUNT, MAX, MIN, SUM, etc.
Ya tenemos por un lado la clase openHelper, encargada de crear por primera vez el esquema y de actualizar en versiones sucesivas la base de datos. También tenemos nuestras clases modelos que actuarán como clase intermedia entre la interfaz del usuario y la base de datos. Y con estas clases, hemos creado nuestras clases daos, las que contienen la codificación propia de la gestión de los registros en la base de datos. Sólo nos queda crear la clase que se encargará de unir todo esto, es decir, lanzará la creación de la base de datos si es necesario, obtendrá la conexión, y dispondrá de los métodos de acceso a los datos. Estos métodos, serán útiles, para poder aplicar transaccionabilidad en los casos, en que estemos tratando a la vez con eventos y contactos. Por ejemplo, cuando queramos insertar un evento, también tendremos que grabar todos los contactos que pertenecen a ese evento.
public class DataManagerImpl implements DataManager{
private Context context;
private SQLiteDatabase db;
private EventoDao eventoDao;
private ContactoDao contactoDao;
private ContactoDelEventoDao contactoDelEventoDao;
public DataManagerImpl(Context context){
this.context = context;
SQLiteOpenHelper openHelper = new OpenHelper(this.context);
db = openHelper.getWritableDatabase();
eventoDao = new EventoDao(db);
contactoDao = new ContactoDao(db);
contactoDelEventoDao = new ContactoDelEventoDao(db);
}
En el constructor recogeremos el contexto, que va a ser necesario a la hora de poder llamar a nuestra claseOpenHelper(). Para que se ejecute la creación o actualización de la base de datos, hay que llamar a los métodos getWritableDatabase() o getReaderDatabase(), en función de si deseamos escribir en la base de datos o acceder sólo en modo de lectura. Por lo general, utilizaremos getReaderDatabase, cuando se produzca algún error en la apertura de la base de datos, como que el disco esté lleno. Estos métodos nos van a devolver un objeto SQLiteDatabase, que será como hemos visto el que necesitaremos para la gestión de los registros. Esta clase se va a comportar como nuestra lógica de negocio, con lo cual, deberíamos de crear todos los métodos que vamos a necesitar para tratar los datos. Vamos a ver unos pocos ejemplos.
Con el método getEvento, obtendremos los datos de un evento, junto con todos sus contactos.
public Evento getEvento(long eventoId) {
Evento evento = eventoDao.get(eventoId);
if (evento != null){
evento.getContactos().addAll(
contactoDelEventoDao.getContactos(eventoId));
}
return evento;
}
El método guardarEvento() nos será útil para ver como podemos utilizar la transaccionalidad. Con el método beginTransaction() indicaremos que vamos a empezar una transacción. Pueden darse dos casos, que el proceso se ejecute con éxito, en este caso insertando el evento y todos los contactos o que falle, ya sea la grabación del evento o de algún contacto. En el primero de los casos, la transacción habrá tendido éxito, con lo cual vamos a marcarla así con setTransactionSuccessful(). Tanto si la transacción fue correcta como si no, finalizaremos con el método endTransaction(). Si indicamos que la transacción fue correcta, se ejecutará el commit en caso contrario se efectuará un roll back, con lo cual no se grabará ningún dato.
public long guardarEvento(Evento evento) {
long eventoId = 0L;
try{
db.beginTransaction();
eventoId = eventoDao.save(evento);
if (evento.getContactos().size() > 0){
for (Contacto c: evento.getContactos()){
long contactoId = 0L;
Contacto contacto = contactoDao.find(c.getNombre());
if (contacto == null){
contactoId = contactoDao.save(c);
}else{
contactoId = contacto.getId();
}
ContactoDelEvento contactoDelEvento =
new ContactoDelEvento(new Long(eventoId), new Long(contactoId));
if (!contactoDelEventoDao.exists(contactoDelEvento)){
contactoDelEventoDao.save(contactoDelEvento);
}
}
}
db.setTransactionSuccessful();
}catch(SQLException e){
Log.e("DataManagerImpl", "Error salvando el evento", e);
eventoId = 0L;
}finally{
db.endTransaction();
}
return eventoId;
}
Con esta estructura de separación por capas, vemos, como si mañana se almacenasen los datos en xmls, no sería necesario modificar el código de la aplicación, simplemente sustituiriamos las clases de acceso a SQLite por las clases de tratamiento de xmls. Con lo cual, aunque vamos a tener que escribir algo más de código, merece la pena.
Acceder a SQLite
Lógicamente, en muchas ocasiones, necesitaremos acceder a la propia base de datos creada en SQLite, para ver las tablas generadas, errores en la ejecución de sentencias sql, los registros, etc. Contamos con varias herramientas para este fin.
En primer lugar, desde el shell de android disponemos del comando SQLite que nos permitirá acceder a la base de datos que indiquemos y podremos ejecutar cualquier sentencia sql admitida por SQLite. Para ello, accederemos a una sesión dos. Veremos qué emuladores de android estamos ejecutando mediante el comando "adb devices".
En nuestro caso tenemos el emulator-5554. Nos conectamos a él con, "adb -s emulator-5554 shell". Para acceder la base de datos disponemos del comando SQLite3, indicándole la ruta donde se encuentra la base de datos. Esta es "/data/data/nombre_del_paquete/databases/nombre_de_la_base_de_datos.db. Una vez conectados a la base de datos, podremos ejecutar tanto comandos propios como cualquier sentencia sql que soporte SQLite (estas deben finalizar en punto y coma). Entre los comandos tenemos algunos como:
- .help: nos muestra todos los comandos disponibles
- .exit: cerramos la instancia abierta a la base de datos.
- .tables: nos muestra una lista de las tablas creadas
- .schema ?TABLE?: nos muestra la sentencia CREATE utilizada para crear la tabla.
También disponemos de herramientas visuales para trabajar con SQLite. Una de ellas es
SQLite Manager, que es una extensión para firefox.
Desde eclipse también podemos ver si tenemos creada una base de datos. Para ello, hemos de utilizar la perspectiva DDMS. En la pestaña "File explorer", buscaremos, a través de la ruta que hemos indicado en el adb para encontrar el fichero de la base de datos. Una vez encontrado el fichero, podemos exportarle mediante la opción "Pull a file from the device" y utilizar, por ejemplo SQLite Manager para abrir la base de datos.
Documentación
- "Android in practice", editorial "Manning".
- "Android. Guía para desarrolladores", editorial "Anaya".
- Documentación oficial de android.
Reader Comments (10)
Hola.
Me interesaría saber la forma de acceder a una BBDD cliente-servidor.
gracias!
Es interesante su articulo, soy nueva en el uso de Android, pero me gustaria saber mas acerca del manejo de la Base de Datos. Tienen algun otro ejemplo... Gracias
Si elimino un regitro en una base de datos Sqlite que contenga un campo numerico autoincrement, ¿Cómo hago para que los demás registros actualicen dicho campo? ¿Tengo que actualizarlos uno por uno o existe una funcion en Sqlite para autonumerarlos?
gracias de antemano.
Felicitaciones muy buen artículo. hay posibilidad de que suban el código fuente para examinarlo.
Muy interesante tu artículo, gracias por compartir tus conocimientos.
Te doy las gracias porque aportes como el tuyo ayudan a ir viendo más en español ayudas y artículos que profundizan en la programación Android. Aún todavía se ve mucho más en lengua inglesa si se quiere uno meter en profundidades pero todo se andará.
¿Una duda sólo, hay limites de caracteres en los campos tipo text en BBDD SQLITE?
Gracias de nuevo.
Muchas gracias por la explicación, todo más claro ahora.
Hola una pregunta
En tu codigo no faltaria un + (copio las 3 lineas de codigo)
en la segunda linea despues de la ultima " o en la tercera linea antes de; "???
REFERENCES " + EventoTabla.TABLE_NAME + "(" +
.BaseColumns._ID + ")");
.sb.append("FOREIGN KEY(" + ContactoDelEventoColumnas.EVENTO_ID + ")" +
Buenas tardes.
Una duda... el tema de caracteres especiales como la "ñ" o vocales con tildes... a mi me corta la palabra cuando la recupero con el getstring()
ejemplo
creo mi cursor
....
String palabra = micursor.getString(1);
....
en la Base de datos está guardado España.... y getString(1) me devuelve "Espa"
Alguna solución? gracias...
Muy buena tu publicacion además e enriquecedora es interesante una pregunta debido a la cantidad de info q maneja Android y a pesar de multiples Bases de Datos mas robustas por qué escoje SQLITE? Y si tuvo alguna complicación?
Tu descripción es excelente. gracias