Сохраняйте данные в локальной базе данных, используя Room 2.x.

Если ваше приложение обрабатывает значительные объемы структурированных данных, локальное хранение этих данных может принести большую пользу. Наиболее распространенный вариант использования — кэширование важных фрагментов данных, чтобы при отсутствии доступа к сети устройство могло просматривать контент в автономном режиме.

Библиотека 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 показана взаимосвязь между различными компонентами помещения.

Рисунок 1. Схема архитектуры библиотеки Room.

Пример реализации

// 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 выполните следующие шаги:

  1. Обновите зависимости , добавив в них Room.
  2. Аннотируйте классы моделей с помощью @Entity , @PrimaryKey и @ColumnInfo .
  3. Создайте DAO для замены вспомогательных методов запросов.
  4. Создайте класс RoomDatabase , ссылающийся на ваши сущности и DAO. Увеличьте номер версии.
  5. Укажите пустой путь миграции, поскольку схема не меняется, меняется только фреймворк: kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} }
  6. Обновите создание экземпляра , чтобы использовать Room.databaseBuilder с путем миграции.