-1

I have a problem with Room DB relationships. I have two model classes. CharacterModel and EpisodeModel.

CharacterModel :

@Entity(tableName = "character_table")
data class CharacterModel(
    val created: String?,
    val episode: List<String>?,
    val gender: String?,
    val id: Int?,
    val image: String?,
    @Embedded
    val location: Location?,
    val name: String?,
    @Embedded
    val origin: Origin?,
    val species: String?,
    val status: String?,
    val type: String?,
    @PrimaryKey
    val characterUrl: String
)

EpisodeModel:

@Entity(tableName = "episode_table")
data class EpisodeModel(
    @SerializedName("air_date")
    val airDate: String?,
    val characters: List<String>?,
    val created: String?,
    val episode: String?,
    val id: Int?,
    val name: String?,
    @PrimaryKey
    val episodeUrl: String
)

They have relations with each other via url. It's like episode in CharacterModel and episode list have urls of EpisodeModel.

Sample:

{
"id": 1,
"name": "Rick Sanchez",
"status": "Alive",
"species": "Human",
"type": "",
"gender": "Male",
"origin": {
"name": "Earth (C-137)",
"url": "https://rickandmortyapi.com/api/location/1"
},
"location": {
"name": "Citadel of Ricks",
"url": "https://rickandmortyapi.com/api/location/3"
},
"image": "https://rickandmortyapi.com/api/character/avatar/1.jpeg",
"episode": [
"https://rickandmortyapi.com/api/episode/1",
"https://rickandmortyapi.com/api/episode/2",
"https://rickandmortyapi.com/api/episode/3",
"https://rickandmortyapi.com/api/episode/4",
"https://rickandmortyapi.com/api/episode/5",
"https://rickandmortyapi.com/api/episode/6",
"https://rickandmortyapi.com/api/episode/7",
"https://rickandmortyapi.com/api/episode/8",
"https://rickandmortyapi.com/api/episode/9",
"https://rickandmortyapi.com/api/episode/10",
"https://rickandmortyapi.com/api/episode/11",
"https://rickandmortyapi.com/api/episode/12",
"https://rickandmortyapi.com/api/episode/13",
"https://rickandmortyapi.com/api/episode/14",
"https://rickandmortyapi.com/api/episode/15",
"https://rickandmortyapi.com/api/episode/16",
"https://rickandmortyapi.com/api/episode/17",
"https://rickandmortyapi.com/api/episode/18",
"https://rickandmortyapi.com/api/episode/19",
"https://rickandmortyapi.com/api/episode/20",
"https://rickandmortyapi.com/api/episode/21",
"https://rickandmortyapi.com/api/episode/22",
"https://rickandmortyapi.com/api/episode/23",
"https://rickandmortyapi.com/api/episode/24",
"https://rickandmortyapi.com/api/episode/25",
"https://rickandmortyapi.com/api/episode/26",
"https://rickandmortyapi.com/api/episode/27",
"https://rickandmortyapi.com/api/episode/28",
"https://rickandmortyapi.com/api/episode/29",
"https://rickandmortyapi.com/api/episode/30",
"https://rickandmortyapi.com/api/episode/31",
"https://rickandmortyapi.com/api/episode/32",
"https://rickandmortyapi.com/api/episode/33",
"https://rickandmortyapi.com/api/episode/34",
"https://rickandmortyapi.com/api/episode/35",
"https://rickandmortyapi.com/api/episode/36",
"https://rickandmortyapi.com/api/episode/37",
"https://rickandmortyapi.com/api/episode/38",
"https://rickandmortyapi.com/api/episode/39",
"https://rickandmortyapi.com/api/episode/40",
"https://rickandmortyapi.com/api/episode/41",
"https://rickandmortyapi.com/api/episode/42",
"https://rickandmortyapi.com/api/episode/43",
"https://rickandmortyapi.com/api/episode/44",
"https://rickandmortyapi.com/api/episode/45",
"https://rickandmortyapi.com/api/episode/46",
"https://rickandmortyapi.com/api/episode/47",
"https://rickandmortyapi.com/api/episode/48",
"https://rickandmortyapi.com/api/episode/49",
"https://rickandmortyapi.com/api/episode/50",
"https://rickandmortyapi.com/api/episode/51"
],
"url": "https://rickandmortyapi.com/api/character/1",
"created": "2017-11-04T18:48:46.250Z"
}

How can I arrange this relation?

1 Answers1

1

First you cannot have columns that are Lists/Arrays. However, these columns are not actually required. Rather you appear to have a many-many relationship between the CharacterModel and the EpisodeModel.

