При использовании библиотеки Room для хранения данных приложения вы взаимодействуете с сохраненными данными, определяя объекты доступа к данным (DAO). Каждый объект DAO содержит функции, которые предоставляют абстрактный доступ к базе данных приложения. Во время компиляции Room автоматически создает реализации определенных вами объектов доступа к данным.
Используя объекты доступа к данным для доступа к базе данных приложения вместо конструкторов запросов или прямых запросов, вы можете сохранить разделение задач – важный архитектурный принцип. Кроме того, 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
Аннотация @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) }