1

The original question is at the very bottom. I have created a minimal (non-)working example of my problem which is hopefully easier to read through. The example is here at gitlab. There is a readme describing the problem. I am pasting some parts of the project here.

The data model is plain simple:

Owner <--(1:N)-- Child --(N:1)--> ReferencedByChild

All I want to do is to read an Owner from the database with all its associated Child objects and for each Child object also the ReferencedByChild object that it references.

The whole code that reproduces my problem is below. What I am not 100% sure about is the @Relation on the OwnerWithEverything POJO. See below please.

@Database(
    entities = [
        Owner::class,
        Child::class,
        ReferencedByChild::class
    ],
    version = 1
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun appDao(): AppDao
}

@Dao
abstract class AppDao {
    @Insert
    abstract fun insertOwner(owner: Owner): Long

    @Insert
    abstract fun insertChild(child: Child): Long

    @Insert
    abstract fun insertReferencedByChild(referencedByChild: ReferencedByChild): Long

    @Query("SELECT * FROM Child INNER JOIN ReferencedByChild ON Child.referencedByChildId = ReferencedByChild.refByChildId ORDER BY Child.childText")
    abstract fun findAllChildrenWithReferencedClasses(): List<ChildWithReferenced>

    // Commenting this query out makes the build pass, so something here is incorrect.
    @Query("SELECT * FROM Owner")
    abstract fun findOwnersWithEverything(): List<OwnerWithEverything>
}

// ENTITIES
@Entity
data class Owner(
    @PrimaryKey(autoGenerate = true)
    val ownerId: Long,
    val ownerText: String
)

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = Owner::class,
            parentColumns = arrayOf("ownerId"),
            childColumns = arrayOf("referencedOwnerId"),
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = ReferencedByChild::class,
            parentColumns = arrayOf("refByChildId"),
            childColumns = arrayOf("referencedByChildId"),
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class Child(
    @PrimaryKey(autoGenerate = true)
    val childId: Long,
    val childText: String,
    val referencedOwnerId: Long,
    val referencedByChildId: Long
)

@Entity
data class ReferencedByChild(
    @PrimaryKey(autoGenerate = true)
    val refByChildId: Long,
    val refText: String
)

// POJOS

// The Child has exactly one ReferencedByChild reference. This POJO joins those two
class ChildWithReferenced(
    @Embedded
    var child: Child,

    @Embedded
    var referencedByChild: ReferencedByChild
)

class OwnerWithEverything {
    @Embedded
    var owner: Owner? = null

    @Relation(
        parentColumn = "ownerId",
        entityColumn = "referencedOwnerId",
        entity = Child::class  // which entity should be defined here?
    )
    var childrenWithReferenced: List<ChildWithReferenced>? = null
}

Building this code results in this error message:

error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: refByChildId)

I think that the Owner query is badly constructed, but I am not entirely sure. If that is the problem, what is the correct way to construct the query?




This is the original question I have a nested POJO structure that should represent a single Game having multiple Rounds and each Round has a single Topic associated with it:

class GameWithRounds {
    @Embedded
    var game: Game? = null

    @Relation(
        parentColumn = "id",
        entityColumn = "gameId",
        entity = RoundRoom::class
    )
    var rounds: List<RoundWithTopic>? = null
}

class RoundWithTopic(
    @Embedded
    var round: RoundRoom,

    @Embedded(prefix = "topic_")
    var topic: Topic
)

The embedded annotation on Topic specifies a prefix because there are clashing id properties.

The Room Query that can fetch those classes is:

@Query("SELECT Topic.id as topic_id, Topic.name as topic_name, (...), RoundRoom.* FROM RoundRoom INNER JOIN Topic ON RoundRoom.topicId = Topic.id")
    abstract fun findRoundsWithTopics(): List<RoundWithTopic>

However, building the project gives me Room errors:

There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such column: topic_id)

Even though when I induce a warning about which fields are actually present, this is what Room tells me:

Columns returned by the query: topic_id, topic_name, topic_description, topic_language, topic_keywords, topic_sourceUrls, topic_furtherUrls, topic_questions, order, gameId, topicId, status, id. Fields in cz.melkamar.sklapecka.model.RoundWithTopic: order, gameId, topicId, status, id, topic_id, topic_name, topic_description, topic_language, topic_keywords, topic_sourceUrls, topic_furtherUrls, topic_questions, topic_image.

The topic_id column is there in the query result! Why am I getting this error?


For completeness, this is the entities:

@Entity
data class Game(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,

    @Embedded
    val gameConfigurationEmbed: GameConfigurationEmbed
)

data class GameConfigurationEmbed(
    var secondsPerTurn: Int,
    var maxSecondsPerTurn: Int,
    var bonusSecondsPerAnswer: Int
)

@Entity(
    foreignKeys = [
        ForeignKey(
            entity = Game::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("gameId"),
            onDelete = ForeignKey.CASCADE
        ),
        ForeignKey(
            entity = Topic::class,
            parentColumns = arrayOf("id"),
            childColumns = arrayOf("topicId"),
            onDelete = ForeignKey.CASCADE
        )
    ]
)
@TypeConverters(RoomConverters::class)
data class RoundRoom(
    val order: Int,
    var gameId: Long,
    val topicId: String,
    var status: RoundStatus = RoundStatus.CREATED,

    @PrimaryKey(autoGenerate = true)
    val id: Long = 0
) {
    enum class RoundStatus {
        CREATED, UPCOMING, IN_PROGRESS, FINISHED
    }
}

