Search code examples
androidandroid-room

How to use a junction table in Room to get data in a specific format


I’m working with Room database in Android and have three data classes representing my tables:

data class Train(id: Int, isFollow: Boolean)
data class TrainStatus(id: Int, trainId: Int, origin: String, destination: String)
data class PreviousTrain(id: Int, trainStatusId: Int, previousTrainStatusId: Int)

In TrainStatus, trainId is a foreign key that references id from the Train class.

PreviousTrain is a junction table where trainStatusId and previousTrainStatusId are foreign keys referencing id from TrainStatus.

I want to query the database and get a response in the following format:

data class Response(
    val train: Train,
    val trainStatusWithPreviousTrain: List<TrainStatusWithPreviousTrain>
)

data class TrainStatusWithPreviousTrain(
    val trainStatus: TrainStatus, 
    val previousTrain: TrainStatus
)

How can I use the junction table to create a Room query that will give me the data in the Response format?


Solution

  • In short you have a POJO with @Embedded for the parent and with @Relation for the child/children using the associateBy parameter with a Junction to specify the table and respective columns of the junction.

    • @Embedded will build the embedded object (if the relevant columns exist)
    • @Relation without the associateBy parameter (assuming the correct parent and children columns) will extract ALL of the related children into a list of objects or a single object (first according to the order of the output)
    • @Relation with the associateBy parameter with correctly coded specified parameters, will utilise the junction (associative/mapping/reference and other terms) table to extract the related objects.
      • Note that for the Junction, the class should be the class of the entity that defines the junction table. The parentColumn should be the column that maps/references the @Embedded table. The entityColumn should be the column that maps/references the table with the related children.

    As a DEMO based upon your code (note Long rather than Int for the id's (as they should really be)) consider:-

    The 3 tables i.e. @Entity annotated classes

    @Entity
    data class Train(
        @PrimaryKey
        val id: Long?=null,
        val isFollow: Boolean
        )
    @Entity
    data class TrainStatus(
        @PrimaryKey
        val id: Long?=null,
        val trainId: Long,
        val origin: String,
        val destination: String
        )
    @Entity( indices = [
        Index("trainStatusId","previousTrainStatusId")
    ])
    data class PreviousTrain(
        @PrimaryKey
        val id: Long?=null,
        val trainStatusId: Long,
        val previousTrainStatusId: Long
        )
    

    A POJO for a TrainStatus with it's related Train:-

    data class TrainStatusWithTrain(
        @Embedded
        val trainStatus: TrainStatus,
        @Relation(
            entity = Train::class,
            parentColumn = "trainId",
            entityColumn = "id"
        )
        val train: Train
    )
    

    The POJO for a TrainStatus (with it's Train) and it's Previous TrainStatus (with it's Train):-

    data class TrainStatusWithPreviousTrainStatus(
        @Embedded
        val trainStatusWithTrain: TrainStatusWithTrain,
        @Relation(
            entity = TrainStatus::class,
            parentColumn = "id",
            entityColumn = "id",
            associateBy = Junction(
                PreviousTrain::class,
                parentColumn = "trainStatusId",
                entityColumn = "previousTrainStatusId"
            )
        )
        val previousTrainStatus: List<TrainStatusWithTrain>
    )
    

    Some Insert and a Query in an @Dao interface (so the use can be demonstrated):-

    @Dao
    interface AllDAOs{
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(train: Train): Long
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(trainStatus: TrainStatus): Long
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(previousTrain: PreviousTrain): Long
    
        @Transaction
        @Query("SELECT * FROM trainStatus")
        fun getAllTrainStatusesWithPreviousTrainStatuses(): List<TrainStatusWithPreviousTrainStatus>
    }
    

    A basic @Database annotated abstract class:-

    @Database(entities = [Train::class,TrainStatus::class,PreviousTrain::class], version = 1, exportSchema = false)
    abstract class TheDatabase: RoomDatabase() {
        abstract fun getAllDAOs(): AllDAOs
    
        companion object {
            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 some activity code (not uses the main thread for brevity):-

    class MainActivity : AppCompatActivity() {
        lateinit var db: TheDatabase
        lateinit var dao: AllDAOs
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            db = TheDatabase.getInstance(this)
            dao = db.getAllDAOs()
    
            /* Add Some Trains */
            val T1Id = dao.insert(Train(isFollow = true))
            val T2Id = dao.insert(Train(isFollow = false))
            val T3Id = dao.insert(Train(isFollow = true))
            /* and some TrainStatuses */
            val TS1Id = dao.insert(TrainStatus(trainId = T1Id, origin = "Place1", destination = "PlaceA"))
            val TS2Id = dao.insert(TrainStatus(trainId = T1Id, origin = "Place2", destination = "PlaceB"))
            val TS3Id = dao.insert(TrainStatus(trainId = T2Id, origin = "Place3", destination = "PlaceC"))
            val TS4Id = dao.insert(TrainStatus(trainId = TS3Id, origin = "Nowhere", destination = "Somewhere"))
            /* and some PreviousTrains */
            dao.insert(PreviousTrain(trainStatusId = TS1Id, previousTrainStatusId = TS2Id))
            dao.insert(PreviousTrain(trainStatusId = TS2Id, previousTrainStatusId = TS3Id))
            dao.insert(PreviousTrain(trainStatusId = TS3Id, previousTrainStatusId = TS1Id))
    
            /* Extract from the database, writing to the log */
            for (tswpts in dao.getAllTrainStatusesWithPreviousTrainStatuses()) {
                val sb = StringBuilder()
                for (pts in tswpts.previousTrainStatus) {
                    sb.append("\n\tPREV TS ID is ${pts.trainStatus.id} " +
                            "TrainsID is ${pts.trainStatus.trainId} " +
                            " ISFollow is ${pts.train.isFollow} " +
                            "ORIGIN is ${pts.trainStatus.origin} " +
                            "DEST is ${pts.trainStatus.destination}")
                }
                Log.d("DBINFO","TS ID is ${tswpts.trainStatusWithTrain.trainStatus.id} " +
                        "TRAIN ID is ${tswpts.trainStatusWithTrain.train.id} ISFollow is ${tswpts.trainStatusWithTrain.train.isFollow} " +
                        "ORIGIN is ${tswpts.trainStatusWithTrain.trainStatus.origin} " +
                        "DEST is ${tswpts.trainStatusWithTrain.trainStatus.destination} " +
                        "TS has ${tswpts.previousTrainStatus.size} PREVIOUS TRAINS STATUSES. They are ${sb}")
            }
        }
    }
    

    Result (after first run, subsequent runs not intended for the demo):-

    D/DBINFO: TS ID is 1 TRAIN ID is 1 ISFollow is true ORIGIN is Place1 DEST is PlaceA TS has 1 PREVIOUS TRAINS STATUSES. They are 
            PREV TS ID is 2 TrainsID is 1  ISFollow is true ORIGIN is Place2 DEST is PlaceB
    D/DBINFO: TS ID is 2 TRAIN ID is 1 ISFollow is true ORIGIN is Place2 DEST is PlaceB TS has 1 PREVIOUS TRAINS STATUSES. They are 
            PREV TS ID is 3 TrainsID is 2  ISFollow is false ORIGIN is Place3 DEST is PlaceC
    D/DBINFO: TS ID is 3 TRAIN ID is 2 ISFollow is false ORIGIN is Place3 DEST is PlaceC TS has 1 PREVIOUS TRAINS STATUSES. They are 
            PREV TS ID is 1 TrainsID is 1  ISFollow is true ORIGIN is Place1 DEST is PlaceA
    D/DBINFO: TS ID is 4 TRAIN ID is 3 ISFollow is true ORIGIN is Nowhere DEST is Somewhere TS has 0 PREVIOUS TRAINS STATUSES. They are 
    
    • also note that a TrainStatus in the above has a single PreviousTrain but could have more (or none as is shown for TS 4). As such List<TrainStatus> could just be TrainStatus (with handling code modified to not expect a List) if there is only the one item.

    The Required Response

    If understood correctly (and for the demo assuming all trains) then

    Adding the POJO:-

    data class TrainWithTrainStatus(
        @Embedded
        val train: Train,
        @Relation(
            entity = TrainStatus::class,
            parentColumn = "id",
            entityColumn = "trainId"
        )
        val trainStatusWithPreviousTrainStatus: List<TrainStatusWithPreviousTrainStatus>
    )
    
    • noting that the use of the TrainStatusWithPreviousTrainStatus introduces the junction/junction table hierarchically.

    And then introducing another function in the @Dao annotated interface:-

    @Transaction
    @Query("SELECT * FROM train")
    fun  getAllTrainsWithTrainStatusetc(): List<TrainWithTrainStatus>
    

    And then adding the activity code:-

        /* Train with TrainStatus etc */
        for (twtse in dao.getAllTrainsWithTrainStatusetc()) {
            val sb = StringBuilder()
            for (twts in twtse.trainStatusWithPreviousTrainStatus) {
                val sb2 = StringBuilder()
                for (pt in twts.previousTrainStatus) {
                    sb2.append("\n\t\t PTTS ID is ${pt.trainStatus.id} " +
                            "PTTS ORIGIN is ${pt.trainStatus.origin} " +
                            "PTTS DEST is ${pt.trainStatus.destination}")
                }
                sb.append("\n\t TS ID is ${twts.trainStatusWithTrain.trainStatus.id} " +
                        "ORIGIN is ${twts.trainStatusWithTrain.trainStatus.origin} " +
                        "DEST is ${twts.trainStatusWithTrain.trainStatus.destination} " +
                        ". There are ${twts.previousTrainStatus.size} PTTS's. They are ${sb2}")
            }
            Log.d("DBINFO","Train ID is ${twtse.train.id} ISFollow is ${twtse.train.isFollow} " +
                    ". There are ${twtse.trainStatusWithPreviousTrainStatus.size} Trains Statuses With Previous Train Statuses. They are ${sb}")
        }
    

    Results in (additionally):-

    D/DBINFO: Train ID is 1 ISFollow is true . There are 2 Trains Statuses With Previous Train Statuses. They are 
             TS ID is 1 ORIGIN is Place1 DEST is PlaceA . There are 1 PTTS's. They are 
                 PTTS ID is 2 PTTS ORIGIN is Place2 PTTS DEST is PlaceB
             TS ID is 2 ORIGIN is Place2 DEST is PlaceB . There are 1 PTTS's. They are 
                 PTTS ID is 3 PTTS ORIGIN is Place3 PTTS DEST is PlaceC
    D/DBINFO: Train ID is 2 ISFollow is false . There are 1 Trains Statuses With Previous Train Statuses. They are 
             TS ID is 3 ORIGIN is Place3 DEST is PlaceC . There are 1 PTTS's. They are 
                 PTTS ID is 1 PTTS ORIGIN is Place1 PTTS DEST is PlaceA
    D/DBINFO: Train ID is 3 ISFollow is true . There are 1 Trains Statuses With Previous Train Statuses. They are 
             TS ID is 4 ORIGIN is Nowhere DEST is Somewhere . There are 0 PTTS's. They are