7 советов по работе с комнатами

Комната - это слой абстракции поверх SQLite, который упрощает и упрощает сохранение данных. Если вы новичок в Room, ознакомьтесь с этим учебником:



В этой статье я хотел бы поделиться некоторыми советами по использованию Room:

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

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

  1. SQLite поддерживает триггеры, которые срабатывают всякий раз, когда в таблице встречается DELETE, UPDATE или INSERT.
  2. Room создает InvalidationTracker, который использует Observers этот трек всякий раз, когда что-то меняется в наблюдаемых таблицах.
  3. И 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, поскольку библиотека поможет автоматически вычислять разницу между элементами списка и обновлять пользовательский интерфейс. .

Впервые в Комнате? Ознакомьтесь с нашими предыдущими статьями: