Rad sa SQLite bazom u Androidu (bez pomoćnih biblioteka)

Contract klasa

Contract klasa ima samo ulogu da bude kontejner za konstante koje definišu imena za URI-je, tabele i kolone. Ova klasa nam omogućava upotrebu istih konstanti u svim ostalim klasama, što nam dosta olakšava posao u radu sa bazom podataka jer omogućava da na jednom mestu menjamo ime kolone ili slično.

1) Kreirnje klase i konstruktora

Prva stvar je da kreiramo klasu i njen prazan konstruktor. Pošto je ovo samo kontejner za konstante koje su statične nije potrebno praviti instancu ove klase, a da bi sprečili da neko slučajno napravi instancu klase, definišemo konstruktor kao privatan:

2) Kreiranje podklase koja predstavlja tabelu

Preporuka je da za svaku tabelu unutar baze podataka kreiramo novu statičnu podklasu koja implementira BaseColumns interfejs. U okviru ove klase definišemo konstante vezane za tabelu kao što je ime tabele i nazive kolona:

NAPOMENA:
Nije potrebno da dodelimo konstante za kolone sa nazivom _ID i _COUNT jer njih dobijamo samim tim što smo implementirali “BaseColumns” interfejs. Pogledajte više o ovome interfejsu ovde.

SQLiteOpenHelper klasa

Za maksimalnu kontrolu nad lokalnim podacima i rad sa SQLite bazom (za izvršavanje SQL zahteva…) je dobro napraviti klasu koja extenduje SQLiteOpenHelper klasu. “SQLiteOpenHelper” klase je kao što joj i samo ime kaže “helper” klasa koja olakšava kreiranje i verzionisanje baze.

sqlite open helper

Konstruktoska metoda

Kao što smo pomenuli naša klasa extenduje SQLiteOpenHelper.java klasu, pa njena konstruktorska metoda koju generiše Android Studio izgleda ovako:

Kao što se vidi ovako generisana konstruktorska metoda ima priličan broj atributa, medjutim ukoliko definišemo konstante za naziv i verziju baze onda bi konstruktorska metoda mogla da izgleda kao u sledećem primeru:

Odmah nakn ekstendovanja klase primećujemo da naša klasa mora da implementira dve metode: onCreate() i onUpgrade(). O njihovoj ulozi ćemo kasnije.

Instanciranje klase koristeći “singleton pattern”

Pošto se baza podataka koristi u celoj aplikaciji (services, fragmenata…) iz tog razloga, preporučena praksa je da primenite tzv. “Singleton Pattern” na instance SQLiteOpenHelper klase kako biste izbegli curenje memorije. Najbolje rešenje je da instancu baze podataka napravite pojedinačnom instancom tokom čitavog životnog ciklusa aplikacije.

Statička metoda getInstance() osigurava da će u bilo kom trenutku postojati samo jedna AppDBHelper instanca i samo jedna buyLista.db baza podataka. Ako instanca helper klase nije inicijalizovana (“sInstance” objekat), stvoriće se jedan objekat, a ako je već stvorena, onda će se jednostavno koristi taj postojeći objekat.

Referenca na bazu podataka

Dobijanje baze koja je kreirana čim je instancirana ova helper klasa se vrši pozivajući metodu getWriteableDatabase(). Pošto je konekcija sa bazom keširana pa nije “skupo” pozivati ovu metodu gde god nam treba u klasi.

Sada kada imamo instancu naše baze koja je nasledila osobine klase SQLiteDatabase.java, možemo koristiti sve njene metode. Neke od često korišćenih metoda možete pogledati ovde.

Definisanje tabele u okviru onCreate()

U okviru onCreate() metode se definiše naša baza. Prvo napišemo “query” koji kreira tabelu, a zatim ga i izvršimo sa metodom execSQL(). SQL query koji kreira tabelu izgleda ovako:

A kada ga primenimo na naš primer ovako:

NAPOMENA:
Mogli bi da izbegnemo ponavljanje sekvence DBContract.TabelaZadataka u okviru query-ija tako što bi importovali sve u contract klasi:

Nakon čega bi mogli da izbacimo DBContract iz query-ija.

Verzionisanje baze podataka

Kao što smo već pomenuli, klasa koja ekstenduje “SQLiteOpenHelper” se koristi i za verzionisanje baze podataka. Verzionisanje baze je bitno jer ukoliko u jednom trenutku u novoj verziji naše aplikacije ipak shvatimo da nam je potrebna još jedna kolona, bez ovoga nećemo moći to da uradimo.
Potrebno je da definišemo u okviru onUpgrade() metode šta želimo da se uradi ukoliko dodje do promene tabele. Ova metoda će se pozvati samo ako već postoji baza podataka sa istim DATABASE_NAME, ali je DATABASE_VERSION različito od verzije baze podataka koja postoji na disku.

