Room Database — уроки, извлеченные из работы с соединениями нескольких таблиц | Эрик Н | январь 2023 г. | ProAndroidDev

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

Рассмотренные API

Начиная с версии Room 2.4.3, существует 3 метода одновременного запроса нескольких таблиц базы данных.

Многотабличный запрос

Плюсы

  • Доступен с версии 1.1
  • Надежная, менее сложная логика/магия соединения под капотом
  • Простой

Минусы

  • Требуется дополнительный класс данных
  • Дополнительный класс данных может быть большим, если нам нужно много столбцов.

Мультикарта

Плюсы

  • Нет необходимости в дополнительных занятиях
  • Казалось бы интуитивно понятный

Минусы

  • Относительно новый, начиная с Room 2.4.0.
  • Немного логики/магии соединения под капотом
  • Не работает для 3 столов и более — мы рассмотрим такие ограничения в нашем примере приложения!

Классы данных с аннотациями @Relation

Плюсы

  • Требуются дополнительные классы данных
  • Каждый класс небольшой, даже если нам нужно много столбцов (в отличие от многотабличного запроса).

Минусы

  • Доступен с 1.0
  • Надежный
  • Работает за 3 столами и более
  • Работает для вложенных отношений — однако следует проявлять осторожность с точки зрения производительности
  • Немного логики/магии соединения под капотом

Пример приложения

Какой лучший способ учиться на практике? Чтобы изучить нюансы многостоловых запросов Room, мы будем использовать простое приложение «Вопросы и ответы» (Q&A).
Наше приложение для вопросов и ответов поддерживает как текстовые вопросы, так и вопросы, основанные на выборе.
Пример текстового вопроса: «Какая ваша любимая еда?». Примером вопроса с несколькими вариантами ответов является «Отношения (приоритеты?)», на который пользователь может ответить «Общение», «Сострадание», «Сотрудничество» и/или «Обязательство».
Структуры данных и их отношения следующие:
Вопросы и ответы.png

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

Вариант использования 1 – запрос ответов на вопросы с произвольным текстом

Здесь хорошо подходит многотабличный запрос из-за его простоты и небольшого количества требуемых столбцов.

@Query("SELECT question.text as question, answer.text_value as answer FROM question, answer WHERE question.id = answer.question_id AND answer.option_id = ''")
fun readTextAnswers(): Flow<List<TextAnswer>>

С дополнительным классом TextAnswer

data class TextAnswer(
val question: String,
val answer: String?
)

Результаты 🎊
Вариант использования статьи о комнате 1.png

Исходный код: /tree/text-based-questions
Конечно, мы можем добиться тех же результатов с помощью multi-map и @Relation, но эти подходы будут слишком сложными для нашего простого варианта использования.

Вариант использования 2. Запрос ответов на вопросы с несколькими вариантами ответов (MCQ)

Вариант использования 2.1 — только вопросы и варианты запросов

Давайте начнем с того, что покажем, какие варианты пользователь может выбрать для каждого вопроса.
И давайте начнем с более нового и, казалось бы, более интуитивно понятного метода — Multi-map.

@Query("SELECT * from question LEFT JOIN option ON question_id")
fun readMcqs(): Flow<Map<QuestionEntity, List<OptionEntity>>>

Дополнительный класс не нужен
Результаты
Пример использования статьи о комнате 2.1.png

Вау, все кажется запутанным. Обратите внимание, что текст, выделенный жирным шрифтом, должен обозначать такие вопросы, как «Отношения», но вместо этого они представляют собой варианты, доступные для этих вопросов.
Это связано с тем, что в настоящее время Room Multi-Map не может поддерживать таблицы с одинаковыми именами столбцов. В нашем случае обе таблицы Question и Option имеют один и тот же первичный ключ «id». Решение состоит в том, чтобы сделать имена столбцов уникальными!
Как только мы изменим наши классы данных на

@Entity(tableName = "question")
data class QuestionEntity(
@PrimaryKey @ColumnInfo(name = "question_id") val id: String,
@ColumnInfo(name = "question_text") val text: String
)

И

@Entity(tableName = "option")
data class OptionEntity(
@PrimaryKey @ColumnInfo(name = "option_id") val id: String,
@ColumnInfo(name = "question_id") val questionId: String,
@ColumnInfo(name = "option_text") val text: String,
val humanId: String?
)

Теперь это работает!
Вариант использования статьи о комнате 2.1, рабочий.png

Исходный код: /tree/multiple-choice-questions-multi-map-fixed-duplicate-columns
Под капотом компилятор Room генерирует старый добрый код Cursor, который извлекает объекты Question и Option из результатов нашего SQL-запроса:

