How can I represent a many to many relation with Room? e.g. I have \"Guest\" and \"Reservation\". Reservation can have many Guest and a Guest can be part of many Reservation
I had a similar issue. Here is my solution.
You can use an extra entity (ReservationGuest
) which keeps the relation between Guest
and 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
)
You can get reservations with their list of guestId
s. (Not the guest objects)
data class ReservationWithGuests(
@Embedded val reservation:Reservation,
@Relation(
parentColumn = "id",
entityColumn = "reservationId",
entity = ReservationGuest::class,
projection = "guestId"
) val guestIdList: List<Long>
)
You can also get guests with their list of reservationId
s. (Not the reservation objects)
data class GuestWithReservations(
@Embedded val guest:Guest,
@Relation(
parentColumn = "id",
entityColumn = "guestId",
entity = ReservationGuest::class,
projection = "reservationId"
) val reservationIdList: List<Long>
)
Since you can get the guestId
s and reservationId
s, you can query Reservation
and Guest
entities with those.
I'll update my answer if I find an easy way to fetch Reservation and Guest object list instead of their ids.
Similar answer
Actually there is one more possibility to get Guest
list, not only id's like in @Devrim answer.
First define class which will represent the connection between Guest
and 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
)
Each time you will be inserting new Reservation
, you will have to insert ReservationGuestJoin
object in order to fulfill foreign key constraint.
And now if you want to get Guest
list you can use power of SQL query:
@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>
}
To see more details visit this blog.
Here is a way to query a full object model through an M:N junction table in a single query. The subqueries are probably not the most efficient way to do this, but it does work until they get @Relation
to properly walk through ForeignKey
. I hand-jammed the Guest/Reservation framework into my working code so there may be typos.
Entity (This has been covered)
@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 (Note we pull in the M:N via a subquery and reduce the extra Reservation
rows with a 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 (This handles the mapping of the query result, note we convert the concatenated string back to a list with @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(","));
}
}
Based on the answer above: https://stackoverflow.com/a/44428451/4992598 only by keeping separate field names between entities you can have models returned (not just ids). All you need to do is:
@Entity data class ReservationGuest(
@PrimaryKey(autoGenerate = true) val id: Long,
val reservationId: Long,
@Embedded
val guest: Guest
)
And yes entities can be embedded in one another as long as you don't keep duplicate fields. So in consequence the ReservationWithGuests class can look like this.
data class ReservationWithGuests(
@Embedded val reservation:Reservation,
@Relation(
parentColumn = "id",
entityColumn = "reservationId",
entity = ReservationGuest::class,
projection = "guestId"
) val guestList: List<Guest>
)
So at this point you can use val guestIdList: List because your ReservationGuest entity actually maps ids with entity models.
With the introduction to Junction in room you can handle many-to-many relationship with ease.
As @Devrim stated you can use an extra entity (ReservationGuest) which keeps the relation between Guest and Reservation(also know as associative table or junction table or join table).
@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
)
Now you can get reservation with guests using this model:
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>
)
You can also get guest with their list of reservations as.
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>
)
Now you can query database for the result as:
@Dao
interface GuestReservationDao {
@Query("SELECT * FROM Reservation")
fun getReservationWithGuests(): LiveData<List<ReservationWithGuests>>
@Query("SELECT * FROM Guest")
fun getGuestWithReservations(): LiveData<List<GuestWithReservations>>
}
For the join table entity, I suggest to use a composite ID indexed:
@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()
}
}
The QUERY_STRING
. We make a inner joins to produce a big table with data from both entities, them we concatenate the data from Reservation
as a column string and lastly we group_concat the rows by the guest ID, concatenating the reservation strings with different separators, our converter will take care of rebuild it as a entity:
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
Note that I changed your column table
name because I think Room do not allow you to use SQLite reserved names.
I didn't test the performance of all this compared with have more flat entity (another option without the concatenations). If I do, I'll update my answer.