Android: zobrazovanie údajov z SQL databázy

Potrebujete vo vašej aplikácii ukladať dáta, ktoré majú pretrvať ukončenie aplikácie, či dokonca reštart systému? Okrem kadejakých textových súborov (s ktorými sa môže pracovať zložito) je štandardom SQL databáza. V Androide máme šťastie: k dispozícii máme klasickú databázu SQLite, ku ktorej existuje zabudované API priamo pripravené na používanie.

Na prípravu budeme potrebovať:

  1. vytvoriť si database open helper, teda triedu pre manažovanie životného cyklu databázy
  2. voliteľne vložiť testovacie dáta
  3. v aktivite vytvoriť jej inštanciu, z ktorej získame objekt SQLiteDatabase pre dopytovanie nad dátami
  4. z neho získame kurzor Cursor… a s ním už môžeme pracovať napr. v zoznamových adaptéroch

Deklarácia tabuliek

Ukážeme si to na príklade jednoduchej aplikácie s debilníčkom, kde budú úlohy ukladané v jedinej tabuľke s názvom task, ktorá bude v databázovej schéme s názvom tasks. Tabuľka bude mať tri stĺpce:

  • primárny kľúč. V tomto prípade sa spoľahneme na dve konvencie: v Androide je viac než vhodné pomenovávať primárne kľúče dohodnutým názvom _id, čo využíva viacero tried: napr. kurzorové adaptéry pre zoznamy. Druhá konvencia súvisí s vlastnosťami SQLite: stĺpce, ktoré majú mať automaticky generované kľúče, stačí zadeklarovať ako INTEGER PRIMARY KEY. Ak vložíme v INSERTe do takéhoto stĺpca NULL, a hodnota sa vytvorí automaticky. (Presnejší popis možno nájsť v dokumentácii).
  • stĺpec pre text položky: name typu TEXT
  • stĺpec is_done, kde indikujeme, že položka je už hotová. V SQLite sa síce nenachádza dátový typ BOOLEAN, ale dokumentácia ho odporúča nasimulovať typom INTEGER s 0 a 1 pre false, resp. true.

DatabaseOpenHelper

Database Open Helper je trieda zodpovedná za vytváranie schémy tabuliek (SQL príkazy CREATE) a za jej voliteľnú aktualizáciu (v prípade, že ju potrebujete zmeniť, pretože vaša aplikácia sa vylepšila, či zmenila).

Vytvoríme vlastnú podtriedu triedy SQLiteOpenHelper a prekryjeme:

  • konštruktor, z ktorého zavoláme rodičovský konštruktor so známymi parametrami, čo si ukážeme na príklade.
  • metódu onCreate(), ktorá sa volá v prípade, že databázová schéma (teda tabuľky) ešte neexistujú a treba ich vytvoriť.
  • metódu onUpgrade(), ktorá sa volá v prípade, že inštalujete novú verziu aplikácie a chcete aktualizovať existujúcu databázovú schému. V našom prípade budeme aktualizáciu ignorovať.

Prvá hlúpa verzia môže vyzerať:

public class TaskDatabaseOpenHelper extends SQLiteOpenHelper  {
    public TaskDatabaseOpenHelper(Context context) {
        super(context, "tasks", null, 1);
    }

    @Override
    public void onCreate(SQLiteDatabase database) {
        String createTableSql = "CREATE TABLE task (_id INTEGER PRIMARY KEY, name TEXT, is_done INTEGER)";
        database.execSQL(createTableSql);

        String insertSql = "INSERT INTO task VALUES(null, ?, ?)";
        database.execSQL(insertSql, new Object[] {"Zubár", 0});
        database.execSQL(insertSql, new Object[] {"Svadba", 0});
        database.execSQL(insertSql, new Object[] {"Raňajky", 1});
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // upgrade is not supported
    }
}

Konštruktor

Vytvoríme si konštruktor, ktorý zoberie jediný premenlivý parameter: a to kontext (obvykle aktivita, ktorá využíva tento helper). Zároveň sme povinní zavolať rodičovský konštruktor, ale všetky ostatné parametre budú zadrôtované:

  • kontext sme spomenuli
  • "tasks" je názov databázovej schémy
  • parameter null indikuje štandardne používanú továreň na kurzory (CursorFactory), čo je koncept, ktorý môžeme ignorovať.
  • posledný parameter indikuje číslo verzie schémy: v tomto prípade vytvárame prvú verziu, teda 1. Ak sa schéma bude vyvíjať, môžeme navyšovať číslo verzie.

Metóda onCreate()

