Центральноым звеном в организации взаимодействия с базой данных через Room является сущность. С каждой таблицей базы данных связан определенный класс сущности, который определяет схему таблицы. Синтаксически сущность представляет стандартный класса Kotlin с некоторыми специальными аннотациями Room. Например:
@Entity(tableName = "users") class User { @PrimaryKey(autoGenerate = true) @NonNull @ColumnInfo(name = "userId") var id: Int = 0 @ColumnInfo(name = "userName") var name: String? = null var age: Int? = null constructor() {} constructor(id: Int, name: String, age: Int) { this.id = id this.name = name this.age = age } constructor(name: String, age: Int) { this.name = name this.age = age } }
Определение сущности начинается с аннотации @Entity, которая настраивает ряд метаданных для сущности. В частности, параметр tableName
указывает, с какой таблицей в базе данных будет сопоставляться эта сущность:
@Entity(tableName = "users") class User {
И здесь мы видим, что класс сущности User будет сопоставляться в базе данных с таблицей "users".
Свойства класса сопоставляются с определенными столбцами. По умолчанию сопоставление между свойствами классов и столбцами в таблице выполняется по имени. Например, свойство age не имеет никаких аннотаций:
var age: Int? = null
Поэтому данные этого свойства будут храниться в таблице базы данных в одноименном столбце "age". Однако с помощью аннотации
@ColumnInfo это можно переопределить с помощью параметра name
:
@ColumnInfo(name = "userName") var name: String? = null
В данном случае мы указываем, что для хранения свойства name в базе данных будет использоваться столбец "userName".
Каждой таблице базы данных необходим столбец, который будет выступать в качестве первичного ключа. Для установки свойства как первичного ключа применяется аннотация @PrimaryKey. В примере выше в качестве такого столбца выступает userId. И с этим столбцом будет сопоставляться свойство id:
@PrimaryKey(autoGenerate = true) @NonNull @ColumnInfo(name = "userId") var id: Int = 0
Кроме того, с помощью параметра autoGenerate = true
аннотации @PrimaryKey
значение id настроено на автоматическую генерацию.
Это значит, что система автоматически сгенерирует идентификатор, присваиваемый новым добавляемым объектам, чтобы избежать дублирования ключей.
Кроме того, к свойству id применяется аннотация @NonNull, которая указывает, что свойство (и соответственно столбец в таблице) не может хранить значение null/NULL.
Если мы не хотим, чтобы данные какого-то столбца хранились в базе данных, то мы можем к соответствующему свойству применить аннотацию @Ignore:
@Ignore var age: Int? = null
Объект доступа к данным или DAO (Data Access Object) предоставляет способ доступа к данным, хранящимся в базе данных SQLite. DAO объявляется как стандартный интерфейс Kotlin с помощью аннотации @Dao:
@Dao interface UserDao { }
Интерфейс DAO может содержать функции, которые с помощью дополнительных аннотаций сопоставляются с конкретными инструкциями SQL. Подобные функции затем может вызывать репозиторий. Например, определим метод getUsers() для получения всех объектов User из таблицы:
@Dao interface UserDao { @Query("SELECT * FROM users") fun getUsers(): LiveData<List<User>> }
В данном случае вызов метода getUsers()
приведет к выполнению SQL-выражения "SELECT * FROM users"
. Этот метод возвращает объект List, который содержит
объекты сущности User для каждой строки таблицы. DAO также использует LiveData, чтобы репозиторий мог отслеживать изменения в базе данных.
Методы интерфейсов также могут принимать параметры, на которые затем можно ссылаться в выражениях SQL в качестве аргументов. Например:
@Query("SELECT * FROM users WHERE name = :userName") fun getUser(userName: String): List<User>
Данный метод получает через параметр userName имя пользователя и ищет по этому имени всех пользователей. А для вставки значения параметра в SQL-выражение применяется двоеточие :.
Для сопоставления метода интерфейса с SQL-выражением добавления данных может применяться специальная аннотация @Insert:
@Insert fun addUser(user: User)
В подобном случае библиотека Room может определить, что сущность User, переданная методу addUser(), должна быть добавлена в базу данных. Причем эта аннотация позволяет добавлять сразу несколько объектов в рамках одной транзакции:
@Insert fun insertUsers(User... userList)
Для удаления данных можно определить SQL-инструкцию:
@Query("DELETE FROM users WHERE userName = :name") fun deleteUser(name: String)
В качестве альтернативы также можно использовать аннотацию @Delete, в том числе для удаления набора данных:
@Delete fun deleteUsers(User... users)
Аналогично для обновления можно использовать аннотацию @Update:
@Update fun updateUsers(User... users)
Методы DAO для вышерассмотренных операций также могут возвращать значение типа Int, которое хранит количество строк, затронутых транзакцией, например:
@Delete fun deleteUsers(User... users): int
База данных Room представляет слой поверх фактической базы данных SQLite, который отвечает за предоставление доступа к экземплярам DAO, связанным с базой данных. Каждое приложение Android должно иметь только один экземпляр базы данных Room.
Синтаксически база данных Room представляет класс, который наследуется от RoomDatabase. Например:
import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.example.helloapp.User import com.example.helloapp.UserDao @Database(entities = [(User::class)], version = 1) abstract class UserRoomDatabase: RoomDatabase() { abstract fun UserDao(): UserDao // реализуем синглтон companion object { private var INSTANCE: UserRoomDatabase? = null fun getInstance(context: Context): UserRoomDatabase { synchronized(this) { var instance = INSTANCE if (instance == null) { instance = Room.databaseBuilder( context.applicationContext, UserRoomDatabase::class.java, "User_database" ).fallbackToDestructiveMigration().build() INSTANCE = instance } return instance } } } }
К классу базы данных Room применяется аннотация @Database, которая объявляет сущности, с которыми должна работать база данных. В остальном класс содержит фактически реализацию паттерна синглтон, который гарантирует, что одномоментно может быть только один объект этого класса.
Репозиторий содержит код, который вызывает методы DAO для выполнения операций с базой данных. Пример репозитория:
import androidx.lifecycle.LiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class UserRepository(private val userDao: UserDao) { private val coroutineScope = CoroutineScope(Dispatchers.Main) val userList: LiveData<List<User>>? = userDao.getUsers() fun addUser(User: User) { coroutineScope.launch(Dispatchers.IO) { userDao.addUser(User) } } fun deleteUser(id:Int) { coroutineScope.launch(Dispatchers.IO) { userDao.deleteUser(id) } } }
При вызове методов DAO важно отметить, что если метод не возвращает экземпляр LiveData (который автоматически выполняет запросы в отдельном потоке), операцию нельзя выполнить в основном потоке приложения. А при попытке сделать это мы получим следующую ошибку:
Cannot access database on the main thread since it may potentially lock the UI for a long period of time
Кроме того, поскольку выполнение некоторых транзакций базы данных может занять много времени, выполнение операций в отдельном потоке позволяет избежать блокировки приложения.
После объявления всех классов необходимо создать и инициализировать экземпляры базы данных, DAO и репозитория, код которых может выглядеть следующим образом:
val userDb = UserRoomDatabase.getInstance(application) val userDao = userDb.UserDao() private val repository: UserRepository = UserRepository(userDao)
Стоит отметить, что библиотека Room также поддерживает базы данных в памяти. Эти базы данных полностью находятся в памяти и уничтожаются при завершении работы приложения.
Единственное изменение, необходимое для работы с базой данных в памяти, — это вызов метода Room.inMemoryDatabaseBuilder() класса Room вместо Room.databaseBuilder()
.
Например, сравнение двух подходов:
// Создание базы данных SQLite val instance = Room.databaseBuilder( context.applicationContext, UserRoomDatabase::class.java, "users_db" ).fallbackToDestructiveMigration() .build() // Создание базы данных в памяти val instance2 = Room.inMemoryDatabaseBuilder( context.applicationContext, UserRoomDatabase::class.java, ).fallbackToDestructiveMigration() .build()
Обратите внимание, что для базы данных в памяти не требуется имя базы данных.