Основи Room

Бібліотека Room надає нам зручну обгортку для роботи з базою даних SQLite. У цьому уроці розглянемо основи. Як підключити до проєкту. Як отримувати, вставляти, оновлювати та видаляти дані.

Бібліотека персистентності Room надає рівень абстракції над SQLite, щоб забезпечити вільний доступ до бази даних, використовуючи при цьому всю потужність SQLite. Зокрема, Room надає наступні переваги:

  • Перевірка SQL-запитів під час компіляції.
  • Зручні анотації, які мінімізують повторюваний і схильний до помилок шаблонний код.
  • Спрощені шляхи міграції баз даних.

З огляду на ці міркування, ми наполегливо рекомендуємо вам використовувати Room замість прямого використання API SQLite. (цитата із документації гугл про андроїд room doc)

dependencies {
    val room_version = "2.6.1"

    implementation("androidx.room:room-runtime:$room_version")

    // If this project uses any Kotlin source, use Kotlin Symbol Processing (KSP)
    // See Add the KSP plugin to your project
    ksp("androidx.room:room-compiler:$room_version")

    // If this project only uses Java source, use the Java annotationProcessor
    // No additional plugins are necessary
    annotationProcessor("androidx.room:room-compiler:$room_version")

    // optional - Kotlin Extensions and Coroutines support for Room
    implementation("androidx.room:room-ktx:$room_version")

    // optional - RxJava2 support for Room
    implementation("androidx.room:room-rxjava2:$room_version")

    // optional - RxJava3 support for Room
    implementation("androidx.room:room-rxjava3:$room_version")

    // optional - Guava support for Room, including Optional and ListenableFuture
    implementation("androidx.room:room-guava:$room_version")

    // optional - Test helpers
    testImplementation("androidx.room:room-testing:$room_version")

    // optional - Paging 3 Integration
    implementation("androidx.room:room-paging:$room_version")
}

Room має три основні компоненти: Entity, Dao і Database. Розглянемо їх на невеликому прикладі, в якому будемо створювати базу даних для зберігання даних щодо співробітників (англ. - employee).

Entity

Анотацією Entity необхідно позначити об'єкт, який ми хочемо зберігати в базі даних. Для цього створимо клас Employee, який буде представляти дані співробітника: id, ім'я, зарплата:

@Entity
public class Employee {

   @PrimaryKey
   public long id;

   public String name;

   public int salary;
}

Клас позначається анотацією Entity. Об'єкти класу Employee будуть використовуватися при роботі з базою даних. Наприклад, ми будемо отримувати їх від бази при запитах даних і відправляти їх у базу при вставці даних.

Цей самий клас Employee буде використано для створення таблиці в базі. Як ім'я таблиці буде використано ім'я класу. А поля таблиці будуть створені відповідно до полів класу.

Анотацією PrimaryKey ми позначаємо поле, яке буде ключем у таблиці.

Dao

В об'єкті Dao ми будемо описувати методи для роботи з базою даних. Нам потрібні будуть методи для отримання списку співробітників і для додавання/зміни/видалення співробітників.

Описуємо їх в інтерфейсі з анотацією Dao.

@Dao
public interface EmployeeDao {

   @Query("SELECT * FROM employee")
   List<Employee> getAll();

   @Query("SELECT * FROM employee WHERE id = :id")
   Employee getById(long id);

   @Insert
   void insert(Employee employee);

   @Update
   void update(Employee employee);

   @Delete
   void delete(Employee employee);

}

Методи getAll і getById дають змогу отримати повний список співробітників або конкретного співробітника за id. В анотації Query нам необхідно прописати відповідні SQL-запити, які будуть використані для отримання даних.

Зверніть увагу, що як ім'я таблиці ми використовуємо employee. Нагадаю, що ім'я таблиці дорівнює імені Entity класу, тобто Employee, але в SQLite не важливий регістр в іменах таблиць, тому можемо писати employee.

Для вставки/оновлення/видалення використовуються методи insert/update/delete з відповідними анотаціями. Тут жодні запити вказувати не потрібно. Назви методів можуть бути будь-якими. Головне - анотації.

Database

Анотацією Database позначаємо основний клас по роботі з базою даних. Цей клас має бути абстрактним і успадковувати RoomDatabase.

