Rad sa SQLite bazom u Androidu uz pomoć Room bibiloteke

Uvod

Room je bibloteka preporučena od strane Google-a kao jedna od komponenti tzv. “Android Architecture Components” pristupa. Room je wrapper oko SQLite baze i predstavlja sloj apstrakcije koji olakšava rad sa bazom podataka. Room bibilioteka preuzima na sebe većinu obaveza tako da sada lakše kreiramo tabele i upravljamo podacima.
Room ima tzv. compile-time checks tj. kontrolu koda pri kompajliranju i ukoliko postoji greška ona će se pokazati pri samom kompajliranju. Room nas na taj način spašava iritantnih sitnih grešaka koje nastaju u radu sa SQL bazom (nedostatak tačka zareza ili razmaka…) a koje izazivaju RunTimeException.

Da bi radili sa ovom bibliotekom potrebno je da se definišu dependencies:

room and MVVM diagram

Room biblioteka se sastoji iz tri glavne komponente:

Entity (tabela)

Entity je klasa koja predstavlja tabelu baze podataka a obeležava se sa @Entity. U nastavku ove anotacije (u okviru zagrada) možemo definisati naziv tabele koji može biti drugačiji od samog naziva klase. Ukoliko ne definišemo ime tabele na ovaj način, Room će generisati tabelu sa istim imenom kao što je naziv klase.

Svaki entitet mora imati konstruktor! Konstruktor se najčešće definiše tako da mu se parametri podudaraju sa poljima (na osnovu tipa i imena). Medjutim konstruktor ne mora da primi sva polja kao parametre, polje koje ne želimo da stavimo u konstruktor moramo da obeležimo sa anotacijom @Ignore u suprotnom će Android studio prikazati grešku. Takodje u okviru konstruktora ne moramo da stavimo polje koje se autogeneriše (tj. autoGenerate = true), ali za to polje mora da postoji public setter (mi ga nećemo koristi ali neophodan je Room biblioteci).

@PrimaryKey

U radu sa Room biblioteko jedna kolona mora da ima definisan tzv. PrimaryKey. Obeležavanje kolone koja će predstavljati “Primary key” se vrši sa dodavanjem anotacije @PrimaryKey kod tog polja. Kad definišemo da je neko polje PrimaryKey onda možemo da koristimo njegovu metodu autoGenerate() ili jednostavnije samo u zagradama da dodamo (autoGenerate = true).

Primer

U ovome primeru prvo polje mId se automatski generiše, pa ga nećemo ubaciti u konstruktor, ali je ipak neophodno da se obezbedi njegov setter jer je potreban Room biblioteci (za ostala polja to nisu neophodni setteri).

NAPOMENA:
Konstruktor eventualno može biti i bez ijednog argumenata ali tada moraju postojati definisani setteri za sva polja.

@ColumnInfo

Room po default-u generiše tabelu sa nazivom kolone istio kao i polje klase, medjutim ukoliko želimo da naziv kolone ima drugačiji naziv od naziva polja koje predstavlja, onda moramo koristiti @ColumnInfo anotaciju i u zagradi definisti drugačije ime kolone.

@Embedded

Sa ovom notacijom je moguće ugneziditi jednu tabelu unutar druge (one-one reacija).

Sada možemo praviti upite (query) u ovoj tabeli a User objekat ima sve kolone: id, firstName, street, state, city, i post_code.

NAPOMENA:
Možemo dodati prefiks svim nazivima kolona ubačene (embendded) tabel ako to definišemo u zagradi sa prefiks:

@ForeignKey

Sa ovom anotacijom povezujemo dva entiteta (tabele) povezujeći njihove kolone (kolona iz child entitea sa vrednosti kolone iz parent entiteta). Ovo je praktično dodatna anotacija u okviru @Entity anotacije child entiteta:

Primer

Prvo definišemo parent entitet (tabelu) Course.

Prethodna tabela može biti povezana sa više studenata, pa ćemo za definisanje child tabele Student koristiti anotaciju @ForeignKey:

