Какие типы связей между таблицами бывают
Что проверяют
Базовую реляционную модель и умение перенести её в локальную базу приложения. На Android вопрос почти сразу переходит в практику: как вы описали бы связь в Room и как загрузить пользователя вместе со списком его заказов.
Как отвечать
Три типа связей и способ реализации каждого:
- один к одному (1:1): пользователь и его настройки. Внешний ключ с уникальным индексом в одной из таблиц или общий первичный ключ. Часто такие таблицы можно объединить, раздельно их держат ради редко читаемых или тяжёлых полей;
- один ко многим (1:N): автор и книги. Внешний ключ
authorIdлежит в таблице «многих»; - многие ко многим (N:M): плейлисты и треки. Нужна промежуточная таблица с парой внешних ключей и составным первичным ключом
(playlistId, trackId).
В Room сущности не хранят ссылки друг на друга, связи собираются запросом. Ограничение объявляется через foreignKeys в @Entity, при необходимости с onDelete = ForeignKey.CASCADE. Для чтения делают отдельный data class: родитель через @Embedded, дети через @Relation(parentColumn = "id", entityColumn = "authorId"). Для N:M в @Relation добавляется associateBy = Junction(PlaylistTrackCrossRef::class). Метод DAO, возвращающий такой класс, помечают @Transaction: Room выполняет несколько запросов, и без транзакции данные между ними могут поменяться.
Стоит упомянуть, что в SQLite проверка внешних ключей по умолчанию выключена и включается PRAGMA foreign_keys = ON. Room включает её сам при открытии базы, а при работе через SQLiteOpenHelper про прагму легко забыть.
Где спотыкаются
Хранят N:M списком в колонке: строка через запятую или JSON через TypeConverter. Искать по такому полю и держать целостность уже нельзя.
Не ставят индекс на колонку внешнего ключа. Room предупреждает об этом при сборке: без индекса изменение или удаление родителя сканирует всю дочернюю таблицу.
Грузят детей в цикле, отдельным запросом на каждого родителя. Это проблема N+1, и её решает как раз @Relation или один запрос с JOIN.