How to count child entities with room efficiently? - android

I have those entities in my app:
#Entity(tableName = "parents")
data class Parent(
#PrimaryKey
val name: String = "",
val numberOfChildren: Int = 0
)
#Entity(tableName = "children")
data class Children(
#PrimaryKey
val parentName: String = "",
val name: String = ""
)
I want to update numberOfChildren to count how many children per parent.
Initially I thought about fetch the children, count and insert to parents and then again to the database but It seems to me very inefficient with a lots of db calls.
what's the best/efficient way to count number of children(based on parent name) and update numberOfChildren field?

i think best way is to create trigger, which will automatically update data in Parent table on insert/delete row in Children
example of how create trigger from sqlitetutorial.net
CREATE TRIGGER [IF NOT EXISTS] trigger_name
[BEFORE|AFTER|INSTEAD OF] [INSERT|UPDATE|DELETE]
ON table_name
[WHEN condition]
BEGIN
statements;
END;
so you create 2 triggers (one for insert and another for delete) will be something like this
create trigger if not exists CHILDREN_COUNT_TRIGGER
after insert on children
begin
update parents set numberOfChildren = (select count(*) from children where parentName = NEW.parentName) where name = NEW.parentName
end;
and same for delete, but change NEW to OLD

Related

Failed rename column while join query

I have a query:
#Query("SELECT entries.*, myProfile.myProfileId as my_profile_id FROM Entry as entries LEFT JOIN MyProfile ON entries.user_public_profile_userId = myProfile.myProfileId")
I just need to get value from second(right) table and put it into entitys' field - "my_profile_id" from first table. But nothing works.
this is how field from first table(left) entity look like -
#ColumnInfo(name = "my_profile_id")
val myUserProfileId: Int? = null,
and this is how it look like this field from second table (right)
#PrimaryKey
val myProfileId: Int,
Result is always null, but condition after ON is working because i tried to change from LEFT JOIN to INNER JOIN ad got results, so the only problem here is to map correctly 'myProfileId' into 'my_profile_id'
What am i doing wrong?
Irrespective of the JOIN type using :-
SELECT entries.*, myProfile.myProfileId as my_profile_id FROM Entry as entries LEFT JOIN MyProfile ON entries.user_public_profile_userId = myProfile.myProfileId
Will result in my_profile_id being the same value that is stored in the user_public_profile_userId column. In that sense the JOIN is a waste of time.
I suspect that you may want to get another useful value from the MyProfile table.
Assuming that you have an Entry entity that is along the lines of :-
#Entity
data class Entry(
#PrimaryKey
val id: Long? = 0,
val name: String,
#ColumnInfo(name = "my_profile_id")
val myUserProfileId: Long? = null,
val user_public_profile_userId: Long
)
And an MyProfile entity that is along the lines of :-
#Entity
data class MyProfile(
#PrimaryKey
val myProfileId: Long? = null,
val otherdata: String
)
and that you want to get the value from the otherdata column then you need an additional POJO to combine the data.
As such consider such a POJO EntryPlus :-
data class EntryPlus(
#Embedded
val entry: Entry,
val profileIdFromMyProfile: Long,
val otherdataFromMyProfile: String
)
#Embedded and the following line is saying that you want all the columns/fields from the Entry table
The other two columns will be from elsewhere (satisfied by the query)
So you could have a Query such as :-
#Query("SELECT entries.*, myProfile.myProfileId AS profileIdFromMyProfile, myProfile.otherdata AS otherdataFromMyProfile FROM Entry as entries LEFT JOIN MyProfile ON entries.user_public_profile_userId = myProfile.myProfileId")
fun getMyOtherData(): List<EntryPlus>
i.e. the query is little different BUT importantly uses AS to name the output columns accordingly to suit the names of the fields in the EntryPlus POJO.
also importantly the result is a list of the POJO (EntryPlus).
Example
Consider the following code that:
inserts some data (3 MyProfile rows and 2 Entry rows) and then
extracts All the Entry rows with no JOIN using SELECT * FROM entry and then
extracts using your original query and then
extracts via the POJO
The code is :-
db = TheDatabase.getInstance(this) // Get database instance
dao = db.getAllDao() // get the Dao
dao.deleteAllMyProfile() // Clear the MyProfile table
dao.deleteAllEntry() // Clear the Entry table
// Add some profile rows
dao.insert(MyProfile(1,"myprofile1"))
dao.insert(MyProfile(2,"myprofile2"))
dao.insert(MyProfile(3,"myprofile3"))
// Add some Entry rows (both link to profile1 in this case)
dao.insert(Entry(100,"Entry1",0,1))
dao.insert(Entry(200,"Entry2",0,1))
// Extract 1 All as Entry List (no join)
for(e: Entry in dao.getAll()) {
Log.d("ENTRYINFO(1)","Entry Name is ${e.name} EntryID is ${e.id} MapToMyProfile is ${e.user_public_profile_userId} Value is ${e.myUserProfileId}" )
}
// Extract 2 All from original query
for(e: Entry in dao.getMyData()) {
Log.d("ENTRYINFO(2)","Entry Name is ${e.name} EntryID is ${e.id} MapToMyProfile is ${e.user_public_profile_userId} Value is ${e.myUserProfileId}" )
}
// Extract 3 getting useful data from the 2nd (JOINED) table
for(ep: EntryPlus in dao.getMyOtherData()) {
Log.d("ENTRYINFO(3)",
"Entry Name is ${ep.entry.name} EntryID is ${ep.entry.id} MapToMyProfile is ${ep.entry.user_public_profile_userId} Myuserprofile(From Entry) ${ep.entry.myUserProfileId}" +
" MyProfileId (From MyProfile) is ${ep.profileIdFromMyProfile} OtherData (From MyProfile) is ${ep.otherdataFromMyProfile}" )
}
The output to the Log is :-
2021-07-07 09:44:12.665 D/ENTRYINFO(1): Entry Name is Entry1 EntryID is 100 MapToMyProfile is 1 Value is 0
2021-07-07 09:44:12.665 D/ENTRYINFO(1): Entry Name is Entry2 EntryID is 200 MapToMyProfile is 1 Value is 0
2021-07-07 09:44:12.666 D/ENTRYINFO(2): Entry Name is Entry1 EntryID is 100 MapToMyProfile is 1 Value is 1
2021-07-07 09:44:12.666 D/ENTRYINFO(2): Entry Name is Entry2 EntryID is 200 MapToMyProfile is 1 Value is 1
2021-07-07 09:44:12.667 D/ENTRYINFO(3): Entry Name is Entry1 EntryID is 100 MapToMyProfile is 1 Myuserprofile(From Entry) 0 MyProfileId (From MyProfile) is 1 OtherData (From MyProfile) is myprofile1
2021-07-07 09:44:12.668 D/ENTRYINFO(3): Entry Name is Entry2 EntryID is 200 MapToMyProfile is 1 Myuserprofile(From Entry) 0 MyProfileId (From MyProfile) is 1 OtherData (From MyProfile) is myprofile1
Notes on the Output
The first two lines note that the MyProfileId value (i.e. Value =) is 0 as was inserted.
The Second two lines, using your original query shows that MyProfileId (Value =) is now the same value as the link/reference/association/relationship (i.e. the user_public_profile_userId column) to the MyProfile row.
The Third shows that the values from the MyProfile table (the otherdata column) have been extracted.
However, you are also appearing to describe the nature of LEFT JOIN in comparison to just JOIN.
If the following line (another Entry row but referencing a non-existent MyProfile row) were added before the extract:-
dao.insert(Entry(300,"Entry3",999,10 /* 10 = No such myprofile row */))
then the changes in the result will be significant in that the 2nd extract retrieves null for the my_profile_id as per :-
D/ENTRYINFO(2): Entry Name is Entry3 EntryID is 300 MapToMyProfile is 10 Value is null
changing to just JOIN (not LEFT JOIN) and the row which references the non-existent MyProfile is omitted. This is the documented impact as per :-
If the join-operator is a "LEFT JOIN" or "LEFT OUTER JOIN", then after the ON or USING filtering clauses have been applied, an extra row is added to the output for each row in the original left-hand input dataset that corresponds to no rows at all in the composite dataset (if any). The added rows contain NULL values in the columns that would normally contain values copied from the right-hand input dataset.
SQLite SELECT
In the case of null values, if this is your issue, then you need to decide what to do.
You could for example use the SQLite COALESCE function to change the null. e.g. SELECT entries.*, coalesce(myProfile.myProfileId,9999) as my_profile_id FROM Entry ....
Perhaps you need to ensure that the referential integrity is maintained, in which case you can utilise FOREIGN KEYS to enforce referential integrity. Perhaps refer to Foreign Key

