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ť:
- vytvoriť si database open helper, teda triedu pre manažovanie životného cyklu databázy
- voliteľne vložiť testovacie dáta
- v aktivite vytvoriť jej inštanciu, z ktorej získame objekt
SQLiteDatabasepre dopytovanie nad dátami - 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ť akoINTEGER PRIMARY KEY. Ak vložíme vINSERTe do takéhoto stĺpcaNULL, a hodnota sa vytvorí automaticky. (Presnejší popis možno nájsť v dokumentácii). - stĺpec pre text položky:
nametypuTEXT - stĺpec
is_done, kde indikujeme, že položka je už hotová. V SQLite sa síce nenachádza dátový typBOOLEAN, ale dokumentácia ho odporúča nasimulovať typomINTEGERs 0 a 1 prefalse, 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
nullindikuje š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ázySQLiteDatabase. 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
groupByje reťazec, ktorý sa objaví v klauzuleGROUP BYhavingje reťazec, ktorý sa objaví v klauzuleHAVINGsortOrderje reťazec, ktorý sa objaví v klauzuleORDER 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);
}