当列名相同时,如何表示与 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
的“多对多”关系?我的列名也一样。
例如我有Guest
和Reservation
。 Reservation
可以有多个Guest
,Guest
可以是多个预订的一部分。
这是我的实体定义:
@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
) 来保持 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
s 和reservationId
s,因此您可以使用这些查询Reservation
和Guest
实体。
如果我找到一种简单的方法来获取 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。
首先定义代表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>
如需了解更多详情,请访问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 的“多对多”关系?的主要内容,如果未能解决你的问题,请参考以下文章