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
SQLiteDatabase
pre 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 vINSERT
e 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:
name
typuTEXT
- 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ť typomINTEGER
s 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
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 SELECT
ov, či INSERT
ov 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é DROP
nutie 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ťSELECT
y). - 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 klauzuleGROUP BY
having
je reťazec, ktorý sa objaví v klauzuleHAVING
sortOrder
je 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 ALIAS
ovať 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 null
ov 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);
}