Marc
Marc

Reputation: 99

Room @Relation annotation with a One To Many relationship

I have a POJO I'd like to get from the database which has a list of POJO's as a property. This, according to docs is doable via a @Relationship annotation. However, it's a one to many relationship where I don't directly reference the table/entity in question. How would I go about getting this back from the DB directly from the DAO? Is this even possible, or do I have to implement some intermediary binding logic manually? The POJO I'd like to get from DB:

data class Chore(
    var name: String,
    //This is the line that doesn't work
    @Relation(parentColumn = "id", entityColumn = "userId", entity = UserEntity::class)
    var contributingUsers: List<User>,
    @DrawableRes var drawableRes: Int,
    var done: Boolean
)

The User POJO I'd like to get automatically mapped:

data class User(
    val userName: String,
    @DrawableRes val userPhoto: Int
)

The One-To-Many reference table/entity:

@Entity(
    tableName = TableNames.CHORE_TO_USER,
    foreignKeys = [
        ForeignKey(
            entity = UserEntity::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("userId"),
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE,
        ),
        ForeignKey(
            entity = ChoreEntity::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("choreId"),
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE,
        )
    ],
    indices = [Index("choreId"), Index("userId")]
)
internal data class ChoreToUser(
    val userId: Int,
    val choreId: Int,
    val canFulfill: Boolean,
): BaseEntity()

The query:

    @Query("SELECT Chores.name, drawableRes," +
            "Users.name as userName, Users.imageRes as userPhoto, " +
            "(COUNT(ChoreLogs.id) = 1) as done " +
            "FROM Chores " +
            "LEFT JOIN ChoreToUsers ON ChoreToUsers.choreId = Chores.id " +
            "LEFT JOIN Users ON ChoreToUsers.userId = Users.id " +
            "LEFT JOIN ChoreLogs ON ChoreLogs.choreToUserId = ChoreToUsers.id")
    fun getChoreTiles(): List<Chore>

TLDR: I wanna embed a list of users into the Chore POJO. It's refferenced via an intermediary table. How would I go about doing this?

Upvotes: 1

Views: 551

Answers (1)

MikeT
MikeT

Reputation: 56943

To use @Relation the parent table must be available so that the parent column can be found.

So you would need something along the lines of :-

data class Chore(
    @Embedded
    var choreEntity: ChoreEntity, //<<<<<
    var name: String,
    //This is the line that doesn't work
    @Relation(parentColumn = "id", entityColumn = "userId", entity = UserEntity::class)
    var contributingUsers: List<User>,
    @DrawableRes var drawableRes: Int,
    var done: Boolean
)

In short @Relation results in a sub query being invoked that retrieves ALL of the children of the parent (which MUST exist/be known) thus an @Embed of that parent is required. Note that these are for use by the convenience methods, which are a little restrictive in nature.

However, as you have an intermediate table (mapping/associative/reference .... table) then you need to tell Room about this by using the associateBy parameter to define the Junction

  • there is no need for such a table for one-to-many relationships, such tables can be used but are really for many-many relationships.

@Relation will build the underlying query to access the children accordingly.

If you want the result of your query then a Chore object could just be:-

data class Chore(
    var name: String,
    var contributingUsers,
    @DrawableRes var drawableRes: Int,
    var done: Boolean
)

BUT a row would exist for every combination that is for every User that is related to a Chore there would be a row i.e. the result is the cartesian product.

If you wanted to build a Chore with a list of it's child Users then you would have to

  • a) either process the entire result building the resultant List or
  • b) just extract the respective Chore and then run a query per extracted Chore that returns the List.

Working Examples

Based upon UserEntity being :-

@Entity( tableName = TableNames.CHORE)
data class ChoreEntity(
    @PrimaryKey
    val id: Long?=null,
    val name: String,
    val drawableRes: Int
)

and UserEntity being :-

@Entity(tableName = TableNames.USER)
data class UserEntity(
    @PrimaryKey
    val id: Long? = null,
    val name: String,
    val imageRes: Int
    /* etc */
)

and ChoreToUser being:-

@Entity(
    tableName = TableNames.CHORE_TO_USER,
    foreignKeys = [
        ForeignKey(
            entity = UserEntity::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("userId"),
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE,
        ),
        ForeignKey(
            entity = ChoreEntity::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("choreId"),
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE,
        )
    ],
    // indices = [Index("choreId"), Index("userId")], // Replaced by required primary key
    primaryKeys = ["choreId","userId"]
)
data class ChoreToUser(
    var userId: Long,
    var choreId: Long,
    @ColumnInfo(index = true)
    var canFulfill: Boolean,
)
  • Note that the index has been replaced with @Primary key (Room requires a Primary Key). They are effectively the same. Additionally @ColumnInfo has been used to also create an index on the userId column (more efficient).

Example 1 - Cartesian Product from you Query

So using the sort of equivalent of your Chore class there is Chore1 :-

