Android Room: Creating a one-to-one relationship entity - android

I'm creating a small project with Room and three tables (jobs, tags, categories).
Essentially:
A job belongs to (or has) 1 and only category, but a job can have 0..N tags.
A category has 0..N jobs.
A tag has 0..N jobs.
I'm having some trouble trying to model all the data, especially when it comes to the relationships between the entities. More concretely, I have:
fun JobListing.toJobEntityList(): List<JobEntity> {
val jobEntityList = mutableListOf<JobEntity>()
this.jobs.take(50).forEach { job: Job ->
jobEntityList.add(
JobEntity(
jobId = job.id,
title = job.title,
url = job.url,
companyName = job.companyName,
jobType = job.jobType,
publicationDate = job.publicationDate,
relocation = job.candidateRequiredLocation,
salary = job.salary
)
)
}
return jobEntityList
}
This extension function is being called when I'm fetching the data from the network, so I can convert it to entities and store them in my DB.
I'm essentially creating a JobEntity, but a job should have 1 category and 0..N tags associated. The problem is that I don't know how to add that data related to the relationship between a job and its category and tags.
This is my JobEntity class:
#Entity(
tableName = "Jobs",
indices = [
Index("id", unique = true)
]
)
data class JobEntity(
#PrimaryKey #ColumnInfo(name = "id") val jobId: Int,
#ColumnInfo(name = "title") val title: String,
#ColumnInfo(name = "url") val url: String,
#ColumnInfo(name = "company_name") val companyName: String,
#ColumnInfo(name = "job_type") val jobType: String,
#ColumnInfo(name = "publication_date") val publicationDate: String,
#ColumnInfo(name = "candidate_required_location") val relocation: String,
#ColumnInfo(name = "salary") val salary: String
)
Thanks in advance!

Hi you should model your database like this:
Job_Category
Tags
Jobs (add category_id field and add 1 foreign key referencing to Category table)
Job_Tags (add tag_id, job_id fields and add 2 foreign keys referencing to Category and Jobs tables)
#Entity(tableName = "Category")
data class CategoryEntity(
#PrimaryKey #ColumnInfo(name = "id") val id: Int,
)
#Entity(tableName = "Tags")
data class TagEntity(
#PrimaryKey #ColumnInfo(name = "id") val id: Int,
)
#Entity(
tableName = "Jobs",
foreignKeys = [ForeignKey(entity = CategoryEntity::class, parentColumns = ["id"], childColumns = ["categoryid"])]
)
data class JobEntity(
#PrimaryKey #ColumnInfo(name = "id") val id: Int,
#ColumnInfo(name = "categoryid", index = true) val categoryid: Int,
// ... other fields
)
#Entity(
tableName = "JobTags",
foreignKeys = [
ForeignKey(entity = TagEntity::class, parentColumns = ["id"], childColumns = ["tagId"]),
ForeignKey(entity = JobEntity::class, parentColumns = ["id"], childColumns = ["jobId"]),
]
)
data class JobTagEntity(
#PrimaryKey #ColumnInfo(name = "id") val id: Int,
#ColumnInfo(name = "tagId", index = true) val tagId: Int,
#ColumnInfo(name = "jobId", index = true) val jobId: Int,
)

Related

Android Room and nested relationships error

