Room vs SQLite: как одна библиотека избавила меня от головной боли с базами данных

Введение

Когда я только начинал писать под Android, сохранение данных на телефоне казалось простой задачей. Берёшь SQLite, пишешь пару запросов и готово. Но чем больше становилось приложение, тем сильнее я ненавидел ручную работу с базами данных. Каждый раз писать execSQL, потом закрывать Cursor, не забыть db.close()… А если опечататься в названии колонки — приложение падает уже у пользователя, а не у тебя в студии.

Я пробовал терпеть, но в какой-то момент понял, что так нельзя. И тогда я нашёл Room. Это библиотека от Google, которая работает поверх SQLite.

В этой статье я покажу на реальных примерах кода, как выглядит работа с SQLite «вручную» и как тот же самый функционал пишется на Room. (Небольшой спойлер: разница как между велосипедом и автомобилем).

Основная часть

2. Что такое ORM и как Room решает проблемы

ORM (объектно-реляционное отображение) — это подход, при котором таблицы базы данных превращаются в обычные классы, а строки — в объекты. Вместо того чтобы писать INSERT INTO users ..., вы просто создаёте объект и вызываете insert(user). Библиотека сама генерирует нужный SQL.

Room — это официальная ORM-библиотека от Google. Она берёт на себя управление соединениями, генерацию запросов и проверки.

Room состоит из трёх основных частей:

  • Entity — класс, который описывает таблицу. Каждое поле класса становится колонкой (Вы просто ставите аннотацию @Entity)
  • DAO (Data Access Object) — интерфейс, в котором вы объявляете методы для работы с данными: insert, update, delete, getAll. (Room автоматически пишет их реализацию)
  • Database — абстрактный класс, наследник RoomDatabase. Он связывает Entity и DAO и даёт доступ к БД

Главные плюсы Room:

  • Проверка SQL на этапе компиляции. Если вы ошиблись в имени таблицы или колонки, приложение даже не соберётся
  • Никакого ручного закрытия Cursor или SQLiteDatabase — Room управляет ресурсами сам
  • Минимум кода. Не нужно писать шаблонные CREATE TABLE и разбирать Cursor по индексам
  • Методы DAO можно сделать suspend, и Room выполнит запрос в фоновом потоке, не блокируя интерфейс

3. Тот же пример на Room

Теперь покажу, как добавить пользователя, найти всех взрослых (возраст > 18), а также попробуем обновить возраст и удалить запись. Всё то же самое, что мы делали на чистом SQLite, но с Room.

Шаг 1. Сущность User

Создаём обычный data-класс и добавляем аннотацию @Entity (Room сам создаст таблицу с нужными колонками)

import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "users") data class User( @PrimaryKey(autoGenerate = true) val id: Int = 0, val name: String, val age: Int )

Никакого CREATE TABLE. Room сам создаст таблицу с колонками id, name, age.

Шаг 2. DAO (интерфейс для работы с данными)

Объявим интерфейс с аннотированными методами (Реализацию Room сгенерирует сам).

import androidx.room.Dao import androidx.room.Insert import androidx.room.Update import androidx.room.Delete import androidx.room.Query @Dao interface UserDao { @Insert suspend fun addUser(user: User) @Update suspend fun updateUser(user: User) @Delete suspend fun deleteUser(user: User) @Query("SELECT * FROM users WHERE age > 18") suspend fun getAdults(): List<User> @Query("SELECT * FROM users WHERE name = :name") suspend fun findByName(name: String): List<User> }

Методы с аннотациями @Insert, @Update, @Delete позволяют не писать SQL вручную — Room делает это сам. Для сложной выборки (запросов) — @Query.

В ответ получаем List<User> (Cursor больше не нужен).

Шаг 3. База данных (синглтон)

Создаём класс, который будет представлять всю базу данных. Он должен быть абстрактным и наследоваться от RoomDatabase. Указываем, какие таблицы в нём будут (в нашем случае — User), и версию базы (пока первая). Внутри объявляем абстрактный метод, который будет возвращать наш DAO — через него мы и будем обращаться к данным.

@Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app.db" ).build().also { INSTANCE = it } } } } }

Это стандартный шаблон, который можно скопировать один раз и забыть.

Шаг 4. Использование в коде (Activity или Fragment)

А вот как выглядит та же логика, когда используешь Room:

val db = AppDatabase.getInstance(applicationContext) lifecycleScope.launch { // Добавляем пользователя db.userDao().addUser(User(name = "Олег", age = 22)) // Получаем список взрослых val adults = db.userDao().getAdults() // Обновляем возраст первого в списке val user = adults.first() user.age = 23 db.userDao().updateUser(user) // Удаляем этого пользователя db.userDao().deleteUser(user) // Ищем по имени val annas = db.userDao().findByName("Анна") }

Обратите внимание:

  • Нет close() — Room сам закрывает соединения.

  • Нет возни с индексами колонок — результат сразу в виде списка объектов.
  • suspend-функции позволяют выполнять запросы в фоновом потоке, не блокируя интерфейс.
  • В методе findByName параметр :name автоматически экранируется — SQL-инъекции исключены.

Для наглядности — схематичное сравнение двух подходов:

Сравнение ручного SQLite и Room на примере операций с БД
Сравнение ручного SQLite и Room на примере операций с БД

Заключение

Мы сравнили два подхода к работе с SQLite на Android: ручной через SQLiteOpenHelper и современный Room. Room заметно сокращает количество кода, убирает рутину с закрытием курсоров и находит ошибки в запросах ещё на этапе компиляции. Для новых проектов я однозначно рекомендую Room — он делает работу с БД простой и понятной. SQLite я использую только в особых случаях, например, когда нужно подключить готовую базу со сложной структурой. В остальном берите Room и не мучайтесь)

1
Начать дискуссию