Room е библиотека за работа с SQLite в Android, която е част от Jetpack. Тя предоставя слой на абстракция над суровия SQLite, автоматизирайки създаването на таблици, изпълнението на заявки и преобразуването на данни в обекти на Kotlin и Java. Според Android Developers, Room компилира SQL заявки по време на компилация, проверявайки коректността на синтаксиса и връзките между Entity и таблици.
Основни точки
Room е библиотека за персистентност от Android Jetpack, която предоставя обектно-релационно картографиране за SQLite. Room решава три основни проблема на суровия SQLite: писане на голямо количество boilerplate код за създаване на таблици, липса на проверка на SQL заявки по време на компилация и ръчно преобразуване на Cursor в обекти.
Библиотеката използва компилатор на анотации (kapt или KSP), който генерира имплементация на абстрактните класове RoomDatabase и DAO по време на компилация. Това гарантира, че синтактичните грешки в SQL и несъответствията на типове се откриват преди стартиране на приложението, а не по време на изпълнение след публикуване в Google Play.
Според Google I/O 2023, Room се използва в 68% от Android приложенията, които работят с локални данни. Това е стандартът за съхранение на данни на устройството, препоръчан от Google за всички нови проекти — вместо остарелите SQLiteOpenHelper и ContentProvider.
Прилагайте Room в проекти, където се изисква локално кеширане на данни от сървъра, офлайн режим или съхранение на структурирани потребителски данни с възможност за сложни SQL заявки.
Room е част от Android Jetpack и е официално препоръчан от Google за всички нови проекти, работещи с локални данни. За разлика от Realm или ObjectBox, Room използва роден SQLite, което гарантира съвместимост с всякакви инструменти на трети страни за работа с база данни — от DB Browser до DataGrip. Разработчикът може да отвори .db файла на приложението и да изпълнява SQL заявки директно, което улеснява отстраняването на грешки и анализа на данни в процеса на разработка.
Entity е клас от данни, анотиран с @Entity, който Room преобразува в таблица на базата данни. Всяко поле на класа става колона на таблицата, а всеки екземпляр — ред. Room използва рефлексия за достъп до полетата, поради което се изисква анотация @PrimaryKey за задължителен идентификатор.
Анотацията @Entity уведомява Room, че класът е таблица. Параметърът tableName задава името на таблицата, ако то се различава от името на класа. @PrimaryKey дефинира първичния ключ с възможност за автоматично генериране чрез 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 задава името на колоната в таблицата, ако то се различава от името на полето в Kotlin. @Ignore изключва полето от таблицата — то няма да бъде запазено в базата данни. @ForeignKey описва външни ключове за връзки между таблици с каскадни операции при изтриване или актуализиране.
Room поддържа вложени обекти чрез анотация @Embedded. Полетата на вложения клас се разгръщат в колони на родителската таблица с префикс за избягване на конфликт на имена. Например клас Address с полета city и street, вграден в User, ще създаде колоните address_city и address_street в таблицата users, елиминирайки необходимостта от създаване на отделни таблици за прости стойностни обекти.
Room поддържа само примитивни типове и техните обвивки. За съхранение на списъци, Date или персонализирани типове се използва @TypeConverter — статични методи за преобразуване между персонализиран тип и примитив на SQLite, например между List и JSON низ.
DAO (Data Access Object) е интерфейс или абстрактен клас, анотиран с @Dao, съдържащ методи за достъп до данни. Всеки метод е анотиран с SQL операция: @Insert, @Update, @Delete или @Query с изрична SQL заявка.
Анотацията @Query приема SQL низ, който се проверява от Room по време на компилация за коректност на синтаксиса и съответствие на имената на колоните с полетата на Entity. Room поддържа параметризирани заявки чрез синтаксис :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 поддържа стратегии OnConflictStrategy за обработка на конфликти при вмъкване на дублиращи се записи. Flow като връщан тип осигурява реактивно актуализиране на UI при всяка промяна на данни в таблицата — абонаментът автоматично се рестартира при всяко INSERT, UPDATE или DELETE.
Анотацията @Transaction гарантира атомарно изпълнение на множество операции в един транзакционен блок. Room заключава базата данни по време на изпълнение, предотвратявайки състояние на надпревара при конкурентен достъп от множество нишки.
RoomDatabase е абстрактен клас, който обединява Entity и DAO в единна точка за достъп до базата данни. Създава се чрез Room.databaseBuilder с посочване на версия на схемата и списък от Entity класове. Препоръчва се инстанцията на базата данни да се създава като сингълтън чрез lazy делегат за избягване на множество връзки.
Миграция в Room е клас Migration, описващ SQL скрипт за преход от стара версия на схемата към нова. Ако миграция не е предоставена при промяна на схемата, Room хвърля IllegalStateException. Това предпазва от случайна загуба на потребителски данни при актуализиране на приложението.
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()
За разработка може да се използва fallbackToDestructiveMigration, който изтрива старата база данни и създава нова при несъответствие на версиите. Този режим е предназначен само за отстраняване на грешки — в продуктови версии задължително се пишат миграции.
За тестване на базата данни Room предоставя специален клас Room.inMemoryTestBuilder, който създава база данни в RAM паметта без запис на диск. След завършване на всеки тест базата данни автоматично се унищожава, което гарантира пълна изолация на тестовите сценарии. В комбинация с библиотеката android-arch-core-testing, разработчикът може да управлява жизнения цикъл на базата данни и да проверява коректността на миграциите без необходимост от ръчно почистване на състоянието.
Производителността на Room зависи пряко от структурата на заявките и индексите. За анализ на бавни заявки Room предоставя флаг enableQueryCallback, който записва всички SQL заявки с време за изпълнение. Разработчикът може да използва този лог за намиране на заявки, които отнемат повече от 100 милисекунди, и да ги оптимизира чрез добавяне на съставни индекси чрез анотация @Index в @Entity или пренаписване на подзаявки на директни JOIN връзки с използване на @Relation.
Room също поддържа криптиране на базата данни чрез SQLCipher. Свързването на библиотеката net.zetetic:android-database-sqlcipher и използването на SupportFactory вместо стандартната осигурява прозрачно криптиране на всички данни на диска без промяна на DAO заявките и структурата на Entity. Това е необходимо за приложения, работещи с лични данни на потребителите, и отговаря на изискванията на GDPR и руския закон 152-ФЗ за защита на личните данни. Паролата за криптиране може да се съхранява в Android Keystore за защита от извличане чрез инструменти на руутнати устройства.
Room поддържа родно Kotlin Coroutines от версия 2.1. DAO методите могат да бъдат suspend функции, изпълняващи заявки на заден план без блокиране на основната нишка. Room автоматично управлява диспечерите, използвайки Dispatchers.IO за заявки за четене и запис.
За реактивни заявки Room връща Flow — студен поток от данни, който излъчва нова стойност при всяка промяна на засегнатата таблица. ViewModel се абонира за Flow чрез stateIn или collect, осигурявайки автоматично актуализиране на UI без ръчно уведомяване на адаптера.
Room поддържа и Paging 3 чрез специална имплементация на PagingSource, която зарежда данни на страници от SQLite. Това е ефективно за големи списъци с хиляди записи: Paging 3 зарежда само видимите на екрана редове и ги актуализира автоматично при промени в базата данни.
Използвайте Paging 3 с Room при показване на новинарски поток, лог на операции или списък с продукти с възможност за офлайн достъп и безкрайно скролване.
Често задавани въпроси
Room автоматизира създаването на таблици, преобразуването на Cursor в обекти и проверката на SQL по време на компилация. SQLiteOpenHelper изисква ръчно писане на схема, обработка на Cursor и няма проверка на заявки преди стартиране на приложението, което увеличава риска от грешки.
Да, при промяна на Entity (добавяне/премахване на поле, промяна на тип) се изисква миграция. Без нея Room хвърля IllegalStateException при стартиране. За разработка може да се включи fallbackToDestructiveMigration, но в релиза са задължителни коректни скриптове за миграция.
Room поддържа @ForeignKey за каскадни операции и @Relation за вложени обекти. За сложни JOIN заявки се използва анотация @Transaction с @Query, връщаща POJO с вложени обекти чрез @Embedded и @Relation.
Да, Room е напълно съвместим с Java. Вместо suspend функции се използват LiveData или RxJava Observable, вместо Flow — LiveData. Room с Java поддържа всички същите анотации, но изисква повече boilerplate код за асинхронни операции.
Room поддържа криптиране чрез SQLCipher от Zetetic. Вместо Room.databaseBuilder използвайте SupportFactory от библиотеката net.zetetic:android-database-sqlcipher, като подадете парола за криптиране. Всички данни на диска ще бъдат криптирани прозрачно за DAO заявки.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също