U ovome primeru smo povezali vrednosti id kolone User tabele sa vrednosti userId Repo tabele. Značenje dodeljenih vrednosti za onDelete/onUpdate su sledeće:

  • int CASCADE – Akcija „CASCADE“ propagira operaciju brisanja ili ažuriranja nadređenog ključa na svaki zavisni podređeni ključ.
  • int NO_ACTION – Podrazumevano ponašanje kada se roditeljski ključ modifikuje ili izbriše iz baze podataka, i ne preduzimaju se neke druge posebne radnje.
  • int RESTRICT – Akcija RESTRICT znači da je aplikaciji zabranjeno brisanje (za onDelete ()) ili menjanje (za onUpdate ()) nadređenog ključa kada postoji jedan ili više podređenih ključeva koji su na njega preslikani.
  • int SET_DEFAULT – Akcije “SET DEFAULT” slične su SET_NULL, osim što je svaki od podređenih stupaca ključa postavljen tako da sadrži podrazumevanu vrednost stupaca umesto NULL.

NAPOMENA:
Poznato je da kreiranje ovakve konekcije ne mora da vodi do relacije izmedju tih tabela, već samo pomaže da se jasno definiše šta će se desiti sa “child entitetom” kada se neki član “parent entiteta” obriše (onDelete) ili ažurira (onUpdate).

@Relations

Sa ovom anotacijom je omogućeno povezivanje dve tabele bez korišćenja @Foreign.

Primer

U prethodnom primeru je prikazano povezivanje dve tabele uz pomoć anotacije @ForeignKey, isti zahtev možemo rešiti bez Foreign kluča koristeći drugu notaciju pod imenom @Relation. Za to je neophodno kreirati novu klasu sa kojom možemo napraviti instancu koja sadrži i parent entity instancu i listu child entity instanci:

Kasnije se za kreira DAO na sledeći način:

Više o ovome u narednoj sekciji.

Data Access Object – DAO

DAO (data access object) kao što mu i samo ime kaže je: objekat za pristup podacima. DAO mora biti ili interfejs ili abstraktna klasa, tj. njegove metode nemaju telo jer će Room generisati sav nepohodan kod u zavisnosti od anotacije (@Insert, @Delete, @Query…). Upravo na taj način se smanjuje količina kod-a koju je neophodno da kreira programer.
Takodje velika prednost ovog objekta je mogućnost da validira SQL naredbe u toku kompajliranja (eng. “at compile-time”) i na taj način nam pravovremeno ukaže za greške što nije bilo moguće u radu sa SQLiteOpenHelper klasom.

@Dao

Sa ovom anotacijom se Room-u daje do znanja da je dati interfejs ili abstraktna klasa ustvari DAO. Generalno se za svaki entitet pravi njegov DAO, pa bi za naš entitet iz primera “BuyItem” Dao ovako izgledao:

@Insert

Sa ovom anotacijom se obeležava metoda koja je zadužena za unos podataka u bazu.

Ovo je sasvim dovoljno da zameni celu metodu insertItemToDB() koju smo koristili u okviru primera iz SQLiteOpenHelper klase:

@Delete

Sa ovom anotacijom se obeležava metoda koja je zadužena za brisanje podataka iz baze.

Ovo je sasvim dovoljno da zameni celu metodu removeItemFromDB() koju smo koristili u okviru primera iz SQLiteOpenHelper klase:

@Query

Sa ovom anotacijom se obeležava metoda koja je zadužena za dobijanje podataka iz baze u zavisnosti od definisnog uslova (query-ja):

Pri pisanju ovog queryja možete primetiti da Android studio ukazuje na greške ib nudi rešenja i promenjive. A ova dva reda su sasvim dovoljno da zamene celu metodu getAllItemsFromDB() koju smo koristili u okviru primera iz SQLiteOpenHelper klase.

Prosledjivanje parametra u query

Često je potrebno proslediti neki parametar sa kojim se filtira query pa to izgleda kao u sledećem primeru:

Primer

Čak možemo proslediti više parametara kao u sledećem primeru:

Primer

Takodje možemo proslediti kao parametari i neku kolekciju (može da vraća LiveData objekat):

