Основные элементы Room

Последнее обновление: 16.04.2024

Определение сущностей

Центральноым звеном в организации взаимодействия с базой данных через 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

Объект доступа к данным или 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

База данных 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()

Обратите внимание, что для базы данных в памяти не требуется имя базы данных.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850