Доступ к данным осуществляется с помощью Room DAO.

При использовании библиотеки Room для хранения данных вашего приложения вы взаимодействуете с хранимыми данными, определяя объекты доступа к данным (DAO). Каждый DAO включает функции, обеспечивающие абстрактный доступ к базе данных вашего приложения. Во время компиляции Room автоматически генерирует реализации определенных вами DAO.

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

Анатомия DAO

Каждый DAO можно определить либо как интерфейс, либо как абстрактный класс. Для базовых случаев обычно используется интерфейс. В любом случае, вы всегда должны аннотировать свои DAO с помощью @Dao . У DAO нет свойств, но они определяют одну или несколько функций для взаимодействия с данными в базе данных вашего приложения.

Приведённый ниже код представляет собой пример DAO, определяющего функции для вставки, удаления и выбора объектов User в базе данных Room:

@Dao
interface UserDao {
    @Insert
    suspend fun insertAll(vararg users: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM user")
    suspend fun getAll(): List<User>
}

Существует два типа функций DAO, определяющих взаимодействие с базой данных:

  • Удобные функции, позволяющие вставлять, обновлять и удалять строки в базе данных без написания SQL-кода.
  • Функции запросов, позволяющие писать собственные SQL-запросы для взаимодействия с базой данных.

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

Функции удобства

Room предоставляет удобные аннотации для определения функций, выполняющих вставку, обновление и удаление данных без необходимости написания SQL-запроса.

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

Вставлять

Аннотация @Insert позволяет определять функции, которые вставляют свои параметры в соответствующую таблицу базы данных. Следующий код демонстрирует примеры допустимых функций @Insert , которые вставляют один или несколько объектов User в базу данных:

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUsers(vararg users: User)

    @Insert
    suspend fun insertBothUsers(user1: User, user2: User)

    @Insert
    suspend fun insertUsersAndFriends(user: User, friends: List<User>)
}

Каждый параметр функции @Insert должен представлять собой либо экземпляр класса сущности данных Room, аннотированный @Entity , либо коллекцию экземпляров классов сущностей данных. При вызове функции @Insert Room вставляет каждый переданный экземпляр сущности в соответствующую таблицу базы данных.

Если функция @Insert принимает один параметр, она может возвращать значение Long , которое является новым rowId для вставляемого элемента. Если параметр представляет собой массив или коллекцию, то вместо этого функция должна возвращать массив или коллекцию значений Long , каждое из которых является rowId одного из вставляемых элементов. Для получения дополнительной информации о возврате значений rowId см. справочную документацию по аннотации @Insert и документацию SQLite по таблицам rowid .

Обновлять

Аннотация @Update позволяет определять функции, обновляющие определенные строки в таблице базы данных. Как и функции @Insert , функции @Update принимают в качестве параметров экземпляры сущностей данных. Следующий код демонстрирует пример функции @Update , которая пытается обновить один или несколько объектов User в базе данных:

@Dao
interface UserDao {
    @Update
    suspend fun updateUsers(vararg users: User)
}

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

Функция @Update может дополнительно возвращать Int значение, указывающее количество строк, которые были успешно обновлены.

Удалить

Аннотация @Delete позволяет определять функции, удаляющие определенные строки из таблицы базы данных. Как и функции @Insert , функции @Delete принимают в качестве параметров экземпляры сущностей данных. Следующий код демонстрирует пример функции @Delete , которая пытается удалить один или несколько объектов User из базы данных:

@Dao
interface UserDao {
    @Delete
    suspend fun deleteUsers(vararg users: User)
}

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

Функция @Delete может дополнительно возвращать Int значение, указывающее количество успешно удаленных строк.

Вставить

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

Подобно функциям @Insert и @Update , функции @Upsert принимают в качестве параметров экземпляры сущностей данных. В следующем коде показан пример функции @Upsert , которая пытается обновить или вставить один или несколько объектов User в базу данных:

@Dao
interface UserDao {
    @Upsert
    suspend fun upsertUsers(vararg users: User)
}