Primer

U ovome primeru je prikazana najjednostavnija impementacija kada se u tom slučaju briše stara tabela i ponovo kreira:

NAPOMENA:
Da bi se uopšte pozvala metoda onUpgrade() potrebno je u kodu promeniti verziju baze:

Query bazi podataka – db.query()

Jedna od najvažnijih funkcija za koju je namenjena ova helper metoda je dobijanje podataka iz baze. Podatke iz baze dobijamo standardnim SQL query-ijm koristeći metodu query() koja vraća Cursor objekat. Više o Cursor objektu i šta on predstavlja možete pogledati ovde.

Primer

Kada imamo podatke u okviru Cursor objekta potrebno je da prodjemo kroz njega i da popunimo listu to se najčešće u vidu jedne metode kao u sledećem primeru:

Ubacivanje podataka – db.insert()

Za ubacivanje podataka u bazu se koristi metoda insert(). Ova metoda za parametare zahteva: “naziv tabele”, “nullColumnHack” i “ContentValues”. ContentValues je klasa zadužena za čuvanje key-value seta vrednosti u okviru jednog objekta. Za skladištenje jednog set-a podataka se koristi njena metoda (String key, TipPodataka value):

Tek kada imamo definisana polja u jednom redu i sačuvana u ContentValues objektu možemo ih ubaciti u bazu sa pomenutom metodom insert():

Brisanje reda iz tabele – db.delete()

Za brisanje reda tabele je potrebno proslediti metodi delete() naziv tabele i WHERE klauzulu na osnovu koje će pronaći željeni red:

Ceo kod ove klase možete videti ovde, dok ceo projekat možete naći na GitHub stranici “SQLdatabaseWithoutLibrary”.

Kada imamo napravljenu ovu helper klasu i definisane njene metode za rad sa bazom podataka, u MVVM arhitekturi njima pristupamo iz Repository klase dok ona dalje emituje promene na više nivoe. Kako izgleda ceo proces pogledajte u članku “Repository – ViewModel – View ciklus”.

×

Cursor

Cursor je interfejs koji predstavlja dvodimenzionalnu tabelu neke baze. Kada želimo da dobijemo podatke iz baze koristeći query() metodu (i u okviru nje SELECT naredbu), baza podataka vraća Cursor objekat, koji je zadužen da prihvati i skladišti dobijene podatke.
Pokazivač na početku uvek pokazuje na 0-tu lokaciju, a prvi podaci se ustvari skladiše na prvu lokaciju (nulta lokacija postoji i kada je Cursor prazan). Iz tog razloga kada želite da preuzemo podatke iz kursora, prvo moramo da pređemo na prvi zapis. Iz tog razlog moramo da koristimo
moveToFirst() metodu koj vodi pokazivač kursora na prvo mesto. Nakon toga tek možemo pristupiti podacima prisutnim u prvom zapisu. Pored ove metode postoje i sledeće metode za iteraciju kroz Cursor objekat:

  • moveToLast()
  • moveToNext()
  • moveToPrevious()
  • moveToPosition(position)

Da biste dobili ukupan broj elemenata rezultujućeg upita, koristićemo metodu getCount(), dok metodu isAfterLast() koristimo za proveru da li je postignut kraj rezultata upita.
Kursor pruža i tzv. geter metode (npr. getLong(columnIndex), getInt(columnIndex) …) za pristup podacima odredjene kolone. Za ovu metodu neophodan parametar je “columnIndex” a njega dobijamo sa metodom getColumnIndex().

Primer

Dok “prolazimo” kroz cursor redove vrednost polja u nekom redu iz željene kolone dobijamo na sledeći način:

Indeks kolone koristimo za dobijanje podatka iz te kolone (npr. string) sa metodom getString():

Primer

Celu iteraciju kroz Cursor objekat može da izgleda ovako:

×

nullColumnHack

Ovaj parametar se koristi da androidu kaže šta da radi u slučaju da je ContentValues prazan. Ovo je bitno jer u SQL-u se koristi naredba:

Kada se bazi prosledi prazan ContentValues onda ona ne zna šta da stavi na mesto zaduženo za “vrednost” (a ne može da ostane prazno). Upravo rešava ovaj parametar “nullColumnHack”, snjim se definiše šta će da se stavi u u polje baze kome nije prosledjeno ništa (najčešće se stavlja null).

×

