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
произойдут другие изменения (удаления, обновления или вставки), которые не имеют никакого отношения к интересующему вас пользователю, что приведёт к ложным уведомлениям. Более того, если ваш запрос включает в себя несколько таблиц, вы будете получать новые сообщения всякий раз, когда что-то поменяется в любой из них.
Вот что происходит за кулисами:
- В 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. Библиотека поможет автоматически вычислить разницу между элементами списка и обновить ваш пользовательский интерфейс.
Читайте также: 7 шагов к использованию Room. Пошаговое руководство по миграции приложения на Room