I have the following 3 entities:
#Entity(tableName = "PROPERTY",
indices = [
androidx.room.Index(value = ["id"], unique = true)
])
data class Property (
#Expose #PrimaryKey override val id: UUID = UUID.randomUUID(),
#SerializedName("type") #ColumnInfo(name = "type") val type: ItemPropertyType,
#SerializedName("code") #ColumnInfo(name = "code") val code: String,
#SerializedName("default_description") #ColumnInfo(name = "default_description") val defaultDescription: String?,
#SerializedName("description_id") #ColumnInfo(name = "description_id") val descriptionId: UUID?,
#SerializedName("default_property") #ColumnInfo(name = "default_property") val defaultProperty: Boolean,
#SerializedName("entity") #ColumnInfo(name = "entity") val entity: String = "ITEM"
)
#Entity(tableName = "PROPERTY_SET_DETAIL",
indices = [
Index(value = ["id"], unique = true),
Index(value = ["property_id"], unique = false)
],
foreignKeys = [
ForeignKey(entity = PropertySet::class, parentColumns = ["id"], childColumns = ["property_set_id"]),
ForeignKey(entity = Property::class, parentColumns = ["id"], childColumns = ["property_id"])])
data class PropertySetDetail (
#Expose #PrimaryKey override val id: UUID = UUID.randomUUID(),
#SerializedName("property_id") #ColumnInfo(name = "property_id") val propertyId: UUID,
#SerializedName("value") #ColumnInfo(name = "value") var value: String?,
#SerializedName("rank") #ColumnInfo(name = "rank") val rank: Int,
#SerializedName("property_set_id") #ColumnInfo(name = "property_set_id") val propertySetId: UUID
)
#Entity(tableName = "PROPERTY_SET",
indices = [Index(value = ["id"], unique = true), Index(value = ["properties_charset"], unique = false), Index(value = ["hashkey"], unique = false)])
data class PropertySet (
#Expose #PrimaryKey override val id: UUID = UUID.randomUUID(),
#SerializedName("item_id") #ColumnInfo(name = "item_id") val itemId: UUID,
#SerializedName("properties_charset") #ColumnInfo(name = "properties_charset") var propertiesCharset: String?,
#SerializedName("hashkey") #ColumnInfo(name = "hashkey") var hashkey: String?,
)
and also the following data classes that I need for the nested relationships among the 3 entities declared above:
data class PropertySetAndDetails(
#Embedded
val set: PropertySet,
#Relation(
entity = PropertySetDetail::class,
parentColumn = "id",
entityColumn = "property_set_id"
) val details: List<PropertyDetailAndProperty>,
)
data class PropertyDetailAndProperty(
#Embedded
val property: Property,
#Relation(
parentColumn = "id",
entityColumn = "property_id"
) val detail: PropertySetDetail
)
and the following DAO query function:
#Transaction
#Query("select * from property_set where item_id = :itemId")
suspend fun getSetsForItem(itemId: UUID): List<PropertySetAndDetails>
There is no way to make it work. I get the following build error:
error: There is a problem with the query: [SQLITE_ERROR] SQL error or
missing database (no such column: type)
I also declared the following convert functions for
ItemPropertyType:
#TypeConverter
fun toItemPropertyType(v: Byte?): ItemPropertyType {
return when (v) {
null -> ItemPropertyType.Unknown
else -> ItemPropertyType.getByValue(v)
}
}
#TypeConverter
fun fromItemPropertyType(t: ItemPropertyType?): Byte {
return when (t) {
null -> ItemPropertyType.Unknown.value
else -> t.value
}
}
Can someone explain me why this nested relationship doesn't work ? Thanks in advance.
Can someone explain me why this nested relationship doesn't work ?
I can't (let's call it Room's magic), but I can hint you how to make this work. Try to change your PropertyDetailAndProperty class to this:
data class PropertyDetailAndProperty(
#Embedded
val detail: PropertySetDetail,
#Relation(
parentColumn = "property_id",
entityColumn = "id"
) val property: Property
)
It seems nested class should contain entity you use in outer class ( PropertySetDetail in your case) beyond #Relation. Why? Well, I guess it just was developers' design.
Have you tried using the converter? Room cannot identify this data ("ItemPropertyType"). This is why you are getting this error.
Doc

Best way to handle this relation in Room Database