That is a CharacterModel can have 0-many EpisodeModels and likewise an EpisodeModel can have 0-many CharacterModels.

For such a relationship you typically have a table know by names such as:-

  • associative table
  • reference table
  • intermediate table
  • junction table
  • mapping table
  • ....

Such a table has two primary columns, one for each table that is mapped. Typically these two columns would form the primary key (a composite primary key). As such the mapped tables don't need to store references to the other table. This third table does that.

As such You could have (note some columns/annotations removed for brevity of the demo, obviously you would include these):-

@Entity(tableName = "character_table")
data class CharacterModel(
    val created: String?,
    //val episode: List<String>?, NOT NEEDED <<<<<
    val gender: String?,
    val id: Int?,
    val image: String?,
    /* removed for brevity
    @Embedded
    val location: Location?,
     */
    val name: String?,
    /* removed for brevity
    @Embedded
    val origin: WebStorage.Origin?,
     */
    val species: String?,
    val status: String?,
    val type: String?,
    @PrimaryKey
    val characterUrl: String

)

and

@Entity(tableName = "episode_table")
data class EpisodeModel(
    //@SerializedName("air_date") removed for brevity
    val airDate: String?,
    //val characters: List<String>?, // NOT NEEDED <<<<<
    val created: String?,
    val episode: String?,
    val id: Int?,
    val name: String?,
    @PrimaryKey
    val episodeUrl: String
)

The new mapping table could be :-

@Entity(
    primaryKeys = ["characterUrlMap","episodeUrlMap"],
    /* Optional but suggested to enforce referential integrity */
    foreignKeys = [
        /* Rule that says the value of the characterUrlMap column MUST be a value
            that exists in the characterUrl column of one of the rows in the character_table table
         */
        ForeignKey(
            entity = CharacterModel::class,
            parentColumns = ["characterUrl"],
            childColumns = ["characterUrlMap"],
            /* Option within foreign key but helpful */
            onDelete = CASCADE, // if parent is deleted then all children (rows in this table) are deleted rather than orphaned
            onUpdate = CASCADE // if characterUrl column is changed then all the children in this table with the parents value are changed
        ),
        ForeignKey(
            entity = EpisodeModel::class,
            parentColumns = ["episodeUrl"],
            childColumns = ["episodeUrlMap"],
            onDelete =  CASCADE,
            onUpdate =  CASCADE
        )
    ]
)
data class CharacterEpisodeMap(
    val characterUrlMap: String,
    @ColumnInfo(index = true)
    val episodeUrlMap: String
)

You then need to be able to get the related items either a CharacterModel with all the EpisodeModels that belong to it it and/or an EpisodeModel with all the CharacterModels that belong to it.

To do so you need a POJO (or 2) that can be used to retrieve the data. So you could have:-

data class CharacterModelWithEpisodeModelList(
    @Embedded
    val characterModel: CharacterModel,
    @Relation(
        entity = EpisodeModel::class,
        parentColumn = "characterUrl",
        entityColumn = "episodeUrl",
        associateBy = Junction(
            value = CharacterEpisodeMap::class,
            parentColumn = "characterUrlMap",
            entityColumn = "episodeUrlMap"
        )
    )
    val episodeModelList: List<EpisodeModel>
)

and/or :-

data class EpisodeModelWithCharacterModelList(
    @Embedded
    val episodeModel: EpisodeModel,
    @Relation(
        entity = CharacterModel::class,
        parentColumn = "episodeUrl",
        entityColumn = "characterUrl",
        associateBy = Junction(
            value = CharacterEpisodeMap::class,
            parentColumn = "episodeUrlMap",
            entityColumn = "characterUrlMap"
        )
    )
    val characterModelList: List<CharacterModel>
)

Demonstration

Using the code above and :-

Dao's such as :-

@Dao
abstract class AllDao {

    @Insert(onConflict = IGNORE)
    abstract fun insert(characterModel: CharacterModel): Long
    @Insert(onConflict = IGNORE)
    abstract fun  insert(episodeModel: EpisodeModel): Long
    @Insert(onConflict = IGNORE)
    abstract fun insert(characterEpisodeMap: CharacterEpisodeMap): Long

    /* Retrieving the related data */
    @Transaction
    @Query("SELECT * FROM character_table")
    abstract fun getAllCharacterModelsWithEpisodeModelList(): List<CharacterModelWithEpisodeModelList>
    @Transaction
    @Query("SELECT * FROM episode_table")
    abstract fun getAllEpisodeModelsWithCharacterModelList(): List<EpisodeModelWithCharacterModelList>
}