data class Chore1(
    var name: String,
    var userName: String, // ADDED for demo
    //This is the line that doesn't work
    //@Relation(parentColumn = "id", entityColumn = "userId", entity = UserEntity::class)
    //var contributingUsers: List<User>,
    @DrawableRes var drawableRes: Int,
    var done: Boolean
)
  • @Relation cannot be used without an @Embedded so commented out.

The dao function used with the above was:-

@Query("SELECT Chores.name, drawableRes," +
        "Users.name as userName, Users.imageRes as userPhoto, " +
        " 10 /*(COUNT(ChoreLogs.id) = 1)*/ as done " + // not using Chorelogs table so fake output
        "FROM Chores " +
        "LEFT JOIN ChoreToUsers ON ChoreToUsers.choreId = Chores.id " +
        "LEFT JOIN Users ON ChoreToUsers.userId = Users.id " +
        "/* LEFT JOIN ChoreLogs ON ChoreLogs.choreToUserId = ChoreToUsers.id */") // SQL commented out for Brevity
fun getChoreTiles(): List<Chore1> /* Cartesian Product */
  • Note for convenience/brevity the ChoreLogs JOIN has been excluded

  • See results for EX01

Example 2 - Using the option a)

Here Chore2 has been used as the resultant class, it being:-

data class Chore2(
    var name: String,
    //This is the line that doesn't work
    //@Relation(parentColumn = "id", entityColumn = "userId", entity = UserEntity::class)
    var contributingUsers: List<User>,
    @DrawableRes var drawableRes: Int,
    var done: Boolean
)
  • As can be seen the contributingUsers is a List

This used in conjunction with 2 queries and a function that uses the queries, these being:-

@Query("SELECT * FROM Chores")
fun getAllChores(): List<ChoreEntity>
@Query("SELECT * FROM  ChoreToUsers JOIN Users ON ChoreToUsers.userId = Users.id WHERE ChoreToUsers.choreId=:choreId")
fun getRelatedUsersForAChore(choreId: Long): List<UserEntity>
@Transaction
@Query("")
fun getRelatedUsersPerChoreAsList(): List<Chore2> {
    var rv = arrayListOf<Chore2>()
    for (ct in getAllChores()) {
        var ul = arrayListOf<User>()
        for (ue in getRelatedUsersForAChore(ct.id!!)) {
            ul.add(User(ue.name,ue.imageRes))
        }
        rv.add(Chore2(ct.name,ul.toList(),ct.drawableRes,false))
    }
    return rv
}
  • again no need for an @Relation as the queries do all that is required.

  • See results for EX02

Example 3 - using option b) BUT via Room

i.e. using @Embedded, with @Relation AND as there is the intermediate associative table associateBy and the Junction.

  • i.e. letting Room build the sub query(ies)

In this case the equivalent class is Chore3 :-

data class Chore3(
    @Embedded
    val chore: ChoreEntity,
    @Relation(
        entity = UserEntity::class,
        parentColumn = "id",
        entityColumn = "id",
        associateBy = Junction(
            value = ChoreToUser::class, parentColumn = "choreId", entityColumn = "userId"
        )
    )
    val contributingUsers: List<UserEntity>
)

The Dao function being:-

@Transaction
@Query("SELECT * FROM Chores")
fun getAllChore3s(): List<Chore3>
  • See results for EX03

Testing/Demonstrating the 3 Examples

The following code was included in an activity (run on the main thread for convenience/brevity):-

const val TAG = "DBINFO"
class MainActivity : AppCompatActivity() {
    lateinit var db: TheDatabase
    lateinit var dao: AllDao
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = TheDatabase.getInstance(this)
        dao = db.getAllDao()

        val u1Id = dao.insert(UserEntity(name = "User1", imageRes = 1000))
        val u2Id = dao.insert(UserEntity(name = "User2", imageRes = 2000))
        val u3Id = dao.insert(UserEntity(name = "user3", imageRes = 3000))
        val u4Id = dao.insert(UserEntity(name = "user4", imageRes = 4000))

        val c1Id = dao.insert(ChoreEntity(name = "Chore1", drawableRes = 10000))
        val c2Id = dao.insert(ChoreEntity(name = "Chore2",drawableRes = 20000))
        val c3Id = dao.insert(ChoreEntity(name = "Chore3",drawableRes = 30000))
        val c4Id = dao.insert(ChoreEntity(name = "Chore4",drawableRes = 40000))
        val c5Id = dao.insert(ChoreEntity(name = "Chore5",drawableRes = 50000))
        val c6Id = dao.insert(ChoreEntity(name = "Chore6",drawableRes = 60000))

        /* Mapping */

        dao.insert(ChoreToUser(u1Id,c1Id,false))
        dao.insert(ChoreToUser(u1Id,c2Id,true))
        dao.insert(ChoreToUser(u1Id,c3Id,false))
        dao.insert(ChoreToUser(u1Id,c4Id,false))

        dao.insert(ChoreToUser(u2Id,c5Id,true))
        dao.insert(ChoreToUser(u2Id,c6Id,true))

