7 полезных советов для тех, кто использует Room


Room — это уровень абстракции поверх SQLite, который упрощает организацию хранения данных. Если вы ещё мало знакомы с Room, то посмотрите эту вводную статью:


7 шагов к использованию Room. Пошаговое руководство по миграции приложения на Room

А в этой статье я хотел бы поделиться несколькими советами о том, как максимально эффективно использовать Room.


1. Предварительное заполнение базы данных


Вам нужно добавить данные по умолчанию в вашу базу данных сразу после её создания или в момент первого обращения к ней? Используйте RoomDatabase#Callback. Вызовите метод addCallback при создании вашей базы данных и переопределите либо onCreate, либо onOpen.


onCreate будет вызываться при первом создании базы данных, сразу после создания таблиц. onOpen вызывается при открытии базы данных. Поскольку доступ к DAO возможен только после завершения этих методов, мы создаём новый поток, в котором получаем ссылку на базу данных, затем получаем DAO и вставляем необходимые данные.


Room.databaseBuilder(context.applicationContext,
        DataDatabase::class.java, "Sample.db")
        // prepopulate the database after onCreate was called
        .addCallback(object : Callback() {
            override fun onCreate(db: SupportSQLiteDatabase) {
                super.onCreate(db)
                // moving to a new thread
                ioThread {
                    getInstance(context).dataDao()
                                        .insert(PREPOPULATE_DATA)
                }
            }
        })
        .build()

Смотрите полный пример здесь.


Примечание: если вы будете использовать подход с ioThread и приложение даст сбой при первом запуске, между созданием базы данных и вставкой, данные никогда не будут вставлены.


2. Использование возможностей наследования DAO


У вас есть несколько таблиц в вашей базе данных и вы копируете одни и те же методы вставки, обновления и удаления? DAO поддерживают наследование, поэтому создайте класс BaseDao<T> и определите там ваши общие методы @Insert, @Update и @Delete. Пусть каждый DAO расширит BaseDao и добавит методы, специфичные для каждого из них.


interface BaseDao<T> {
    @Insert
    fun insert(vararg obj: T)
}

@Dao
abstract class DataDao : BaseDao<Data>() {
    @Query("SELECT * FROM Data")
    abstract fun getData(): List<Data>
}

Смотрите подробности здесь.


DAO должны быть интерфейсами или абстрактными классами, потому что Room генерирует их реализации во время компиляции, включая методы из BaseDao.


3. Выполнение запросов в транзакциях без шаблонного кода


Аннотирование метода с помощью @Transaction гарантирует, что все операции базы данных, которые вы выполняете в этом методе, будут выполняться внутри одной транзакции. Транзакция не будет выполнена, если в теле метода возникнет исключение.


@Dao
abstract class UserDao {

    @Transaction
    open fun updateData(users: List<User>) {
        deleteAllUsers()
        insertAll(users)
    }

    @Insert
    abstract fun insertAll(users: List<User>)

    @Query("DELETE FROM Users")
    abstract fun deleteAllUsers()
}

Возможно, вы захотите использовать аннотацию @Transaction для методов @Query, которые используют оператор select в случаях:


  • Когда результат запроса довольно большой. Делая запрос одной транзакцией, вы гарантируете, что если результат запроса не поместится в одной «порции» курсора, то он не будет повреждён из-за изменений в базе данных между перестановками курсора.
  • Когда результатом запроса является POJO с полями @Relation. Каждое поле является запросом само по себе, поэтому запуск их в одной транзакции гарантирует согласованные результаты между запросами.

Методы @Delete, @Update и @Insert, имеющие несколько параметров, автоматически запускаются внутри транзакции.


4. Чтение только того, что вам нужно


Когда вы делаете запрос к базе данных, используете ли вы все поля, которые получаете в ответе? Позаботьтесь об объёме памяти, используемой вашим приложением, и загрузите только те поля, которые вы в конечном итоге будете использовать. Это также увеличит скорость ваших запросов за счёт снижения затрат на ввод-вывод. Room сделает сопоставление между столбцами и объектом за вас.


Рассмотрим этот сложный объект User:


@Entity(tableName = "users")
data class User(@PrimaryKey
                val id: String,
                val userName: String,
                val firstName: String, 
                val lastName: String,
                val email: String,
                val dateOfBirth: Date, 
                val registrationDate: Date)

На некоторых экранах нам не нужно отображать всю эту информацию. Таким образом, вместо этого мы можем создать объект UserMinimal, который содержит только необходимые данные.


data class UserMinimal(val userId: String,
                       val firstName: String, 
                       val lastName: String)

В классе DAO мы определяем запрос и выбираем правильные столбцы из таблицы users.


@Dao
interface UserDao {
    @Query(“SELECT userId, firstName, lastName FROM Users)
    fun getUsersMinimal(): List<UserMinimal>
}

5. Контроль зависимостей между сущностями с внешними ключами


Даже несмотря на то, что Room напрямую не поддерживает связи между сущностями, он позволяет вам определять зависимости между объектами с помощью внешних ключей.


В Room есть аннотация @ForeignKey, которая является частью аннотации @Entity. По функциональности она аналогична внешним ключам в SQLite. Она гарантирует сохранение связей между сущностями при изменениях в базе данных. Чтобы добавить её, определите объект, на который необходимо ссылаться, а также столбцы в текущем объекте и том, на который ссылаетесь.


Рассмотрим класс User и Pet. У Pet есть владелец — идентификатор пользователя, на который ссылается внешний ключ.


