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)
}
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"%'
Say I have a DB with two main entities (Song and Tag) and a many-to-many relationship between them. Using Room, I want to query the Songs that have a series of Tags (all of them) by their names.
So, given this example data in the cross ref table (SongTagCrossRef):
Song
Tag
song1
tag1
song1
tag2
song1
tag3
song2
tag2
song3
tag2
song3
tag3
I want the query to return only song1 if I enter tag1 and tag2, as it's the only song related to both.
I've come up with this #Query in the corresponding Dao:
#Query("""
SELECT s.* FROM Song s
JOIN SongTagCrossRef st ON s.song_id = st.song_id
JOIN Tag t ON st.tag_id = t.tag_id
WHERE t.name IN (:tagNames)
GROUP BY s.song_id
HAVING COUNT(*) = (SELECT COUNT(*) FROM Tag WHERE name IN (:tagNames))
""")
fun getSongsWithAllOfTheTagsByName(vararg tagNames: String): List<SongEntity>
Since I can't access tagNames.size in the #Query, I've had to use a subquery to artificially get it. This subquery shouldn't be too heavy, but it would always be better to somehow access tagNames.size.
After reading the answers to a slightly related question, I've been toying with creating a #RawQuery and calling it from a function that takes only tagNames, something along these lines:
#RawQuery
fun getSongsWithAllOfTheTagsByName(query: SupportSQLiteQuery): List<SongEntity>
fun getSongsWithAllOfTheTagsByName(vararg tagNames: String): List<SongEntity> {
val query = SimpleSQLiteQuery("""
SELECT s.* FROM Song s
JOIN SongTagCrossRef st ON s.song_id = st.song_id
JOIN Tag t ON st.tag_id = t.tag_id
WHERE t.name IN (?)
GROUP BY s.song_id
HAVING COUNT(*) = ?
""", arrayOf(tagNames, tagNames.size))
return getSongsWithAllOfTheTagsByName(query)
}
(only converting tagNames to something it can actually swallow)
But I've discarded this approach because I don't want to expose a function that takes a query.
Is there a simpler, more elegant way to write this query?
I finally did it, so I want to share what I found out. It's actually not quite straightforward, but it does the trick.
Going through the SQLite documentation, I came upon the JSON1 extension and more specifically the json_array() and json_array_length() functions.
However, to use this extension, as CommonsWare points out in this answer and Hooman summarises here, Requery's standalone library must be used, through RequerySQLiteOpenHelperFactory.
In conclusion:
build.gradle file
dependencies {
...
implementation 'com.github.requery:sqlite-android:3.36.0'
...
}
Room database class
Room.databaseBuilder(...)
...
.openHelperFactory(RequerySQLiteOpenHelperFactory())
...
.build()
Dao interface
#Query("""
SELECT s.* FROM Song s
JOIN SongTagCrossRef st ON s.song_id = st.song_id
JOIN Tag t ON st.tag_id = t.tag_id
WHERE t.name IN (:tagNames)
GROUP BY s.song_id
HAVING COUNT(*) = JSON_ARRAY_LENGTH(JSON_ARRAY(:tagNames))
""")
fun getSongsWithAllOfTheTagsByName(vararg tagNames: String): List<SongEntity>
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 would like to make the following query to my database:
SELECT type, COUNT(*) FROM offerings GROUP BY type
This query works well with an Sqlite browser. Now I want to use this query in my Dao:
#Query("SELECT type, COUNT(*) FROM offerings GROUP BY type")
LiveData<Map<String, Integer>> getOfferingsGroupedByType();
But I am getting the error: ... not sure how to convert a cursor to this method's return type
How can I query a table with 2 columns? --> that is, [type, count(type)] ?
Step #1: Give a name to the count: SELECT type, COUNT(*) AS count FROM offerings GROUP BY type
Step #2: Create a Java class with suitable fields:
public class Thingy {
public String type;
public int count;
}
Step #3: Have your return type from the DAO method use that class:
#Query("SELECT type, COUNT(*) FROM offerings GROUP BY type")
LiveData<List<Thingy>> getOfferingsGroupedByType();
I don't recall Room supporting returning a Map, so you will need to handle that aspect yourself, either in the observer or via a MediatorLiveData that wraps the LiveData you get from the DAO and does the conversion.
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