Room to biblioteka ORM z pakietu Android Jetpack, która zapewnia warstwę abstrakcji nad SQLite do pracy z lokalnymi bazami danych na Androidzie. Według oficjalnej dokumentacji Android Developers, 2025, Room automatycznie generuje implementacje DAO na podstawie adnotacji podczas kompilacji, co eliminuje około 70% kodu szablonowego w porównaniu z bezpośrednim użyciem SQLiteOpenHelper. Biblioteka przeprowadza walidację zapytań SQL na etapie kompilacji, co pozwala wykryć błędy składniowe przed uruchomieniem aplikacji na urządzeniu.
Najważniejsze
Room to biblioteka ORM z pakietu Android Jetpack, stworzona przez Google w celu uproszczenia pracy z lokalnymi bazami danych SQLite na platformie Android. Zapewnia adnotacje do opisywania schematu danych i automatycznie generuje implementację interfejsów DAO na etapie kompilacji. W przeciwieństwie do bezpośredniego użycia SQLiteOpenHelper, Room uwalnia programistę od pisania znacznej ilości kodu szablonowego do tworzenia, otwierania i zarządzania połączeniem z bazą danych.
Biblioteka została zaprezentowana na Google I/O 2017 jako część komponentów architektonicznych Android. Od tego czasu Room stał się standardem de facto lokalnego przechowywania danych, wyprzedzając pod względem popularności takie rozwiązania jak GreenDAO i Realm dla Android. Według Google, biblioteka jest używana w ponad 60% aplikacji opublikowanych w Google Play, które pracują z lokalnymi danymi na urządzeniu.
Kluczowa cecha — walidacja zapytań SQL na etapie kompilacji za pomocą procesora adnotacji. Jeśli programista popełni błąd w poleceniu SQL, na przykład poda nieistniejącą nazwę kolumny, kompilacja zakończy się błędem przed instalacją aplikacji. To radykalnie różni się od podejścia SQLiteOpenHelper, gdzie takie błędy są wykrywane dopiero podczas wykonania, często w produkcji.
SQLite obsługuje tylko pięć typów danych: TEXT, INTEGER, REAL, BLOB i NULL. Jednak w Javie i Kotlinie używane są złożone typy: Date, List, Enum i niestandardowe obiekty. Do ich przechowywania Room udostępnia mechanizm TypeConverters — statycznych metod przekształcających złożony typ w prymitywny, zrozumiały dla SQLite. Na przykład obiekt Date konwertowany jest na Long (timestamp), a List<String> — na ciąg JSON przez Gson lub Moshi.
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
val db = Room
.databaseBuilder(context, AppDatabase::class.java, "app-db")
.build()
Aby zadeklarować konwerter, wystarczy dodać adnotację @TypeConverter do statycznej metody i wskazać klasę konwertera w adnotacji @TypeConverters na poziomie bazy danych. Room automatycznie stosuje konwerter przy odczycie i zapisie odpowiedniego typu w każdym zapytaniu SQL bez ręcznego wywoływania metod konwersji.
Room składa się z trzech głównych komponentów: Entity, DAO i Database. Każdy pełni ściśle określoną rolę i jest adnotowany odpowiednią adnotacją. Razem tworzą pełnoprawną warstwę dostępu do danych, która izoluje logikę biznesową aplikacji od szczegółów implementacji SQLite.
Entity to klasa danych opisująca strukturę jednej tabeli w bazie. Każde pole klasy odpowiada kolumnie tabeli, a każdy wiersz w bazie — jednemu wystąpieniu klasy. Adnotacja @Entity informuje Room, że klasa jest tabelą. Pole z adnotacją @PrimaryKey określa klucz główny, który może być autoinkrementowany lub złożony. Do łączenia tabel służy @ForeignKey, zapewniająca integralność danych na poziomie bazy.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "full_name")
val name: String,
val age: Int,
val email: String
)
DAO (Data Access Object) to interfejs lub klasa abstrakcyjna deklarująca operacje do pracy z danymi: wstawianie, odczyt, aktualizację i usuwanie. Każda operacja jest adnotowana @Insert, @Query, @Update lub @Delete. Room automatycznie generuje implementację tego interfejsu na etapie kompilacji. Szczególną wartość ma adnotacja @Query, która przyjmuje zapytanie SQL jako ciąg znaków i sprawdza jego poprawność na etapie budowania.
@Dao
interface UserDao {
@Insert
suspend fun insert(user: User): Long
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>>
@Delete
suspend fun delete(user: User)
}
Database to abstrakcyjna klasa dziedzicząca po RoomDatabase, służąca jako punkt wejścia do bazy danych. Zawiera listę wszystkich Entity i udostępnia abstrakcyjne metody do uzyskiwania DAO. Klasa jest adnotowana @Database, gdzie określa się wersję schematu i listę encji. Tworzenie instancji bazy odbywa się przez Room.databaseBuilder z podaniem kontekstu aplikacji, nazwy pliku i klasy Database.
Room nie zastępuje SQLite, ale działa na nim jako warstwa abstrakcji. Wewnętrzna architektura obejmuje procesor adnotacji, generator kodu i pulę połączeń. Na etapie kompilacji procesor adnotacji analizuje klasy Entity, DAO i Database, a następnie generuje klasy implementacyjne z sufiksem _Impl. Wszystkie wygenerowane klasy umieszczane są w pakiecie kompilacji i nie są widoczne bezpośrednio dla programisty.
Generowanie kodu na etapie kompilacji — centralny mechanizm Room. Dla każdego interfejsu DAO generowana jest klasa z pełną implementacją wszystkich adnotowanych metod. Zapytania SQL z adnotacji @Query są sprawdzane pod kątem poprawności: procesor dopasowuje nazwy kolumn do pól Entity i sprawdza składnię SQL. W przypadku wykrycia błędu kompilacja jest przerywana z czytelnym komunikatem. To niemożliwe przy użyciu surowego SQLiteOpenHelper, gdzie błędy ujawniają się dopiero w runtime.
Proces generowania obejmuje trzy etapy. Pierwszy — walidacja schematu: procesor sprawdza, czy wszystkie klasy wymienione w @Database są poprawnymi Entity. Drugi — generowanie treści DAO: dla każdej metody tworzona jest implementacja z użyciem wewnętrznego obiektu RoomSQLiteQuery, wykonującego przygotowane zapytania. Trzeci — generowanie klasy Database_Impl, implementującej tworzenie i otwieranie bazy oraz inicjalizację wszystkich obiektów DAO.
class UserDao_Impl(private val __db: RoomDatabase) : UserDao {
private val __insertionAdapter = __db
.createInsertionAdapter(User::class, 0)
override suspend fun insert(user: User): Long {
__db.assertNotSuspendingTransaction()
return __db.runInTransaction {
__insertionAdapter.insertAndReturnId(user)
}
}
}
Room nie tworzy osobnej puli wątków do operacji na bazie. Domyślnie zapytania są wykonywane w wątku wywołującym z jednym ograniczeniem: odczyt i zapis blokują wątek. Do pracy asynchronicznej Room integruje się z korutynami Kotlin przez funkcje suspend, z LiveData przez wartości zwracane i z Flow przez reaktywne otoczki. Daje to programiście elastyczność wyboru rozwiązania architektonicznego dla konkretnego zadania.
Rozpatrzmy praktyczny przykład tworzenia aplikacji do przechowywania notatek z użyciem Room. Aplikacja zawiera jedną tabelę Note z polami id, title, content i timestamp. Użytkownik będzie mógł dodawać, przeglądać i usuwać notatki. Do demonstracji zastosowano korutyny do operacji asynchronicznych.
Aby podłączyć Room do projektu Android, należy dodać zależności w pliku build.gradle modułu aplikacji. Room wymaga trzech komponentów: biblioteki runtime, procesora adnotacji kapt i opcjonalnego wsparcia dla korutyn. Wersja biblioteki jest określana w zmiennej room_version dla wygody aktualizacji. Od Room 2.4.0 dostępny jest KSP jako alternatywa dla kapt z wyższą szybkością budowania.
dependencies {
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// Opcjonalnie: testowanie
testImplementation "androidx.room:room-testing:$room_version"
}
Po skonfigurowaniu zależności tworzone są trzy pliki: Entity Note, interfejs NoteDao i klasa AppDatabase. Entity Note zawiera pola z adnotacjami @PrimaryKey i @ColumnInfo. DAO udostępnia metody do wstawiania, pobierania listy i usuwania. Database łączy Entity i DAO przez adnotację @Database.
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val content: String,
@ColumnInfo(name = "created_at")
val timestamp: Long = System.currentTimeMillis()
)
@Dao
interface NoteDao {
@Insert
suspend fun insert(note: Note)
@Query("SELECT * FROM notes ORDER BY created_at DESC")
fun getAllNotes(): Flow<List<Note>>
@Delete
suspend fun delete(note: Note)
}
Plik AppDatabase deklarowany jest jako abstrakcyjna klasa dziedzicząca po RoomDatabase. W adnotacji @Database wymieniane są wszystkie Entity bieżącej wersji i numer wersji schematu. Do uzyskania instancji używany jest wzorzec singleton przez metodę build Room.databaseBuilder z kontekstem aplikacji. Buforowanie instancji bazy zapobiega wielokrotnemu tworzeniu, które mogłoby prowadzić do wycieków pamięci.
Migracje w Room to mechanizm do zmiany schematu bazy danych przy aktualizacji aplikacji bez utraty istniejących danych. Gdy użytkownik instaluje nową wersję ze zmienionymi Entity, Room wykrywa niezgodność wersji i wykonuje określone kroki migracyjne. Bez migracji baza danych zostanie usunięta i utworzona od nowa, co spowoduje utratę wszystkich zapisanych przez użytkownika danych.
Migracja jest opisywana klasą Migration, przyjmującą początkową i końcową wersję bazy. Wewnątrz metody migrate wykonywane jest zapytanie SQL ALTER TABLE lub CREATE TABLE w celu zmiany schematu. Room nie potrafi automatycznie określać zmian schematu — programista musi napisać migrację ręcznie dla każdej zmiany Entity. Od wersji Room 2.4.0 dostępna jest eksperymentalna funkcja autoMigrations do automatycznego generowania migracji.
Funkcja autoMigrations pozwala Room automatycznie generować migracje na podstawie różnic między wersjami Entity. Aby z niej skorzystać, wystarczy dodać adnotację @AutoMigration w @Database i określić eksport schematu do JSON. Room porównuje schematy sąsiednich wersji i generuje niezbędne zapytania ALTER. Jednak autoMigrations obsługuje tylko zmiany wstecznie kompatybilne: dodawanie kolumn, tworzenie indeksów i zmiany typów z kompatybilnymi konwersjami.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE users ADD COLUMN phone TEXT"
)
}
}
val db = Room
.databaseBuilder(context, AppDatabase::class.java, "app-db")
.addMigrations(MIGRATION_1_2)
.build()
Przy dodawaniu złożonych zmian, takich jak zmiana nazwy kolumn lub łączenie tabel, wymagana jest ręczna migracja z użyciem tabel pośrednich. Typowy scenariusz: utworzyć tymczasową tabelę ze starym schematem, skopiować dane ze starej tabeli do nowej z przekształceniami, usunąć starą tabelę i zmienić nazwę tymczasowej. Room gwarantuje, że wszystkie migracje są wykonywane w jednej transakcji, a w przypadku błędu zmiany są w pełni wycofywane.
Często zadawane pytania
Room zapewnia abstrakcję ORM z adnotacjami i walidacją SQL na etapie kompilacji, podczas gdy SQLiteOpenHelper wymaga ręcznego pisania wszystkich zapytań i zarządzania połączeniem. Room automatycznie generuje kod dla operacji CRUD i integruje się z komponentami architektonicznymi Android, w tym LiveData i Flow.
Room obsługuje wszystkie prymitywne typy Java: Int, Long, Boolean, Float, Double, a także String, ByteArray i Date. Dla złożonych typów, takich jak List lub Enum, używane są TypeConverters — statyczne metody konwersji, które przekształcają niestandardowe typy w formaty obsługiwane przez SQLite.
Tak, Room obsługuje synchroniczne wywołania bez korutyn, ale blokują one wątek, w którym są wykonywane. Do pracy asynchronicznej można użyć LiveData lub RxJava zamiast korutyn. Google zaleca stosowanie korutyn jako głównego sposobu asynchronicznego dostępu do danych w nowych projektach.
Jeśli Room wykryje niezgodność wersji bazy danych i nie znajdzie odpowiedniej migracji, domyślnie występuje IllegalStateException z opisem błędu. Programista może nadpisać to zachowanie metodą fallbackToDestructiveMigration, która usunie istniejącą bazę i utworzy nową z utratą wszystkich danych.
Room obsługuje relacje przez zagnieżdżone obiekty z adnotacją @Embedded i przez klasy relacji z adnotacją @Relation. Do złożonych zapytań z łączeniem tabel używane są niestandardowe klasy POJO, których pola są wypełniane z wyników @Query z operatorem JOIN w SQL.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również