7 полезных советов для тех, кто использует Room
Room — это уровень абстракции поверх SQLite, который упрощает организацию хранения данных. Если вы ещё мало знакомы с 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 = [ForeignKey( entity = User::class, parentColumns = ["userId"], childColumns = ["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”) fun getUsers(): List<User> @Query(“SELECT * FROM Pets where owner = :userId”) fun getPetsForUser(userId: String): List<Pet>
Затем мы будем перебирать список пользователей и каждый раз обращаться к таблице 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”) fun getUsers(): List<UserAndAllPets>
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 произойдут другие изменения (удаления, обновления или вставки), которые не имеют никакого отношения к интересующему вас пользователю, что приведёт к ложным уведомлениям. Более того, если ваш запрос включает в себя несколько таблиц, вы будете получать новые сообщения всякий раз, когда что-то поменяется в любой из них.
Вот что происходит за кулисами:
- В SQLite есть триггеры, которые срабатывают всякий раз, когда в таблице происходит DELETE, UPDATE или INSERT.
- Room создаёт InvalidationTracker, который использует Observers, которые отслеживают все изменения в наблюдаемых таблицах.
- И 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. Библиотека поможет автоматически вычислить разницу между элементами списка и обновить ваш пользовательский интерфейс.