You need an @Database annotated class (with the 3 entities) such as :-

@Database(entities = [CharacterModel::class,EpisodeModel::class,CharacterEpisodeMap::class], version = 1, exportSchema = false)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getAllDao(): AllDao

    companion object {
        @Volatile
        private var instance: TheDatabase? = null

        fun getInstance(context: Context): TheDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(context,TheDatabase::class.java,"the_database.db")
                    .allowMainThreadQueries()
                    .build()
            }
            return instance as TheDatabase
        }
    }
}

Finally to put it altogether an activity MainActivity :-

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()
        doSomething()
    }

    fun doSomething() {
        val c1url = "c1url"
        dao.insert(CharacterModel("now","x",1,"c1image","Fred","test","ongoing","a type",c1url))
        val c2url = "c2url"
        dao.insert(CharacterModel("yesterday","x",2,"c1image","Mary","test","finished","a type",c2url))
        val c3url = "c3url"
        dao.insert(CharacterModel("tomorrow","x",3,"c1image","Jane","test","starting","a type",c3url))

        val e1url = "e1url"
        dao.insert(EpisodeModel("then","now","1",1,"The First for C1",e1url))
        val e2url = "e2url"
        dao.insert(EpisodeModel("tomorrow","now","1",2,"The Second for C1",e2url))
        val e3url = "e3url"
        dao.insert(EpisodeModel("then","now","1",3,"The Third for C1",e3url))
        val e4url = "e4url"
        dao.insert(EpisodeModel("then","now","1",1,"The First for C2",e4url))

        dao.insert(CharacterEpisodeMap(c1url,e1url))
        dao.insert(CharacterEpisodeMap(c2url,e1url)) // to show multiple characters in episode 
        dao.insert(CharacterEpisodeMap(c1url,e2url))
        dao.insert(CharacterEpisodeMap(c1url,e3url))

        dao.insert(CharacterEpisodeMap(c2url,e4url))
        // and so on

        for (cwe: CharacterModelWithEpisodeModelList in dao.getAllCharacterModelsWithEpisodeModelList()) {
            Log.d(TAG,"Character is ${cwe.characterModel.name} created is ${cwe.characterModel.created} etc.")
            for(e: EpisodeModel in cwe.episodeModelList) {
                Log.d(TAG,"\tEpisode is ${e.name} etc.")
            }
        }

        for(ewc: EpisodeModelWithCharacterModelList in dao.getAllEpisodeModelsWithCharacterModelList()) {
            Log.d(TAG,"Episode is ${ewc.episodeModel.name} etc.")
            for(c: CharacterModel in ewc.characterModelList) {
                Log.d(TAG,"\tCharacter is ${c.name} etc.")
            }
        }
    }
}

Result

When run the log includes :-

2022-01-12 08:55:46.938  D/DBINFO: Character is Fred created is now etc.
2022-01-12 08:55:46.938  D/DBINFO:  Episode is The First for C1 etc.
2022-01-12 08:55:46.938  D/DBINFO:  Episode is The Second for C1 etc.
2022-01-12 08:55:46.938  D/DBINFO:  Episode is The Third for C1 etc.
2022-01-12 08:55:46.938  D/DBINFO: Character is Mary created is yesterday etc.
2022-01-12 08:55:46.938  D/DBINFO:  Episode is The First for C1 etc.
2022-01-12 08:55:46.938  D/DBINFO:  Episode is The First for C2 etc.
2022-01-12 08:55:46.938  D/DBINFO: Character is Jane created is tomorrow etc.


2022-01-12 08:55:46.946  D/DBINFO: Episode is The First for C1 etc.
2022-01-12 08:55:46.947  D/DBINFO:  Character is Fred etc.
2022-01-12 08:55:46.947  D/DBINFO:  Character is Mary etc.
2022-01-12 08:55:46.947  D/DBINFO: Episode is The Second for C1 etc.
2022-01-12 08:55:46.947  D/DBINFO:  Character is Fred etc.
2022-01-12 08:55:46.947  D/DBINFO: Episode is The Third for C1 etc.
2022-01-12 08:55:46.947  D/DBINFO:  Character is Fred etc.
2022-01-12 08:55:46.947  D/DBINFO: Episode is The First for C2 etc.
2022-01-12 08:55:46.947  D/DBINFO:  Character is Mary etc.
MikeT
  • 51,415
  • 16
  • 49
  • 68