Akademia Lekcje
- Lekcja 1 – Activity (link)
- Lekcja 2 – MVVM (link)
- Lekcja 3 – Obsługa Bazy Danych – Room (jesteś tutaj)
- Lekcja 4 – Wdrożenie DI (link)
Jak najefektywniej studiować artykuł?
W ostaniej mojej publikacji, przedstawiłem podstawy wzorca Model-View-ViewModel. Stworzyłem też, przykładową aplikację komunikującą się z bazą danych: [Link do Repo].
Fragmenty kodu jakie znajdziesz w niniejszej publikacji są zaczerpnięte właśnie z tej aplikacji. Bardzo polecam Ci posiłkować się jej kodem. Dzięki temu, załapiesz bibliotekę Room w mgnieniu oka!
Polecam Ci też przejrzenie wspomnianego wcześniej artykułu o wzorcu MVVM ? [Link do artykułu]
Na początek… Co to SQLite?
SQLite to biblioteka oprogramowania, która zapewnia system do zarządzania relacyjnymi bazami danych. Lite w SQLite oznacza „lekkość” pod względem potrzebnych zasobów, niezbędnej konfiguracji oraz administrowania bazą danych. Po co nam wiedza o SQLite? Otóż to właśnie z niego korzysta system Androida.
Kluczowe cechy SQLite:
- Samowystarczalny (wymaga minimalnego wsparcia ze strony systemu operacyjnego lub biblioteki zewnętrznej. Właśnie dlatego SQLite świetnie nadaje się i jest wykorzystywany na urządzeniach mobilnych).
- Bez serwerowy (zwykle RDBMS, taki jak MySQL, PostgreSQL itp., do działania, wymaga procesu serwera. SQLite NIE ? Baza danych SQLite jest zintegrowana z aplikacją. Aplikacje współpracują z bazą danych SQLite, odczytując i zapisując bezpośrednio z plików bazy danych na dysku).
- Zerowej konfiguracji (dzięki architekturze bez serwerowej nie trzeba „instalować” oprogramowania SQLite przed jego użyciem. Nie ma procesu serwera, który trzeba konfigurować, uruchamiać i zatrzymywać).
- Transakcyjny (wszystkie transakcje w SQLite są w pełni zgodne z ACID).
Czym jest Room?
Room to biblioteka ORM (Object Relational Mapping). Innymi słowy… mapuje nasze obiekty bazy danych na obiekty Java. Room zapewnia warstwę abstrakcji w stosunku do SQLite, aby umożliwić płynny dostęp do bazy danych przy jednoczesnym wykorzystaniu pełnej mocy SQLite.
Dlaczego warto korzystać z biblioteki Room?
- Weryfikuje zapytania SQL w czasie kompilacji
- Znacząco zmniejsza nam ilość kodu „Boilerplate”
- Łatwo integruje się z innymi „Architecture Components” (takimi jak LiveData).
Komponenty Room: Entity, Dao, Database
Istnieją trzy główne komponenty, których zastosowanie jest niezbędne do prawidłowego działania biblioteki Room. Oto one:
1. Entity – Reprezentuje tabelę w bazie danych
Room tworzy tabelę dla każdej klasy, która ma adnotację
@Entity
, pola w klasie odpowiadają kolumnom w tabeli. W związku z tym są to zwykle małe klasy modeli, które nie zawierają żadnej logiki.
Adnotacje:
- @Entity – Jak już zostało powiedziane, za pomocą tej adnotacji tworzymy model tabeli.
- „tableName„ – definiujemy nazwę tabeli. Gdy nazwa klasy jest taka sama jak nazwa tabeli, możemy ją zignorować.
- „foreignKeys„ – nazwy kluczy obcych (opcjonalne)
- „indices„ – lista indeksów w tabeli (opcjonalne)
- „primaryKeys„ – klucze podstawowe Entity (może być puste, ale tylko gdy klasa ma pole z adnotacją @PrimaryKey)
@Entity(tableName = "name_of_our_table")
data class SampleModel(
@PrimaryKey(autoGenerate = true)
val id: Long,
@ColumnInfo(name = "column_name")
val name: String
)
- @PrimaryKey – jak mówi nam nazwa, ta adnotacja wskazuje na klucz podstawowy jednostki. Jeśli
"autoGenerate"
jeśli ustawione na true, to SQLite wygeneruje unikalny identyfikator dla kolumny.
@PrimaryKey(autoGenerate = true)
- @ColumnInfo – umożliwia określenie niestandardowych informacji o kolumnie. Przykładowo jej nazwę:
@ColumnInfo(name = "column_name")
- @Ignore — pole nie będzie „utrwalone” przez Room
- @Embeded — pole zagnieżdżone. Można się do niego odwoływać bezpośrednio w zapytaniach SQL.
2. DAO – Definiuje zapytania SQL
„DAO (eng. Data Access Objects) są odpowiedzialne za definiowanie metod, które mają dostęp do naszej bazy danych. Możemy po prostu zdefiniować nasze zapytania za pomocą adnotacji w klasie
Dao
.”
Zapytania „tworzymy” za pomocą metody bezpośredniej wraz z odpowiednią adnotacją, tak jak na przykładnie poniżej:
@Dao
interface SampleDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun storeItem(sampleModel: SampleModel)
@Delete
fun removeItem(sampleModel: SampleModel)
@Query("SELECT * FROM name_of_our_table")
fun fetchAllItems(): LiveData<List<SampleModel>>
}
- @Query – to główna adnotacja używana w klasach DAO. Umożliwia wykonywanie operacji odczytu / zapisu w bazie danych. Każda metoda
@Query
jest weryfikowana w czasie kompilacji, więc jeśli wystąpi problem z zapytaniem, zamiast błędu w czasie wykonywania, wystąpi błąd kompilacji. - @Insert – Kiedy tworzysz metodę DAO i dodajesz do niej adnotacje
@Insert
, Room generuje implementację, która wstawia wszystkie parametry do bazy danych w jednej transakcji. - @Delete – Metoda z adnotacją
@Delete
usuwa z bazy danych zbiór jednostek podanych jako parametry. Używa kluczy podstawowych, aby znaleźć jednostki do usunięcia. - @Update – Metoda z adnotacją
@Update
modyfikuje zbiór jednostek podanych jako parametry w bazie danych. Używa zapytania, które pasuje do klucza podstawowego każdej jednostki.
3. Database – główny punkt dostępu
Zawiera „właściciela” bazy danych (eng. database holder) i służy jako główny punkt dostępu do połączenia z relacyjnymi danymi aplikacji.
Ta klasa musi spełniać następujące warunki:
- Klasa abstrakcyjna, którą rozszerza
RoomDatabase
. - Zawiera listę
Entities
skojarzonych z bazą danych. - Zawierają metodę abstrakcyjną, która niema argumentów i zwraca klasę oznaczoną adnotacją
@Dao
.
@Database(entities = [(SampleModel::class)], version = 1)
abstract class SampleDatabase : RoomDatabase() {
abstract fun sampleDao(): SampleDao
}
W jaki sposób uzyskać instancję Database
? wywołując Room.databaseBuilder()
lub Room.inMemoryDatabaseBuilder()
.
database = Room.databaseBuilder(
this, // context
SampleDatabase::class.java, // class
"name_of_our_table" //name
).build()
Implementacja biblioteki Room
Tyle teorii wystarczy ? Teraz spójrzmy jak wygląda implementacja tej biblioteki w realnym projekcie. Przypominam, że kod całej aplikacji znajdziesz na zdalnym repozytorium GitHub. [Link do Repo]. Tak więc do dzieła!
- Pierwszym krokiem będzie dodanie niezbędnych zależności do pliku
build.gradle
.
PS. W momencie gdy to czytasz, numer wersji może być wyższy.
kapt "androidx.room:room-compiler:2.2.5"
implementation "androidx.room:room-runtime:2.2.5"
implementation "androidx.room:room-ktx:2.2.5"
testImplementation "androidx.room:room-testing:2.2.5"
Pamiętaj także o dodaniu: "apply plugin: 'kotlin-kapt'"
do tego samego pliku build.gradle
.
2. Kolejnym krokiem będzie stworzenie klasy Modelu odpowiadającemu tabeli w naszej bazie danych. Tak więc dodajemy niezbędne adnotacje: @Entity
, określamy tableName
oraz ustalamy klucz podstawowy (na przykład poprzez adnotację @PrimaryKey
).
@Entity(tableName = "films_table")
data class FilmModel(
@PrimaryKey @NonNull val title: String
)
3. Teraz nadszedł czas na zdefiniowanie metod mających dostęp do naszej bazy danych. Mowa tu oczywiście o interfejsie DAO
. Przykład poniżej:
@Dao
interface FilmsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun storeFilm(filmModel: FilmModel)
@Delete
fun removeFilm(filmModel: FilmModel)
@Query("SELECT * FROM films_table ORDER BY title ASC")
fun fetchAllFilms(): LiveData<List<FilmModel>>
}
Możesz zastanawiać się dlaczego nasze @Query
zwraca wartość typu LiveData
. Otóż Room pozwala obserwować zmiany danych w bazie, ujawniając takie zmiany za pomocą obiektów LiveData. Jeśli nie wiesz co to LiveData, to zerknij do tego artykułu: [Link do artykułu].
4. Teraz lecimy z implementacją ostatniego z kluczowych komponentów. Mowa oczywiście o komponencie Database
. Pamiętaj o adnotacji @Database
oraz o rozszerzeniu naszej klasy przez RoomDatabase
.
@Database(entities = [(FilmModel::class)], version = 1)
abstract class FilmsDatabase : RoomDatabase() {
abstract fun filmDao(): FilmsDao
}
Teraz zdefiniujmy obiekt naszego komponentu Database. Oczywiście, zgrabniej moglibyśmy zrobić to za pomocą DI (Dependency Injection) np. Koina lub Daggera… ale spokojnie, na DI przyjdzie czas w kolejnym artykule. Póki co, małe kroczki ?
class FilmsApplication : Application() {
override fun onCreate() {
super.onCreate()
database = Room.databaseBuilder(
this,
FilmsDatabase::class.java,
"films_database"
).build()
}
companion object {
lateinit var database: FilmsDatabase
}
}
5. I ostatni krok. Przypieczętowanie komunikacji z naszą bazą. Musimy wyciągnąć instancję naszego DAO… i wykonywać operacje na naszej bazie. Pamiętaj, że takie akcje wykonujemy ZAWSZE na innym wątku (niż główny).
class FilmsRepository {
/* TODO Should be injected by DI :) */
private val filmsDao: FilmsDao = FilmsApplication.database.filmDao()
fun fetchAllFilms(): LiveData<List<FilmModel>> =
filmsDao.fetchAllFilms()
fun storeFilm(film: FilmModel) {
filmsDao.storeFilm(film)
}
fun removeFilm(filmModel: FilmModel) {
filmsDao.removeFilm(filmModel)
}
}
Na powyższym przykładzie zastosowałem warstwę Repository by oddzielić źródło danych (Baza danych) od reszty aplikacji. Więcej o tym podejściu przeczytasz w tym artykule: [Link do artykułu].
6. Poniżej przykład użycia przy zastosowaniu ViewModelu. Dokładne omówienie w tym artykule: [Link do artykułu].
class FilmsViewModel(
private val repository: FilmsRepository = FilmsRepository()
) : ViewModel() {
fun storeFilm(title: String) {
val filmModel = FilmModel(title)
InsertAsyncTask(repository).execute(filmModel)
}
}
private class InsertAsyncTask(private val repository: FilmsRepository) : AsyncTask<FilmModel, Void, Void>() {
override fun doInBackground(vararg params: FilmModel): Void? {
repository.storeFilm(params[0])
return null
}
}
To było by na tyle…
Wierzę, że ta publikacja dała Ci nieco wartości. Jeśli miałbyś jakiekolwiek pytania śmiało daj mi znać w komentarzu, na facebooku lub poprzez formularz kontaktowy.
PS. wpadnij też po NIESPODZIANKĘ i zapisz się na mailing by nie przegapić maili merytorycznych oraz kolejnych postów.
Pozdrawiam, Kamil Krzysztof.