Что такое Room и зачем он нужен?
Room — это библиотека для работы с базами данных в Android-приложениях, написанных на Kotlin или Java. Она предоставляет слой абстракции поверх SQLite, позволяя работать с базой данных через объектно-ориентированный подход, а не через сырые SQL-запросы.
Основные преимущества Room:
- Минимизация шаблонного кода — Room автоматически генерирует код для работы с SQLite на основе аннотаций.
- Проверка SQL-запросов на этапе компиляции — ошибки в запросах обнаруживаются до запуска приложения.
- Интеграция с Coroutines и Flow — поддержка реактивного программирования и асинхронных операций.
- Миграции базы данных — встроенные механизмы для обновления схемы без потери данных.
- TypeConverters — возможность хранить сложные типы данных (например, Date, List) в базе.
Room идеально подходит для приложений, которым необходимо локальное хранение структурированных данных: заметки, кэш, пользовательские настройки, офлайн-режим и т.д.
Установка Room
Room не устанавливается через pip или npm, так как это библиотека для Android. Добавление в проект происходит через файл build.gradle (уровня модуля).
Шаг 1: Добавьте плагин KSP (Kotlin Symbol Processing)
В корневом build.gradle добавьте плагин KSP:
plugins {
id 'com.google.devtools.ksp' version '1.9.0-1.0.13' apply false
}
Шаг 2: Подключите Room в модуле приложения
В build.gradle модуля app добавьте:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp'
}
android {
// ... ваша конфигурация
}
dependencies {
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version" // Для Coroutines
}
После синхронизации Gradle библиотека готова к использованию.
Быстрый старт с примером кода
Создадим простое приложение для хранения списка задач (Todo).
Шаг 1: Определите сущность (Entity)
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "tasks")
data class Task(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val title: String,
val description: String,
val isCompleted: Boolean = false
)
Шаг 2: Создайте DAO (Data Access Object)
import androidx.room.*
import kotlinx.coroutines.flow.Flow
@Dao
interface TaskDao {
@Query("SELECT * FROM tasks ORDER BY id DESC")
fun getAllTasks(): Flow<List<Task>>
@Insert
suspend fun insertTask(task: Task)
@Update
suspend fun updateTask(task: Task)
@Delete
suspend fun deleteTask(task: Task)
}
Шаг 3: Создайте базу данных
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Task::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database"
).build()
INSTANCE = instance
instance
}
}
}
}
Шаг 4: Используйте в Activity или ViewModel
class MainActivity : AppCompatActivity() {
private lateinit var db: AppDatabase
private lateinit var taskDao: TaskDao
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
db = AppDatabase.getDatabase(this)
taskDao = db.taskDao()
// Пример: вставка задачи
lifecycleScope.launch {
taskDao.insertTask(Task(title = "Купить молоко", description = "В магазине на углу"))
}
// Наблюдение за списком задач
taskDao.getAllTasks().observe(this) { tasks ->
// Обновление UI
}
}
}
Основные методы и функции Room
1. @Insert — вставка данных
Что делает: Вставляет одну или несколько записей в таблицу.
Параметры:
entity— объект сущности или коллекция объектов.onConflict— стратегия при конфликте (REPLACE, ABORT, IGNORE).
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task)
@Insert
suspend fun insertTasks(tasks: List<Task>)
2. @Update — обновление данных
Что делает: Обновляет существующую запись по первичному ключу.
Параметры: объект сущности с заполненным первичным ключом.
@Update
suspend fun updateTask(task: Task)
3. @Delete — удаление данных
Что делает: Удаляет запись по первичному ключу.
Параметры: объект сущности.
@Delete
suspend fun deleteTask(task: Task)
4. @Query — произвольный SQL-запрос
Что делает: Выполняет любой SQL-запрос. Возвращает результат в виде объекта, списка или Flow.
Параметры: SQL-запрос с возможными параметрами через :.
@Query("SELECT * FROM tasks WHERE isCompleted = :isCompleted")
fun getTasksByStatus(isCompleted: Boolean): Flow<List<Task>>
@Query("SELECT * FROM tasks WHERE id = :taskId")
suspend fun getTaskById(taskId: Int): Task?
5. @Transaction — атомарные операции
Что делает: Гарантирует выполнение нескольких операций как единого целого.
Параметры: аннотация для метода DAO.
@Transaction
suspend fun moveTaskToCompleted(task: Task) {
updateTask(task.copy(isCompleted = true))
// Дополнительные операции
}
6. @TypeConverter — конвертация типов
Что делает: Позволяет хранить сложные типы (например, Date, List) в базе.
Параметры: статические методы с аннотациями @TypeConverter.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
// Подключение в базе данных:
@Database(entities = [Task::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
7. @ForeignKey — внешние ключи
Что делает: Определяет связи между таблицами.
Параметры: entity, parentColumns, childColumns, onDelete, onUpdate.
@Entity(
tableName = "subtasks",
foreignKeys = [ForeignKey(
entity = Task::class,
parentColumns = ["id"],
childColumns = ["taskId"],
onDelete = ForeignKey.CASCADE
)]
)
data class SubTask(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val taskId: Int,
val title: String
)
8. @Index — индексы для ускорения поиска
Что делает: Создает индекс для указанных колонок.
Параметры: массив колонок, уникальность.
@Entity(
tableName = "users",
indices = [Index(value = ["email"], unique = true)]
)
data class User(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val email: String,
val name: String
)
9. @Embedded — вложенные объекты
Что делает: Позволяет хранить поля вложенного объекта в той же таблице.
Параметры: префикс для колонок (опционально).
data class Address(
val street: String,
val city: String
)
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
@Embedded val address: Address
)
10. @RawQuery — выполнение сырых SQL-запросов
Что делает: Позволяет выполнять динамические SQL-запросы, которые не известны на этапе компиляции.
Параметры: объект SupportSQLiteQuery.
@RawQuery(observedEntities = [Task::class])
fun getTasksFromRawQuery(query: SupportSQLiteQuery): Flow<List<Task>>
// Использование:
val query = SimpleSQLiteQuery("SELECT * FROM tasks WHERE title LIKE '%' || ? || '%'", arrayOf("молоко"))
taskDao.getTasksFromRawQuery(query)
Реальный практический пример использования
Создадим приложение для ведения дневника питания с возможностью поиска по дате.
Сущность FoodEntry
@Entity(tableName = "food_entries")
data class FoodEntry(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val foodName: String,
val calories: Int,
val date: Long // timestamp
)
DAO с расширенными запросами
@Dao
interface FoodDao {
@Query("SELECT * FROM food_entries ORDER BY date DESC")
fun getAllEntries(): Flow<List<FoodEntry>>
@Query("SELECT * FROM food_entries WHERE date BETWEEN :startDate AND :endDate")
fun getEntriesBetweenDates(startDate: Long, endDate: Long): Flow<List<FoodEntry>>
@Query("SELECT SUM(calories) FROM food_entries WHERE date = :date")
fun getTotalCaloriesForDate(date: Long): Flow<Int>
@Insert
suspend fun insertEntry(entry: FoodEntry)
@Delete
suspend fun deleteEntry(entry: FoodEntry)
}
ViewModel для управления данными
class FoodViewModel(application: Application) : AndroidViewModel(application) {
private val dao = AppDatabase.getDatabase(application).foodDao()
val allEntries: Flow<List<FoodEntry>> = dao.getAllEntries()
fun getEntriesForDate(date: Long): Flow<List<FoodEntry>> {
val startOfDay = // вычисление начала дня
val endOfDay = // вычисление конца дня
return dao.getEntriesBetweenDates(startOfDay, endOfDay)
}
fun addEntry(foodName: String, calories: Int) {
viewModelScope.launch {
dao.insertEntry(FoodEntry(
foodName = foodName,
calories = calories,
date = System.currentTimeMillis()
))
}
}
}
Советы и типичные ошибки
Советы:
- Используйте Flow для реактивного обновления UI — Room автоматически уведомляет об изменениях в данных.
- Всегда используйте suspend функции для операций записи — это предотвращает блокировку главного потока.
- Создавайте миграции для изменения схемы — используйте
Migrationклассы вместоfallbackToDestructiveMigration(). - Используйте KSP вместо KAPT — KSP работает быстрее и лучше поддерживает Kotlin.
- Тестируйте DA
Также в библиотеке
Redis клиент для TypeScript: ioredis и redis
Juniper: Элегантная GraphQL библиотека на Rust для современных веб-приложений
Авторизация и аутентификация в TypeScript: полное руководство
Работа с ZIP-архивами в Java: полное руководство по java.util.zip
Redis клиент для C++: подключение, установка и примеры использования