I have three tables and I have a many to many relationship here:
Contact table:
#Entity(tableName = "T_Contacts")
class Contact(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "_id") var id: Long,
#ColumnInfo(name = "phoneNumber")
var phoneNumber: String = "",
#ColumnInfo(name = "name")
var name: String = "",
#ColumnInfo(name = "active")
var active: Int = 1
#ColumnInfo(name = "contactId")
var contactId: Long = -1
#ColumnInfo(name = "phoneContactId")
var phoneContactId: String = ""
}
Email table:
#Entity(tableName = "T_EmailAddress")
class EmailAddress(
#PrimaryKey(autoGenerate = true) #ColumnInfo(name = "_id")
var id : Long,
#ColumnInfo(name="emailAddress")
var emailAddress: String,
#ColumnInfo(name="active")
var active : Int) {
}
And the associative table:
#Entity(tableName = "T_EmailAddressContact",
foreignKeys = [ForeignKey(
entity = Contact::class,
parentColumns = arrayOf("contactId"),
childColumns = arrayOf("contactId"),
onDelete=ForeignKey.CASCADE),
ForeignKey(
entity = EmailAddress::class,
parentColumns = arrayOf("emailId"),
childColumns = arrayOf("emailId"),
onDelete=ForeignKey.CASCADE)]
)
class EmailAddressContactCrossRef(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "_id")
var id: Long,
#ColumnInfo(name="contactId")
var contactId: Long,
#ColumnInfo(name="emailId")
var emailId : Long,
#ColumnInfo(name="active")
var active : Int ) {
How can I get a list of all the email contacts using relations? I tried:
data class EmailAddressContact(
#Embedded val contact: EmailAddress,
#Relation(parentColumn = "contactId",
entityColumn = "contactId",
associateBy = Junction(EmailAddressContactCrossRef::class)
)
val listContacts: List<Contact>
)
but this only gets me a list of all contacts for a certain email address. I want something like this:
data class EmailAddressContact {
val id: Long,
val emailAddress: EmailAddress,
val contact: Contact,
val active: Boolean
}
by calling a query:
#Transaction
#Query("SELECT * FROM T_EmailAddressContact")
fun getAllEmailAddressAndContacts(): List<EmailAddressContact>
According to your EmailAddressContact structure it seems you don't need to use junction, just relations. Try this way:
data class EmailAddressContact(
#Embedded val emailAddressContactCrossRef: EmailAddressContactCrossRef, // <- there you'll get "id" and "active" fields
#Relation(
parentColumn = "contactId",
entityColumn = "contactId"
)
val contact: Contact,
#Relation(
parentColumn = "emailId",
entityColumn = "_id"
)
val emailAddress: EmailAddress
)
and query you mentioned in your question should fit

many-to-many relation in Room database. Kotlin Junction class ignored

I have the following Kotlin entities for Room ver. 2.2.5.
PARENT ENTITY
#Entity(tableName = "ITEM",
indices = [Index(value = ["id"], unique = true), Index(value = ["code"], unique = true), Index(value = ["status"], unique = false)]
)
data class Item (
#PrimaryKey #ColumnInfo(name = "id") override val id: UUID = UUID.randomUUID(),
#ColumnInfo(name = "code") #NotNull val code: String,
#ColumnInfo(name = "valid") val valid: Boolean = true,
#ColumnInfo(name = "value") val value: Double?,
#ColumnInfo(name = "price") val price: Double?,
#ColumnInfo(name = "default_description") val defaultDescription: String?,
#ColumnInfo(name = "description") val description: String?
)
CHILD ENTITY
#Entity(tableName = "LOCATION",
indices = [Index(value = ["id"], unique = true), Index(value = ["code"], unique = true)]
)
data class Location (
#ColumnInfo(name = "id") #PrimaryKey override val id: UUID = UUID.randomUUID(),
#ColumnInfo(name = "code") val code: String,
#ColumnInfo(name = "latitude") val latitude: Double?,
#ColumnInfo(name = "longitude") val longitude: Double?,
#ColumnInfo(name = "default_description") val defaultDescription: String?
)
JUNCTION ENTITY
#Entity(
tableName = "ITEM_LOCATION_L",
primaryKeys = [
"item_id", "location_id"
],
foreignKeys = [
ForeignKey(entity = Item::class, parentColumns = ["id"], childColumns = ["item_id"]),
ForeignKey(entity = Location::class, parentColumns = ["id"], childColumns = ["location_id"])
],
indices = [
Index("id"),
Index("item_id"),
Index("location_id")])
data class ItemLocationLink (
#ColumnInfo(name = "id") override val id: UUID = UUID.randomUUID(),
#ColumnInfo(name = "item_id") val itemId: UUID, /** Item ID - parent entity */
#ColumnInfo(name = "location_id") val locationId: UUID, /** Location ID - child entity */
#ColumnInfo(name = "quantity") val quantity: Double /** Quantity of the item in the referenced location */
)
RESULT CLASS
class ItemLocationRelation {
#Embedded
lateinit var item: Item
#Relation(
parentColumn = "id",
entityColumn = "id",
associateBy = Junction(value = ItemLocationLink::class, parentColumn = "item_id", entityColumn = "location_id")
) lateinit var locations: List<Location>
}
DAO INTERFACE
#Dao
interface ItemLocationLinkDao {
#Transaction
#Query("SELECT * FROM ITEM WHERE id = :itemId")
fun getLocationsForItem(itemId: UUID): List<ItemLocationRelation>
}
DATABASE TYPE CONVERTER
class DBTypesConverter {
#TypeConverter
fun fromUUID(uid: UUID?): String? {
if (uid != null)
return uid.toString()
return null
}
#TypeConverter
fun toUUID(str: String?): UUID? {
if (str != null) {
return UUID.fromString(str)
}
return null
}
#TypeConverter
fun fromDate(d: Date?): Long? {
return d?.time
}
#TypeConverter
fun toDate(l: Long?): Date? {
return if (l != null) Date(l) else null
}
}
When I call getLocationsForItem I get in return an instance of ItemLocationRelation with a valid Item but no child objects. I've checked the generated code and there is no sign of the Junction class. The generated code behaves like it is not a many-to-many relation, the Junction class is completely ignored, I can even specify a fake class in the #Junction attribute of the relation and the result would be exactly the same without errors.
If I add a function to the DAO class that returns the results of the following query:
select * from item
inner join item_location_l as link on link.item_id = item.id
inner join LOCATION as l on link.location_id = l.id
where item.id = '99a3a64f-b0e6-e911-806a-68ecc5bcbe06'
I get 2 rows as expected. So the SQLite database is ok. Please help.
So, in the end the problem was really subtle. For reasons I cannot explain the project I was working on had the following gradle settings (app gradle):
kapt "android.arch.persistence.room:compiler:2.2.5"
I replaced it with
kapt "androidx.room:room-compiler:2.2.5"
And then everything was fine. The hard-coded query generated by the plugin now contains the JOIN and it works....

