Если ваше приложение обрабатывает значительные объемы структурированных данных, локальное хранение этих данных может принести большую пользу. Наиболее распространенный вариант использования — кэширование важных фрагментов данных, чтобы при отсутствии доступа к сети устройство могло просматривать контент в автономном режиме.
Библиотека Room для обеспечения постоянного доступа к данным предоставляет абстрактный слой поверх SQLite, позволяющий беспрепятственно получать доступ к базе данных, используя при этом все возможности SQLite.
Обустройте комнату 2.x
Чтобы использовать Room 2.x в вашем приложении, добавьте следующие зависимости в файл build.gradle вашего приложения:
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
Основные компоненты
Комната состоит из трех основных компонентов:
- Класс базы данных , который содержит базу данных и служит основной точкой доступа для подключения к сохраненным данным вашего приложения.
- Сущности данных , представляющие таблицы в базе данных вашего приложения.
- Объекты доступа к данным (DAO) предоставляют методы, которые ваше приложение может использовать для запроса, обновления, вставки и удаления данных в базе данных.
На рисунке 1 показана взаимосвязь между различными компонентами помещения.

Пример реализации
// Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Usage
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
Определяйте данные с помощью сущностей.
Каждая сущность Room представляет собой таблицу в базе данных. Вы определяете каждую сущность как класс, аннотированный @Entity .
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap? = null
)
- Пользовательские имена таблиц и столбцов : По умолчанию Room использует имя класса в качестве имени таблицы и имена свойств в качестве имен столбцов. Чтобы настроить их, используйте свойство
tableNameв аннотации@Entityи аннотацию@ColumnInfo(name = "...") - Первичный ключ : Для определения первичного ключа используйте
@PrimaryKey. Для составных ключей используйте свойствоprimaryKeysаннотации `@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - Игнорировать поля : Чтобы предотвратить сохранение полей, используйте
@Ignore.
Типовые преобразователи
Иногда возникает необходимость хранить пользовательские типы, например, Date , в одном столбце. Предоставьте методы @TypeConverter для преобразования пользовательских типов в типы, которые Room может сохранять, и обратно.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}
// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
Доступ к данным с помощью DAO.
DAO определяют методы для взаимодействия с базой данных. Аннотируйте интерфейс или абстрактный класс с помощью @Dao .
Удобные методы
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Метод ` insert` может возвращать объект
Long, представляющий идентификатор вставленной строки, илиList<Long>, содержащий идентификаторы всех вставленных строк. - Обновление или удаление : Методы обновления или удаления могут возвращать
Intпредставляющее количество затронутых строк.
методы запросов
Аннотируйте методы с помощью @Query для написания SQL-запросов. Room проверяет запросы во время компиляции.
@Dao
interface UserDao {
// Simple query
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
// Return a subset of columns using a POJO or tuple
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// Pass parameters
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
// Collection of parameters
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
// Join tables
@Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
fun findBooksBorrowedByName(userName: String): List<Book>
}
Типы возвращаемых значений Multimap
В Room 2.4 и выше методы запросов могут возвращать мультикарту напрямую, используя тип Map :
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
Асинхронные запросы DAO
Во избежание зависаний пользовательского интерфейса запросы к базе данных не могут выполняться в основном потоке. Сделайте ваши запросы асинхронными, используя одну из следующих интеграций:
Корутины Kotlin и Flow
Требуется зависимость room-ktx .
@Dao
interface UserDao {
// One-shot async query
@Insert
suspend fun insertUsers(vararg users: User)
// Observable query using Flow
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
}
Java с RxJava
Требуется room-rxjava2 или room-rxjava3 .
@Dao
interface UserDao {
@Insert
fun insertUsers(users: List<User>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flowable<User>
}
Java с LiveData и Guava
Для работы ListenableFuture требуется room-guava .
@Dao
interface UserDao {
// LiveData for observable queries
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): LiveData<User>
// Guava ListenableFuture for one-shot queries
@Insert
fun insertUsers(users: List<User>): ListenableFuture<Integer>
}
Определите взаимосвязи в комнате 2.x
Чтобы предотвратить отложенную загрузку в потоке пользовательского интерфейса, нельзя использовать прямые ссылки на объекты между сущностями. Вместо этого определяйте связи с помощью промежуточных классов данных, используя аннотацию @Relation .
Один на один
У каждого пользователя всего одна библиотека.
@Entity
data class User(@PrimaryKey val userId: Long, val name: String)
@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)
// Intermediate class
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
Один ко многим
У каждого пользователя может быть несколько плейлистов.
@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
Связь «многие ко многим»
Плейлисты могут содержать много песен, и песни могут находиться во многих плейлистах. Требуется таблица соединений.
@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
Вложенные отношения
Запрашивайте информацию о пользователях, их плейлистах и всех песнях в этих плейлистах.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
Управление базами данных
В этом разделе рассматриваются различные аспекты управления базой данных Room, включая представления базы данных, предварительное заполнение данных и миграцию базы данных.
Представления базы данных
Сложную задачу можно инкапсулировать в класс, аннотированный @DatabaseView .
@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)
// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }
Предварительно заполните базу данных
Заполните базу данных при инициализации данными из файла ресурсов или файловой системы.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
Миграции
При изменении схемы увеличьте номер версии базы данных и определите объект Migration .
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2)
.build()
- Автоматическая миграция : Если вы используете Room 2.4.0 или выше, вы можете использовать
@AutoMigrationдля автоматической миграции основных изменений схемы. Для этого необходимо установитьexportSchemaвtrueв конфигурации базы данных:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - Деструктивный резервный вариант : если потеря данных допустима при отсутствии путей миграции, вызовите метод
.fallbackToDestructiveMigrationпри создании базы данных.
Тестовые миграции
Для проверки миграций используйте MigrationTestHelper из артефакта room-testing . Для этого убедитесь, что вы экспортировали схемы в конфигурации build.gradle .
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1To2() {
var db = helper.createDatabase("test-db", 1).apply {
execSQL("INSERT INTO User VALUES (1, 'John')")
close()
}
db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
// Verify data was migrated correctly
}
}
Перенос данных с SQLite на Room
Для миграции вашего приложения с SQLite на Room выполните следующие шаги:
- Обновите зависимости , добавив в них Room.
- Аннотируйте классы моделей с помощью
@Entity,@PrimaryKeyи@ColumnInfo. - Создайте DAO для замены вспомогательных методов запросов.
- Создайте класс RoomDatabase , ссылающийся на ваши сущности и DAO. Увеличьте номер версии.
- Укажите пустой путь миграции, поскольку схема не меняется, меняется только фреймворк:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - Обновите создание экземпляра , чтобы использовать
Room.databaseBuilderс путем миграции.