Как я могу представить отношение многих к многим с Android Room?

Как я могу представить отношение многих к многим с комнатой? например У меня есть "Гость" и "Бронирование". В резервировании может быть много гостей, а гость может быть частью многих резерваций.

Вот мои определения сущностей:

@Entity data class Reservation(
    @PrimaryKey val id: Long,
    val table: String,
    val guests: List<Guest>
)

@Entity data class Guest(
    @PrimaryKey val id: Long,
    val name: String,
    val email: String
)

При просмотре документов я наткнулся на @Relation. Я нашел это действительно запутанным, хотя.

В соответствии с этим я хотел бы создать POJO и добавить туда отношения. Итак, в моем примере я сделал следующее

data class ReservationForGuest(
    @Embedded val reservation: Reservation,
    @Relation(
        parentColumn = "reservation.id", 
        entityColumn = "id", 
        entity = Guest::class
    ) val guestList: List<Guest>
)

Свыше я получаю ошибку компилятора:

Невозможно понять, как читать это поле из курсора.

Мне не удалось найти рабочий образец @Relation.

Ответы

Ответ 1

У меня была аналогичная проблема. Вот мое решение.

Вы можете использовать дополнительную сущность (ReservationGuest), которая сохраняет связь между Guest и Reservation.

@Entity data class Guest(
    @PrimaryKey val id: Long,
    val name: String,
    val email: String
)

@Entity data class Reservation(
    @PrimaryKey val id: Long,
    val table: String
)

@Entity data class ReservationGuest(
    @PrimaryKey(autoGenerate = true) val id: Long,
    val reservationId: Long,
    val guestId: Long
)

Вы можете получить резервирование со своим списком guestId s. (Не гостевые объекты)

data class ReservationWithGuests(
    @Embedded val reservation:Reservation,
    @Relation(
        parentColumn = "id",
        entityColumn = "reservationId",
        entity = ReservationGuest::class,
        projection = "guestId"
    ) val guestIdList: List<Long>
)

Вы также можете получить гостей со списком reservationId s. (Не объекты резервирования)

data class GuestWithReservations(
    @Embedded val guest:Guest,
    @Relation(
        parentColumn = "id",
        entityColumn = "guestId",
        entity = ReservationGuest::class,
        projection = "reservationId"
   ) val reservationIdList: List<Long>
)

Поскольку вы можете получить guestId и reservationId s, вы можете запросить Reservation и Guest сущности с ними.

Я обновлю свой ответ, если найду простой способ получить список объектов Reservation и Guest вместо своих идентификаторов.

Подобный ответ

Ответ 2

На самом деле есть еще одна возможность получить список Guest, а не только идентификатор, как в @Devrim ответе.

Сначала определите класс, который будет представлять связь между Guest и Reservation.

@Entity(primaryKeys = ["reservationId", "guestId"],
        foreignKeys = [
            ForeignKey(entity = Reservation::class,
                    parentColumns = ["id"],
                    childColumns = ["reservationId"]),
            ForeignKey(entity = Guest::class,
                    parentColumns = ["id"],
                    childColumns = ["guestId"])
        ])
data class ReservationGuestJoin(
    val reservationId: Long,
    val guestId: Long
)

Каждый раз, когда вы будете вставлять новый Reservation, вам нужно будет вставить объект ReservationGuestJoin, чтобы выполнить ограничение внешнего ключа. И теперь, если вы хотите получить список Guest, вы можете использовать мощность SQL-запроса:

@Dao
interface ReservationGuestJoinDao {

    @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
    @Query("""
        SELECT * FROM guest INNER JOIN reservationGuestJoin ON
        guest.id = reservationGuestJoin.guestId WHERE
        reservationGuestJoin.reservationId = :reservationId
        """)
    fun getGuestsWithReservationId(reservationId: Long): List<Guest>
}

Чтобы узнать больше, посетите этот блог.

Ответ 3

Вот способ запроса полной объектной модели через таблицу соединений M: N в одном запросе. Подзапросы, вероятно, не самый эффективный способ сделать это, но он работает, пока они не получат @Relation, чтобы правильно пройти через ForeignKey. Я вручную закрепил рамки Guest/Reservation в своем рабочем коде, чтобы могли быть опечатки.

Entity (это было рассмотрено)

@Entity data class Guest(
    @PrimaryKey val id: Long,
    val name: String,
    val email: String
)

@Entity data class Reservation(
    @PrimaryKey val id: Long,
    val table: String
)

@Entity data class ReservationGuest(
    @PrimaryKey(autoGenerate = true) val id: Long,
    val reservationId: Long,
    val guestId: Long
)

