How to use onDelete = RESTRICT in Room? - android

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.

Related

How to query from Multiple Tables

I have three Models
#Entity(foreignKeys = [ForeignKey(entity = SelfHelpGroup::class, parentColumns = ["shgId"], childColumns = ["shgId"], onDelete = CASCADE), ForeignKey(entity = Member::class, parentColumns = ["memberId"], childColumns = ["memberId"], onDelete = CASCADE)])
data class Committee(
#PrimaryKey(autoGenerate = true)
#SerializedName("committeeId")
val committeeId: Int?= null,
#SerializedName("shgId")
val shgId: Int?,
#SerializedName("memberId")
val memberId: Int?,
#SerializedName("date")
val date: String?
)
#Entity(tableName = "Member", foreignKeys = [ForeignKey(entity = SelfHelpGroup::class, parentColumns = ["shgId"], childColumns = ["shgId"], onDelete = CASCADE), ForeignKey(entity = Role::class, parentColumns = ["roleId"], childColumns = ["roleId"], onDelete = CASCADE)])
data class Member(
#PrimaryKey(autoGenerate = true)
#SerializedName("memberId")
val memberId: Int ?= null,
#SerializedName("shgId")
val shgId: Int,
#SerializedName("name")
val name: String,
#SerializedName("address")
val address: String,
#SerializedName("phoneNumber")
val phoneNumber: String,
#SerializedName("emailId")
val emailId: String,
#SerializedName("roleId")
val roleId: Int?,
#SerializedName("password")
val password: String?
)
#Entity(foreignKeys = [
ForeignKey(entity = Committee::class, parentColumns = ["committeeId"], childColumns = ["committeeId"], onDelete = CASCADE),
ForeignKey(entity = Member::class, parentColumns = ["memberId"], childColumns = ["memberId"], onDelete = CASCADE),
])
data class Attendance(
#PrimaryKey(autoGenerate = true)
#SerializedName("attendanceId")
val attendanceId: Int?= null,
#SerializedName("committeeId")
val committeeId: Int,
#SerializedName("memberId")
val memberId: Int,
/*#SerializedName("status")
val status: AttendanceStatus,*/
#SerializedName("isPresent")
var isPresent: Boolean = false,
#SerializedName("leaveApplied")
var leaveApplied: Boolean = false
)
Relation between 3 models :
Any member can host a committee.
The hosted memberId is saved in the table Member.
Other members can join the committee.
To track the attendance of these members, we are using the Table Attendance.
So I need help queriying the data in such a way that the result structure would look like below
data class CommitteeDetails (
val committeeId: Int,
val member: Member,
val attendances: List<Attendance>,
val dateTime: String
)
Since there are more than many committees, I need to query to get Listof CommitteeDetails
val committees = List<CommitteeDetails>()
The easiest way would be to use:-
data class CommitteeDetails (
//val committeeId: Int, /* part of the Committee so get the Committee in it's entireity */
#Embedded
val committee: Committee,
#Relation(entity = Member::class, parentColumn = "memberId", entityColumn = "memberId")
val member: Member,
#Relation(entity = Attendance::class, parentColumn = "committeeId", entityColumn = "committeeId")
val attendances: List<Attendance>
//val dateTime: String
)
This does then retrieve a little more information but the Query is very simple as you just query the committee table.
e.g. to get ALL Committees with the Member and the attendances then you can use
#Transaction
#Query("SELECT * FROM committee")
#Tranaction is not mandatory but if not used will result in a warning e.g.
warning: The return value includes a POJO with a `#Relation`. It is usually desired to annotate this method with `#Transaction` to avoid possibility of inconsistent results between the POJO and its relations. See https://developer.android.com/reference/androidx/room/Transaction.html for details.
This is because #Relation results in Room effectively running subqueries to obtain the related items. Note that using #Relation will return ALL the related items for the #Embedded.
IF you wanted to get exactly what you have asked for then it's a little more convoluted as you would then have to not use #Embedded for the Committee and thus you could then not use #Relation.
In theory you would SELECT FROM the committee table, JOIN the member table and also JOIN the attendance table. The issue is that the result is the cartesian map so for every attendance per committee you would get a result that contained the committee columns (id and date) the member columns (all to build the full Member class) and all the columns from the attendance. However, the issue, is then building the CommitteeDetails.
However, you can mimic how room works and just get the desired committee column along with the member columns and then invoke a subquery to obtain the related attendances (potentially filtering them).
So say you have (wanting a List of these as the end result):-
data class CommitteeDetailsExact (
val committeeId: Int,
val member: Member,
val attendances: List<Attendance>,
val dateTime: String
)
The to facilitate the initial extraction of the committee and members columns you could have another POJO such as:-
data class CommitteeIdAndDateAsDateTimeWithMember(
val committeeId: Int,
#Embedded
val member: Member,
val dateTime: String
)
To facilitate extracting the data you could have functions such as:-
#Query("SELECT committee.committeeId, committee.date AS dateTime, member.* FROM committee JOIN member ON committee.memberId = member.memberId")
fun getCommitteeExactLessAttendances(): List<CommitteeIdAndDateAsDateTimeWithMember>
#Query("SELECT * FROM attendance WHERE committeeId=:committeeId")
fun getCommitteeAttendances(committeeId: Int): List<Attendance>
To obtain the end result then the above functions need to be used together, so you could have a function such as:-
#Transaction
#Query("")
fun getExactCommitteeDetails(): List<CommitteeDetailsExact> {
var rv = arrayListOf<CommitteeDetailsExact>()
for (ciadadwm in getExactCommitteeDetails()) {
rv.add(CommitteeDetailsExact(ciadadwm.committeeId,ciadadwm.member,getCommitteeAttendances(ciadadwm.committeeId),ciadadwm.dateTime))
}
return rv.toList()
}
This will:-
return the desired list of committee details (albeit List<CommitteeDetailsExact> to suite the two answers given)
run as a single transaction (the #Query(") enables Room to apply the #Transaction)
Obtains the list of committee and member columns an then
Loops through the list extract the respective list of attendances
in short it, in this case, is very much similar to the first answer other than limiting the columns extracted from the committee table.

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 "Entities and POJOs must have a usable public constructor"

I have 3 entities:
product_table
#Entity(tableName = "products_table")
data class Product (
#PrimaryKey(autoGenerate = true) val id: Int,
val name: String,
val price: Double
)
planned_lists_table
#Entity(tableName = "planned_lists_table")
data class PlannedList (
#PrimaryKey(autoGenerate = true) val id: Int,
val name: String,
val budget: Double
)
and selected_products_table
#Entity(tableName = "selected_products_table",
foreignKeys = [
ForeignKey(
entity = PlannedList::class,
parentColumns = ["id"],
onDelete = ForeignKey.CASCADE,
childColumns = ["productListId"])
])
data class SelectedProduct (
#PrimaryKey(autoGenerate = true) val id: Int,
val productListId: Int,
#Embedded(prefix = "ref_") val product: Product,
val amount: Int,
val checked: Boolean // checked if a user has gotten a product
)
I'm trying to figure out how to fetch data from both tables - product_table and selected_products_table. I've chosen #Embedded annotation, but now when I compile the project, Room says next:
Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type). - kotlin.Unit
I'm aware that I can't use embedded classes that contain something like List and my tables seem not to have any.
What do I need to do to fix the problem?
It appears that you are getting mixed up with the selected_products_table.
I believe that what you are looking for is like (see note) :-
#Entity(tableName = "selected_products_table",
foreignKeys = [
ForeignKey(
entity = Product::class,
parentColumns = ["id"],
onDelete = ForeignKey.CASCADE,
childColumns = ["productListId"])
])
data class SelectedProduct (
#PrimaryKey(autoGenerate = true) val id: Int,
#ColumnInfo(index = true) val productListId: Int,
/* you get the product via the join/relationship according to the productListId*/
//#Embedded(prefix = "ref_") val product: Product, /* you get the product via a join*/
val amount: Int,
val checked: Boolean // checked if a user has gotten a product
)
#ColumnInfo added to add an index on the ProductListId column
comments explain why you don't want the embedded here.
Note it is unclear whether you are relating to a PlannedList or a Product so Product has been used (easy to switch)
You probably want to present the list including the products details.
To extract this data you then have a POJO for the joined/related data, such as:-
data class SelectedProductList(
#Embedded
val selectedProduct: SelectedProduct,
#Relation(entity = Product::class,parentColumn = "productListId",entityColumn = "id")
val product: Product
)
This is what undertakes the JOIN and populates the product with the data from the products_table.
As an example using your code, the above code and the following :-
dao.insert(Product(id = 1,name = "Apples", price = 30.55))
dao.insert(Product(id = 2,name = "Pears", price = 25.70))
dao.insert(Product(id = 3,"Oranges",price = 47.23))
dao.insert(SelectedProduct(1,1,25,false))
dao.insert(SelectedProduct(2,3,10,true))
for (spl: SelectedProductList in dao.getSelectedList()) {
Log.d(
"DBINFO",
"Product is ${spl.product.name} " +
"price is ${spl.product.price} " +
"Order is for ${spl.selectedProduct.amount} " +
"cost is ${spl.selectedProduct.amount * spl.product.price} " +
"Checked is ${spl.selectedProduct.checked}"
)
}
The the result output to the log is :-
D/DBINFO: Product is Apples price is 30.55 Order is for 25 cost is 763.75 Checked is false
D/DBINFO: Product is Oranges price is 47.23 Order is for 10 cost is 472.29999999999995 Checked is true

Android Room data modeling with embedded FKs

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()

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