Android Room: select list of custom objects removing duplicates by a specific property

I'm using Room library to retrive a List without duplicates for property "text".
This is the code of the query in MyObjectDao class:
#Query("SELECT DISTINCT * FROM historyentity WHERE text LIKE :inputText || '%'")
List<MyObject> findByText(String inputText);
I also post MyObject class:
#Entity
public class MyObject {
#PrimaryKey(autoGenerate = true)
public int uid;
#ColumnInfo(name = "text")
public String text;
#ColumnInfo(name = "timestamp")
public Long timestamp;
}
Anyway I still get get results with duplicates for field "text". How can I get a List that does not contains duplicates on field "text"?
For example if I have the following three elements in the database
database.addMyObject(new MyObject("dog", System.currentTimeInMills());
database.addMyObject(new MyObject("cat", System.currentTimeInMills());
database.addMyObject(new MyObject("dog", System.currentTimeInMills());
when I call my query I want to get only the first two elements.
If you want just list with unique text values you can try this one:
#Query("SELECT DISTINCT text FROM historyentity WHERE text LIKE :inputText || '%'")
List<String> findByText(String inputText); // <-- changed type to List<String>
UPDATE
You can try this query (it gets only one item with text value - with maximal id - or you can use maximal (or minimal) timestamp):
Select * from historyentity as t1
INNER JOIN (select text,max(uid) as uid from historyentity WHERE text LIKE :inputText group by text) t2
ON t1.text = t2.text and t1.uid = t2.uid

Android Room - Getting all base children of parent

Right now, my relation hierarchy is A -> (A -> ... -> A) -> B where there are some amount (could also be 0) of A in the middle and each arrow is a one-to-many relationship.
Each A "tree" will end in a B.
Each A has either A or B as a child (but not both)
My entities are:
#Entity(tableName = "table_A")
data class A(
id, // ID of the A
parentAid // ID of the parent A (nullable)
)
#Entity(tableName = "table_B")
data class B(
id, // ID of this B
parentAid // ID of the parent A (non-nullable)
)
Currently, my method is to query "SELECT COUNT(*) FROM table_A WHERE parentAid = :id" to get the amount of A a parent has. If this number is bigger than 0, repeat with each of the child A. Else, SELECT * FROM table_B WHERE parentAid = :id is run to get the list of B which is added to a list.
Is this an effective way of doing things? Is there a query in Room that can easily do this?
That can be achieved as below: (Am using Kotlin syntax, so please relate to convert the solution to Java)
Create another data class like below:
data class TableAWithTableB(
#Embedded val tableA: TableAEntity,
#Relation(
parentColumn = "parentAid",
entityColumn = "parentAid"
)
val tableBList: List<TableBEntity>
)
Now select from ur room Database as below:
#Transaction
#Query("SELECT * FROM table_A WHERE parentAid=:id")
fun selectTableAWithTableB(id:Int): List<TableAWithTableB>
In the above code, the query returns a list of values for A with B. (List), meaning for every A a list of values from B will be returned where the parentAid is a foreign key in B.
Note: For more information please refer to: https://developer.android.com/training/data-storage/room/relationships#one-to-many

room db update multiple rows with #query

I would like to update multiple rows in a room database. But I don't have the objects - I have only the ids.
If I update one row I write something like this to my DAO:
#Query("UPDATE items SET place = :new_place WHERE id = :id;")
fun updateItemPlace(id:Int, new_place:String)
With multiple rows I would need something like this:
#Query("UPDATE items SET place = :new_place WHERE id = :ids;")
fun updateItemPlaces(ids:List<Int>, new_place:String)
OR
#Query("UPDATE items SET place = :new_place WHERE id IN :ids;")
fun updateItemPlaces(ids:String, new_place:String)
where I write something like '(1,4,7,15)' to my ids-String
Can someone tell me a good way to make such an update
because something like
val ids = ListOf(1,4,7,15)
ids.forEach{
itemDao.updateItemPlace(it,'new place')
}
does not seem to be a good solution
#Query("UPDATE items SET place = :new_place WHERE id IN (:ids)")
fun updateItemPlaces(ids:List<Int>, new_place:String)
But keep in mind if your list of ids contains more than 999 items SQLite will throw an exception:
SQLiteException too many SQL variables (Sqlite code 1)
This is what you need for doing your work.
#Query("Update brand_table set name = :name where id In (:ids)")
void updateBrands(List<Integer> ids, String name);

Kotlin / SQLite delete() in batch issue

I would like to delete mutiple items from SQLite in batch basing on their ID column.
What I have is a HashMap which contains objects which one of field is pID (unique ID in DB).
So, here's my code:
/*
Delete rows from DB
*/
val selection = "${BaseColumns._ID} = ?"
// Create a list of product ID's to delete
val dbDeletor = dbHelper.writableDatabase
// Temp array to store ID's in String format
val tempIDs = ArrayList<String>()
// Loop through array of items to be deleted
for(i in ProductsRecyclerAdapter.productsToDeleteArray)
tempIDs.add(i.value.pID.toString())
// Perform deletion in DB
val deletedRowsCount = dbDeletor.delete(ProductsEntry.TABLE_NAME, selection, tempIDs.toTypedArray())
// Show snackbar with count of deleted items
Snackbar.make(mainCoordinatorLayout, "Products deleted: $deletedRowsCount", Snackbar.LENGTH_SHORT).show()
Everything works great when I'm deleting only 1 item but if tempIDs array contains 2 or more I'm receiving following Exception:
Too many bind arguments. 3 arguments were provided but the statement needs 1 arguments.
Maybe the reason is that I'm converting pID of type Long into String in order to delete rows in batch? I did not find any other solution. Please take a look and comment.
Your query looks somewhat like that:
DELETE FROM ProductsEntry.TABLE_NAME WHERE BaseColumns._ID = ?
There is only 1 argument ? but you're passing 3 values (IDs). You want to use IN statement instead, and print your params separated by comma:
// IN instead of equal to compare multiple values
val selection = "${BaseColumns._ID} IN (?)"
// your code to obtain IDs here
// .....
// combine all values into single string, ie. 1, 2, 3, 4 and wrap it as an array
val selectionArg = arrayOf(tempIDs.joinToString())
// Perform deletion in DB
val deletedRowsCount = dbDeletor.delete(ProductsEntry.TABLE_NAME, selection, selectionArg)

Categories

Resources