NAPOMENA:
Kada prosleđujete podatke kroz slojeve arhitekture aplikacije (iz baze podataka Room, kroz Repository klasu, dalje zatim kroz ViewModel klasu sve do korisničkog interfejsa tj. View klase (aktivnost ili fragmenta), ti podaci moraju biti LiveData u svim slojevima, ili drugim rečima svi podaci koje Room šalje iz DAO kroz neki query u Repository, a zatim iz Repository u VievModel, moraju biti LiveData. Objašnjenje ovoga leži u tome da nigde u aplikaciji nemamo potrebu da setujemo to radi Room za nas, tako da nam ne treba nigde MutableLiveData objekat (koji za razliku od LiveData objekta ima public settere) metode).

Primer

Ovako izgleda jedan tipičan Dao interfejs:

DataBase

Klasa koja predstavalja Room bazu podataka mora da bude abstract i da ekstenduje klasu RoomDatabase:

Pored ovoga je potrebno da obeležimo ovu klasu da bi Room znao o kojoj klasi je reč, a to se postiže kroz anotaciju @Database. U nastavku ove anotacije (u zagradi) definišemo koje sve entitete (tabele) ova baza sadrži, kao i trenutnou verziju baze:

Kreiranje baze

Pri kreiranju baza je “pametno” da koristimo singleton patern. Baza se kreira uz pomoć Room metode pod nazivom databaseBuilder(). Ova metoda prihvata parametre: context, “klasu koja definiše bazu” i naziv baze. Da bi se baza kreirala i inicijalizovala potrebno je da pozovemo metodu build():

Ako želimo da sprečimo probleme pri migraciji baza onda je dobro da pozovemo metodu fallbackToDestructiveMigration().

NAPOMENA:
Ako radi debuging-a želite da pristupite bazi (koristeći DeviceFileExplore) i da je pregledate koristeći DB Browser for SQLite ili neku sličnu aplikaciju, potrebno je da pri kreiranju baze pozovete i metodu setJournalMode(JournalMode.TRUNCATE), u protivnom će baza koju pregledate će biti prazna.

Pored ovaga potrebno je kreirati abstraktnu metodu koja će da vraća odgovarajući Dao objekat:

Primer

Ovako izgleda jedna klasa u celosti:

Repository

Iako ova klasa ne pripada direktno Room biblioteci već je hijerarhijski iznad nje, ipak u ovome članku će biti obradjena zbog toga što Room biblioteka ne dozvoljava izvršenje operacija nad bazom iz glavnog threed-a! Zbog ovoga razloga sve metode vezane za CRUD operacije moraju da se izvršavaju na background thread-u, a jedini izuzetak je metoda koja vraća LiveData objekat, jer o njoj Room vodi računa i automatski je poziva iz background thread-a.

Jedan od načina da se neka metoda izvrši na background thread-u je da ona extenduje AsyncTask klasu, medjutim od verzije androida 11 (API 30) je klasa AsyncTask ukinuta, te je neophodno koristiti drugi pristup (pogledajte ovde kako bi izgledala naša Repository klasa ako bi koristili AsyncTask metode). Pošto moramo da izbegnemo korišćenje AsyncTask, drugi način za izvršenje je korišćenje Executor objekta.

Exexutor servis se kreira u Database klasi uz definisanje neohodnog broja thread-ova:

Sada u okviru naše repository klase možemo iskoristi ExecutorService i izvršiti DAO metodu asihorono sa backround thread-a.

Primer

Ceo projekat iz primera možete naći na GIthub-u pod naziom “sqliteWithRoomLib”.

×

Primer

×

×

×

×

fallbackToDestructiveMigration()

Ova metoda je zadužena da migraciju baze kada menjamo verziju baze i ima sličnu ulogu kao OnUpgrade() metoda iz klase SQLiteOpenHelper.

Ova metoda omogućava Room biblioteci da destruktivno ponovo kreira tabele baze podataka ako nisu pronađene migracije koje bi migrirale stare šeme baze podataka na najnoviju verziju šeme. Više o migraciji sa Room biblotekom pročitajte ovde.

×

Primary key

Primary key je kolona koja predstavlja jedinstveni indetifiaktor reda u tabeli baze podataka. Svaka tabela mora da ima bar jedan Primary key koji ne sme biti NULL. Primary key se definiše pri kreiranju tabele na sledeći način:

Druga sintaksa za definisanje Primary key-a ovako izgleda: