How can I represent a "many to many" relation with Android Room when column names are same?

Solution 1:

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

Solution 2:

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

}

Solution 3:

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.

Solution 4:

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