Na vstupe máme parameter typu SQLiteDatabase, kde vieme volať metódu execSQL(), ktorou vieme vykonať nad databázou ľubovoľný dopyt. V prvom použití natvrdo zadrôtujeme CREATE pre vytvorenie tabuľky. V ďalšej trojici vkladáme vzorové dáta. Využívme tradičné správanie, kde v dopyte môžeme použiť otázniky reprezentujúce parametre a následne pri volaní execSQL() vieme dodať pole hodnôt, ktoré ich nahradia. (To je presne v súlade so zaužívanou filozofiou JDBC.) Výhodou otáznikov je aj typová bezpečnosť: nemusíme sa starať o úvodzovky a predchádzame tak bezpečnostnej diere SQL Injection. Databáza sama zistí, či je parameter typu reťazec, ktorý treba obaliť úvodzovkami alebo napr. typu číslo, kde úvodzovky nie sú potrebné.

K triede SQLiteDatabase sa ešte o chvíľu vrátime: je to totiž základná trieda pre vykonávanie SELECTov, či INSERTov nad tabuľkami.

Metóda onUpgrade()

Metóda sa volá vo chvíli, ak sa zistí, že číslo verzie databázy uvedené v konštruktore je vyššie než číslo verzie v databázovom súbore. Obvykle prebieha rozumné DROPnutie databázy a jej vytvorenie, ale ako sme spomínali vyššie: tu to nebudeme riešiť.

Použitie databázy v aktivite

Vytvorme si zoznamovú aktivitu, ktorá zobrazí dáta z databázy. Ak používame databázu, budeme pracovať s kurzormi, čo skvele funguje s kurzorovým adaptérom pre zoznamy SimpleCursorAdapter. Vyrobme si v aktivite pomocnú metódu:

private Cursor getAllTasks() {
    TaskDatabaseOpenHelper dbOpenHelper = new TaskDatabaseOpenHelper(this);
    SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
    Cursor cursor = db.query("task", 
        /* no projection */     null, 
        /* no selection */      null, 
        /* no selection args */ null, 
        /* no GROUP BY */       null, 
        /* no HAVING */         null, 
        /* no ORDER BY */       null);

    return cursor;
}

Filozofia je:

  • vytvoríme inštanciu nášho database open helpera pomocou konštruktora, ktorý sme zadeklarovali
  • zavolaním metódy getReadableDatabase()) získame inštanciu databázy SQLiteDatabase. Daná inštancia bude určená len na čítanie (budeme totiž len vykonávať SELECTy).
  • nad databázou budeme volať dopytovacie metódy, ktoré budú vracať kurzory.

Metóda query() má tonu parametrov a zodpovedá klauzule SELECT. Na základe parametrov vo vnútri vybuduje dopyt a odošle ho do databázy, pričom vráti kurzor ako výsledok. Najdôležitejší je názov tabuľky v prvom parametri. Ostatné parametre môžeme uviesť ako null, čím ich z budovaného dopytu vynecháme.

  • projekcia udáva zoznam stĺpcov, ktoré sa objavia v klazule SELECT _____ FROM. Odporúča sa uviesť len tie stĺpce, ktoré sa vo výsledku naozaj použijú.
  • selekcia udáva podmienku do klauzuly WHERE. Tá môže obsahovať otáznikové parametre, ktoré sa využijú v nasledovnom parametri.
  • parametre selekcie predstavujú pole objektov, ktoré sa majú nahradiť za prípadné otázniky v predošlom parametri
  • groupBy je reťazec, ktorý sa objaví v klauzule GROUP BY
  • having je reťazec, ktorý sa objaví v klauzule HAVING
  • sortOrder je reťazec, ktorý sa objaví v klauzule ORDER BY

Alternatívne dopytovanie cez rawQuery()

Alternatívna metóda [rawQuery()](http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#rawQuery(java.lang.String, java.lang.String[])) umožňuje uviesť celý SQL dopyt. Štandardne nie je odporúčaná (môže totiž dôjsť k bezpečnostným chybám), ale v niektorých prípadoch (napr. ak potrebujeme ALIASovať stĺpce) sa môže hodiť. Ukážkové volanie je ekvivalentné predošlému príkladu:

Cursor cursor = db.rawQuery("SELECT * FROM task", /* no selection args */ null);

Dopyt opäť môže obsahovať otáznikové parametre, ktoré môžeme voliteľne dodať cez pole objektov (alebo ho vynechať tým, že uvedieme null).

Kurzor v aktivite

Ak máme k dispozícii kurzor, ide všetko v rovnakom duchu, ako keď používate napr. content providerov. Ukážka metódy onCreate(), kde v zozname zobrazíme v každej položke hodnotu stĺpca name, teda názov položky:

Cursor cursor = getAllTasks();
String[] from = { "name" };
int[] to = { android.R.id.text1 };

SimpleCursorAdapter listAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, cursor, from, to);

setListAdapter(listAdapter);

