Room Database w Androidzie 2026: Migracje, Relacje i Współbieżność z Coroutines
Kompleksowy przewodnik po Room Database w Androidzie 2026 - od migracji schematu, przez relacje między encjami, po integrację z Kotlin Coroutines i najlepsze praktyki.

Room Database pozostaje fundamentalnym narzędziem do lokalnego przechowywania danych w aplikacjach Android. W 2026 roku biblioteka ta oferuje jeszcze więcej możliwości, włączając w to ulepszoną obsługę migracji, zaawansowane relacje między encjami oraz pełną integrację z Kotlin Coroutines. Ten artykuł przedstawia kompleksowe podejście do wykorzystania Room w nowoczesnych projektach Android.
Room Database jest częścią Android Jetpack i stanowi warstwę abstrakcji nad SQLite, eliminując potrzebę pisania boilerplate code oraz zapewniając weryfikację zapytań SQL w czasie kompilacji.
Konfiguracja Room w projekcie Android 2026
Rozpoczęcie pracy z Room wymaga dodania odpowiednich zależności do projektu. W 2026 roku zaleca się korzystanie z najnowszych wersji biblioteki wraz z KSP (Kotlin Symbol Processing) dla lepszej wydajności kompilacji.
plugins {
id("com.google.devtools.ksp") version "2.1.0-1.0.29"
}
dependencies {
val roomVersion = "2.7.0"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
ksp("androidx.room:room-compiler:$roomVersion")
}Po skonfigurowaniu zależności można przystąpić do definiowania encji, które reprezentują tabele w bazie danych.
Definiowanie encji i podstawowych operacji
Encje w Room to klasy danych oznaczone adnotacją @Entity. Każda encja odpowiada tabeli w bazie SQLite.
@Entity(tableName = "users")
data class User(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
@ColumnInfo(name = "first_name")
val firstName: String,
@ColumnInfo(name = "last_name")
val lastName: String,
@ColumnInfo(name = "email")
val email: String,
@ColumnInfo(name = "created_at")
val createdAt: Long = System.currentTimeMillis()
)DAO (Data Access Object) definiuje metody dostępu do danych. Room generuje implementację tych interfejsów automatycznie.
@Dao
interface UserDao {
@Query("SELECT * FROM users ORDER BY created_at DESC")
suspend fun getAllUsers(): List<User>
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserById(userId: Long): User?
@Query("SELECT * FROM users WHERE email = :email LIMIT 1")
suspend fun getUserByEmail(email: String): User?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User): Long
@Update
suspend fun updateUser(user: User)
@Delete
suspend fun deleteUser(user: User)
@Query("DELETE FROM users")
suspend fun deleteAllUsers()
}Migracje schematu bazy danych
Migracje są kluczowym elementem zarządzania ewolucją schematu bazy danych. Room wymaga jawnego zdefiniowania migracji przy każdej zmianie wersji bazy.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE users ADD COLUMN phone_number TEXT")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("""
CREATE TABLE IF NOT EXISTS user_preferences (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
user_id INTEGER NOT NULL,
theme TEXT NOT NULL DEFAULT 'system',
notifications_enabled INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
""".trimIndent())
db.execSQL("CREATE INDEX index_user_preferences_user_id ON user_preferences(user_id)")
}
}W przypadku skomplikowanych migracji, takich jak zmiana typu kolumny lub reorganizacja danych, konieczne jest zastosowanie strategii z tabelą tymczasową.
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(db: SupportSQLiteDatabase) {
// Tworzenie nowej tabeli z poprawionym schematem
db.execSQL("""
CREATE TABLE users_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL,
phone_number TEXT,
created_at INTEGER NOT NULL,
is_verified INTEGER NOT NULL DEFAULT 0
)
""".trimIndent())
// Kopiowanie danych ze starej tabeli
db.execSQL("""
INSERT INTO users_new (id, first_name, last_name, email, phone_number, created_at)
SELECT id, first_name, last_name, email, phone_number, created_at FROM users
""".trimIndent())
// Usunięcie starej tabeli
db.execSQL("DROP TABLE users")
// Zmiana nazwy nowej tabeli
db.execSQL("ALTER TABLE users_new RENAME TO users")
// Odtworzenie indeksów
db.execSQL("CREATE UNIQUE INDEX index_users_email ON users(email)")
}
}Auto-migracje w Room 2.7
Room 2.7 wprowadza uproszczone auto-migracje dla prostych zmian schematu. System automatycznie wykrywa dodanie nowych kolumn lub tabel.
@Database(
version = 5,
entities = [User::class, UserPreferences::class, Article::class],
autoMigrations = [
AutoMigration(from = 4, to = 5)
],
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun articleDao(): ArticleDao
}Dla bardziej złożonych auto-migracji można zdefiniować specyfikację migracji.
@Database(
version = 6,
entities = [User::class, UserPreferences::class, Article::class],
autoMigrations = [
AutoMigration(from = 5, to = 6, spec = Migration5To6::class)
]
)
abstract class AppDatabase : RoomDatabase() {
@RenameColumn(tableName = "users", fromColumnName = "phone_number", toColumnName = "mobile_phone")
@DeleteColumn(tableName = "users", columnName = "legacy_field")
class Migration5To6 : AutoMigrationSpec
}Relacje między encjami
Room obsługuje różne typy relacji między encjami: jeden-do-jednego, jeden-do-wielu oraz wiele-do-wielu.
Relacja jeden-do-wielu
@Entity(tableName = "articles")
data class Article(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val authorId: Long,
val title: String,
val content: String,
val publishedAt: Long = System.currentTimeMillis()
)
data class UserWithArticles(
@Embedded
val user: User,
@Relation(
parentColumn = "id",
entityColumn = "authorId"
)
val articles: List<Article>
)
@Dao
interface UserDao {
@Transaction
@Query("SELECT * FROM users WHERE id = :userId")
suspend fun getUserWithArticles(userId: Long): UserWithArticles?
@Transaction
@Query("SELECT * FROM users")
fun getAllUsersWithArticles(): Flow<List<UserWithArticles>>
}Relacja wiele-do-wielu
@Entity(tableName = "tags")
data class Tag(
@PrimaryKey(autoGenerate = true)
val tagId: Long = 0,
val name: String
)
@Entity(
tableName = "article_tag_cross_ref",
primaryKeys = ["articleId", "tagId"],
foreignKeys = [
ForeignKey(
entity = Article::class,
parentColumns = ["id"],
childColumns = ["articleId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Tag::class,
parentColumns = ["tagId"],
childColumns = ["tagId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class ArticleTagCrossRef(
val articleId: Long,
val tagId: Long
)
data class ArticleWithTags(
@Embedded
val article: Article,
@Relation(
parentColumn = "id",
entityColumn = "tagId",
associateBy = Junction(ArticleTagCrossRef::class)
)
val tags: List<Tag>
)Integracja z Kotlin Coroutines i Flow
Room oferuje natywną integrację z Kotlin Coroutines, umożliwiając reaktywne obserwowanie zmian w bazie danych.
@Dao
interface ArticleDao {
// Jednorazowe pobranie danych
@Query("SELECT * FROM articles ORDER BY publishedAt DESC")
suspend fun getAllArticles(): List<Article>
// Reaktywne Flow - automatycznie emituje przy zmianach
@Query("SELECT * FROM articles ORDER BY publishedAt DESC")
fun observeAllArticles(): Flow<List<Article>>
// Flow z parametrem
@Query("SELECT * FROM articles WHERE authorId = :authorId ORDER BY publishedAt DESC")
fun observeArticlesByAuthor(authorId: Long): Flow<List<Article>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticle(article: Article): Long
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticles(articles: List<Article>)
@Query("DELETE FROM articles WHERE id = :articleId")
suspend fun deleteArticleById(articleId: Long)
}W ViewModel dane z Flow można przekształcić w StateFlow dla łatwiejszego zarządzania stanem UI.
class ArticleViewModel(
private val articleDao: ArticleDao
) : ViewModel() {
val articles: StateFlow<List<Article>> = articleDao
.observeAllArticles()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
fun addArticle(title: String, content: String, authorId: Long) {
viewModelScope.launch {
_isLoading.value = true
try {
val article = Article(
title = title,
content = content,
authorId = authorId
)
articleDao.insertArticle(article)
} finally {
_isLoading.value = false
}
}
}
}Transakcje i operacje wsadowe
Room wspiera transakcje dla operacji wymagających atomowości. Adnotacja @Transaction zapewnia wykonanie wszystkich operacji w jednej transakcji.
@Dao
interface ArticleDao {
@Transaction
suspend fun replaceAllArticles(articles: List<Article>) {
deleteAllArticles()
insertArticles(articles)
}
@Query("DELETE FROM articles")
suspend fun deleteAllArticles()
@Insert
suspend fun insertArticles(articles: List<Article>)
}Dla bardziej złożonych scenariuszy można wykorzystać withTransaction z obiektu bazy danych.
class ArticleRepository(
private val database: AppDatabase,
private val articleDao: ArticleDao,
private val tagDao: TagDao
) {
suspend fun createArticleWithTags(
article: Article,
tagNames: List<String>
): Long = database.withTransaction {
val articleId = articleDao.insertArticle(article)
tagNames.forEach { tagName ->
val existingTag = tagDao.getTagByName(tagName)
val tagId = existingTag?.tagId ?: tagDao.insertTag(Tag(name = tagName))
tagDao.insertArticleTagCrossRef(
ArticleTagCrossRef(articleId = articleId, tagId = tagId)
)
}
articleId
}
}Testowanie Room Database
Room oferuje wsparcie dla testowania z wykorzystaniem bazy danych w pamięci.
@RunWith(AndroidJUnit4::class)
class UserDaoTest {
private lateinit var database: AppDatabase
private lateinit var userDao: UserDao
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries()
.build()
userDao = database.userDao()
}
@After
fun teardown() {
database.close()
}
@Test
fun insertAndRetrieveUser() = runTest {
val user = User(
firstName = "Jan",
lastName = "Kowalski",
email = "jan@example.com"
)
val userId = userDao.insertUser(user)
val retrievedUser = userDao.getUserById(userId)
assertThat(retrievedUser).isNotNull()
assertThat(retrievedUser?.firstName).isEqualTo("Jan")
assertThat(retrievedUser?.email).isEqualTo("jan@example.com")
}
@Test
fun testMigration() = runTest {
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
// Tworzenie bazy w wersji 1
helper.createDatabase(TEST_DB, 1).apply {
execSQL("INSERT INTO users (first_name, last_name, email, created_at) VALUES ('Test', 'User', 'test@example.com', 0)")
close()
}
// Migracja do wersji 2
helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)
}
companion object {
private const val TEST_DB = "test-database"
}
}Gotowy na rozmowy o Android?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Najczęstsze pytania rekrutacyjne dotyczące Room
Podczas rozmów kwalifikacyjnych na stanowiska Android Developer często pojawiają się pytania dotyczące Room Database:
Czym różni się Room od SQLite? Room jest warstwą abstrakcji nad SQLite, która oferuje weryfikację zapytań SQL w czasie kompilacji, automatyczne generowanie kodu DAO oraz natywną integrację z Kotlin Coroutines i LiveData/Flow.
Jak Room obsługuje wątkowość?
Room domyślnie wymaga wykonywania operacji bazodanowych poza głównym wątkiem. Dzięki integracji z Coroutines, metody DAO oznaczone jako suspend automatycznie wykonują się w odpowiednim kontekście.
Kiedy używać auto-migracji a kiedy ręcznych migracji? Auto-migracje sprawdzają się przy prostych zmianach jak dodanie kolumny lub tabeli. Ręczne migracje są niezbędne przy zmianie typu danych, reorganizacji tabel lub transformacji istniejących danych.
Podsumowanie
Room Database w 2026 roku oferuje dojrzałe i stabilne rozwiązanie do lokalnego przechowywania danych w aplikacjach Android. Kluczowe aspekty to prawidłowe zarządzanie migracjami schematu, efektywne modelowanie relacji między encjami oraz wykorzystanie Kotlin Coroutines do reaktywnego dostępu do danych. Znajomość tych zagadnień jest niezbędna dla każdego programisty Android pracującego nad aplikacjami wymagającymi lokalnej bazy danych.
Tagi
Udostępnij
Powiązane artykuły

Kotlin Flow vs StateFlow vs SharedFlow: pytania rekrutacyjne Android w 2026
Pytania o Kotlin Flow vs StateFlow vs SharedFlow, które rekruterzy Androida zadają w 2026 roku, z jasnymi odpowiedziami, tabelą porównawczą i kodem gotowym do produkcji.

Kotlin Coroutines na Androida: Kompletny Przewodnik 2026
Kompleksowy przewodnik po korutynach Kotlin w programowaniu Android: funkcje suspend, zakresy, dispatchery, Flow i zaawansowane wzorce.

Kotlin 2.3 dla Androida: dekonstrukcja po nazwach, KMP i pytania rekrutacyjne 2026
Pytania rekrutacyjne z Kotlin 2.3: dekonstrukcja po nazwach, Kotlin Multiplatform, parametry kontekstowe, korutyny i Flow. Przygotowanie do rozmów Android developera w 2026 z praktycznymi przykładami kodu.