Room to biblioteka do pracy z SQLite w Androidzie, wchodząca w skład Jetpack. Zapewnia warstwę abstrakcji nad czystym SQLite, automatyzując tworzenie tabel, wykonywanie zapytań i konwersję danych do obiektów Kotlin i Java. Według Android Developers, Room kompiluje zapytania SQL na etapie budowania, sprawdzając poprawność składni i powiązań między Entity a tabelami.
Najważniejsze
Room to biblioteka trwałości danych z pakietu Android Jetpack, zapewniająca obiektowo-relacyjne mapowanie dla SQLite. Room rozwiązuje trzy główne problemy czystego SQLite: konieczność pisania dużej ilości kodu boilerplate do tworzenia tabel, brak sprawdzania zapytań SQL na etapie kompilacji oraz ręczną konwersję Cursor na obiekty.
Biblioteka wykorzystuje kompilator adnotacji (kapt lub KSP), który generuje implementację abstrakcyjnych klas RoomDatabase i DAO na etapie budowania. Gwarantuje to, że błędy składniowe w SQL i niezgodności typów są wykrywane przed uruchomieniem aplikacji, a nie w runtime po publikacji w Google Play.
Według Google I/O 2023, Room jest używany w 68% aplikacji Android, które pracują z danymi lokalnymi. Jest to standard przechowywania danych na urządzeniu, rekomendowany przez Google dla wszystkich nowych projektów — zamiast przestarzałych SQLiteOpenHelper i ContentProvider.
Wdrażaj Room w projektach, gdzie wymagane jest lokalne buforowanie danych z serwera, tryb offline lub przechowywanie strukturyzowanych danych użytkownika z możliwością złożonych zapytań SQL.
Room jest częścią Android Jetpack i oficjalnie rekomendowany przez Google dla wszystkich nowych projektów pracujących z danymi lokalnymi. W przeciwieństwie do Realm czy ObjectBox, Room używa natywnego SQLite, co gwarantuje kompatybilność z dowolnymi narzędziami zewnętrznymi do pracy z bazą — od DB Browser do DataGrip. Deweloper może otworzyć plik .db aplikacji i wykonywać zapytania SQL bezpośrednio, co ułatwia debugowanie i analizę danych w trakcie rozwoju.
Entity to klasa danych z adnotacją @Entity, którą Room przekształca w tabelę bazy danych. Każde pole klasy staje się kolumną tabeli, a każda instancja — wierszem. Room używa refleksji do dostępu do pól, dlatego wymagana jest adnotacja @PrimaryKey dla obowiązkowego identyfikatora.
Adnotacja @Entity informuje Room, że klasa jest tabelą. Parametr tableName określa nazwę tabeli, jeśli różni się od nazwy klasy. @PrimaryKey definiuje klucz główny z możliwością auto-generacji poprzez autoGenerate = true.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
@ColumnInfo(name = "full_name")
val name: String,
@Ignore
val tempData: String?
)
@ColumnInfo określa nazwę kolumny w tabeli, jeśli różni się od nazwy pola Kotlin. @Ignore wyklucza pole z tabeli — nie zostanie zapisane do bazy. @ForeignKey opisuje klucze obce dla powiązań między tabelami z operacjami kaskadowymi przy usuwaniu lub aktualizacji.
Room obsługuje zagnieżdżone obiekty poprzez adnotację @Embedded. Pola zagnieżdżonej klasy są rozwijane w kolumny tabeli nadrzędnej z prefiksem dla uniknięcia konfliktu nazw. Na przykład klasa Address z polami city i street, wbudowana w User, utworzy kolumny address_city i address_street w tabeli users, eliminując potrzebę tworzenia oddzielnych tabel dla prostych obiektów-wartości.
Room obsługuje tylko typy prymitywne i ich opakowania. Do przechowywania list, Date lub niestandardowych typów używa się @TypeConverter — statycznych metod konwersji między typem niestandardowym a prymitywem SQLite, na przykład między List a stringiem JSON.
DAO (Data Access Object) to interfejs lub klasa abstrakcyjna z adnotacją @Dao, zawierająca metody dostępu do danych. Każda metoda jest opatrzona adnotacją SQL: @Insert, @Update, @Delete lub @Query z jawnym zapytaniem SQL.
Adnotacja @Query przyjmuje string SQL, który jest sprawdzany przez Room na etapie kompilacji pod kątem poprawności składni i zgodności nazw kolumn z polami Entity. Room obsługuje zapytania parametryzowane przez składnię :paramName.
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Int): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
@Query("SELECT * FROM users ORDER BY name ASC")
fun getAllUsers(): Flow<List<User>>
}
@Insert obsługuje strategie OnConflictStrategy do obsługi konfliktów przy wstawianiu duplikujących się rekordów. Flow jako typ zwracany zapewnia reaktywną aktualizację UI przy każdej zmianie danych w tabeli — subskrypcja automatycznie restartuje się przy każdym INSERT, UPDATE lub DELETE.
Adnotacja @Transaction gwarantuje atomowe wykonanie wielu operacji w jednym bloku transakcyjnym. Room blokuje bazę na czas wykonania, zapobiegając stanom wyścigu przy współbieżnym dostępie z wielu wątków.
RoomDatabase to klasa abstrakcyjna, która łączy Entity i DAO w jeden punkt dostępu do bazy. Jest tworzona przez Room.databaseBuilder z podaniem wersji schematu i listy klas Entity. Instancję bazy zaleca się tworzyć jako singleton przez delegat lazy, aby uniknąć wielokrotnych połączeń.
Migracja w Room to klasa Migration opisująca skrypt SQL do przejścia ze starej wersji schematu do nowej. Jeśli migracja nie jest dostarczona przy zmianie schematu, Room rzuca IllegalStateException. Chroni to przed przypadkową utratą danych użytkownika przy aktualizacji aplikacji.
val migration_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
val db = Room.databaseBuilder(
getApplication(),
AppDatabase::class.java,
"app_database"
).addMigrations(migration_1_2)
.build()
Do rozwoju można użyć fallbackToDestructiveMigration, który usuwa starą bazę i tworzy nową przy niezgodności wersji. Ten tryb jest przeznaczony tylko do debugowania — w wydaniach produkcyjnych obowiązkowo pisze się migracje.
Do testowania bazy danych Room udostępnia specjalną klasę Room.inMemoryTestBuilder, która tworzy bazę w pamięci RAM bez zapisu na dysk. Po zakończeniu każdego testu baza jest automatycznie niszczona, co gwarantuje pełną izolację scenariuszy testowych. W połączeniu z biblioteką android-arch-core-testing deweloper może zarządzać cyklem życia bazy i sprawdzać poprawność migracji bez konieczności ręcznego czyszczenia stanu.
Wydajność Room bezpośrednio zależy od struktury zapytań i indeksów. Do analizy wolnych zapytań Room udostępnia flagę enableQueryCallback, która loguje wszystkie zapytania SQL z czasem wykonania. Deweloper może użyć tego logu do znalezienia zapytań działających dłużej niż 100 milisekund i zoptymalizować je poprzez dodanie indeksów złożonych przez adnotację @Index w @Entity lub przepisanie podzapytań na bezpośrednie złączenia JOIN z użyciem @Relation.
Room obsługuje również szyfrowanie bazy danych przez SQLCipher. Podłączenie biblioteki net.zetetic:android-database-sqlcipher i użycie SupportFactory zamiast standardowego zapewnia transparentne szyfrowanie wszystkich danych na dysku bez zmiany zapytań DAO i struktury Entity. Jest to niezbędne dla aplikacji pracujących z danymi osobowymi użytkowników i spełnia wymogi GDPR oraz rosyjskiej ustawy 152-FZ o ochronie danych osobowych. Hasło szyfrowania może być przechowywane w Android Keystore w celu ochrony przed wyodrębnieniem przez narzędzia na zrootowanych urządzeniach.
Room natywnie obsługuje Kotlin Coroutines od wersji 2.1. Metody DAO mogą być funkcjami suspend, wykonującymi zapytania w tle bez blokowania głównego wątku. Room automatycznie zarządza dyspozytorami, używając Dispatchers.IO dla zapytań odczytu i zapisu.
Do zapytań reaktywnych Room zwraca Flow — zimny strumień danych, który emituje nową wartość przy każdej zmianie w odpowiedniej tabeli. ViewModel subskrybuje Flow przez stateIn lub collect, zapewniając automatyczną aktualizację UI bez ręcznego powiadamiania adaptera.
Room obsługuje również Paging 3 przez specjalną implementację PagingSource, która ładuje dane stronicowo z SQLite. Jest to efektywne dla dużych list z tysiącami rekordów: Paging 3 ładuje tylko widoczne na ekranie wiersze i automatycznie aktualizuje je przy zmianach w bazie.
Używaj Paging 3 z Room przy wyświetlaniu kanału wiadomości, dziennika operacji lub listy produktów z możliwością dostępu offline i nieskończonym przewijaniem.
Często zadawane pytania
Room automatyzuje tworzenie tabel, konwersję Cursor na obiekty i sprawdzanie SQL na etapie kompilacji. SQLiteOpenHelper wymaga ręcznego pisania schematu, obsługi Cursor i nie ma sprawdzania zapytań przed uruchomieniem aplikacji, co zwiększa ryzyko błędów.
Tak, przy zmianie Entity (dodanie/usunięcie pola, zmiana typu) wymagana jest migracja. Bez niej Room rzuca IllegalStateException przy uruchomieniu. Do rozwoju można włączyć fallbackToDestructiveMigration, ale w wydaniu obowiązkowe są poprawne skrypty migracji.
Room obsługuje @ForeignKey dla operacji kaskadowych i @Relation dla zagnieżdżonych obiektów. Do złożonych zapytań JOIN używa się adnotacji @Transaction z @Query, zwracającym POJO z zagnieżdżonymi encjami przez @Embedded i @Relation.
Tak, Room jest w pełni kompatybilny z Java. Zamiast funkcji suspend używa się LiveData lub RxJava Observable, zamiast Flow — LiveData. Room z Java obsługuje wszystkie te same adnotacje, ale wymaga więcej kodu boilerplate dla operacji asynchronicznych.
Room obsługuje szyfrowanie przez SQLCipher od Zetetic. Zamiast Room.databaseBuilder użyj SupportFactory z biblioteki net.zetetic:android-database-sqlcipher, przekazując hasło szyfrowania. Wszystkie dane na dysku będą zaszyfrowane transparentnie dla zapytań DAO.
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ż