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>
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'm trying to use one method for dao (Room) to get items by a specific sorting dynamically, but compiler gives an error for the next SQL query
So it's not possible? Do I have to create duplicate methods with different sorting?
you can not perform if and else logic in the sqlite query
You should use #RawQuery like this:
#Dao
interface RawDao {
#RawQuery
fun getTestItems(SupportSQLiteQuery query): DataSource.Factory
}
// Usage of RawDao
// for example set: restOfQuery = sortBy + "ASC"
val query = SimpleSQLiteQuery(
"SELECT * FROM Items ORDER BY ?",
new Object[]{ restOfQuery });
val result = rawDao.getTestItems(query);
Or another way is that you use multiple functions for multiple orderings.
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.
My Problem:
I'm struggling to eliminate the compiling error on the following Room #Query statement in a Room DAO. As you can see, the SQLite query statement is joining various fields from different tables. The missing fields identified by the error are a part of the Notes class constructor identified in the List type for the method. I think I need to change the List type identified. If I'm right, I need some guidance/suggestion on how I should resolve it. Do I need to create a new Class and DAO with just those specific fields queried? Or maybe just a class since there is not table specific to these fields only. The error is:
error: The columns returned by the query does not have the fields [commentID,questionID,quoteID,termID,topicID,deleted] in com.mistywillow.researchdb.database.entities.Notes even though they are annotated as non-null or primitive. Columns returned by the query: [NoteID,SourceID,SourceType,Title,Summary]
List getNotesOnTopic(String topic);
#Query("SELECT n.NoteID, s.SourceID, s.SourceType, s.Title, c.Summary FROM Comments as c " +
"LEFT JOIN Notes as n ON n.CommentID = c.CommentID " +
"LEFT JOIN Sources as s ON n.SourceID = s.SourceID " +
"LEFT JOIN Topics as t ON n.TopicID = t.TopicID WHERE t.Topic = :topic AND n.Deleted = 0")
List<Notes> getNotesOnTopic(String topic);
What I'm trying to do:
I'm attempting to convert and existing Java desktop app with an embedded an SQLite database. The above query does work fine in that app. I only want to pass field data from these tables.
What I've tried:
I've done some googling and visited some forums for the last few days (e.g. Android Forum, Developer.Android.com) but most of the Room #Query examples are single table full field queries (e.g. "Select * From table"). Nothing I found yet (there is probably something) quite addresses how and what to do if you are joining and querying only specific fields across tables.
I think I may have fixed my issue. I just created a new class called SourceTable and designated the queried fields in the constructor. The only catch was I, according to a follow up error, was that the parameters had to match the field names.
public class SourcesTable {
private int NoteID;
private int SourceID;
private String SourceType;
private String Title;
private String Summary;
public SourcesTable(int NoteID, int SourceID, String SourceType, String Title, String Summary){
this.NoteID = NoteID;
this.SourceID = SourceID;
this.SourceType = SourceType;
this.Title = Title;
this.Summary = Summary;
}
}
and then I update my list method:
List<SourcesTable> getNotesOnTopic(String topic);
I'm trying to convert the following SQL statement into DBFlow method calls:
SELECT t.SSID, t.BSSID, t.Latitude, t.Longitude, t.Timestamp
FROM wlan_events t
INNER JOIN (SELECT BSSID, MAX(Timestamp) AS MaxTimestamp FROM wlan_events GROUP BY BSSID) groupedt
ON t.BSSID = groupedt.BSSID AND t.Timestamp = groupedt.MaxTimestamp
What I got so far:
SQLite.select(WifiEvent_Table.SSID, WifiEvent_Table.BSSID, WifiEvent_Table.latitude,
WifiEvent_Table.longitude)
.from(WifiEvent.class)
.as("t")
.innerJoin(WifiEvent.class) // ????
;
How do i create that inner join's select statement using dbflow?
This is what I found:
SELECT EMP_ID, NAME, DEPT FROM COMPANY LEFT OUTER JOIN DEPARTMENT
ON COMPANY.ID = DEPARTMENT.EMP_ID
in DBFlow:
SQLite.select(Company_Table.EMP_ID, Company_Table.DEPT)
.from(Company.class)
.leftOuterJoin(Department.class)
.on(Company_Table.ID.withTable().eq(Department_Table.EMP_ID.withTable()))
.queryList();
Hope this helps: (Link updated)
https://agrosner.gitbooks.io/dbflow/content/