
7 советов по работе с комнатами
Комната - это слой абстракции поверх SQLite, который упрощает и упрощает сохранение данных. Если вы новичок в Room, ознакомьтесь с этим учебником:
В этой статье я хотел бы поделиться некоторыми советами по использованию Room:
- Предварительно заполните базу данных через
RoomDatabase#Callback - Используйте возможность наследования DAO
- Выполнять запросы в транзакциях с минимальным стандартным кодом через
@Transaction - Читай только то, что тебе нужно
- Обеспечить соблюдение ограничений между сущностями с внешними ключами
- Упростите запросы типа« один ко многим с помощью
@Relation» - Избегайте ложных срабатываний уведомлений для наблюдаемых запросов
1. Предварительно заполните базу данных.
Вам нужно добавить данные по умолчанию в вашу базу данных сразу после ее создания или при открытии базы данных? Используйте RoomDatabase#Callback! Вызовите метод addCallback при построении базы данных RoomDatabase и замените 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.
У вас есть несколько таблиц в базе данных и вы копируете одни и те же методы Insert, Update и Delete? DAO поддерживают наследование, поэтому создайте BaseDao<T> class и определите там свои общие методы @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 методов, которые имеют оператор выбора, в следующих случаях:
- Когда результат запроса довольно большой. Запрашивая базу данных в одной транзакции, вы гарантируете, что если результат запроса не умещается в одном окне курсора, он не будет поврежден из-за изменений в базе данных между переключениями окон курсора.
- Когда результатом запроса является 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 мы определяем запрос и выбираем нужные столбцы из таблицы пользователей.
@Dao
interface UserDao {
@Query(“SELECT userId, firstName, lastName FROM Users)
fun getUsersMinimal(): List<UserMinimal>
}
5. Обеспечьте соблюдение ограничений между объектами с внешними ключами.
Несмотря на то, что 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.
В примере previous_42 _-_ 43_ мы можем сказать, что у нас есть отношение "один ко многим": у пользователя может быть несколько домашних животных. Допустим, мы хотим получить список пользователей со своими домашними животными: 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.
Чтобы упростить эту задачу, аннотация Room @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. Избегайте ложных срабатываний уведомлений о наблюдаемых запросах.
Допустим, вы хотите получить пользователя на основе идентификатора пользователя в наблюдаемом запросе:
@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 произойдут другие изменения (удаления, обновления или вставки), которые не имеют ничего общего с интересующим вас User, что приведет к ложным срабатываниям уведомлений. Более того, если ваш запрос включает несколько таблиц, вы будете получать новое сообщение всякий раз, когда что-то изменится в любой из них.
Вот что происходит за кулисами:
- SQLite поддерживает триггеры, которые срабатывают всякий раз, когда в таблице встречается
DELETE,UPDATEилиINSERT. - Room создает
InvalidationTracker, который используетObserversэтот трек всякий раз, когда что-то меняется в наблюдаемых таблицах. - И
LiveData, иFlowableзапросы полагаются на уведомлениеInvalidationTracker.Observer#onInvalidated. Когда это получено, запускается повторный запрос.
Room знает только, что таблица была изменена, но не знает, почему и что изменилось. Следовательно, после повторного запроса результат запроса выдается LiveData или Flowable. Поскольку Room не хранит никаких данных в памяти и не может предположить, что объекты имеют equals(), он не может определить, являются ли это теми же данными или нет.
Вы должны убедиться, что ваш DAO фильтрует выбросы и реагирует только на отдельные объекты.
Если наблюдаемый запрос реализован с использованием Flowables, используйте Flowable#distinctUntilChanged.
@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()
}
Подробнее см. Здесь.
Примечание: если вы возвращаете список для отображения, рассмотрите возможность использования Библиотеки подкачки и возврата LivePagedListBuilder, поскольку библиотека поможет автоматически вычислять разницу между элементами списка и обновлять пользовательский интерфейс. .
Впервые в Комнате? Ознакомьтесь с нашими предыдущими статьями: