66

How can I represent a "many to many" relation with Room? My column names are also the same.

e.g. I have Guest and Reservation. Reservation can have many Guest's and a Guest can be part of many Reservations.

Here are my entity definitions:

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

While looking into docs I came across @Relation. I found it really confusing though.

According to this I would want to create a POJO and add the relationships there. So, with my example I did the following:

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

With above I get the compiler error:

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

I wasn't able to find a working sample of @Relation.

Olivia Stork
  • 4,660
  • 5
  • 27
  • 40
bond
  • 11,236
  • 7
  • 48
  • 62

6 Answers6

92

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 guestIds. (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 reservationIds. (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 guestIds and reservationIds, 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

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
Devrim
  • 15,345
  • 4
  • 66
  • 74
  • 4
    Do you have a working implementation in a sample app or something like that? That'd be great! – yennsarah Aug 08 '17 at 06:59
  • 20
    Have you, by any chance, found a way to fetch the whole object instead of the ids? I'm afraid there is no way to achieve that in a single query with a M:N relationship. – fast3r Mar 14 '18 at 14:54
  • 1
    Using Room 2.0.0, projection is looking or an `Array`, not a `String`...has the API changed? – Bryan Bryce Jan 15 '19 at 17:17
  • @Devrim can you please give me a hand on this? Looks similar to this but can not make it work... https://stackoverflow.com/questions/56918019/how-to-store-what-user-have-searched-and-its-result – StuartDTO Jul 07 '19 at 08:48
  • 1
    Try this for many to many, https://developer.android.com/training/data-storage/room/relationships#many-to-many – Deepak Joshi Aug 05 '19 at 07:44
  • [Here](https://developer.android.com/training/data-storage/room/referencing-data#understand-no-object-references) is why Room doesn't provide Relation to Object mapping – Bugs Happen Aug 20 '19 at 11:01
49

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

}
Nischal
  • 870
  • 1
  • 10
  • 16
  • 7
    This should be considered the accepted answer from now on, Junctions solves the issue + you get the entire object back and not only the ID. Its also the solution provided by the Android docs. If you're using Java you have to use @Junction annotation. https://developer.android.com/training/data-storage/room/relationships#many-to-many . – christophercarlsson Dec 30 '19 at 13:25
  • 1
    This save me the headache. From https://developer.android.com/training/data-storage/room/relationships, it is missing something in `@Relation` which drive me crazy!!!! – Yeung Jul 16 '20 at 11:16
  • 3
    This answer is more useful than the current official docs because the docs repeat the same field names (songId and playlistId) across entities so it's a little confusing. Thanks! – Daniel Wilson Jul 31 '20 at 09:11
  • 1
    Insert to both tables with a `@Transaction` or manually, right? Also when a Guest deleted should it also be deleted manually, right or does Room handle that? – Thracian Sep 11 '20 at 20:35
  • Can I use IDs in query using such a setup? For example, I want to select reservations made by a specific guest ID? – Alvin Dizon Sep 29 '20 at 09:50
  • @Thracian You have to maintain relationship between guest table, reservation table and the join table. – Nischal Sep 29 '20 at 10:11
  • 1
    @akubi Yes you can modify the query as per your need. – Nischal Sep 29 '20 at 10:12
  • How to filter this query's before transaction? – AlexS Oct 18 '21 at 12:14
  • Do we add the junction table - ReservationGuest to the Database class' list of entities??? – ikmazameti Nov 15 '22 at 13:05
  • Thanks, I think that I achieve make work this. Anyway It's ovbius that this relation definition tools are enough clear yet – Genaut May 29 '23 at 16:52
20

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.

Nominalista
  • 4,632
  • 11
  • 43
  • 102
9

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(","));
    }
}
Anthony
  • 7,638
  • 3
  • 38
  • 71
  • Is the `table` in the second line of the SQL statement right or is a typo for `tables`? – Allan Veloso Aug 23 '18 at 19:11
  • 1
    `table` selects a column, `tables` is the query result – Anthony Aug 24 '18 at 01:48
  • TABLE is a keyword in SQLite, even with the column name changed the lint still points errors and I this would make the user lose the ID attribute of `Reservation`. To preserve an abitrary number of attributes from `Reservation` another concatenation should be done between its columns and another split on the converter. I will post an answer with my implementation if anyone needs more than one attribute. – Allan Veloso Aug 24 '18 at 14:30
-1

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.

andu
  • 146
  • 9
  • 1
    `ReservationGuest` is an additional copy of the `Guest` table with the one-way reservation relation. This isn't really M:N. – Anthony Dec 30 '17 at 19:05
  • What @Embedded do is create for you the columns that the embedded object has on the table of the entity that you used it. If you do like this you going to have the same kind of data for Guest in two tables. – Allan Veloso Aug 23 '18 at 11:02
-2

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.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
Allan Veloso
  • 5,823
  • 1
  • 38
  • 36