当列名相同时,如何表示与 Android Room 的“多对多”关系?

Posted

技术标签:

【中文标题】当列名相同时,如何表示与 Android Room 的“多对多”关系?【英文标题】:How can I represent a "many to many" relation with Android Room when column names are same? 【发布时间】: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 的工作样本。

【问题讨论】:

【参考方案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 你能帮我解决这个问题吗?看起来与此类似,但无法使其工作...***.com/questions/56918019/… 多对多试试这个,developer.android.com/training/data-storage/room/…【参考方案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://***.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 来定义连接表)。

以上是关于当列名相同时,如何表示与 Android Room 的“多对多”关系?的主要内容,如果未能解决你的问题,请参考以下文章

排序与分组函数(未补充)

Android Room LiveData观察器未更新

更新android Room中实体的某些特定字段

重命名列名时的 SQLITE 语法错误代码 1

Android Room 插入所有问题

NonUniqueDiscoveredSqlAliasException 当两个表具有相同的列名时