Metode SQLiteDatabase klase
  • beginTransaction() – startovanje transakcije u EXCLUSIVE modu kada želimo zapisivati u bazu.
  • beginTransactionNonExclusive()() – startovanje transakcije u IMMEDIATE modu.
  • endTransaction() – zaustavljanje transakcije
  • execSQL(String sql) – izvršavanje jednog SQL izraza koji NIJE SELECT ili bilo koji drugi SQL izraz koji vraća podatke.
  • getVersion() – vraća verziju baze.
  • insert(String table, String nullColumnHack, ContentValues values) – Ubacuje novi član/red u tabelu
  • insertOrThrow(String table, String nullColumnHack, ContentValues values) – Ubacuje novi član/red u tabelu ali izbacuje exception ako je transakcija neuspešna. Ova metod će vratiti -1 ako kod pravilno izvršen a izbaciće grešku ako nije.
  • query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) – Izvršava prosledjeni SQL query i vraća Cursor, ima zaštitu od SQL injections.
  • rawQuery(String sql, String[] selectionArgs) – Izvšava prosledjeni SQL query i vraća Cursor. Nema zaštitu i ogranjičenja, fleksibilniji pa se koristi kompleksnije upite.
  • replace(String table, String nullColumnHack, ContentValues initialValues) – Zamenjuje neki red u tabeli sa drugim
  • replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues) – Zamenjuje neki red u tabeli sa drugim, ali izbacuje exception ako je transakcija neuspešna.
  • setForeignKeyConstraintsEnabled(boolean enable) – Postavlja ograničenja u zavisnosti da li se koristi ForeignKey.
  • setVersion(int version) – Setuje verziju baze programirano.
  • update(String table, ContentValues values, String whereClause, String[] whereArgs) – Ažurira član/red tabele sa novim podacima.

×

SQL nivoi zaključavanja baze

SQL ima sledeće nivoe zaključavanja baze:

  • UNLOCKED: Što znači: defaultno neaktivno zaključavanje.
  • SHARED: Što znači: “Sada čitam podatke sada, nemojte pisati (ako se mora pisati u bazu mora će pričekati)”
  • RESERVED: Što znači: “Uskoro planiram pisati.” Samo jedna veza može dobiti RESERVED zaključavanje, svi ostali pokušaji zapisa moraju pričekati svoj red.”
  • PENDING: Što znači: “Pisaću čim svi ostali prestanu raditi stvari.”
  • EXCLUSIVE: Što znači: “Pišem SADA, odlazi!” Sve se zaustavlja dok se DB ažurira.
Tipovi transakcija u zavisnosti od zaključavanja

U zavisnosti od ovih nivoa zaključavanja postoje tri vrste transakcija:

  • DEFERRED : Automatski se obavalja zaključavnje i odključavanje svake SQL operacija. Filozofija ovdje je Just-In-Time. (Ovo je zadano kada nije naveden stvarni način rada.)
  • IMMEDIATE : Odmah pokušajte pribaviti i zadržati RESERVED zaključavanje na svim bazama podataka koje je otvorila ova veza. Ovo trenutno blokira sve ostale pisce za vreme trajanja ove transakcije . BEGIN IMMEDIATE TRANSACTIONblokirat će ili neće uspjeti ako druga veza ima REZERVIRANO ili EKSKLUZIVNO zaključavanje na bilo kojem od otvorenih DB-ova ove veze.
  • EXCLUSIVE : Odmah pokreće EXCLUSIVE zaključavanje na svim bazama podataka koje je otvorila ova veza. Ovo trenutno blokira sve ostale veze za vrijeme trajanja ove transakcije . BEGIN EXCLUSIVE TRANSACTIONće blokirati ili uspjeti ako neka druga veza ima bilo kakvu blokadu na bilo kojem od otvorenih DB-ova ove veze.

×

×

BaseColumns interfejs

Ovaj interfejs obezbedjuje dve kolone koje se najšeće koriste u okviru tabla: _ID i _COUNT. Njegov kod bi u osnovi ovako izgledao:

Mi možemo definisati i sopstvene konstante za id bez upotrebe ovog određenog interfejsa, ali metode poput CursorAdapter.java zahteva baš konstantu poput “_ID” pa je dobro koristiti priloženi interfejs. Još jedan primer je i adapter ListView-a koristi kolonu _ID da bi vam dao jedinstveni ID stavke liste na koju se klikće u okviru OnItemClickListener.onItemClick (), bez potrebe da svaki put izričito navedete koji je vaš ID kolone.
Korišćenje uobičajenih imena omogućava Android platformi (kao i programerima takođe) da se obrate jedinici bilo koje stavke podataka, bez obzira na njenu ukupnu strukturu (tj. Druge, kolone koje nisu ID). Definisanje konstanti za najčešće korišćene nizove u interfejsu / klasi izbegava ponavljanje i greške u kucanju po celom kodu.