November 5, 2019

Room: Хранение данных на Android для всех и каждого

Room — это относительно новый способ сохранить данные приложений в Android-приложении, представленный на Google I/O 2017. Это часть Android Architecture – группы библиотек от Google, которые позволяют организовать удобную архитектуру приложений. Room предлагается в качестве альтернативы Realm, ORMLite, GreenDao и многим другим библиотекам.

Room — это высокоуровневый интерфейс для низкоуровневых привязок SQLite, встроенных в Android, о которых вы можете узнать больше в документации. Он выполняет большую часть своей работы во время компиляции, создавая API-интерфейс поверх встроенного SQLite API, поэтому вам не нужно работать с Cursor или ContentResolver.

Использование Room

Во-первых, добавьте Room в свой проект. После этого вам нужно будет указать Room, как выглядят ваши данные. Предположим, имеется простой класс модели, который выглядит следующим образом:

data class Person(
    val name: String,
    val age: Int,
    val favoriteColor: String
)

Чтобы рассказать Room о классе Person, добавляем аннотацию @Entity к классу и @PrimaryKey к ключу:

@Entity 
data class Person(
    @PrimaryKey
    val name: String,
    val age: Int,
    val favoriteColor: String
)

Благодаря этим двум аннотациям Room теперь знает, как создать таблицу для хранения экземпляров Person.

Важная вещь, которую следует учитывать при настройке ваших моделей: каждое поле, которое хранится в базе данных, должно быть общедоступным или иметь геттер и сеттер в стандартном стиле Java Beans (если вы используете Kotlin, то достаточно указать модификатор data в объявлении класса, а все необходимые свойства класса указывать как val).

В классе Person теперь есть вся информация, которая требуется Room для создания таблиц, но у вас нет способа фактически добавлять, запрашивать или удалять данные из базы данных. Вот почему вам нужно будет сделать объект доступа к данным (DAO). DAO предоставляет интерфейс в самой базе данных и занимается манипулированием хранимыми данными Person.

Вот простой интерфейс DAO для класса Person:

@Dao
interface PersonDao {

    // Добавление Person в бд
    @Insert
    fun insertAll(vararg people: Person)

    // Удаление Person из бд
    @Delete
    fun delete(person: Person)

    // Получение всех Person из бд
    @Query("SELECT * FROM person")
    fun getAllPeople(): List<Person>

    // Получение всех Person из бд с условием
    @Query("SELECT * FROM person WHERE favoriteColor LIKE :color") 
    fun getAllPeopleWithFavoriteColor(color: String): List<Person>
}

Первое, что нужно заметить, это то, что PersonDaoэто интерфейс, а не класс. Другая интересная деталь — это инструкции SQL в аннотациях @Query(). Операторы SQL говорят Room, какую информацию вы хотите получить из базы данных. Они также проверяются во время компиляции. Поэтому, если вы измените подпись метода fun getAllPeopleWithFavoriteColor(color: String): List<Person> на fun getAllPeopleWithFavoriteColor(color: Int): List<Person>, Room выдаст ошибку во время компиляции:

incompatible types: int cannot be converted to String

И если вы сделаете опечатку в выражении SQL, например, напишите favoriteColors вместо favoriteColor, Room также выдаст ошибку компиляции:

There is a problem with the query:
[SQLITE_ERROR] SQL error or missing database (no such column: favoriteColors)

Вы не можете получить экземпляр PersonDao, потому что это интерфейс. Чтобы иметь возможность использовать классы DAO, вам необходимо создать класс базы данных. За кулисами этот класс будет отвечать за ведение самой базы данных и предоставление экземпляров DAO.

Вы можете создать свой класс базы данных всего за пару строк:

@Database(entities = [Person::class /*, AnotherEntityType::class, AThirdEntityType::class */],
          version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun getPersonDao(): PersonDao
}

Это лишь описание структуры базы данных, но сама база данных будет жить в одном файле. Чтобы получить экземпляр AppDatabase, сохраненный в файле с именем populus-database, вы должны написать:

val db = Room.databaseBuilder(applicationContext,
        AppDatabase::class, "populus-database").build()

