При использовании библиотеки 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:
- Включите артефакт
androidx.room3:room3-pagingв конфигурацию сборки. - Добавьте аннотацию
@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) }