Android Architecture Components – Biblioteka Room

W tym artykule opowiem Ci o bibliotece Room. Przybliżę Ci jego idee oraz na podstawie prostej aplikacji poznasz jej zastosowanie.

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:

  1. 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).
  2. 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).
  3. 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ć).
  4. 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!

  1. 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.

  1. Link Do Repozytorium na GitHub
  2. Link do Niespodzianki