Android Room data modeling with embedded FKs - android

I'm trying to represent the following entities:
Warning: A warning entity with a unique id, title, subtitle, and up to 6 actions (below)
Action: An action entity with a unique id, text, icon, etc. relevant to zero or more warnings
Examples of this relationship would be:
Missile Lock warning with actions Evasive Maneuver, Release Chaff, and Deploy Flare.
Incoming gunfire warning with actions Evasive Maneuver, Return Fire, and GTFO
Here's what I have:
The Warning entity
#Entity(tableName = WARNING_TABLE,
indices = [Index(unique = true, value = [WARNING_TITLE, WARNING_SUBTITILE])]
)
data class Warning(
#field:PrimaryKey #ColumnInfo(name = WARNING_ID) var id: String,
#ColumnInfo(name = WARNING_TITLE) var text1: String,
#ColumnInfo(name = WARNING_SUBTITILE) var text2: String?,
#ColumnInfo(name = WARNING_ACTIVE, defaultValue = "0") var active: Boolean = false
)
The Action entity
#Entity(tableName = ACTION_TABLE,
indices = [
Index(unique = true, value = [ACTION_TEXT]),
Index(unique = true, value = [ACTION_ICON])
]
)
data class Action(
#field:PrimaryKey #ColumnInfo(name = ACTION_ID) var id: String,
#ColumnInfo(name = ACTION_TEXT) var text: String,
#ColumnInfo(name = ACTION_ICON) var icon: String,
#ColumnInfo(name = ACTION_ICON_SRC, defaultValue = "$DRAWABLE_SOURCE_RESOURCES") var iconSrc: DrawableSource? = DrawableSource.Resources
)
... and the table that links the two, the warning_action table:
#Entity(tableName = WARNING_ACTION_TABLE,
primaryKeys = [WARNING_ID, ACTION_ID],
foreignKeys = [
ForeignKey(entity = Warning::class,
parentColumns = [WARNING_ID],
childColumns = [WARNING_ID],
onUpdate = ForeignKey.NO_ACTION,
onDelete = ForeignKey.CASCADE),
ForeignKey(entity = Action::class,
parentColumns = [ACTION_ID],
childColumns = [ACTION_ID],
onUpdate = ForeignKey.NO_ACTION,
onDelete = ForeignKey.CASCADE)
],
indices = [Index(ACTION_ID), Index(WARNING_ID)]
)
data class WarningAction(#ColumnInfo(name = ACTION_ID) val action: String,
#ColumnInfo(name = WARNING_ID) val warning: String)
Now what I want to do is to be able to load a list of all Warnings and their associated Actions (or a Single warning and its associated actions).
This is what I have so far, but it isn't working:
data class WarningWithActions(#Embedded val warning: Warning,
#Relation(parentColumn = WARNING_ID,
entityColumn = ACTION_ID,
associateBy = Junction(WarningAction::class))
val actions: List<Action>){
val id get() = Warning.id
}
... and in the DAO:
#Query("SELECT * FROM '$WARNING_TABLE' WHERE $WARNING_ID = :id")
fun load (id:String): Maybe<WarningWithActions>
I also tried:
data class WarningWithActions(#Embedded val warning: Warning,
#Relation(parentColumn = WARNING_ID,
entity= Action::class,
entityColumn = ACTION_ID,
associateBy = Junction(value = WarningAction::class, parentColumn = WARNING_ID, entityColumn = ACTION_ID))
val actions: List<Action>){
val id get() = warning.id
}
I get an empty list of actions and I'm pretty sure I'm missing something that tells room how to get those, but I can't figure out what I'm missing here.
I've already used Database Inspector and verified that the actions exist, the warnings exist, and the link entries in the link table also exist.
UPDATE:
Turns out the above implementation is correct. The DAO method that was adding the action to the warning had a Single<Long> (RX) return value:
#Transaction
#Query ("INSERT OR REPLACE INTO '$WARNING_ACTION_TABLE' ($WARNING_ID, $ACTION_ID) VALUES (:wid, :aid)")
fun addAction (eid:String, aid:String):Single<Long>
I guess at some point while writing the unit test that tested this, I was distracted and forgot to subscribe()