Дао (обратите внимание, что мы втягиваем M: N через подзапрос и уменьшаем лишние строки Reservation с GROUP_CONCAT

@Query("SELECT *, " +
            "(SELECT GROUP_CONCAT(table) " +
                "FROM ReservationGuest " +
                "JOIN Reservation " +
                "ON Reservation.id = ReservationGuest.reservationId " +
                "WHERE ReservationGuest.guestId = Guest.id) AS tables, " +
        "FROM guest")
abstract LiveData<List<GuestResult>> getGuests();

GuestResult (Это обрабатывает отображение результата запроса, обратите внимание, что мы преобразуем конкатенированную строку обратно в список с помощью @TypeConverter)

@TypeConverters({ReservationResult.class})
public class GuestResult extends Guest {
    public List<String> tables;

    @TypeConverter
    public List<String> fromGroupConcat(String reservations) {
        return Arrays.asList(reservations.split(","));
    }
}

Ответ 4

Для объекта объединяющей таблицы я предлагаю использовать составной индексированный индекс:

@Entity(
    primaryKeys = ["reservationId", "guestId"],
    indices = [Index(value =["reservationId", "guestId"], unique = true)]
)
data class ReservationGuestJoin(
    @PrimaryKey(autoGenerate = true) var id: Long,
    var reservationId: Long = 0,
    var guestId: Long = 0
)

The GuestDao.kt:

@Dao
@TypeConverters(GuestDao.Converters::class)
interface GuestDao {

    @Query(QUERY_STRING)
    fun listWithReservations(): LiveData<List<GuestWithReservations>>

    data class GuestWithReservations(
        var id: Long? = null,
        var name: String? = null,
        var email: String? = null,
        var reservations: List<Reservation> = emptyList()
    )

    class Converters{
        @TypeConverter
        fun listReservationFromConcatString(value: String?): List<Reservation>? = value?.let { value ->
                .split("^^")
                .map { it.split("^_") }
                .map { Reservation(id = it.getOrNull(0)?.toLongOrNull(), name = it.getOrNull(1)) }
        } ?: emptyList()
    }
}

QUERY_STRING. Мы создаем внутренние объединения, чтобы создать большую таблицу с данными обеих сущностей, затем мы объединяем данные из Reservation в виде строки столбца и, наконец, группируем строки по идентификатору гостя, объединяя строки резервирования с различными разделителями, наш конвертер позаботится о том, чтобы перестроить его как единое целое:

SELECT 
    t.id, t.name, t.email, GROUP_CONCAT(t.reservation, '^^') as reservations 
FROM (
    SELECT 
        guestId as id, name, email, (reservationId || '^_' || reservationTable) as reservation 
    FROM  
        GuestReservationJoin
        INNER JOIN Guest ON Guest.id = GuestReservationJoin.guestId 
        INNER JOIN Reservation ON Reservation.id = GuestReservationJoin.reservationId
    ) as t 
GROUP BY t.id

Обратите внимание, что я изменил имя вашего столбца table, потому что я думаю, что Room не позволяет вам использовать зарезервированные имена SQLite.

Я не тестировал производительность всего этого по сравнению с более плоской сущностью (другой вариант без конкатенаций). Если я это сделаю, я обновлю свой ответ.

Ответ 5

С введением Junction в комнату вы можете легко справиться с отношениями "многие ко многим".

Как было сказано @Devrim, вы можете использовать дополнительную сущность (ReservationGuest), которая сохраняет связь между Гостем и Резервированием (также известную как ассоциативная таблица или соединительная таблица или соединительная таблица).

@Entity
data class Guest(
  @PrimaryKey
  val gId: Long,
  val name: String,
  val email: String
)

@Entity
data class Reservation(
  @PrimaryKey
  val rId: Long,
  val table: String
)

@Entity(
  primaryKeys = ["reservationId", "guestId"]
)
data class ReservationGuest(     
  val reservationId: Long,
  val guestId: Long
)

Теперь вы можете забронировать номер с помощью этой модели:

data class ReservationWithGuests (
    @Embedded
    val reservation: Reservation,
    @Relation(
            parentColumn = "rId",
            entity = Guest::class,
            entityColumn = "gId",
            associateBy = Junction(
                    value = ReservationGuest::class,
                    parentColumn = "reservationId",
                    entityColumn = "guestId"
            )
    )
    val guests: List<Guest>
)

Вы также можете получить гостя с их списком бронирования как.

data class GuestWithReservations (
  @Embedded
  val guest: Guest,
  @Relation(
        parentColumn = "gId",
        entity = Reservation::class,
        entityColumn = "rId",
        associateBy = Junction(
                value = ReservationGuest::class,
                parentColumn = "guestId",
                entityColumn = "reservationId"
        )
  )
  val reservations: List<Reservation>
)

Теперь вы можете запросить у базы данных результат:

@Dao
interface GuestReservationDao {
  @Query("SELECT * FROM Reservation")
  fun getReservationWithGuests(): LiveData<List<ReservationWithGuests>>

  @Query("SELECT * FROM Guest")
  fun getGuestWithReservations(): LiveData<List<GuestWithReservations>>

}

Ответ 6

Исходя из вышеприведенного ответа: fooobar.com/info/296288/..., только сохраняя отдельные имена полей между объектами вы можете вернуть модели (а не только идентификаторы). Все, что вам нужно сделать, это:

@Entity data class ReservationGuest(
    @PrimaryKey(autoGenerate = true) val id: Long,
    val reservationId: Long,
    @Embedded
    val guest: Guest
)

И да объекты могут быть встроены друг в друга, если вы не сохраняете повторяющиеся поля. Таким образом, класс ReservationWithGuests может выглядеть следующим образом.

data class ReservationWithGuests(
    @Embedded val reservation:Reservation,
    @Relation(
        parentColumn = "id",
        entityColumn = "reservationId",
        entity = ReservationGuest::class,
        projection = "guestId"
    ) val guestList: List<Guest>
)

Итак, на этом этапе вы можете использовать val guestIdList: List, потому что ваш объект ReservationGuest фактически отображает идентификаторы с моделями сущностей.