@Entity
data class Topic(
    @PrimaryKey val id: String,
    val name: String,
    val description: String,
    val language: String,
    val keywords: List<String>,
    val sourceUrls: List<String>,
    val furtherUrls: List<String>,
    val questions: List<String>,
    val image: ByteArray?
)
Martin Melka
  • 7,177
  • 16
  • 79
  • 138
  • please post your `GameConfigurationEmbed`, think you copied stuff wrong there – a_local_nobody Aug 01 '19 at 06:33
  • sure thing, done – Martin Melka Aug 01 '19 at 12:09
  • could you maybe post your entire SELECT statement too ? you might have a space somewhere in a table name alias – a_local_nobody Aug 01 '19 at 17:09
  • I refactored the question, created a new project that reproduces the issue. Now it contains everything. – Martin Melka Aug 02 '19 at 17:34
  • No worries, it's usually hard to see into an example that does not include everything :) – Martin Melka Aug 02 '19 at 17:36
  • Hmm, if I am reading it right, it seems that I will have to hand-craft the SQL query, which is what I thought Room would handle for me automatically, since it does have all the information about how the entities are connected. That's disappointing. – Martin Melka Aug 02 '19 at 17:51
  • The other option is to create a "fake" join entity and insert that every time the "real" entities are inserted. That screams problems to me, as relational databases prefer normalized data structures, not denormalized for the sake of lookups :( – Martin Melka Aug 02 '19 at 17:53
  • I think that's what I will end up doing, though that only gives me the IDs so I still cannot have a single `Owner` object in my ViewModel and just traverse its fields - I need to execute multiple queries which is what I was hoping would not be necessary. But at least I know that what I wanted to achieve is not actually possible, that's also a good answer :) Thanks! – Martin Melka Aug 02 '19 at 17:55
  • Also, it seems you removed your answer from this question - if you could summarize what we are talking about here and put it into an answer, I'll give you the bounty – Martin Melka Aug 02 '19 at 17:56

2 Answers2

0

After some research, specifically looking at this link : How can I represent a many to many relation with Android Room? the only answers we found would be to

  1. either create a hand-made sql query to handle this type of situation where you have a many-to-many relationship

    OR

  2. to alternatively have an additional joining entity which gets updated as the rest of the objects are updated. With this approach, you can get the ID's and then create additional queries as needed

a_local_nobody
  • 7,947
  • 5
  • 29
  • 51
0

It seems embedded fields and type converter is not properly used on observing the question. I don't want to go in detail in the solution of the question since it is trying to use complex relations and I cannot test it replicating in my machine.

But I want to provide insight on using Embedded fields and TypeConverter.

Let's take an example from the question above: Game table has fields id, secondsPerTurn, maxSecondsPerTurn, bonusSecondsPerAnswer.

It is okay to create entity like below.

@Entity
data class Game(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,

    @Embedded
    val gameConfigurationEmbed: GameConfigurationEmbed
)

data class GameConfigurationEmbed(
    var secondsPerTurn: Int,
    var maxSecondsPerTurn: Int,
    var bonusSecondsPerAnswer: Int
)

Here in the SQLite table, data is actually stored in four different columns but Room does CRUD operation based on data class structure giving higher feasibilty to the developers.

TypeConverter

Type converter will be helpful if we want to store non primitive data types or same type of data which will not be covered by @Embedded.

For example, a football game can be held in two places: home and away. Home and Away can have the same field names like placeName, latitude, longitude. In this case, we can create data class and type converters like below:

data class GamePlace(
    val placeName:String,
    val latitude:String,
    val longitude:String
)

@Entity
data class Game(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,

    @Embedded
    val gameConfigurationEmbed: GameConfigurationEmbed

    @TypeConverters
    var home: GamePlace? = null,

    @TypeConverters
    var away: GamePlace? = null,
)

object Converters {

    private val gson = Gson()

    @TypeConverter
    @JvmStatic
    fun fromGamePlace(gamePlace: GamePlace?): String? {
        return if (gamePlace == null) null else gson.toJson(gamePlace)
    }

    @TypeConverter
    @JvmStatic
    fun toGamePlace(jsonData: String?): GamePlace? {
        return if (jsonData.isNullOrEmpty()) null
        else gson.fromJson(jsonData, object : TypeToken<GamePlace?>() {}.type)
    }
}

While using type converter, converter class should be defined in database class as below:

@Database(
    entities = [Game::class /* ,more classes here*/],
    version = 1
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {

    abstract fun gameDao(): GameDao

    //.....
}

I hope this will help to deal with Embedded and TypeConverter in Room.

Sagar Chapagain
  • 1,683
  • 2
  • 16
  • 26
  • Thanks. I am using converters for some other data types, but I didn't think they were necessary here. To reiterate, are you suggesting that instead of using the embedded `GameConfigurationEmbed` I should serialize the object to a single string? Why is that necessary? I am able to successfully work with the `Game` object with embedded field, as long as I am not trying to fetch that through the POJO classes with `@Embedded`. – Martin Melka Aug 02 '19 at 15:09
  • @MartinMelka After seeing `@TypeConverters(RoomConverters::class)` annotation in class `RoundRoom`, I thought you were not properly using type converters. So I had written some details on using `@Embedded` and `TypeConverter` to show example implementation, and who visit your question can also understand their usage. Also, you don't need to serialize `GameConfigurationEmbed `. It seems fine. – Sagar Chapagain Aug 02 '19 at 15:19