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
Related
matching multiple title in single query using like keyword
I am trying to get all records if that matches with given titles.
below is the structure of database please see
database screenshot
when i pass single like query it returns data
#Query("SELECT * FROM task WHERE task_tags LIKE '%\"title\":\"Priority\"%'")
when i try to generate query dynamically to search multiple match it return 0 data
val stringBuilder = StringBuilder()
for (i in 0 until tags.size) {
val firstQuery = "%\"title\":\"Priority\"%"
if (i == 0) {
stringBuilder.append(firstQuery)
} else stringBuilder.append(" OR '%\"title\":\"${tags[i].title}\"%'")
}
this is function I have made
#Query("SELECT * FROM task WHERE task_tags LIKE:tagQuery ")
fun getTaskByTag(stringBuilder.toString() : String): List<Task>
The single data is fine. However, you simply cannot use the second method.
First you are omitting the space after LIKE,
Then you are omitting the full test i.e. you have task_ tags LIKE ? OR ?? when it should be task_tags LIKE ? OR task_tags LIKE ?? ....
And even then, due to the way that a parameter is handled by room the entire parameter is wrapped/encased as a single string, so the OR/OR LIKE's all become part of what is being searched for as a single test.
The correct solution, as least from a database perspective, would be to not have a single column with a JSON representation of the list of the tags, but to have a table for the tags and then, as you want a many-many relationship (a task can have many tags and a single tag could be used by many tasks) an associative table and you could then do the test using a IN clause.
As a get around though, you could utilise a RawQuery where the SQL statement is built accordingly.
As an example:-
#RawQuery
fun rawQuery(qry: SimpleSQLiteQuery): Cursor
#SuppressLint("Range")
fun getTaskByManyTags(tags: List<String>): List<Task> {
val rv = ArrayList<Task>()
val sb=StringBuilder()
var afterFirst = false
for (tag in tags) {
if (afterFirst) {
sb.append(" OR task_tags ")
}
sb.append(" LIKE '%").append(tag).append("%'")
afterFirst = true
}
if (sb.isNotEmpty()) {
val csr: Cursor = rawQuery(SimpleSQLiteQuery("SELECT * FROM task WHERE task_tags $sb"))
while (csr.moveToNext()) {
rv.add(
Task(
csr.getLong(csr.getColumnIndex("tid")),
csr.getString(csr.getColumnIndex("task_title")),
csr.getString(csr.getColumnIndex("task_tags"))))
// other columns ....
}
csr.close()
}
return rv
}
Note that the complex string with the embedded double quotes is, in this example, passed rather than built into the function (relatively simple change to incorporate) e.g. could be called using
val tasks1 = taskDao.getTaskByManyTags(listOf()) would return no tasks (handling no passed tags something you would need to decide upon)
val tasks2 = taskDao.getTaskByManyTags(listOf("\"title\":\"Priority\""))
val tasks3 = taskDao.getTaskByManyTags(listOf("\"title\":\"Priority\"","\"title\":\"Priority\"","\"title\":\"Priority\"")) obviously the tags would change
Very limited testing has been undertaken (hence just the 3 columns) but the result of running all 3 (as per the above 3 invocations) against a very limited database (basically the same row) results in the expected (as per breakpoint):-
the first returns the empty list as there are no search arguments.
the second and third both return all 4 rows as "title":"Priority" is in all 4 rows
the main reason for the 3 search args was to check the syntax of multiple args, rather than whether or not the correct selections were made.
The resultant query of the last (3 passed tags) being (as extracted from the getTaskaByManyTags function):-
SELECT * FROM task WHERE task_tags LIKE '%"title":"Priority"%' OR task_tags LIKE '%"title":"Priority"%' OR task_tags LIKE '%"title":"Priority"%'
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
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
I have two tables. Table A and Table B. I am using Android with Room and Reactive Streams with ktx.
The Table A has two columns Title, Ids.
Row 1 - ['example', '1,2,3,4'].
The Table B has two columns Id, Desc.
Row 1 - [1, 'long desc']
Row 2 - [2, 'long desc 2'].
I am using a Flowable to get the data from the database but they are two different streams.
How do I get a list of rows which are in Table B, that have the ID's in table A. Table A stores the ids of Table B as a string.
I don't know detail of your use case but do you mean like this?
interface TableADao {
fun findById(id: Long): Flowable<ItemA>
}
interface TableBDao {
fun findByIds(ids: Array<Long>): Flowable<List<ItemB>>
}
tableADao.findById(id)
.switchMap { itemA ->
val ids = someLongArrayConverter(itemA.ids)
tableBDao.findByIds(ids)
}
.subscribe { itemBList ->
Log.d("result", itemBList)
}
I would like to have method like this in my dao object
#Query("SELECT c.name, sum(p.value) FROM payments p, paymentCategories c WHERE p.categoryId = c.id GROUP BY c.name")
fun getCategoryStats(): Map<String, Float>
but i get the error
error: Not sure how to convert a Cursor to this method's return type
public abstract java.util.Map
Is it possible to change it to working version?
So it can be different type to return but the main conditions are
It must be only one query in db
I would like to avoid extra code like creating additional data structure only for this method
I'm little late for a party but maybe someone will still search for it.
From Room 2.4 we are able to use multimap return type with annotation #MapInfo which allow us to define the mapping for key and value.
In your case it will be sth like this:
#MapInfo(keyColumn = "name", valueColumn = "sum")
#Query("SELECT c.name AS name, sum(p.value) AS sum FROM payments p, paymentCategories c WHERE p.categoryId = c.id GROUP BY c.name")
fun getCategoryStats(): Map<String, Float>
More info:
https://developer.android.com/training/data-storage/room/accessing-data#multimap
While I don't think this can be done in the Dao it can easily be done when querying the LiveData in the repository or the viewModel (or where ever you are querying the list) using a transformation. Since I don't know your datanames I'm using made up ones:
val categoryStatsMap: LiveData<Map<String, Float>> =
Transformations.map(
database.categoryStatsDao.getCategoryStats()) {it ->
it.map {it.key to it.value}.toMap()
}
'it.key' and 'it.value' are the fields in the entity object that you want to use as... key and value pairs in the map.
Transformations give you a live data object based on another live data object. I don't know what the overhead is, but I assume it shouldn't be too big.