1

This question is about relational database. I am creating an app from the Seatgeek api. Now with many to many relationship you will have to create a junction. Events and Performers have many to many relationship. Hence I created the junction.The eventid is the parent column whereas the performerId is the entity column. Next I created the required method in my Dao.

I created a method in my ViewModel to subscribe to the EventDetails.

it goes like this

private fun subscribeToEventDetails(eventId: Int) {
        viewModelScope.launch {
            try {
                val event = getEventDetails(eventId)

                onEventsDetails(event)
            }catch (t: Throwable) {
                onFailure(t)}
        }
    }

private fun onEventsDetails(event: EventsWithDetails) {

val eventDetails = uiEventDetailsMapper.mapToView(event)

_state.update { EventDetailsViewState.EventsDetails(eventsDetails)}
}

The getEventDetails logic is

class GetEventDetails @Inject constructor(
    private val eventRepository: EventRepository,
    private val dispatchersProvider: DispatchersProvider,
    private val performerRepository: PerformerRepository,
) {

    suspend operator fun invoke(eventId: Int): EventWithDetails {
        return withContext(dispatchersProvider.io()) {
            eventRepository.getEvent(eventId)
        }
    }
}

Everything is working smoothly except one thing.

In EventWith details class. Performer is found there.

Now remember in my post. I already talked about

Event and Performer Having many to many relation.

data class EventWithDetails(val id:Int,
val title:String,
val media:Media,
val details: Details,
val dateTimeLocal:LocalDateTime,
val visibleUntilUtc: LocalDateTime,
val type:String
)

data class Details(val description: String,
val stats: Stats,
val venue: Venue,
val performers: List<Performer>)

data class UIEventDetailed(val id:Int,
val title:String,
val image:String,
val performer:List<Performer>,
val description:String,
val averagePrice: Int,
val highestPrice: Int,
val listingCount: Int,
val lowestPrice: Int,
val medianPrice: Int,
)

so I created a mapping between EventWithDetails and UIEventDetailed.

I am able to get all data except list of performer. Every thing you see in details holds eventId as a foreign key I guess that's why. However with Performer and Event I had to create a junction.

My question is how will you be able to get performer data. I did debug though and it shows that no data was in performers

Here is the results I get from debugging

EventWithDetails(id=5866659, title=Alpha 9, media=Media(
images=[Image(huge=https://seatgeek.com/images/performers-landscape/alpha-9-8b77b7/231372/huge.jpg, x320=)]), 

details=Details(description=,
stats=Stats(averagePrice=71, highestPrice=72, listingCount=72, lowestPrice=1, medianPrice=0, visibleListingCount=49, lowestPriceGoodDeals=0, lowestSgBasePrice=71, lowestSgBasePriceGoodDeals=1), 

venue=Venue(id=7336, name=Exchange LA, address=Address(address1=618 S. Spring Street, address2=Los Angeles, CA 90014, city=Los Angeles, state=CA, postalCode=90014, country=US), 
timeZone=America/Los_Angeles, 
info=Info(capacity=1500, numUpcomingEvents=6, venueScore=0.44, url=https://seatgeek.com/venues/exchange-la/tickets, hasUpcomingEvents=true), location=Location(lat=34.0452, lon=-118.251)),

performers=[]),

dateTimeLocal=2023-01-14T21:00, 

visibleUntilUtc=2023-01-15T09:00, type=concert)

Any help will be highly appreciative. If you need more info please do let me know.

Doe
  • 47
  • 6

2 Answers2

1

For many to many relationships, a third table should be used. We have Events, and Performers, so a third Performances table can be added instead of using any lists.

With this system we eliminate the list of performers stored on the event and vice versa. That is 100% handled by the performances table. Starting with say an Event you have the event id and you can look up all the performances relevant, and then for each performance get all the performers. Since you have 3 independent tables and daos, you can add a performer, performance, or event separately without affecting the other 2 tables. There could be meta data for each performance like duration, title, etc which is rightly independent of events and the performers so good single responsibility principle here.

Many to many database design, third table?

Sample new Dao:

@Dao
interface PerformancesDao {
    @Query("SELECT events.* 
        FROM events 
        JOIN performances ON events.id = performances.eve_id 
        JOIN performers ON performances.perf_id = performers.id 
        WHERE performers.name = :name")
    fun getEventsByPerformer(name: String): List<Event>
}

Sample new Entity:

@Entity
data class Performances (
    @PrimaryKey(autoGenerate = true)
    var id: Long = 0,

    @ColumnInfo(name = "perf_id")
    var perfId: Long,

    @ColumnInfo(name = "event_id")
    var eveId: Long,

    // more columns for performance details
)
ryankuck
  • 320
  • 3
  • 15
  • What is the difference between the above and creating a Junction. I like this one though. The link explains it exactly like creating a junction. Only thing is this seem to be working unlike the Junction I created. Will try this one. – Doe Jan 17 '23 at 14:21
0

The devil in the detail, as far as Room is concerned, is with the POJOs (for handling the junction(s)).

your POJO's don't appear to consider this as per

data class Details(val description: String,
val stats: Stats,
val venue: Venue,
val performers: List<Performer> /* <<<<<<<<<< */)

and

data class UIEventDetailed(val id:Int,
val title:String,
val image:String,
val performer:List<Performer>, /* <<<<<<<<<< */
val description:String,
val averagePrice: Int,
val highestPrice: Int,
val listingCount: Int,
val lowestPrice: Int,
val medianPrice: Int,
)
  • see comments <<<<<<<<<<

To handle the junctions (via Room's convenience methodology) the junction is handled by the POJO using an @Relation with the associateBy parameter defining the Junction (note that the entity parameter of the Junction should specify the Parent class if the column names differ (it is suggested that columns names are always unique as that then rules out potential issues with ambiguities)).

  • by saying via Room's convenience methodology it should be noted that Room uses subqueries (hence the @Transaction as used/demonstrated) that
    • will retrieve ALL children of a parent, and
    • will retrieve them in the order that is determined by the SQlite query optimiser.
    • this irrespective of SQL that species JOINs and WHERE clauses that do not affect the parents. That is the Room only considers the parents returned from such a query.
      • hence in the query in the Dao all that is required is just SELECT * FROM event but it still retrieves all the children.

Demo of the devil in the detail

The following is a condensed, working, version somewhere along the lines of you question.

It consists of 3 core tables (Event, Detail and Performer) and 3 Junctions (mapping/reference/associative and other descriptions tables (perhaps over the top but able to cater with many-many(n-n) as well as 1-n and 1-1)).

As per:-

@Entity
data class Event(
    @PrimaryKey
    val eventid: Long?=null,
    val title: String,
    /* etc */
)
@Entity
data class Performer(
    @PrimaryKey
    val performerid: Long?=null,
    val performerName: String
    /* etc */
)
@Entity
data class Detail(
    @PrimaryKey
    val detailid: Long?=null,
    val venueName: String
    /* etc */
)

/*  Long the lines of the comment:-
    Events and Performers have many to many relationship.
    Hence I created the junction.
    The eventid is the parent column whereas the performerId is the entity column.
 */
@Entity(
    primaryKeys = ["eventid_map","performerid_map"]
)
data class EventPerformerJunction(
    val eventid_map: Long,
    @ColumnInfo(index = true)
    val performerid_map: Long
)
/* ADDED/ASSUMED JUNCTION BETWEEN DETAIL AND PERFORMER  */
@Entity(
    primaryKeys = ["detailid_link","performerid_link"]
)
data class DetailPerformerJunction(
    val detailid_link: Long,
    @ColumnInfo(index = true)
    val performerid_link: Long
)
/* JUNCTION BEWTEEN EVENT AND DETAIL (allowing many details per Event and a detail to be in many events perhaps overboard) */
@Entity(
    primaryKeys = ["eventid_ref","detailid_ref"]
)
data class EventDetailJunction(
    val eventid_ref: Long,
    @ColumnInfo(index = true)
    val detailid_ref: Long
)
  • note that column names are unique as suggested

And then POJO's:-

data class EventWithPerformerListPOJO(
    @Embedded
    val event: Event,
    /* all the other columns omitted for brevity */
    @Relation(
        entity = Performer::class,
        parentColumn = "eventid",
        entityColumn = "performerid",
        associateBy = Junction(
            value = EventPerformerJunction::class,
            parentColumn = "eventid_map",
            entityColumn = "performerid_map"
        )
    )
    val peformerList: List<Performer>
)

/* POJO  for details with the performers */
data class DetailWithPerformersListPOJO(
    @Embedded
    val detail: Detail,
    @Relation(
        entity = Performer::class,
        parentColumn = "detailid",
        entityColumn = "performerid",
        associateBy = Junction(
            value = DetailPerformerJunction::class,
            parentColumn = "detailid_link",
            entityColumn = "performerid_link"
        )
    )
    val performers: List<Performer>
)

And finally the POJO for the Final Result that hierarchically handles the detailsWithPerformersListPOJO within a Detail:-

data class EventWithListOfDetailsWithPerformersListPOJO (
    @Embedded
    val event: Event,
    /* The List of performers at the Event level */
    @Relation(
        entity = Detail::class,
        parentColumn = "eventid",
        entityColumn = "detailid",
        associateBy = Junction(
            value = EventDetailJunction::class,
            parentColumn = "eventid_ref",
            entityColumn = "detailid_ref"
        )
    )
    /* List of the details with each detail having a list of performers */
    val details: List<DetailWithPerformersListPOJO>,
    @Relation(
        entity = Performer::class,
        parentColumn = "eventid",
        entityColumn = "performerid",
        associateBy = Junction(
            value = EventPerformerJunction::class,
            parentColumn = "eventid_map",
            entityColumn = "performerid_map"
        )
    )
    val performers: List<Performer>
)

It should be noted that for brevity the main thread has been used, rather than MVVM, so no suspends on the Daos, which are:-

interface TheDAOs {
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(event: Event): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(detail: Detail): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(performer: Performer): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(eventPerformerJuntion: EventPerformerJunction): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(eventDetailJunction: EventDetailJunction): Long
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(detailPerformerJunction: DetailPerformerJunction): Long

    /*<<<<<<<<<< The query being demonstrated >>>>>>>>>>*/
    @Transaction
    @Query("SELECT * FROM event")
    fun getAllEventSWithDetailsWithThePerformersAsPerDetail(): List<EventWithListOfDetailsWithPerformersListPOJO>

}

The @Database annotated class used for the demo:-

@Database(entities = [
    Event::class,
    Detail::class,
    Performer::class,
    EventDetailJunction::class,
    DetailPerformerJunction::class,
    EventPerformerJunction::class],
    exportSchema = false,
    version = 1
)
abstract class TheDatabase: RoomDatabase() {
    abstract fun getTheDAOs(): TheDAOs

    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() /* For brevity/convenience */
                    .build()
            }
            return instance as TheDatabase
        }
    }
}

In an activity, to actually demonstrate the following:-

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

        /* Add some events */
        val e1=dao.insert(Event(title = "Event 1"))
        val e2=dao.insert(Event(title = "Event 2"))
        /* Add some Performers */
        val p1=dao.insert(Performer(performerName = "Performer 1"))
        val p2=dao.insert(Performer(performerName = "Performer 2"))
        val p3=dao.insert(Performer(performerName = "Performer 3"))
        val p4=dao.insert(Performer(performerName = "Performer 4"))
        /* Add some details */
        val d1=dao.insert(Detail(venueName = "Venue 1"))
        val d2=dao.insert(Detail(venueName = "Venue 2"))
        val d3=dao.insert(Detail(venueName = "Venue 3"))
        val d4=dao.insert(Detail(venueName = "Venue 4"))

        /* Tie performers to details */
        dao.insert(DetailPerformerJunction(d1,p1))
        dao.insert(DetailPerformerJunction(d1,p2))
        dao.insert(DetailPerformerJunction(d1,p3))
        dao.insert(DetailPerformerJunction(d2,p4))
        dao.insert(DetailPerformerJunction(d2,p2))
        dao.insert(DetailPerformerJunction(d4,p3))
        dao.insert(DetailPerformerJunction(d4,p3)) /* IGNORED AS DUPLICATE */
        dao.insert(DetailPerformerJunction(d4,p4))
        /* Note has no performers */

        /* Tie details to events */
        dao.insert(EventDetailJunction(e1, d1))
        dao.insert(EventDetailJunction(e1,d3))
        dao.insert(EventDetailJunction(e2,d2))
        dao.insert(EventDetailJunction(e2,d4))

        /* Tie Performers to Events */
        dao.insert(EventPerformerJunction(e1,p4))
        dao.insert(EventPerformerJunction(e1,e2))
        dao.insert(EventPerformerJunction(e2,p1))
        dao.insert(EventPerformerJunction(e2,p3))


        /* See the result */
        for (ewdwp in dao.getAllEventSWithDetailsWithThePerformersAsPerDetail()) {
            Log.d(TAG,"Event is ${ewdwp.event.title} ID is ${ewdwp.event.eventid} it has ${ewdwp.details.size} Details. They are:-")
            for (d in ewdwp.details) {
                Log.d(TAG,"\tDetail is ${d.detail.venueName} ID is ${d.detail.detailid} it has ${d.performers.size} details. They are:-")
                for (p in d.performers) {
                    Log.d(TAG,"\t\tPeformer is ${p.performerName} ID is ${p.performerid}")
                }
            }
            Log.d(TAG,"\t the Event also has ${ewdwp.performers.size} Performers (ex detail): They are:-")
            for (p in ewdwp.performers) {
                Log.d(TAG,"\tEx Detail Performer is ${p.performerName} ID is ${p.performerid}")
            }
        }
    }
}

When run for the first time (not designed to be run more than once) then the log includes (the expected results):-

D/DBINFO: Event is Event 1 ID is 1 it has 2 Details. They are:-
D/DBINFO:   Detail is Venue 1 ID is 1 it has 3 details. They are:-
D/DBINFO:       Peformer is Performer 1 ID is 1
D/DBINFO:       Peformer is Performer 2 ID is 2
D/DBINFO:       Peformer is Performer 3 ID is 3
D/DBINFO:   Detail is Venue 3 ID is 3 it has 0 details. They are:-
D/DBINFO:    the Event also has 2 Performers (ex detail): They are:-
D/DBINFO:   Ex Detail Performer is Performer 2 ID is 2
D/DBINFO:   Ex Detail Performer is Performer 4 ID is 4


D/DBINFO: Event is Event 2 ID is 2 it has 2 Details. They are:-
D/DBINFO:   Detail is Venue 2 ID is 2 it has 2 details. They are:-
D/DBINFO:       Peformer is Performer 2 ID is 2
D/DBINFO:       Peformer is Performer 4 ID is 4
D/DBINFO:   Detail is Venue 4 ID is 4 it has 2 details. They are:-
D/DBINFO:       Peformer is Performer 3 ID is 3
D/DBINFO:       Peformer is Performer 4 ID is 4
D/DBINFO:    the Event also has 2 Performers (ex detail): They are:-
D/DBINFO:   Ex Detail Performer is Performer 1 ID is 1
D/DBINFO:   Ex Detail Performer is Performer 3 ID is 3

Note For brevity and simplicity and to avoid potential confusion Foreign Keys have not been specified. Foreign keys are suggested as they help to enforce and maintain referential integrity.

For example:-

@Entity(
    primaryKeys = ["eventid_map","performerid_map"],
    foreignKeys = [
        ForeignKey(
            /* Enforces RI (referential integrity) */
            entity = Event::class,
            parentColumns = ["eventid"],
            childColumns = ["eventid_map"],
            /* For help in maintaining RI Optional within a FKey */
            onDelete = ForeignKey.CASCADE, /* deletes children of a deleted parent automatically */
            onUpdate = ForeignKey.CASCADE /* will update the children if the value of the column that is referenced in the parent is changed */
                 ),
        ForeignKey(
            entity = Performer::class,
            parentColumns = ["performerid"],
            childColumns = ["performerid_map"],
            onDelete = ForeignKey.CASCADE,
            onUpdate = ForeignKey.CASCADE
        )
    ]
)
data class EventPerformerJunction(
    val eventid_map: Long,
    @ColumnInfo(index = true)
    val performerid_map: Long
)
MikeT
  • 51,415
  • 16
  • 49
  • 68
  • details just hold a list of classes in my domain.I wanted to separate them from the primitive types. What you said above I have them all. – Doe Jan 15 '23 at 07:38
  • 1
    @Doe Are you using the simple select query as per the example or a more complex select with a join or joins? If the latter the issue could be common use of **id**, the ambiguity. That is on a per row basis **id**'s will be the last occurrence of id the output row. That could result in no rows being obtained. e,g, say person's id (last id) then the event id could be 20, there would be no junction rows where the event's id is 20 and thus no children even though the event's id is really 1. The easiest fix would be to use unique column names. – MikeT Jan 15 '23 at 09:02
  • So It goes like this. I created a data class CacheAggregate then I embedded the main entity. I then created a relation for images, stats and a called Junction for Performer and event. In the DAO I created a method holding all that's in the CacheAggregate as parameters. I then Uses a for loop to insert them.Hence in the api repo I called them using the destructuring declaration. Hence everything work. From the Debugging of my viewmodel it is clear that no data it coming from performer.Also I cant seem to make a query for the junction created. – Doe Jan 15 '23 at 10:35
  • 1
    What happens if you use your performer dao to look up the performer data directly? Or if it’s set up differently then is there SOME way to get the performer data directly to test its there? – ryankuck Jan 16 '23 at 01:36
  • @kuck1 yes I am able to get data for performer. The app has three navigations to be be precise. Event nav, Search nav and Performer Nav. What I want to do is, say a user click an event, I want the user to be able to see the Event and its details. Such as the venue where the event is gonna take place, description, and Performer(s) for the events. Also with the Performer let say user click on a performer he can see the performer details like number of upcoming event and the List of event(s). So regarding your question yes I can get data for performer in PerformerDao. – Doe Jan 16 '23 at 17:11
  • Posted an answer after some investigation, hope that it makes sense or we can discuss in the thread there – ryankuck Jan 17 '23 at 04:00