Get column from another entity using foreign keys in Room

I have two different entities. One has two references to the other one and I need to get a attribute of the reference.
my_main_table.primary_type is a foreign key of types._id and my_main_table.secondary_type is a foreign key of types._id that can be null.
Is a prepopulated database copied using RoomAsset library, so the scheme is already done in the database. Here is the diagram of the database:
Here is my main entity:
#Entity(
tableName = "my_main_table",
foreignKeys = [
ForeignKey(
entity = Type::class,
parentColumns = ["_id"],
childColumns = ["secondary_type"],
onDelete = ForeignKey.RESTRICT,
onUpdate = ForeignKey.RESTRICT
),
ForeignKey(
entity = Type::class,
parentColumns = ["_id"],
childColumns = ["primary_type"],
onDelete = ForeignKey.RESTRICT,
onUpdate = ForeignKey.RESTRICT
)
]
)
data class MainTable(
#PrimaryKey
#ColumnInfo(name = "_id", index = true)
val id: Int,
#ColumnInfo(name = "number")
val number: String,
#ColumnInfo(name = "name")
val name: String,
#ColumnInfo(name = "primary_type")
val primaryType: String,
#ColumnInfo(name = "secondary_type")
val secondaryType: String?
)
And here is my reference:
#Entity(tableName = "types")
data class Type(
#PrimaryKey
#ColumnInfo(name = "_id")
val id: Int,
#ColumnInfo(name = "name")
val name: String
)
Finally the SQL code for #Query:
SELECT p._id AS _id,
p.number AS number,
p.name AS name,
pt.name AS primary_type,
st.name AS secondary_type
FROM my_main_table p
INNER JOIN types pt ON p.primary_type == pt._id
LEFT JOIN types st ON p.secondary_type == st._id
What I want is to get the value of types.name throught the relation. But I can't figure out how. Should I need another method in my repository to get the value of the name?
Thanks.

Android Room Insert To Join Table Doesn't Work