Если функция @Upsert принимает один параметр, она может возвращать значение Long . Если в результате вставляется новая строка, она возвращает ` rowId новой вставленной строки. Если в результате обновляется существующая строка, она возвращает -1 . Если параметром является массив или коллекция, функция должна возвращать массив или коллекцию значений типа Long .

Функции запросов

Аннотация @Query позволяет писать SQL-запросы и предоставлять к ним доступ в виде функций DAO. Используйте эти функции запросов для получения данных из базы данных вашего приложения или когда вам необходимо выполнить более сложные операции вставки, обновления и удаления.

Room проверяет SQL-запросы на этапе компиляции. Это означает, что если в вашем запросе обнаружится проблема, возникнет ошибка компиляции, а не сбой во время выполнения.

Простые запросы

Приведенный ниже код определяет функцию, которая использует запрос SELECT для возврата всех объектов User из базы данных:

@Query("SELECT * FROM user")
suspend fun loadAllUsers(): List<User>

В следующих разделах показано, как модифицировать этот пример для типичных сценариев использования.

Возвращает подмножество столбцов таблицы.

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

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

data class NameTuple(
    @ColumnInfo(name = "first_name") val firstName: String,
    @ColumnInfo(name = "last_name") val lastName: String
)

Затем вы можете вернуть этот объект данных из функции запроса:

@Query("SELECT first_name, last_name FROM user")
suspend fun loadFullName(): List<NameTuple>

Поскольку запрос возвращает значения для столбцов first_name и last_name , Room сопоставляет эти значения со свойствами класса NameTuple . Если запрос возвращает столбец, который не соответствует свойству в возвращаемом объекте, Room выводит предупреждение.

Хотя в предыдущем примере для получения подмножества столбцов используется пользовательский класс данных, Room также поддерживает возврат kotlin.Pair и kotlin.Triple для удобства, когда запрос возвращает ровно два или три столбца. При использовании этих типов столбцы сопоставляются в порядке их определения в операторе запроса, поэтому порядок столбцов в операторе SELECT должен соответствовать порядку типов в Pair или Triple .

Передача простых параметров в запрос

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

Например, следующий код определяет функцию, которая возвращает всех пользователей старше определенного возраста:

@Query("SELECT * FROM user WHERE age > :minAge")
suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>

Также можно передавать несколько параметров или ссылаться на один и тот же параметр несколько раз в запросе, как показано в следующем коде:

@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

@Query(
    """
    SELECT * FROM user
    WHERE first_name LIKE :search OR last_name LIKE :search
    """
)
suspend fun findUserWithName(search: String): List<User>

Передайте набор параметров в запрос.

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

Например, следующий код определяет функцию, которая возвращает информацию обо всех пользователях из подмножества регионов:

@Query("SELECT * FROM user WHERE region IN (:regions)")
suspend fun loadUsersFromRegions(regions: List<String>): List<User>

Запрос к нескольким таблицам

Для вычисления результата некоторым вашим запросам может потребоваться доступ к нескольким таблицам. В SQL-запросах можно использовать оператор JOIN для ссылки на несколько таблиц.

Приведенный ниже код определяет функцию, которая объединяет три таблицы для получения списка книг, находящихся в данный момент на выдаче конкретному пользователю:

@Query(
    """
    SELECT * FROM book
    INNER JOIN loan ON loan.book_id = book.id
    INNER JOIN user ON user.id = loan.user_id
    WHERE user.name LIKE :userName
    """
)
suspend fun findBooksBorrowedByName(userName: String): List<Book>

Вы также можете определить объекты данных, которые возвращают подмножество столбцов из нескольких объединенных таблиц. Для получения дополнительной информации см. раздел «Возвращение подмножества столбцов таблицы» . Следующий код определяет объект данных с функцией, которая возвращает имена пользователей и названия взятых ими книг:

interface UserBookDao {
    @Query(
        """
        SELECT user.name AS userName, book.name AS bookName
        FROM user, book
        WHERE user.id = book.user_id
        """
    )
    fun loadUserAndBookNames(): Flow<List<UserBook>>
}

data class UserBook(val userName: String, val bookName: String)

Возвращает мультикарту

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

Рассмотрим пример из раздела «Запросы к нескольким таблицам» . Вместо того чтобы возвращать список экземпляров пользовательского класса данных, содержащего пары экземпляров User и Book , вы можете вернуть сопоставление User и Book непосредственно из функции запроса:

@Query(
    """
    SELECT * FROM user
    JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNames(): Map<User, List<Book>>

Когда ваша функция запроса возвращает объект Multimap, вы можете писать запросы с использованием предложений GROUP BY , что позволяет вам воспользоваться возможностями SQL для сложных вычислений и фильтрации. Например, вы можете изменить функцию loadUserAndBookNames так, чтобы она возвращала только пользователей, у которых взято напрокат три или более книг:

@Query(
    """
    SELECT * FROM user
    JOIN book ON user.id = book.user_id
    GROUP BY user.name HAVING COUNT(book.id) >= 3
    """
)
suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>

Если вам не нужно сопоставлять целые объекты, вы также можете возвращать сопоставления между конкретными столбцами в вашем запросе, используя аннотацию @MapColumn для общих параметров возвращаемого типа.

@Query(
    """
    SELECT user.name AS username, book.name AS bookname FROM user
    JOIN book ON user.id = book.user_id
    """
)
suspend fun loadUserAndBookNamesColumns(): Map<
    @MapColumn(columnName = "username") String,
    List<@MapColumn(columnName = "bookname") String>
    >

Особые виды возврата

Room предоставляет несколько специальных типов возвращаемых значений для интеграции с другими библиотеками API.

Запросы с постраничной навигацией с использованием библиотеки Paging

Room поддерживает постраничные запросы благодаря интеграции с библиотекой Paging . Для использования типов возвращаемых значений Paging 3 необходимо зарегистрировать преобразователи типов возвращаемых значений Paging в вашей базе данных или DAO:

  1. Включите артефакт androidx.room3:room3-paging в конфигурацию сборки.
  2. Добавьте аннотацию @Database или @Dao к вашему объявлению @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) .

После регистрации ваши DAO смогут возвращать объекты PagingSource для использования с Paging 3 :

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM users WHERE label LIKE :query")
    fun pagingSource(query: String): PagingSource<Int, User>
}

Для получения дополнительной информации о выборе параметров типа для PagingSource см. раздел «Выбор типов ключей и значений» .

Прямой доступ к подключению к базе данных

Если логика вашего приложения требует прямого низкоуровневого доступа к подключению к базе данных, вы можете использовать API подключения Room. Вы можете получить подключение, используя useReaderConnection для операций только для чтения или useWriterConnection для операций записи в экземпляр RoomDatabase , а также использовать usePrepared для выполнения запросов:

val result: List<Pair<Long, String>> =
    roomDatabase.useReaderConnection { connection ->
        connection.usePrepared(
            "SELECT * FROM user WHERE age > :minAge LIMIT 5"
        ) { stmt ->
            // Bind arguments if needed
            stmt.bindLong(1, minAge.toLong())
            buildList {
                // Step through the results
                while (stmt.step()) {
                    add(stmt.getLong(0) to stmt.getText(1))
                }
            }
        }
    }

Если вам необходимо выполнять низкоуровневые транзакции с базой данных непосредственно через соединение, вы можете использовать вспомогательные функции immediateTransaction , deferredTransaction или exclusiveTransaction для экземпляра Transactor внутри блока useWriterConnection :

roomDatabase.useWriterConnection { transactor ->
    transactor.immediateTransaction {
        // Perform transactional database operations using transactor
    }
}

В качестве альтернативы, если вам необходимо выполнять только высокоуровневые операции DAO в рамках транзакции, используйте вспомогательные функции расширения withReadTransaction или withWriteTransaction в вашем экземпляре RoomDatabase :

// Perform transactional read operations (DEFERRED transaction)
val userCount = roomDatabase.withReadTransaction {
    userDao.countUsers()
}

// Perform transactional write operations (IMMEDIATE transaction)
roomDatabase.withWriteTransaction {
    userDao.insert(newUser)
    userDao.update(existingUser)
}