Related

How to use onDelete = RESTRICT in Room?

To prevent the deletion of a parent row which has one or more related child rows in my Room database, I've set my ForeignKey onDelete method to RESTRICT.
My database has two tables: products and document_products which has the ForeignKey on products, during the application usage, the user is able to delete all items from the products table but I need to still keep the items in document_products but the RESTRICT seems not to be working as even with it I'm getting:
FOREIGN KEY constraint failed (code 1811 SQLITE_CONSTRAINT_TRIGGER)
My DocumentProduct entity looks like this:
#JsonClass(generateAdapter = true)
#Entity(
tableName = "document_products",
foreignKeys = [
ForeignKey(
entity = Document::class,
parentColumns = ["id"],
childColumns = ["documentId"],
onDelete = CASCADE
),
ForeignKey(
entity = Product::class,
parentColumns = ["products_id"],
childColumns = ["document_products_productIdMap"],
onDelete = RESTRICT
)
],
indices = [Index("document_products_productIdMap"), Index("documentId"), Index(
value = ["document_products_productIdMap", "documentId", "labelType"],
unique = true
)]
)
data class DocumentProduct(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "document_products_id")
var id: Long,
#ColumnInfo(name = "document_products_productIdMap")
var productId: String,
#ColumnInfo(name = "document_products_quantity")
var quantity: Float,
var orderQuantity: Float,
#ColumnInfo(name = "document_products_purchase")
var purchase: Float,
var documentId: Long,
var labelType: String?,
var timestamp: Long?
)
While Product:
#Entity(tableName = "products")
open class Product(
#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "products_id")
open var id: String,
open var description: String?,
#ColumnInfo(defaultValue = "PZ")
open var unitOfMeasure: String,
#ColumnInfo(name = "products_purchase")
open var purchase: Float,
open var price: Float,
#ColumnInfo(name = "products_quantity")
open var quantity: Float
)
And in the application settings the user is able to run the following query from ProductDAO:
#Query("DELETE FROM products")
suspend fun deleteAll(): Int
What I'm looking for is a solution in which I can keep the parent rows which has one or more related child rows OR where I can keep the ForeignKey without a real relation.
RESTRICT is working as intended, it is not meant to exit quietly and leave things as before. Rather it is used to immediately exit rather than at the end of the current statement as per:-
RESTRICT: The "RESTRICT" action means that the application is prohibited from deleting (for ON DELETE RESTRICT) or modifying (for ON UPDATE RESTRICT) a parent key when there exists one or more child keys mapped to it. The difference between the effect of a RESTRICT action and normal foreign key constraint enforcement is that the RESTRICT action processing happens as soon as the field is updated - not at the end of the current statement as it would with an immediate constraint, or at the end of the current transaction as it would with a deferred constraint. Even if the foreign key constraint it is attached to is deferred, configuring a RESTRICT action causes SQLite to return an error immediately if a parent key with dependent child keys is deleted or modified.
https://www.sqlite.org/foreignkeys.html#fk_actions
You could simply not use Foreign Keys they are not mandatory for a relationship to exist. They are to enforce referential integrity.
An alternative approach with referential integrity would be to have the Products and DocumentsProducts independent relationships wise (i.e. drop the Foregin Keys and the productId column) and to then have an table for any relationships this catering for a many-many relationship between Products and DocumentProducts (which inherently supports 1-many and 1-1).
Such a table (a mapping table/crossref table/associative table ....) would have 2 columns one for the reference/map/association with the Product, the other for the DocumentProduct. You could have 2 Foreign Keys and also you could CASCADE for when a deletion happens.
The Delete (and update if coded) would CASCADE to this table not to the Product or the DocumentProduct, thus just removing cross reference between the two.
The Primary Key would be a composite of the two columns, you would have to use the primaryKey parameter of the #Entity annotation to define this.
The following code is along the lines of what would suit:-
#Entity(
tableName = "document_products",/*
foreignKeys = [
ForeignKey(
entity = Document::class,
parentColumns = ["id"],
childColumns = ["documentId"],
onDelete = CASCADE
),
ForeignKey(
entity = Product::class,
parentColumns = ["products_id"],
childColumns = ["document_products_productIdMap"],
onDelete = RESTRICT
)
],*/
indices = [/*Index("document_products_productIdMap"),*/ Index("documentId"), Index(
value = [/*"document_products_productIdMap",*/ "documentId", "labelType"],
unique = true
)]
)
data class DocumentProduct(
#PrimaryKey(autoGenerate = true)
#ColumnInfo(name = "document_products_id")
var id: Long,
//#ColumnInfo(name = "document_products_productIdMap")
//var productId: String,
#ColumnInfo(name = "document_products_quantity")
var quantity: Float,
var orderQuantity: Float,
#ColumnInfo(name = "document_products_purchase")
var purchase: Float,
var documentId: Long,
var labelType: String?,
var timestamp: Long?
)
#Entity(tableName = "products")
open class Product(
#PrimaryKey(autoGenerate = false)
#ColumnInfo(name = "products_id")
open var id: String,
open var description: String?,
#ColumnInfo(defaultValue = "PZ")
open var unitOfMeasure: String,
#ColumnInfo(name = "products_purchase")
open var purchase: Float,
open var price: Float,
#ColumnInfo(name = "products_quantity")
open var quantity: Float
)
#Entity(
primaryKeys = ["productIdMap","documentProductIdMap"],
foreignKeys = [
ForeignKey(
entity = Product::class,
parentColumns = ["products_id"],
childColumns = ["productIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
),
ForeignKey(
entity = DocumentProduct::class,
parentColumns = ["document_products_id"],
childColumns = ["documentProductIdMap"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class ProductDocumentProductMap(
val productIdMap: Long,
#ColumnInfo(index = true)
var documentProductIdMap: Long
)
note commenting out code has been used to indicate code that isn't needed or must be changed to suit.

How to Query in a many to many relationship Room database?

I have a many to many relationship Room database with three tables:
First one :
data class Name(
#PrimaryKey(autoGenerate = true)
var nameId : Long = 0L,
#ColumnInfo(name = "name")
var name : String = "",
#ColumnInfo(name = "notes")
var notes: String=""
)
Second:
#Entity(tableName = "tags_table")
data class Tag(
#PrimaryKey(autoGenerate = true)
var tagId : Long = 0L,
#ColumnInfo(name = "tag_name")
var tagName : String = ""
)
Third:
#Entity(
tableName = "tagInName_table",
primaryKeys = ["nameId", "tagId"],
foreignKeys = [
ForeignKey(
entity = Name::class,
parentColumns = ["nameId"],
childColumns = ["nameId"]
),
ForeignKey(
entity = Tag::class,
parentColumns = ["tagId"],
childColumns = ["tagId"]
)
]
)
data class TagInName(
#ColumnInfo(name = "nameId")
var nameId: Long = 0L,
#ColumnInfo(name = "tagId")
var tagId: Long = 0L
)
The data class I use for a return object in a Query:
data class NameWithTags(
#Embedded
val name: Name,
#Relation(
parentColumn = "nameId",
entityColumn = "tagId",
associateBy = Junction(value = TagInName::class)
)
val listOfTag : List<Tag>
)
This is how I query to get all NamesWithTags:
#Query("SELECT * FROM names_table")
#Transaction
fun getNamesWithTags() : LiveData<List<NameWithTags>>
So the thing I need to do is, I need to Query to return LiveData<List<NameWithTags>> where every NamesWithTags has a list which contains the Tag ID that I Query for.
From my interpretation of what you say you need to do, then :-
#Transaction
#Query("SELECT names_table.* FROM names_table JOIN tagInName_table ON names_table.nameId = tagInName_table.nameId JOIN tags_table ON tagInName_table.tagId = tags_table.tagId WHERE tags_table.tagId=:tagId ")
fun getNameWithTagsByTagId(tagId: Long): LiveData<List<NamesWithTags>>
Note the above is in-principle code and has not been compiled or tested, so it may contain some errors.
A NameWithTags will contain ALL related tags whcih should be fine according to (where every NamesWithTags has a list which contains the Tag ID ), if you wanted just certain Tags in the List of Tags then it's a little more complex, this is explained in a recent answer at Android Room query with condition for nested object

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

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.

Categories

Resources