În Tips & Tricks

Lucrul cu baze de date în Android

android date

Într-unul dintre articolele precedente, am vorbit despre persistarea datelor, dar am văzut doar cum se pot memora anumite valori care țin de configurarea aplicației. De multe ori însă, vor apărea și situații în care vom avea de memorat mult mai mult decât simple setări.

De exemplu, putem să stocăm local date care sunt trimise de un server, pentru a le folosi atunci când conexiunea la Internet nu este disponibilă sau când avem de-a face cu fișiere de dimensiuni mari, cum ar fi cele de imagine, pe care nu ne permitem să le descărcăm de fiecare dată.

Pentru stocarea de informație de dimensiuni mai mari, Android ne pune la dispoziție o bază de date locală, unde o putem memora într-un mod structurat și ușor accesibil.

Baza de date SQLite

De cele mai multe ori, întâlnim bazele de date în contextul unor aplicații care au conexiune la unul sau mai multe servere și care extrag și scriu informație din/în acestea. Când ne gândim la baze de date, cel mai probabil, ne vin în minte Oracle, SQL Server sau altele.

Dacă aceste baze de date sunt proiectate pentru a gestiona un volum foarte mare de informație, SQLite are o altă abordare, fiind o bază de date open-source, folosită cel mai des pentru a gestiona informația la nivel local. Cu toate că poate prima impresie lasă de dorit, e bine de știut că SQLite este folosită în multe produse de renume, cum ar fi Mac OS X, Chrome și chiar Dropbox.

La fel cum indică și numele, SQLite folosește Structured Query Language pentru a facilita accesul la datele stocate și a fost proiectată în special pentru a fi rapidă, cu dimensiuni reduse și ușor de folosit. Dar asta nu înseamnă că nu este capabilă. SQLite oferă suport pentru tranzacții (care sunt atomice chiar și după crash-uri de sistem), funcții, foreign keys, triggers și multe altele.

Pentru a proteja datele, Android restricționează accesul la aceste baze de date, făcându-le direct accesibile doar aplicației care le-a creat. Astfel, putem avea oricâte baze de date cu oricâte tabele fiecare, ele fiind accesibile claselor din aplicație direct prin intermediul numelor lor.

Facilitățile din SDK

SDK-ul beneficiază de o serie de clase prin intermediul cărora putem lucra cu SQLite. Ele sunt structurate în două pachete: android.database și android.database.sqlite. Primul, android.database, nu este legat specific de SQLite și oferă o serie de abstractizări care ne ajută în implementarea de clase specifice aplicației noastre. Conținute în acest pachet sunt interfața Cursor și clasa AbstractCursor.

Cursorul reprezintă un concept simplu pentru traversarea result set-urilor din baza de date, iterând peste ele și oferind acces la date rând cu rând. Interfața Cursor definește o serie de metode pentru accesul de read/write la baza de date, iar AbstractCursor oferă o implementare de bază a acestora. De cele mai multe ori, vom fi nevoiți să subclasăm AbstractCursor pentru a oferi o implementare specifică nevoilor noastre.

Cel de-al doilea pachet, android.database.sqlite, conține o serie de clase specifice SQLite. Cele mai importante ar fi SQLiteCursor, care este practic o implementare a Cursor pentru a traversa seturi de rezultate din SQLite, SQLiteDatabase, care este un wrapper ce expune metodele de access la o bază de date cum ar fi deschiderea și închiderea conexiunilor și execuția de query-uri, și nu în ultimul rând, SQLiteOpenHelper.

O mână de ajutor

SQLiteOpenHelper ocupă un loc mai aparte, pentru că este o clasă menită să creeze și să actualizeze bazele de date, precum și versiunile lor. Este recomandat ca atunci când creăm o bază de date nouă să subclasăm această clasă și să oferim o implementare specifică. Iată un exemplu simplu:

public class DictionaryOpenHelper extends SQLiteOpenHelper {
    private static final int DATABASE_VERSION = 2;
    private static final String DICTIONARY_TABLE_NAME ="dictionary";
    private static final String DICTIONARY_TABLE_CREATE =

                "CREATE TABLE " + DICTIONARY_TABLE_NAME + " (" +
                KEY_WORD + " TEXT, " +
                KEY_DEFINITION + " TEXT);";

    DictionaryOpenHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
       db.execSQL(DICTIONARY_TABLE_CREATE);
   }
}

Un lucru important de știut atunci când lucrăm cu helper-ul este acela că baza de date nu este creată în momentul creării instanței de helper, ci doar la apelul metodelor ce facilitează accesul efectiv la aceasta: getWritableDatabase(), dacă dorim să scriem, sau getReadableDatabase(), dacă dorim doar să citim.

Aceste două metode trec printr-o serie de operații, în special când sunt apelate pentru prima dată. Este foarte important ca ele să nu fie apelate de pe main thread pentru că pot duce la blocarea temporară a aplicației. Pentru mai multe detalii legate de main thread vezi articolul anterior. La primul apel, metodele vor executa la rândul lor metodele onCreate(), onUpgrade() și/sau onOpen().

Aici, versiunea joacă un rol important. Ea începe implicit de la 1 și trebuie incrementată pentru fiecare modificare succesivă. Astfel, atunci când dorim să lansăm o versiune nouă a aplicației care folosește o bază de date cu structura modificată, versiunea trebuie incrementată. Dacă ea este mai mare decât cea curentă, metoda onUpgrade() va rula operațiile necesare modificării structurii bazei de date existente, pentru a o aduce la noua formă. Aceste operații trebuie făcute cu atenție, pentru a evita pierderea datelor deja existente.

Din punct de vedere al interogării bazei de date, toate query-urile se pot plasa prin metode dedicate. Se pot folosi atât metode care primesc o serie de parametri și construiesc interogarea, cât și query-uri directe. Fiecare interogare va returna un Cursor, cu ajutorul căruia putem să iterăm prin rândurile rezultatului.

Iată un exemplu de query pe o bază de date:

public Cursor getAllContacts() {         
   String selectQuery = "SELECT  * FROM table_name;         
   SQLiteDatabase db = this.getReadableDatabase();         
   Cursor cursor = db.rawQuery(selectQuery, null);

   return cursor;     
}

Aspectele specifice platformei se opresc însă aici. Tratarea result-set-urilor, parcurgerea prin intermediul cursorilor sau folosirea de query-uri mai complexe sunt similare cu cele din Java sau alte limbaje orientate pe obiecte. Desigur, și restul recomandărilor de good-practice legate de lucrul cu baze de date, cum ar fi crearea unui model obiectual pentru date și a unui data access layer pentru abstractizare, rămân valabile.

În articolul următor vom vedea cum putem expune informația pe care o avem stocată spre exterior, făcând-o accesibilă, într-un mod controlat, altor aplicații.