Room е ORM библиотека от състава на Android Jetpack, която предоставля слой на абстракция над SQLite за работа с локални бази данни на Android. Според официалната документация Android Developers, 2025, Room автоматично генерира реализации на DAO на базата на анотации по време на компилиране, което елиминира около 70% от повтарящия се код в сравнение с директното използване на SQLiteOpenHelper. Библиотеката извършва проверка на SQL заявки на етапа на компилиране, което позволява откриване на синтактични грешки преди стартиране на приложението на устройството.
Основни неща
Room е ORM библиотека от състава на Android Jetpack, създадена от Google за опростяване на работата с локални SQLite бази данни на платформата Android. Тя предоставля анотации за описание на схемата на данни и автоматично генерира реализация на DAO интерфейси на етапа на компилиране. За разлика от директното използване на SQLiteOpenHelper, Room освобождава разработчика от писане на значително количество повтарящ се код за създаване, отваряне и управление на връзката с базата данни.
Библиотеката беше представена на Google I/O 2017 като част от архитектурните компоненти на Android. Оттогава Room се превърна в де факто стандарт за локално съхранение на данни, изпреварвайки по популярност решения като GreenDAO и Realm за Android. Според данни на Google, библиотеката се използва в над 60% от приложенията, публикувани в Google Play, които работят с локални данни на устройството.
Ключова характеристика — проверка на SQL заявки на етапа на компилиране с помощта на процесор на анотации. Ако разработчикът допусне грешка в SQL команда, например посочи несъществуващо име на колона, компилирането ще завърши с грешка преди инсталиране на приложението. Това коренно се различава от подхода на SQLiteOpenHelper, при който такива грешки се откриват едва по време на изпълнение, често в продукционна среда.
SQLite поддържа само пет типа данни: TEXT, INTEGER, REAL, BLOB и NULL. В Java и Kotlin обаче се използват сложни типове: Date, List, Enum и персонализирани обекти. За тяхното съхранение Room предоставля механизма TypeConverters — статични методи, които преобразуват сложен тип в примитивен тип, разбираем за SQLite. Например обект Date се конвертира в Long (timestamp), а List<String> — в JSON низ чрез Gson или 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()
За да декларирате конвертор, достатъчно е да добавите анотация @TypeConverter към статичен метод и да посочите класа на конвертора в анотация @TypeConverters на ниво база данни. Room автоматично прилага конвертора при четене и запис на съответния тип във всяка SQL заявка без ръчно извикване на методи за преобразуване.
Room се състои от три основни компонента: Entity, DAO и Database. Всеки изпълнява строго определена роля и се анотира със съответната анотация. Заедно те формират пълноценен слой за достъп до данни, който изолира бизнес логиката на приложението от детайлите на имплементация на SQLite.
Entity е клас от данни, който описва структурата на една таблица в базата. Всяко поле на класа съответства на колона от таблицата, а всеки ред в базата — на един екземпляр на класа. Анотацията @Entity указва на Room, че класът е таблица. Полето с анотация @PrimaryKey определя първичния ключ, който може да бъде автоинкрементен или съставен. За връзка между таблици се използва @ForeignKey, осигуряваща целостта на данните на ниво база.
@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) е интерфейс или абстрактен клас, който декларира операции за работа с данни: вмъкване, четене, актуализиране и изтриване. Всяка операция се анотира с @Insert, @Query, @Update или @Delete. Room автоматично генерира имплементация на този интерфейс на етапа на компилиране. Особена стойност има анотацията @Query, която приема SQL заявка като низ и проверява нейната коректност на етапа на изграждане.
@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 е абстрактен клас, наследяващ RoomDatabase, служещ като точка на вход в базата данни. Той съдържа списък на всички Entity и предоставля абстрактни методи за получаване на DAO. Класът се анотира с @Database, където се посочват версията на схемата и списъкът на обекти. Създаването на екземпляр на базата се извършва чрез Room.databaseBuilder с посочване на контекста на приложението, името на файла и класа Database.
Room не замества SQLite, а работи над него като слой на абстракция. Вътрешната архитектура включва процесор на анотации, генератор на код и пул от връзки. На етапа на компилиране процесорът на анотации анализира класовете Entity, DAO и Database, след което генерира класове за имплементация с наставка _Impl. Всички генерирани класове се поставят в пакета за изграждане и не са пряко видими за разработчика.
Генерирането на код на етапа на компилиране — централният механизъм на Room. За всеки DAO интерфейс се генерира клас с пълна имплементация на всички анотирани методи. SQL заявките от анотация @Query се проверяват за коректност: процесорът съпоставя имената на колони с полетата на Entity и проверява синтаксиса на SQL. При откриване на грешка компилирането се прекъсва с разбираемо съобщение. Това е невъзможно при използване на суров SQLiteOpenHelper, където грешките се проявяват едва по време на изпълнение.
Процесът на генериране включва три етапа. Първи — валидиране на схемата: процесорът проверява дали всички класове, изброени в @Database, са валидни Entity. Втори — генериране на тялото на DAO: за всеки метод се създава имплементация с помощта на вътрешния обект RoomSQLiteQuery, който изпълнява подготвени заявки. Трети — генериране на класа Database_Impl, който имплементира създаването и отварянето на базата, както и инициализацията на всички 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 не създава отделен пул от нишки за операции с базата. По подразбиране заявките се изпълняват в извикващата нишка с едно ограничение: четенето и писането блокират нишката. За асинхронна работа Room се интегрира с корутини на Kotlin чрез suspend функции, с LiveData чрез върнати стойности и с Flow чрез реактивни обвивки. Това дава на разработчика гъвкавост при избора на архитектурно решение за конкретна задача.
Нека разгледаме практически пример за създаване на приложение за съхранение на бележки с използване на Room. Приложението съдържа една таблица Note с полета id, title, content и timestamp. Потребителят ще може да добавя, преглежда и изтрива бележки. За демонстрация се използват корутини за асинхронни операции.
За да свържете Room с Android проект, трябва да добавите зависимости във файла build.gradle на модула на приложението. Room изисква три компонента: runtime библиотека, процесор на анотации kapt и опционална поддръжка за корутини. Версията на библиотеката се посочва в променливата room_version за удобство при актуализиране. От Room 2.4.0 нататък се поддържа KSP като алтернатива на kapt с по-висока скорост на изграждане.
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"
// Опционално: тестване
testImplementation "androidx.room:room-testing:$room_version"
}
След конфигуриране на зависимостите се създават три файла: Entity Note, интерфейс NoteDao и клас AppDatabase. Entity Note съдържа полета с анотации @PrimaryKey и @ColumnInfo. DAO предоставля методи за вмъкване, получаване на списък и изтриване. Database свързва Entity и DAO чрез анотация @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)
}
Файлът AppDatabase се декларира като абстрактен клас, наследяващ RoomDatabase. В анотация @Database се посочват всички Entity от текущата версия и номерът на версията на схемата. За получаване на екземпляр се използва шаблонът сингълтън чрез метода build на Room.databaseBuilder с контекст на приложението. Кеширането на екземпляра на базата предотвратява многократно създаване, което може да доведе до изтичане на памет.
Миграциите в Room са механизъм за промяна на схемата на базата данни при актуализиране на приложението без загуба на съществуващи данни. Когато потребителят инсталира нова версия с променени Entity, Room открива несъответствие на версиите и изпълнява посочените миграционни стъпки. Без миграция базата данни ще бъде изтрита и създадена наново, което ще доведе до загуба на всички запазени потребителски данни.
Миграцията се описва от класа Migration, който приема начална и крайна версия на базата. Вътре в метода migrate се изпълнява SQL заявка ALTER TABLE или CREATE TABLE за промяна на схемата. Room не може автоматично да открива промени в схемата — разработчикът трябва ръчно да напише миграция за всяка промяна на Entity. От версия Room 2.4.0 е достъпна експерименталната функция autoMigrations за автоматично генериране на миграции.
Функцията autoMigrations позволява на Room автоматично да генерира миграции на базата на разлики между версиите на Entity. За да я използвате, достатъчно е да добавите анотация @AutoMigration в @Database и да посочите експорт на схемата в JSON. Room сравнява схемите на съседни версии и генерира необходимите ALTER заявки. Въпреки това autoMigrations поддържа само обратно съвместими промени: добавяне на колони, създаване на индекси и промяна на типове със съвместими преобразувания.
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()
При добавяне на сложни промени, като преименуване на колони или обединяване на таблици, се изисква ръчна миграция с използване на временни таблици. Типичен сценарий: създайте временна таблица със старата схема, копирайте данните от старата таблица в новата с преобразувания, изтрийте старата таблица и преименувайте временната. Room гарантира, че всички миграции се изпълняват в една транзакция и при възникване на грешка промените се отменят напълно.
Често задавани въпроси
Room предоставля ORM абстракция с анотации и проверка на SQL на етапа на компилиране, докато SQLiteOpenHelper изисква ръчно писане на всички заявки и управление на връзката. Room автоматично генерира код за CRUD операции и се интегрира с архитектурните компоненти на Android, включително LiveData и Flow.
Room поддържа всички примитивни типове на Java: Int, Long, Boolean, Float, Double, както и String, ByteArray и Date. За сложни типове, като List или Enum, се използват TypeConverters — статични методи за преобразуване, които конвертират нестандартни типове във формати, поддържани от SQLite.
Да, Room поддържа синхронни извиквания без корутини, но те блокират нишката, в която се изпълняват. За асинхронна работа могат да се използват LiveData или RxJava вместо корутини. Google препоръчва използването на корутини като основен начин за асинхронен достъп до данни в нови проекти.
Ако Room открие несъответствие на версията на базата данни и не намери подходяща миграция, по подразбиране възниква IllegalStateException с описание на грешката. Разработчикът може да замени това поведение с метода fallbackToDestructiveMigration, който ще изтрие съществуващата база и ще създаде нова със загуба на всички данни.
Room поддържа връзки чрез вложени обекти с анотация @Embedded и чрез класове за връзка с анотация @Relation. За сложни заявки с обединяване на таблици се използват персонализирани POJO класове, чиито полета се попълват от резултатите на @Query с оператор JOIN в SQL.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също