Bases de datos en Android LSUB, GYSC, URJC Base de Datos Relacional • Permite guardar datos relacionados • Preservando ACID http://www.amazon.com/dp/0321197844/ SQL • Lenguaje estándar (casi todas las implementaciones tienen extensiones) http://www.iso.org/iso/iso_catalogue/catalogue_tc/catalogue_detail.htm?csnumber=53681 • Para acceder a bases de datos relacionales Álgebra relacional: Relación • Una base de datos guarda relaciones: ej. El nombre de un empleado y su fecha de nacimiento • Una relación es un conjunto de tuplas de atributos (Juan Perez, 1998, C/ Luna) es una tupla, Juan Perez, 1998, C/ Luna son atributos • Las tuplas de valores son filas, los conjuntos del elementos i de la tupla son columnas Álgebra relacional: operaciones • Proyección (quedarme con algunas columnas): SELECT X, Y FROM AA • Selección (quedarme con algunas filas): SELECT * FROM R WHERE XXX • Producto Cartesiano: combinar relaciones (tamaño NxM, ojo) SELECT * FROM R, S (producto cartesiano, CROSS JOIN) - Producto Cartesiano + Selección XXX (JOIN, INNER JOIN) SELECT * FROM R JOIN S ON XXX (si ON es una condición de igualdad EQUIJOIN) - NATURAL JOIN: join R con S la comparación de los conjuntos de atributos de igual nombre en SELECT * FROM R NATURAL JOIN S (es un tipo especial de equijoin) • Unión de conjuntos/Diferencia de conjuntos Claves • Para identificar unívocamente a una tupla: dos tuplas, diferentes claves (ej: para cambiar un atributo) • Puede un atributo o varios • Se puede asignar automáticamente (AUTOINCREMENT, implícita) • Puede haber más de una clave, pero hay una esencial: clave primaria • Si un atributo es clave de otra tabla: foreign key, clave ajena • Índices: traducen de clave a tupla, hay uno siempre, para la clave primaria, puedo crear otros para ir más rápido (árbol B) Normalización • Cuidado al definir las tablas • Anomalías de actualización: - Ej: La clave son dos atributos, pero los datos dependen de uno, datos repetidos... • Tres formas normales. Al final: - Cualquier atributo (no clave) debe decir algo sobre la clave, toda la clave y nada más que la clave SQLite • Implementación de SQL, más detalles: http://it-ebooks.info/book/147/ • Android permite usar bases de datos SQLite de serie. • No requiere administración. Una base de datos SQLite es un fichero único. • Permite almacenar datos: TEXT (como String de Java), INTEGER (como long de Java), REAL (como double de Java), BLOB binario (x’ac23ab’ de longitud arbitraria) SQLite • “Dynamic typing”: cualquier tipo de datos puede insertarse en cualquier columna (¡aunque no coincida el tipo!). Ojo. • Si queremos comprobación de violación de claves ajenas (foreign keys), hay que activarlo: PRAGMA foreign_keys = ON; SQLite • Una base de datos tiene un schema que la define (tablas, índices, etc.). • Toda base de datos tiene una tabla llamada SQLITE_MASTER (sólo lectura) que representa el esquema de la base de datos (se actualiza al cambiar el esquema). Sus columnas son: • • • • • type (TEXT): tabla o índice. name (TEXT): nombre. tbl_name (TEXT): nombre de la tabla para índices. rootpage (INTEGER): estructura de almacenamiento (b-tree). sql (TEXT): sentencia que creó la tabla o índice. SQLite • Ejemplo: SQLite CREATE TABLE Designations ( DesignationId INTEGER PRIMARY KEY AUTOINCREMENT, Designation TEXT, Description TEXT ); SQLite CREATE TABLE Departments ( DeptId INTEGER PRIMARY KEY AUTOINCREMENT, DeptName TEXT, Description TEXT ); SQLite CREATE TABLE Employees ( EmpId INTEGER PRIMARY KEY AUTOINCREMENT, EmpCode INTEGER, FirstName TEXT, LastName TEXT, Email TEXT, DeptId INTEGER, DesignationId INTEGER, ManagerId INTEGER, Address TEXT, FOREIGN KEY(DeptId) REFERENCES Departments(DeptId), FOREIGN KEY(DesignationId) REFERENCES Designations(DesignationId) ); SQLite También se puede volcar el esquema con el comando .schema SQLite ! INSERT INTO Departments ( DeptName, Description ) VALUES( "IT", "Technical staff" ); ! INSERT INTO Departments ( DeptName, Description ) VALUES( "Sales", "Sales executives" ); ! SQLite INSERT INTO Designations ( Designation, Description ) VALUES( "Programmer", "C and Java developer" ); ! SQLite INSERT INTO Employees ( EmpCode, FirstName, LastName, Email, DeptId, DesignationId, ManagerId, Address ) VALUES ( 554, "John", "Doe", "[email protected]", 1, 1, 32, "Foo Rd. 12 D3" ); ! SQLite SELECT * FROM Departments; ! SELECT * FROM Designations; ! SELECT * FROM Employees; ! SELECT FirstName, LastName, DesignationId FROM Employees WHERE LastName == "Doe"; ! SQLite SELECT Employees.FirstName, Employees.LastName, Designations.Description FROM Employees NATURAL JOIN Designations WHERE Employees.LastName == "Doe" ; ! SELECT Employees.FirstName, Employees.LastName, Departments.DeptName FROM Employees NATURAL JOIN Departments WHERE Employees.LastName == "Doe" ; ! ! ! SQLite SELECT Employees.FirstName, Employees.LastName, Departments.DeptName, Designations.Description FROM Employees INNER JOIN Designations ON Employees.DesignationId == Designations.DesignationId INNER JOIN Departments ON Employees.DeptId == Departments.DeptId WHERE Employees.LastName == "Doe" ; SQLite UPDATE Employees SET FirstName = "Manolo" WHERE EmpId == 1; SQLite • DELETE: borra filas. • DROP: borra tablas/índices. • Más info: http://www.sqlite.org/lang.html SQLiteOpenHelper • Heredando de esa clase podemos creamos nuestra clase para acceder a la base de datos. • En el constructor, al llamar a super(), hay que proporcionar el nombre de la base de datos y la versión que queremos. • Hay que cerrarlo después de usarlo. SQLiteOpenHelper public class DB extends SQLiteOpenHelper { private final static String NAME = "company.db"; private final static int VERSION = 1; ! public DB(Context context){ super(context, NAME, null, VERSION); } ! ... } SQLiteOpenHelper • Tendremos que redefinir los métodos: • onCreate(): se invoca cuando se crea la base de datos si no existe. • onUpgrade(): se invoca cuando se invoca super() con una nueva versión de la DB. Aquí debemos hacer lo necesario para pasar de una versión a otra (borrar tablas, crear tablas, etc.). SQLiteOpenHelper ! public final static String DESIGNATIONS = "Designations"; private final static String CREATE_DESIGNATIONS = "CREATE TABLE " + DESIGNATIONS + " (" + " _id INTEGER PRIMARY KEY AUTOINCREMENT, " + " Designation TEXT, " + " Description TEXT);"; ! private final static String DESIGNATIONS = "Designations"; private final static String EMPLOYEES = "Employees"; // the two other CREATE SQL queries (CREATE_DEPARTMENTS and CREATE_EMPLOYEES) are excluded ... ! ! @Override public void onCreate(SQLiteDatabase database) { Log.v(DB.class.getName(),"Creating DB."); database.execSQL(CREATE_DESIGNATIONS); database.execSQL(CREATE_DEPARTMENTS); database.execSQL(CREATE_EMPLOYERS); } private void doReset(SQLiteDatabase database){ database.execSQL("DROP TABLE IF EXISTS " + DESIGNATIONS); database.execSQL("DROP TABLE IF EXISTS " + DEPARTMENTS); database.execSQL("DROP TABLE IF EXISTS " + EMPLOYEES); onCreate(database); } @Override public void onUpgrade(SQLiteDatabase database, int from, int to) { Log.v(DB.class.getName(),"Upgrade DB, new version: " + to + ", deleting data"); doReset(database); } SQLiteOpenHelper • De la clase helper podemos conseguir un objeto SQLiteDatabase para acceder a los datos: • getReadableDatabase(): da acceso de sólo lectura a la base de datos. • getWriteableDatabase(): da acceso de escritura. SQLiteDatabase • Esta clase representa una DB tiene métodos para realizar queries SQL. • Convenio en Android: la clave primaria de una tabla se debe llamar _id • Hay que cerrarlo después de usarlo. SQLiteDatabase ! • insert(), update(), delete(): métodos que facilitan este tipo de peticiones. public void insertDepartment(Department d) { //myHelper is a DB reference (SQLiteDatabaseHelper) SQLiteDatabase database = MyHelper.getWritableDatabase(); ContentValues values = new ContentValues(); ! } values.put("DeptName", d.getDeptname()); values.put("Description", d.getDescription()); if(database.insert(DEPARTMENTS, null , values) == -1){ database.close(); throw new RuntimeException("Can't insert department in database"); } database.close(); SQLiteDatabase • rawQuery(): ejecuta una sentencia SQL expresada en una String. La query no debe acabar en ‘;’. Retorna un Cursor con los resultados. • Un parámetro (selection): array de Strings para insertar valores en la query de forma cómoda. • Cursor: clase que representa los resultados de una query. Es una colección iterable de filas. SQLiteDatabase private Integer getDepartmentID(SQLiteDatabase database, Department dept) { String query = "SELECT _id FROM "+ DEPARTMENTS +" WHERE DeptName == ?"; String selection[] = {dept.getDeptname()}; Cursor cursor = database.rawQuery(query, selection); if(cursor.getCount() != 1){ cursor.close(); throw new RuntimeException("Department does not exist"); } cursor.moveToFirst(); int ret = cursor.getInt(0); cursor.close(); return ret; } SQLiteDatabase public String listEmployees(){ String text = null; SQLiteDatabase database = this.getReadableDatabase(); String query = "SELECT " + EMPLOYEES + ".FirstName, " + EMPLOYEES + ".LastName, " + DEPARTMENTS + ".DeptName, " + DESIGNATIONS + ".Description " + " FROM " + EMPLOYEES + " INNER JOIN "+ DESIGNATIONS + " ON " + EMPLOYEES + ".DesignationId == " + DESIGNATIONS + "._id " + " INNER JOIN " + DEPARTMENTS + " ON " + EMPLOYEES + ".DeptId == " + DEPARTMENTS + "._id"; Cursor cursor = database.rawQuery(query, null); if(cursor.moveToFirst()){ text = ""; do{ text = text + cursor.getString(cursor.getColumnIndex("FirstName")) + " " + cursor.getString(cursor.getColumnIndex("LastName")) + ", Department: " +cursor.getString(cursor.getColumnIndex("DeptName")) + ", Current designation: " + cursor.getString(cursor.getColumnIndex("Description")) + "\n"; }while(cursor.moveToNext()); } cursor.close(); database.close(); return text; } SQLiteDatabase • query() argumentos: parámetros de la consulta. Retorna un Cursor con los resultados. • execSQL(): para ejecutar una sentencia SQL que no retorne datos, una String. • setForeignKeyConstraintsEnabled(): activa la comprobación de claves ajenas en SQLite. SQLiteQueryBuilder • Es una clase que facilita la realización de queries. El método query() public Cursor query (SQLiteDatabase db, String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, StringsortOrder, String limit) • • • • • • • projectionIn: lista de columnas a retornar, null significa todas. selection: filtro para seleccionar las filas, que contiene las expresiones del WHERE. selectionArgs: array con los valores de los ‘?’. groupBy: filtro para agrupar las filas (la cláusula GROUP BY de SQL). having: cláusula HAVING de SQL. sortOrder: cláusula ORDER BY de SQL. limit: número máximo de filas retornadas, formateadas como la cláusula LIMIT. SQLiteQueryBuilder • query(uri, selection=”column=”+value, selectionArgs=null, sortOrder) query(uri, “number=2423434”, sortOrder) • query(uri, projection, selection=”column=?”, selectionArgs={value_as_string}, sortOrder) • SELECT * from contacts_table WHERE number=‘134134134’ SQLiteQueryBuilder SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder(); SQLiteDatabase database = MyHelper.getReadableDatabase(); qBuilder.setTables(EMPLOYEES); ! Cursor c = qBuilder.query(database, projection, selection, selectionArgs, null, null, sortOrder); ContentProvider • Abstracción general de conjunto de datos • Las aplicaciones acceden a ellos deben a través de un ContentResolver ContentProvider • Las bases de datos de una aplicación son privadas. • Para ofrecer datos a otras aplicaciones: crear un ContentProvider. • Un ContentProvider abstrae la DB subyacente. • El esquema puede cambiar, la interfaz con el cliente no. • Por omisión es accesible desde otras aplicaciones. • Tiene que ocuparse de controlar el acceso concurrente a los datos. ContentProvider • Los datos del ContentProvider se representan mediante URIs en el manifiesto de la aplicación. • AndroidManifiest.xml: ! <provider ! android:authorities="org.lsub.employees.contentprovider" android:name=".contentprovider.EmployeesContentProvider" > </provider> • Sólo para uso interno, debemos poner en el manifiesto: android:exporte=false ContentProvider • Para crear un ContenteProvider clase que extiende android.content.ContentProvider. • Hay que reescribir, (reciben como parámetro una URI representando el recurso): • onCreate(): inicialización del proveedor. • query(), insert(), update(), delete(). • getType(): devuelve el tipo MIME de la URI. • Si no se soporta alguna operación, elevar UnsupportedOperationException. • Los métodos que modifican contenidos deben llamar a notifyChange() por cortesía. Usar ContentProvider • • • Hacen falta permisos. En el manifiesto. Ej: para los contactos hay que usar el permiso • android.permission.READ_CONTACTS Usar ContentProvider Add →Uses Permission Usar ContentProvider Usar ContentProvider La organización de los datos de los contactos es la siguiente: ! • ContactsContract.Contacts: tabla con los contactos, con la clave de raw contact. • ContactsContract.RawContacts: tabla con el resumen de los datos de un contacto, como nombre de cuenta, tipo de cuenta (p. ej. Google), etc. sin estructurar. ! • ContactsContract.Data: tabla con los detalles de un contacto estructurados, nombre y apellidos, email o número de teléfono. Tiene columnas como el tipo MIME, etc. ! http://developer.android.com/guide/topics/providers/contacts-provider.html Usar ContentProvider ! • Hay un contrato entre el proveedor y los clientes. • El contrato define las URIs y las columnas que ofrece el proveedor, independientemente de sus esquema interno. • El contrato de los contactos es http://developer.android.com/reference/android/provider/ContactsContract.html • Por ej. podemos usar el contrato del recurso para localizar la URI de una tabla: Uri uri = ContactsContract.Contacts.CONTENT_URI; Usar ContentProvider • Por ejemplo: conseguir el _id y nombre de todos los contactos, ordenados por su _id: Uri uri = ContactsContract.Contacts.CONTENT_URI; String[] projection = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME }; String sortOrder = ContactsContract.Contacts._ID; Cursor cursor = getContentResolver().query(uri, projection, null, null, sortOrder); ! Usar ContentProvider (REST) • Por ejemplo: conseguir el nombre de un _id concreto: //append the record number for the query, REST style. Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id); String[] projection = new String[] {ContactsContract.Contacts.DISPLAY_NAME}; ! cursor = getContentResolver().query(uri, projection, null, null, null); Usar ContentProvider (Data) ! • Para acceder a los datos comunes (número de teléfono, email, etc.) de la tabla ContactContract.Data se usa ContactContract.CommonDataKinds: uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI; projection = new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER}; selection = ContactsContract.CommonDataKinds.Phone.CONTACT_ID +" = "+ contactId; ! cursor = getContentResolver().query(uri, projection, selection, null, null);