Если вы хотите получить все данные обо всех Person, которые находятся в базе данных, вы можете написать:

val everyone: List<Person> = db.getPersonDao().getAllPeople()

Преимущества использования Room

В отличие от большинства ORM, Room использует обработчик аннотации для выполнения всей своей манеры сохранения данных. Это означает, что ни ваши классы приложений, ни классы моделей не должны ничего расширять в Room, в отличие от многих других ORM, включая Realm и SugarORM. Как вы видели при ошибках с аннотациями @Query() выше, вы также получаете возможность проверки корректности SQL-запросов во время компиляции, что может сэкономить вам много хлопот.

Room также позволяет вам наблюдать за изменениями данных, интегрируя их как с API LiveData Архитектурных Компонентов, так и с RxJava 2. Это означает, что если у вас сложная схема, где изменения в базе данных должны появляться в нескольких местах вашего приложения, Room делает уведомления об изменениях. Это мощное дополнение может быть включено одной строкой. Все, что вам нужно сделать, – это изменить тип возвращаемых значений.

Например, этот метод:

@Query("SELECT * FROM person") 
fun getAllPeople(): List<Person>

cтановится следующим:

@Query("SELECT * FROM person")
fun getAllPeople(): LiveData<List<Person>> // or Observable<List<Person>>

Самое большое ограничение в Room: взаимосвязи

Самым большим ограничением в Room является то, что он не будет обрабатывать отношения с другими типами сущностей для вас автоматически, как другие ORM. Это означает, что если вы хотите отслеживать домашних животных:

@Entity 
data class Person(
    @PrimaryKey
    val name: String,
    val age: Int,
    val favoriteColor: String,
    val pets: List<Pet>
)
@Entity 
data class Pet(
    @PrimaryKey 
    val name: String,
    val breed: String
)

То Room выдаст ошибку компиляции, так как не знает, как сохранить отношения между Person и Pet:

Cannot figure out how to save this field into database.
You can consider adding a type converter for it.

Ошибка при компиляции предлагает конвертер типов, который преобразует объекты в примитивы, которые могут быть непосредственно сохранены в SQL. Поскольку List нельзя свести к примитиву, вам нужно сделать что-то другое. Это отношение «один ко многим», где у одного Person может быть много Pet. Room не может моделировать такие отношения, но она может справиться с обратными отношениями — у каждого Pet есть один Person. Чтобы смоделировать это, удалите поле для Pet в Person и добавьте поле ownerId в класс Pet:

@Entity
data class Person(
    @PrimaryKey
    val name: String,
    val age: Int,
    val favoriteColor: String
)
@Entity(foreignKeys = @ForeignKey( 
        entity = Person::class, 
        parentColumns = "name", 
        childColumns = "ownerId")) 
data class Pet(
    @PrimaryKey
    val name: String,
    val breed: String,
    val ownerId: String // this ID points to a Person 
)

Это приведет к тому, что Room обеспечит огранич��ние внешнего ключа между объектами. Room не будет вызывать отношения «один-ко-многим» и «много-к-одному», но она дает вам инструменты для выражения этих отношений.

Чтобы получить всех домашних животных, принадлежащих конкретному человеку, вы можете использовать запрос, который находит всех домашних животных с данным идентификатором владельца. Например, вы можете добавить в свой DAO следующий метод:

@Query("SELECT * FROM pet WHERE ownderId IS :ownerId") 
fun getPetsForOwner(ownerId: String): List<Pet>

Стоит ли использовать Room?

Если вы уже настроили сохранение данных в своем приложении и довольны им, то ничего не изменяйте. Каждая ORM и встроенная реализация SQLite будут продолжать работать так же, как и раньше. Room — это всего лишь еще один вариант сохранения данных.

Если вы используете SQLite или собираетесь использовать его, вы должны попробовать Room. Он обладает всеми возможностями, необходимыми для выполнения расширенных запросов, одновременно устраняя необходимость писать SQL-запросы для поддержки базы данных самостоятельно.

Источник: Room: Хранение данных на Android для всех и каждого