I've been looking around and couldn't find any explanation why this was happening.
I've tried opening my db file in apps like sqliteman and running "INSERT INTO" queries myself and it works just fine that way. Yet, whatever Room does doesn't seem to be working, it doesn't crash or anything, it just doesn't insert anything.
So basically I have 2 tables connected in a m-n relation via a join table:
1) Song table
#Entity(tableName = RoomDbConstants.TABLE_NAME_SONGS,
foreignKeys = [ForeignKey(entity = Album::class,
parentColumns = [RoomDbConstants.COLUMN_ALBUM_ARTIST_NAME, RoomDbConstants.COLUMN_ALBUM_NAME],
childColumns = [RoomDbConstants.COLUMN_SONG_ARTIST_NAME, RoomDbConstants.COLUMN_SONG_ALBUM_NAME],
onDelete = ForeignKey.CASCADE),
ForeignKey(entity = Genre::class,
parentColumns = [RoomDbConstants.COLUMN_GENRE_NAME],
childColumns = [RoomDbConstants.COLUMN_SONG_GENRE_NAME],
onDelete = ForeignKey.CASCADE)])
data class Song(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_ID)
val id: Long?,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_DATA)
val songData: String,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_NAME)
val name: String,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_ALBUM_NAME)
val albumName: String,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_ARTIST_NAME)
val artistName: String,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_GENRE_NAME)
val genreName: String,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_TRACK_NUMBER)
val trackNumber: Int = RoomDbConstants.TRACK_NUMBER_DEFAULT,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_YEAR_PUBLISHED)
val yearPublished: Int = RoomDbConstants.YEAR_PUBLISHED_NONE,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_START_TIME_IN_MILLIS)
val startTimeInMillis: Int,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_END_TIME_IN_MILLIS)
val endTimeInMillis: Int,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_DURATION_IN_MILLIS)
val durationInMillis: Int,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONG_COVER_DATA)
val coverData: String?)
2) Playlist table
#Entity(tableName = RoomDbConstants.TABLE_NAME_PLAYLISTS,
indices = [Index(value = [RoomDbConstants.COLUMN_PLAYLIST_NAME], unique = true)])
data class Playlist(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = RoomDbConstants.COLUMN_PLAYLIST_ID)
val id: Long?,
#ColumnInfo(name = RoomDbConstants.COLUMN_PLAYLIST_NAME)
val name: String,
#ColumnInfo(name = RoomDbConstants.COLUMN_PLAYLIST_COVER_DATA)
val coverData: String?)
3) SongsPlaylistsLink (The table that connects between Songs and Playlists)
#Entity(tableName = RoomDbConstants.TABLE_NAME_SONGS_PLAYLISTS_LINKS,
primaryKeys = [RoomDbConstants.COLUMN_SONGS_PLAYLISTS_LINK_SONG_ID,
RoomDbConstants.COLUMN_SONGS_PLAYLISTS_LINK_PLAYLIST_ID],
foreignKeys = [ForeignKey(entity = Song::class,
parentColumns = [RoomDbConstants.COLUMN_SONG_ID],
childColumns = [RoomDbConstants.COLUMN_SONGS_PLAYLISTS_LINK_SONG_ID],
onDelete = ForeignKey.CASCADE),
ForeignKey(entity = Playlist::class,
parentColumns = [RoomDbConstants.COLUMN_PLAYLIST_ID],
childColumns = [RoomDbConstants.COLUMN_SONGS_PLAYLISTS_LINK_PLAYLIST_ID],
onDelete = ForeignKey.CASCADE)],
indices = [Index(value = [RoomDbConstants.COLUMN_SONGS_PLAYLISTS_LINK_SONG_ID,
RoomDbConstants.COLUMN_SONGS_PLAYLISTS_LINK_PLAYLIST_ID], unique = true)])
data class SongsPlaylistsLink(
#ColumnInfo(name = RoomDbConstants.COLUMN_SONGS_PLAYLISTS_LINK_SONG_ID)
val songId: Long,
#ColumnInfo(name = RoomDbConstants.COLUMN_SONGS_PLAYLISTS_LINK_PLAYLIST_ID)
val playlistId: Long)
Insert function:
#Insert
fun insert(songsPlaylistsLink: SongsPlaylistsLink)
So here's what I tried to do:
I already have songs and playlists in the database.
So let's say I have a song with an ID of 1 and a playlist with an ID of 1.
When I do insert(SongsPlaylistsLink(1,1)) nothing happens.
The table is still empty.
Any help on why this happens will be greatly appreciated!
Looks like running db.beginTransaction() and db.endTransaction() before and after the insert statements respectively doesn't work all that well. Removing it fixed it for me.

Categories

Resources