@Override
public Flow<Map<QuestionEntity, List<OptionEntity>>> readMcqs() {
  final String _sql = "SELECT * from question INNER JOIN option ON option.question_id = question.question_id";
  final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
  return CoroutinesRoom.createFlow(__db, false, new String[]{"question","option"}, new Callable<Map<QuestionEntity, List<OptionEntity>>>() {
    @Override
    public Map<QuestionEntity, List<OptionEntity>> call() throws Exception {
      final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
      try {
        final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "question_id");
        
        final Map<QuestionEntity, List<OptionEntity>> _result = new LinkedHashMap<QuestionEntity, List<OptionEntity>>();
        while (_cursor.moveToNext()) {
        
        }
        return _result;
      } finally {
      _cursor.close();
      }
    }
    @Override
    protected void finalize() {
      _statement.release();
    }
  });
}

Я заметил, что есть некоторые странные неиспользуемые индексы курсора, такие как final int _cursorIndexOfId_1 = CursorUtil.getColumnIndexOrThrow(_cursor, "question_id") но я предполагаю, что это, вероятно, просто проблема с читабельностью, а не функциональная проблема с сгенерированным кодом Room.

Вариант использования 2.2. Запрос 3 объектов Вопрос, вариант и ответ

К сожалению, на данный момент мультикарта не поддерживает такое использование. Я надеюсь, что будущие версии Multi-map будут поддерживать запрос 3 связанных объектов.
Я пробовал следующее:

@Query("SELECT * FROM (SELECT * from question INNER JOIN option ON option.question_id = question.question_id) AS q INNER JOIN answer ON answer.question_id = q.question_id")
fun readMcqAnswers2(): Flow<Map<Map<QuestionEntity, List<OptionEntity>>, List<AnswerEntity>>>

И Room не смог сгенерировать код, отвечающий требованиям:

error: Not sure how to convert a Cursor to this method's return type (kotlinx.coroutines.flow.Flow<java.util.Map<java.util.Map<app.ericn.myqa.QuestionEntity, java.util.List<app.ericn.myqa.OptionEntity>>, java.util.List<app.ericn.myqa.AnswerEntity>>>).
public abstract kotlinx.coroutines.flow.Flow<java.util.Map<java.util.Map<app.ericn.myqa.QuestionEntity, java.util.List<app.ericn.myqa.OptionEntity>>, java.util.List<app.ericn.myqa.AnswerEntity>>> readMcqAnswers2();

Точно так же Room не знает, как генерировать код для следующего:

@Query("SELECT * FROM question")
fun readMcqAnswers5(): Flow<Map<Map<QuestionEntity, List<OptionEntity>>, List<AnswerEntity>>>
@Query("SELECT * FROM question INNER JOIN (SELECT * FROM option INNER JOIN answer ON answer.option_id = option.option_id) as a ON a.question_id = question.question_id")
fun readMcqAnswers3(): Flow<Map<QuestionEntity, Map<OptionEntity, AnswerEntity>>>
@Query("SELECT * FROM question")
fun readMcqAnswers4(): Flow<Map<QuestionEntity, Map<OptionEntity, AnswerEntity>>>

Наконец, я обратился к классическому методу аннотации @Relation, и здесь он удовлетворил нашим требованиям:
Модель данных

data class QuestionWithRelations(
    @Embedded
    val question: QuestionEntity,
    @Relation(
        parentColumn = "question_id",
        entityColumn = "question_id"
    )
    val options: List<OptionEntity>,
    @Relation(
        parentColumn = "question_id",
        entityColumn = "question_id"
    )
    val answers: List<AnswerEntity>
)

ДАО

@Query("SELECT * FROM question")
fun readMcqAnswers1(): Flow<List<QuestionWithRelations>>

Вариант использования статьи о комнате 2.2.png

Исходный код: /tree/multiple-choice-questions-answers
Под капотом Room также генерирует код, который использует старый добрый SQLite Cursor, но мне он кажется более читаемым по сравнению с кодом Multi-map. Я не вижу причин, по которым будущие итерации Multi-map не могут соответствовать функциональным возможностям @Relation и @Embedded для 3 объектов. Просто неясно, насколько распространено такое требование и какое место оно занимает в таблице приоритетов команды разработчиков Room.

Дизайн базы данных имеет значение

Если бы каждый ответ включал список идентификаторов опций, была бы логика проще? Дайте мне знать ваши мысли в ответ.
Как насчет нескольких отдельных SQL-запросов?
Несколько запросов SQL медленнее, чем один запрос SQL
Параллельные запросы SQL не обязательно быстрее, чем последовательные запросы!

Заключение

Как всегда, нет универсальной пули на все случаи жизни. Вы должны выбрать инструмент, который наилучшим образом соответствует вашим потребностям и ограничениям. В нашем приложении мы в настоящее время используем все 3 различных метода
Многотабличные запросы для простых текстовых вопросов и ответов
Multi-Map для 2 объектных отношений, например, профили и фотографии
@Relation и @Embedded для 3 объектных отношений, например Вопросы, Варианты и Ответы

Исходный код

Кредиты

Престижность Christa Mabee за то, что она рассказала о существовании Room Multi-Map и вашей исследовательской работе над ней. Большое спасибо также за корректуру этой статьи.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *