【问题标题】:How can I represent a "many to many" relation with Android Room when column names are same?当列名相同时,如何表示与 Android Room 的“多对多”关系?
【发布时间】:2017-11-05 19:21:50
【问题描述】:

如何表示与Room 的“多对多”关系?我的列名也一样。

例如我有GuestReservationReservation 可以有多个GuestGuest 可以是多个预订的一部分。

这是我的实体定义:

@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>
)

上面我得到编译器错误:

> Cannot figure out how to read this field from a cursor.

我找不到@Relation 的工作样本。

【问题讨论】:

    标签: android kotlin android-room android-architecture-components


    【解决方案1】:

    我遇到了类似的问题。这是我的解决方案。

    您可以使用额外的实体 (ReservationGuest) 来保持 GuestReservation 之间的关系。

    @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
    )
    

    您可以使用他们的guestIds 列表进行预订。 (不是来宾对象)

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

    您还可以使用他们的reservationIds 列表获取客人。 (不是保留对象)

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

    由于您可以获得guestIds 和reservationIds,因此您可以使用这些查询ReservationGuest 实体。

    如果我找到一种简单的方法来获取 Reservation 和 Guest 对象列表而不是它们的 id,我会更新我的答案。

    Similar answer

    【讨论】:

    • 您在示例应用程序或类似的应用程序中有一个有效的实现吗?那太好了!
    • 你有没有找到一种方法来获取整个对象而不是 id?恐怕没有办法在具有 M:N 关系的单个查询中实现这一点。
    • 使用 Room 2.0.0,正在寻找投影或 Array,而不是 String...API 是否已更改?
    • @Devrim 你能帮我解决这个问题吗?看起来与此类似,但无法使其工作...stackoverflow.com/questions/56918019/…
    【解决方案2】:

    通过房间内Junction的介绍,您可以轻松处理多对多关系。

    正如@Devrim 所说,您可以使用一个额外的实体 (ReservationGuest) 来保持 Guest 和 Reservation 之间的关系(也称为关联表或联结表或连接表)。

    @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>>
    
    }
    

    【讨论】:

    • 从现在开始,这应该被认为是公认的答案,Junctions 解决了这个问题 + 你得到了整个对象,而不仅仅是 ID。它也是 Android 文档提供的解决方案。如果您使用的是 Java,则必须使用 @Junction 注释。 developer.android.com/training/data-storage/room/….
    • 这让我免于头疼。来自developer.android.com/training/data-storage/room/relationships,它在@Relation 中遗漏了一些让我发疯的东西!!!!
    • 这个答案比当前的官方文档更有用,因为文档在实体之间重复相同的字段名称(songId 和 playlistId),所以有点混乱。谢谢!
    • 使用@Transaction 或手动插入两个表,对吗?此外,当客人被删除时,它也应该被手动删除,对还是 Room 处理?
    • @akubi 是的,您可以根据需要修改查询。
    【解决方案3】:

    其实还有一种可能得到Guest列表,不仅仅是@Devrim答案中的id。

    首先定义代表GuestReservation之间连接的类。

    @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>
    }
    

    如需了解更多详情,请访问this blog

    【讨论】:

      【解决方案4】:

      这是一种在单个查询中通过 M:N 联结表查询完整对象模型的方法。子查询可能不是执行此操作的最有效方法,但它确实有效,直到它们让@Relation 正确遍历ForeignKey我将 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
      )
      

      Dao(注意我们通过子查询拉入 M:N 并使用 GROUP_CONCAT 减少额外的 Reservation

      @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(","));
          }
      }
      

      【讨论】:

      • SQL 语句第二行中的table 是正确的还是tables 的拼写错误?
      • table选择一列,tables是查询结果
      • TABLE 是 SQLite 中的关键字,即使更改了列名,lint 仍然会指出错误,这会使用户丢失Reservation 的 ID 属性。要保留来自Reservation 的任意数量的属性,应在其列之间进行另一个连接,并在转换器上进行另一个拆分。如果有人需要多个属性,我将在我的实现中发布答案。
      【解决方案5】:

      基于上面的答案:https://stackoverflow.com/a/44428451/4992598 仅通过在实体之间保留单独的字段名称 您可以返回模型(不仅仅是 id)。您需要做的就是:

      @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 实体实际上将 id 映射到实体模型。

      【讨论】:

      • ReservationGuest 是具有单向预留关系的Guest 表的附加副本。这不是真正的 M:N。
      • @Embedded 所做的是为您创建嵌入对象在您使用它的实体表上的列。如果您这样做,您将在两个表中为 Guest 提供相同类型的数据。
      【解决方案6】:

      对于连接表实体,我建议使用复合ID索引:

      @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
      )
      

      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 的数据连接为列字符串,最后我们通过访客 ID 对行进行分组连接,将预订字符串与不同的分隔符连接起来,我们的转换器将负责将其重建为一个实体:

      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 保留名称。

      与拥有更多扁平实体(另一个没有串联的选项)相比,我没有测试所有这些的性能。如果我这样做了,我会更新我的答案。

      【讨论】:

      • 实际上我已经回答了我的问题,是的,这似乎是要走的路(创建一个@Entity 来定义连接表)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-11-20
      • 2022-07-08
      • 2023-01-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多