        dao.insert(ChoreToUser(u3Id,c1Id,false))
        dao.insert(ChoreToUser(u3Id,c2Id,false))
        dao.insert(ChoreToUser(u3Id,c3Id,false))
        dao.insert(ChoreToUser(u3Id,c4Id,false))
        dao.insert(ChoreToUser(u3Id,c5Id,false))
        dao.insert(ChoreToUser(u3Id,c6Id,false))

        /* EX01 - Cartesain result */
        for (ct in dao.getChoreTiles()) {
            Log.d(TAG+"_EX01","Chore is ${ct.name} + User is ${ct.userName}")
        }

        /* EX02 - using SQl with JOINS */
        for (ct in dao.getRelatedUsersPerChoreAsList()) {
            Log.d(TAG+"EX02","Chore is ${ct.name}, image is ${ct.drawableRes}, there are ${ct.contributingUsers.size}  contributing Users:-" )
            for (u in ct.contributingUsers) {
                Log.d(TAG+"EX02","\tUser is ${u.userName}, photo is ${u.userPhoto}")
            }
        }

        /* EX03 = using @Embedded/@Relation and associateBy/Junction */
        for (c3 in dao.getAllChore3s()) {
            Log.d(TAG+"EX03","Chore is ${c3.chore.name}, image is ${c3.chore.drawableRes}, there are ${c3.contributingUsers.size} contributing users:-")
            for (u in c3.contributingUsers) {
                Log.d(TAG+"EX03","\tUser is ${u.name}, photo is ${u.imageRes}")
            }
        }
    }
}
  • The majority of the code is just loading the data which ends up being:-

enter image description here

enter image description here

and

enter image description here

  • Note that above data takes advantage of the many-many allowable by an associative table.

Results (aka output included in the log, split per example)

DBINFO_EX01: Chore is Chore1 + User is User1
DBINFO_EX01: Chore is Chore1 + User is user3
DBINFO_EX01: Chore is Chore2 + User is User1
DBINFO_EX01: Chore is Chore2 + User is user3
DBINFO_EX01: Chore is Chore3 + User is User1
DBINFO_EX01: Chore is Chore3 + User is user3
DBINFO_EX01: Chore is Chore4 + User is User1
DBINFO_EX01: Chore is Chore4 + User is user3
DBINFO_EX01: Chore is Chore5 + User is User2
DBINFO_EX01: Chore is Chore5 + User is user3
DBINFO_EX01: Chore is Chore6 + User is User2
DBINFO_EX01: Chore is Chore6 + User is user3


DBINFOEX02: Chore is Chore1, image is 10000, there are 2  contributing Users:-
DBINFOEX02:     User is User1, photo is 1000
DBINFOEX02:     User is user3, photo is 3000
DBINFOEX02: Chore is Chore2, image is 20000, there are 2  contributing Users:-
DBINFOEX02:     User is User1, photo is 1000
DBINFOEX02:     User is user3, photo is 3000
DBINFOEX02: Chore is Chore3, image is 30000, there are 2  contributing Users:-
DBINFOEX02:     User is User1, photo is 1000
DBINFOEX02:     User is user3, photo is 3000
DBINFOEX02: Chore is Chore4, image is 40000, there are 2  contributing Users:-
DBINFOEX02:     User is User1, photo is 1000
DBINFOEX02:     User is user3, photo is 3000
DBINFOEX02: Chore is Chore5, image is 50000, there are 2  contributing Users:-
DBINFOEX02:     User is User2, photo is 2000
DBINFOEX02:     User is user3, photo is 3000
DBINFOEX02: Chore is Chore6, image is 60000, there are 2  contributing Users:-
DBINFOEX02:     User is User2, photo is 2000
DBINFOEX02:     User is user3, photo is 3000


DBINFOEX03: Chore is Chore1, image is 10000, there are 2 contributing users:-
DBINFOEX03:     User is User1, photo is 1000
DBINFOEX03:     User is user3, photo is 3000
DBINFOEX03: Chore is Chore2, image is 20000, there are 2 contributing users:-
DBINFOEX03:     User is User1, photo is 1000
DBINFOEX03:     User is user3, photo is 3000
DBINFOEX03: Chore is Chore3, image is 30000, there are 2 contributing users:-
DBINFOEX03:     User is User1, photo is 1000
DBINFOEX03:     User is user3, photo is 3000
DBINFOEX03: Chore is Chore4, image is 40000, there are 2 contributing users:-
DBINFOEX03:     User is User1, photo is 1000
DBINFOEX03:     User is user3, photo is 3000
DBINFOEX03: Chore is Chore5, image is 50000, there are 2 contributing users:-
DBINFOEX03:     User is User2, photo is 2000
DBINFOEX03:     User is user3, photo is 3000
DBINFOEX03: Chore is Chore6, image is 60000, there are 2 contributing users:-
DBINFOEX03:     User is User2, photo is 2000
DBINFOEX03:     User is user3, photo is 3000
  • as can be seen EX02 and EX03 produce the same output.

Upvotes: 3

Related Questions