@Database(entities = {Employee.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
   public abstract EmployeeDao employeeDao();
}

У параметрах анотації Database вказуємо, які Entity будуть використовуватися, і версію бази. Для кожного Entity класу зі списку entities буде створено таблицю.

У Database класі необхідно описати абстрактні методи для отримання Dao об'єктів, які вам знадобляться.

Практика

Усі необхідні для роботи об'єкти створено. Давайте подивимося, як використовувати їх для роботи з базою даних.

Database об'єкт - це стартова точка. Його створення виглядає так:

AppDatabase db =  Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database").build();

Використовуємо Application Context, а також вказуємо AppDatabase клас і ім'я файлу для бази.

Враховуйте, що під час виклику цього коду Room щоразу створюватиме новий екземпляр AppDatabase. Ці екземпляри дуже важкі і рекомендується використовувати один екземпляр для всіх ваших операцій. Тому вам необхідно подбати про синглтон для цього об'єкта. Це можна зробити за допомогою Dagger (будем вчити на наступних зустрічах), наприклад.

Якщо ви не використовуєте Dagger (або інший DI механізм), то можна використовувати Application клас для створення і зберігання AppDatabase:

public class App extends Application {

    public static App instance;

    private AppDatabase database;

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        database = Room.databaseBuilder(this, AppDatabase.class, "database")
                .build();
    }

    public static App getInstance() {
        return instance;
    }

    public AppDatabase getDatabase() {
        return database;
    }
}

Не забудьте додати App клас у маніфест

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <application
        android:name=".App"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name">
        <!-- Інші параметри додатку -->

        <activity android:name=".MainActivity">
            <!-- Інші параметри активності -->
        </activity>
    </application>

</manifest>

У коді отримання бази матиме такий вигляд:

AppDatabase db = App.getInstance().getDatabase();

З Database об'єкта отримуємо Dao.

EmployeeDao employeeDao = db.employeeDao();

Тепер ми можемо працювати з Employee об'єктами. Але ці операції мають виконуватися не в UI потоці. Інакше ми отримаємо Exception.

Додавання нового співробітника в базу буде виглядати так:

Employee employee = new Employee();
employee.id = 1;
employee.name = "John Smith";
employee.salary = 10000;

employeeDao.insert(employee);

Метод getAll поверне нам усіх співробітників у List<Employee>

List<Employee> employees = employeeDao.getAll();

Отримання співробітника за id:

Employee employee = employeeDao.getById(1);

Оновлення даних по співробітнику.

employee.salary = 20000;
employeeDao.update(employee);

Room шукатиме в таблиці запис за ключовим полем, тобто за id. Якщо в об'єкті employee не заповнене поле id, то за замовчуванням у нашому прикладі воно дорівнюватиме нулю, і Room просто не знайде такого співробітника (якщо, звісно, у вас немає запису з id = 0).

Видалення співробітника

employeeDao.delete(employee);

Аналогічно оновленню, Room шукатиме запис за ключовим полем, тобто за id

Давайте для прикладу додамо ще один тип об'єкта - Car.

Описуємо Entity об'єкт

@Entity
public class Car {

   @PrimaryKey
   public long id;

   public String model;

   public int year;

}

Тепер Dao для роботи з Car об'єктом

@Dao
public interface CarDao {

   @Query("SELECT * FROM car")
   List<Car> getAll();

   @Insert
   void insert(Car car);

   @Delete
   void delete(Car car);

}

Будемо вважати, що нам треба тільки читати всі записи, додавати нові та видаляти старі.

У Database необхідно додати Car у список entities і новий метод для отримання CarDao

@Database(entities = {Employee.class, Car.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
   public abstract EmployeeDao employeeDao();
   public abstract CarDao carDao();
}

Оскільки ми додали нову таблицю, змінилася структура бази даних. І нам необхідно підняти версію бази даних до 2. Але про це ми детально поговоримо пізніше. А поки що можна залишити версію, що дорівнює 1, видалити стару версію застосунку і поставити нову.

UI потік

Повторюся, операції з роботи з базою даних - синхронні, і повинні виконуватися не в UI потоці.

У випадку з Query операціями ми можемо зробити їх асинхронними, використовуючи LiveData або RxJava. У разі insert/update/delete ви можете обернути ці методи в асинхронний RxJava.

Також, ви можете використовувати allowMainThreadQueries у білдері створення AppDatabase

AppDatabase db =  Room.databaseBuilder(getApplicationContext(),
       AppDatabase.class, "database")
       .allowMainThreadQueries()
       .build();

У цьому випадку ви не отримуватимете Exception під час роботи в UI потоці. Але ви повинні розуміти, що це погана практика, і може додати відчутних гальм вашому застосунку.