Room — это библиотека, которая является частью архитектурных компонентов Android. Она облегчает работу с объектами SQLiteDatabase
в приложении, уменьшая объём стандартного кода и проверяя SQL-запросы во время компиляции.
У вас уже есть Android-проект, который использует SQLite для хранения данных? Если это так, то вы можете мигрировать его на Room. Давайте посмотрим, как взять уже существующий проект и отрефакторить его для использования Room за 7 простых шагов.
TL;DR: обновите зависимости gradle, создайте свои сущности, DAO и базу данных, замените вызовы SQLiteDatabase
вызовами методов DAO, протестируйте всё, что вы создали или изменили, и удалите неиспользуемые классы. Вот и всё!
В нашем примере приложения для миграции мы работаем с объектами типа User
. Мы использовали product flavors для демонстрации различных реализаций уровня данных:
- sqlite — использует
SQLiteOpenHelper
и традиционные интерфейсы SQLite. - room — заменяет реализацию на Room и обеспечивает миграцию.
Каждый вариант использует один и тот же слой пользовательского интерфейса, который работает с классом UserRepository
благодаря паттерну MVP.
В варианте sqlite вы увидите много кода, который часто дублируется и использует базу данных в классах UsersDbHelper
и LocalUserDataSource
. Запросы строятся с помощью ContentValues
, а данные, возвращаемые объектами Cursor
, читаются столбец за столбцом. Весь этот код способствует появлению неявных ошибок. Например, можно пропустить добавление столбца в запрос или неправильно собрать объект из базы данных.
Давайте посмотрим, как Room улучшит наш код. Изначально мы просто копируем классы из варианта sqlite и постепенно будем изменять их.
Шаг 1. Обновление зависимостей gradle
Зависимости для Room доступны через новый Google Maven-репозиторий. Просто добавьте его в список репозиториев в вашем основном файле build.gradle
:
allprojects {
repositories {
google()
jcenter()
}
}
Определите версию библиотеки Room в том же файле. Пока она находится в альфа-версии, но следите за обновлениями версий на страницах для разработчиков:
ext {
roomVersion = '2.1.0-alpha03'
}
В вашем файле app/build.gradle
добавьте зависимости для Room:
dependencies {
implementation “android.arch.persistence.room:runtime:$rootProject.roomVersion”
annotationProcessor “android.arch.persistence.room:compiler:$rootProject.roomVersion”
androidTestImplementation “android.arch.persistence.room:testing:$rootProject.roomVersion”
}
Чтобы мигрировать на Room, нам нужно увеличить версию базы данных, а для сохранения пользовательских данных нам потребуется реализовать класс Migration. Чтобы протестировать миграцию, нам нужно экспортировать схему. Для этого добавьте следующий код в файл app/build.gradle
:
android {
defaultConfig {
...
// used by Room, to test migrations
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
// used by Room, to test migrations
sourceSets {
androidTest.assets.srcDirs +=
files("$projectDir/schemas".toString())
}
...
Шаг 2. Обновление классов модели до сущностей
Room создаёт таблицу для каждого класса, помеченного @Entity. Поля в классе соответствуют столбцам в таблице. Следовательно, классы сущностей, как правило, представляют собой небольшие классы моделей, которые не содержат никакой логики. Наш класс User
представляет модель для данных в базе данных. Итак, давайте обновим его, чтобы сообщить Room, что он должен создать таблицу на основе этого класса:
- Аннотируйте класс с помощью
@Entity
и используйте свойствоtableName
, чтобы задать имя таблицы. - Задайте первичный ключ, добавив аннотацию
@PrimaryKey
в правильные поля — в нашем случае это идентификатор пользователя. - Задайте имя столбцов для полей класса, используя аннотацию
@ColumnInfo(name = "column_name")
. Этот шаг можно пропустить, если ваши поля уже названы так, как следует назвать столбец. - Если в классе несколько конструкторов, добавьте аннотацию
@Ignore
, чтобы указать Room, какой следует использовать, а какой — нет.
@Entity(tableName = "users")
public class User {
@PrimaryKey
@ColumnInfo(name = "userid")
private String mId;
@ColumnInfo(name = "username")
private String mUserName;
@ColumnInfo(name = "last_update")
private Date mDate;
@Ignore
public User(String userName) {
mId = UUID.randomUUID().toString();
mUserName = userName;
mDate = new Date(System.currentTimeMillis());
}
public User(String id, String userName, Date date) {
this.mId = id;
this.mUserName = userName;
this.mDate = date;
}
...
}
Примечание: для плавной миграции обратите пристальное внимание на имена таблиц и столбцов в исходной реализации и убедитесь, что вы правильно устанавливаете их в аннотациях @Entity
и @ColumnInfo
.
Шаг 3. Создание объектов доступа к данным (DAO)
DAO отвечают за определение методов доступа к базе данных. В первоначальной реализации нашего проекта на SQLite все запросы к базе данных выполнялись в классе LocalUserDataSource
, где мы работали с объектами Cursor
. В Room нам не нужен весь код, связанный с курсором, и мы можем просто определять наши запросы, используя аннотации в классе UserDao
.
Например, при запросе всех пользователей из базы данных Room выполняет всю «тяжелую работу», и нам нужно только написать:
@Query(“SELECT * FROM Users”)
List<User> getUsers();
Шаг 4. Создание базы данных
Мы уже определили нашу таблицу Users
и соответствующие ей запросы, но мы ещё не создали базу данных, которая объединит все эти составляющие Room. Для этого нам нужно определить абстрактный класс, который расширяет RoomDatabase
. Этот класс помечен @Database
, в нём перечислены объекты, содержащиеся в базе данных, и DAO, которые обращаются к ним. Версия базы данных должна быть увеличена на 1 в сравнении с первоначальным значением, поэтому в нашем случае это будет 2.
@Database(entities = {User.class}, version = 2)
@TypeConverters(DateConverter.class)
public abstract class UsersDatabase extends RoomDatabase {
private static UsersDatabase INSTANCE;
public abstract UserDao userDao();
Поскольку мы хотим сохранить пользовательские данные, нам нужно реализовать класс Migration
, сообщающий Room, что он должен делать при переходе с версии 1 на 2. В нашем случае, поскольку схема базы данных не изменилась, мы просто предоставим пустую реализацию:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Поскольку мы не изменяли таблицу, здесь больше ничего не нужно делать.
}
};
Создайте объект базы данных в классе UsersDatabase
, определив имя базы данных и миграцию:
database = Room.databaseBuilder(context.getApplicationContext(),
UsersDatabase.class, "Sample.db")
.addMigrations(MIGRATION_1_2)
.build();
Чтобы узнать больше о том, как реализовать миграцию баз данных и как они работают под капотом, посмотрите этот пост.
Шаг 5. Обновление репозитория для использования Room
Мы создали нашу базу данных, нашу таблицу пользователей и запросы, так что теперь пришло время их использовать. На этом этапе мы обновим класс LocalUserDataSource
для использования методов UserDao
. Для этого мы сначала обновим конструктор: удалим Context
и добавим UserDao
. Конечно, любой класс, который создаёт экземпляр LocalUserDataSource
, также должен быть обновлен.
Далее мы обновим методы LocalUserDataSource
, которые делают запросы с помощью вызова методов UserDao
. Например, метод, который запрашивает всех пользователей, теперь выглядит так:
public List<User> getUsers() {
return mUserDao.getUsers();
}
А теперь время запустить то, что у нас получилось.
Одна из лучших функций Room — это то, что если вы выполняете операции с базой данных в главном потоке, то ваше приложение упадёт со следующим сообщением об ошибке:
java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Один надёжный способ переместить операции ввода-вывода из основного потока — это создать новый Runnable
, который будет создавать новый поток для каждого запроса к базе данных. Поскольку мы уже используем этот подход в варианте sqlite, никаких изменений не потребовалось.
Шаг 6. Тестирование на устройстве
Мы создали новые классы — UserDao
и UsersDatabase
и изменили наш LocalUserDataSource
для использования базы данных Room. Теперь нам нужно их протестировать.
Тестирование UserDao
Чтобы протестировать UserDao
, нам нужно создать тестовый класс AndroidJUnit4
. Потрясающая особенность Room — это возможность создавать базу данных в памяти. Это исключает необходимость очистки после каждого теста.
@Before
public void initDb() throws Exception {
mDatabase = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getContext(),
UsersDatabase.class)
.build();
}
Нам также нужно убедиться, что мы закрываем соединение с базой данных после каждого теста.
@After
public void closeDb() throws Exception {
mDatabase.close();
}
Например, чтобы протестировать вход пользователя, мы добавим пользователя, а затем проверим, сможем ли мы получить этого пользователя из базы данных.
@Test
public void insertAndGetUser() {
// Добавление пользователя в базу данных
mDatabase.userDao().insertUser(USER);
// Проверка возможности получения пользователя из базы данных
List<User> users = mDatabase.userDao().getUsers();
assertThat(users.size(), is(1));
User dbUser = users.get(0);
assertEquals(dbUser.getId(), USER.getId());
assertEquals(dbUser.getUserName(), USER.getUserName());
}
Тестирование использования UserDao в LocalUserDataSource
Убедиться, что LocalUserDataSource
по-прежнему работает правильно, легко, поскольку у нас уже есть тесты, которые описывают поведение этого класса. Всё, что нам нужно сделать, это создать базу данных в памяти, получить из нее объект UserDao
и использовать его в качестве параметра для конструктора LocalUserDataSource
.
@Before
public void initDb() throws Exception {
mDatabase = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getContext(),
UsersDatabase.class)
.build();
mDataSource = new LocalUserDataSource(mDatabase.userDao());
}
Опять же, нам нужно убедиться, что мы закрываем базу данных после каждого теста.
Тестирование миграции базы данных
Подробнее почитать о том, как реализовать тесты миграции баз данных, а также, как работает MigrationTestHelper
, можно в этом посте.
Вы также можете посмотреть код из более детального примера приложения миграции.
Шаг 7. Удаление всего ненужного
Удалите все неиспользуемые классы и строки кода, которые теперь заменены функциональностью Room. В нашем проекте нам просто нужно удалить класс UsersDbHelper
, который расширял класс SQLiteOpenHelper
.
Если у вас есть большая и более сложная база данных, и вы хотите постепенно перейти на Room, то рекомендуем этот пост.
Теперь количество стандартного кода, подверженного ошибкам, уменьшилось, запросы проверяются во время компиляции, и всё тестируется. За 7 простых шагов мы смогли мигрировать наше существующее приложение на Room. Пример приложения можете посмотреть здесь.
Комментарии (2)
maslyaev
07.03.2019 23:20Очередной симпатичный ORMчик, построенный на в общем и целом ошибочном предрассудке, что строчки в табличках реляционной БД предназначены для хранения объектов.
anegin
Runnable не создает новый поток. Он вообще ничего не создает, это просто интерфейс. А вот его реализацию уже можно передать, например, в Thread, или Executor, или Handler