@Entity(tableName = "pets",
        foreignKeys = arrayOf(
            ForeignKey(entity = User::class,
                       parentColumns = arrayOf("userId"),
                       childColumns = arrayOf("owner"))))
data class Pet(@PrimaryKey val petId: String,
              val name: String,
              val owner: String)

При желании вы можете определить, какое действие необходимо предпринять, когда родительский объект удаляется или обновляется в базе данных. Вы можете выбрать один из следующих вариантов: NO_ACTION, RESTRICT, SET_NULL, SET_DEFAULT или CASCADE, которые ведут себя так же, как в SQLite.


Примечание: в Room SET_DEFAULT работает как SET_NULL, т.к. Room ещё не позволяет устанавливать значения по умолчанию для столбцов.


6. Упрощение запросов один-ко-многим с помощью @Relation


В предыдущем примере User-Pet можно сказать, что есть отношение один-ко-многим: у пользователя может быть несколько питомцев. Допустим, мы хотим получить список пользователей со своими питомцами: List<UserAndAllPets>.


data class UserAndAllPets (val user: User,
                           val pets: List<Pet> = ArrayList())

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


@Query(“SELECT * FROM Users”)
public List<User> getUsers();

@Query(“SELECT * FROM Pets where owner = :userId”)
public List<Pet> getPetsForUser(String userId);

Затем мы будем перебирать список пользователей и каждый раз обращаться к таблице Pets.


Аннотация @Relation упростит нам жизнь: она автоматически запросит связанные объекты. @Relation можно применять только к List или Set. Обновим класс UserAndAllPets:


class UserAndAllPets {

   @Embedded
   var user: User? = null

   @Relation(parentColumn = “userId”,
             entityColumn = “owner”)
   var pets: List<Pet> = ArrayList()
}

В DAO мы определяем один запрос, а Room будет запрашивать таблицы Users и Pets и самостоятельно сопоставлять объекты.


@Transaction
@Query(“SELECT * FROM Users”)
List<UserAndAllPets> getUsers();

7. Избежание ложных уведомлений observable-запросов


Допустим, вы хотите получить пользователя по его идентификатору с помощью observable-запроса:


@Query(“SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): LiveData<User>

// or

@Query(“SELECT * FROM Users WHERE userId = :id)
fun getUserById(id: String): Flowable<User>

Вы будете получать новый объект User каждый раз, когда он будет обновляться. Но вы также получите этот объект, когда в таблице Users произойдут другие изменения (удаления, обновления или вставки), которые не имеют никакого отношения к интересующему вас пользователю, что приведёт к ложным уведомлениям. Более того, если ваш запрос включает в себя несколько таблиц, вы будете получать новые сообщения всякий раз, когда что-то поменяется в любой из них.


Вот что происходит за кулисами:


  1. В SQLite есть триггеры, которые срабатывают всякий раз, когда в таблице происходит DELETE, UPDATE или INSERT.
  2. Room создаёт InvalidationTracker, который использует Observers, которые отслеживают все изменения в наблюдаемых таблицах.
  3. И LiveData-, и Flowable-запросы полагаются на уведомление InvalidationTracker.Observer#onInvalidated. Когда оно получено, происходит повторный запрос.

Room знает только то, что таблица была изменена, но не знает, почему и что изменилось. Следовательно, после повторного запроса результат запроса передаётся с помощью LiveData или Flowable. Т.к. Room не хранит никаких данных в памяти, он не может определить, те же самые это данные или нет.


Вы должны убедиться, что ваш DAO фильтрует запросы и реагирует только на необходимые объекты.


Если observable-запрос реализован с использованием Flowables, используйте Flowable#diverUntilChanged.


@Dao
abstract class UserDao : BaseDao<User>() {

/**
* Get a user by id.
* @return the user from the table with a specific id.
*/
@Query(“SELECT * FROM Users WHERE userid = :id”)
protected abstract fun getUserById(id: String): Flowable<User>

fun getDistinctUserById(id: String): 
   Flowable<User> = getUserById(id)
                          .distinctUntilChanged()
}

Если ваш запрос возвращает LiveData, вы можете использовать MediatorLiveData, которая будет получать только нужные объекты из источника.


fun <T> LiveData<T>.getDistinct(): LiveData<T> {
    val distinctLiveData = MediatorLiveData<T>()
    distinctLiveData.addSource(this, object : Observer<T> {
        private var initialized = false
        private var lastObj: T? = null

        override fun onChanged(obj: T?) {
            if (!initialized) {
                initialized = true
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            } else if ((obj == null && lastObj != null) 
                       || obj != lastObj) {
                lastObj = obj
                distinctLiveData.postValue(lastObj)
            }
        }
    })

    return distinctLiveData
}

В ваших DAO метод, который возвращает LiveData, сделайте public, а метод, который запрашивает базу данных, protected.


@Dao
abstract class UserDao : BaseDao<User>() {

@Query(“SELECT * FROM Users WHERE userid = :id”)
   protected abstract fun getUserById(id: String): LiveData<User>

fun getDistinctUserById(id: String): 
         LiveData<User> = getUserById(id).getDistinct()
}

Полный пример кода смотрите здесь.


Примечание: если вы запрашиваете список для отображения, обратите внимание на библиотеку Paging Library, которая будет возвращать LivePagedListBuilder. Библиотека поможет автоматически вычислить разницу между элементами списка и обновить ваш пользовательский интерфейс.


Читайте также: 7 шагов к использованию Room. Пошаговое руководство по миграции приложения на Room

Комментарии (0)