Bokom poznamenajme, že SimpleCursorAdapter sa spolieha, že v tabuľke, z ktorej sa získava kurzor, sa bude nachádzať primárny kľúč pod názvom _id.

Štábna kultúra

Teraz, keď sme pochopili základnú filozofiu, môžeme sa venovať upratovaniu kódu. Základná problém spočíva vo využívaní názvov stĺpcov a tabuliek na viacerých miestach bez toho, aby sme využili konštanty.

Kontraktová trieda

Konvencia hovorí, že je vhodné vytvoriť triedu s rovnakým názvom ako má aplikácia (napr. Taskr) a v nej vytvoriť podtriedy zodpovedajúce jednotlivým tabuľkám, ktoré budú obsahovať konštanty pre názvy jednotlivých stĺpcov:

package sk.upjs.ics.taskr;

import android.provider.BaseColumns;

public abstract class Taskr {
    public static class Task implements BaseColumns {
        public static final String TABLE_NAME = "task";

        public static final String NAME = "name";

        public static final String IS_DONE = "is_done";
    }
}

Trieda Task implementuje interfejs BaseColumns, od ktorého zdedí konštantu pre stĺpec primárneho kľúča (_id).

Konštanty namiesto null

Ďalej si môžeme vytvoriť pomocnú triedu s konštantami používanými namiesto nullov v metóde query():

package sk.upjs.ics.taskr;

public interface CursorUtils {
    public static final String[] NO_PROJECTION = null;

    public static final String NO_SELECTION = null;

    public static final String[] NO_SELECTION_ARGS = null;

    public static final String NO_SORT_ORDER = null;

    public static final String NO_GROUP_BY = null;

    public static final String NO_HAVING = null;

    public static final String AUTOGENERATED_ID = null;
}

Upravený database open helper

Takto môžeme upraviť aj database open helper, kde využijeme konštanty z kontraktovej triedy Taskr.Task a z kurzorovej pomocnej triedy CursorUtils.

Zároveň definujeme ďalšie konštanty: pre aktuálnu verziu databázy (DATABASE_VERSION), pre názov databázovej schémy (DATABASE_NAME) a pre implicitnú továreň na kurzory (DEFAULT_CURSOR_FACTORY).

Vytváranie tabuliek potom spočíva vo vytvorení dopytu pomocou klasickej metódy String#format().

Prečo nepoužívame otázniky? Tie bohužiaľ fungujú len pre hodnoty v stĺpcoch. Nedajú sa použiť ako zástupné znaky pre mená tabuliek, či názvy stĺpcov: povedie to k strašným syntaktickým chybám pri pokuse o vykonanie dopytu.

public class TaskDatabaseOpenHelper extends SQLiteOpenHelper  {

    private static final int DATABASE_VERSION = 1;

    private static final String DATABASE_NAME = "tasks";

    private static final CursorFactory DEFAULT_CURSOR_FACTORY = null;

    public TaskDatabaseOpenHelper(Context context) {
        super(context, DATABASE_NAME, DEFAULT_CURSOR_FACTORY, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase database) {
        String createTableSql = String.format(
                "CREATE TABLE %s (%s INTEGER PRIMARY KEY, %s TEXT, %s INTEGER)",
                Taskr.Task.TABLE_NAME, Taskr.Task._ID, Taskr.Task.NAME, Taskr.Task.IS_DONE);
        database.execSQL(createTableSql);

        String insertSql = String.format("INSERT INTO %s VALUES(?, ?, ?)", Taskr.Task.TABLE_NAME);
        database.execSQL(insertSql, new Object[] {AUTOGENERATED_ID, "Zubár", 0});
        database.execSQL(insertSql, new Object[] {AUTOGENERATED_ID, "Svadba", 0});
        database.execSQL(insertSql, new Object[] {AUTOGENERATED_ID, "Raňajky", 1});
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // upgrade is not supported
    }

}

Upravená metóda getAllTasks()

Metóda pre získanie všetkých úloh vyzerá s konštantami tiež prehľadnejšie:

private Cursor getAllTasks() {
    TaskDatabaseOpenHelper dbOpenHelper = new TaskDatabaseOpenHelper(this);
    SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
    Cursor cursor = db.query(Taskr.Task.TABLE_NAME, NO_PROJECTION, NO_SELECTION, NO_SELECTION_ARGS, NO_GROUP_BY, NO_HAVING, NO_SORT_ORDER);

    return cursor;
}

Upravená metóda onCreate()

Rovnako sa dá sprehľadniť aj metóda onCreate() v aktivite:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Cursor cursor = getAllTasks();
    String[] from = { Taskr.Task.NAME };
    int[] to = { android.R.id.text1 };

    SimpleCursorAdapter listAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, cursor, from, to);

    setListAdapter(listAdapter);
}

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *