Reputation: 47
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.
Upvotes: 1
Views: 103
Reputation: 848
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
)
Upvotes: 1
Reputation: 56958
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,
)
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)).
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
)
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
)
Upvotes: 0