androidkotlinandroid-room

Using Room query to filter out multiple items?


I'm unable to get a room query to allow for a multiple wildcard query against my db.

    @Query (
        """
        SELECT * FROM sports 
        WHERE accountId = :accountId
        AND title NOT LIKE (:filters)
        ORDER BY date DESC
        """
    )
    fun queryForSports(
        accountId: Int,
        filters: Set<String>,
    )

Trying to do this, will not work if I pass multiple items to it. For instance if I only pass "%golf%", it works just fine. Like this...

var filters = listOf("%golf%").toSet()
sportDao.queryForSports(accountId, filters)

But if I want to filter out multiple items, by updating the list, it filters everything, and no results come back.

var filters = listOf("%golf%", "%tennis%").toSet()
sportDao.queryForSports(accountId, filters)

Is there a way to use the wildcard (%) for "NOT LIKE", for multiple items? I can't seem to find a good way to do it. I also tried, "NOT IN ( )" rather than NOT LIKE, and it behaved the same way, incorrectly.

A lot of times in these situations you would include an "OR ", but the way that room is structured, I don't see any meaningful way to do this simply by passing a Set as a parameter to the queryForSports() function.


Solution

  • You could have a function with a body that builds the SQL for use by an @RawQuery, noting that the SQL cannot be checked at compile time, by looping through the collect/array of filters.

    You could have a recursive/CTE based query WITH RECURSIVE .... that separates the filter (e.g. if it were passed as a CSV).

    You could have a function with a body that

    1. loops through the filter collection/array that extracts the rowids (adding them to a Long array/collection) of the respective rows to be excluded (to be excluded), and then

      1. this would utilise a previously created @Query annotated function that takes the single filter.
    2. that then drives a final query that uses the .... WHERE rowid NOT IN(:suitable_var_or_val_for_the_collecttion_of_rowids) ....

      1. it would not matter if the same rowid were extracted multiple times

    Demo

    For ease, preferring the last option from the above then consider the following:-

    In the @Dao annotated interface (AllDAOs) the following:-

    @Insert(onConflict = OnConflictStrategy.IGNORE)
    fun insert(sports: Sports): Long
    
    @Query("SELECT rowid FROM sports WHERE accountid=:accountid AND title LIKE('%'||:filter||'%')")
    fun getRowidsForAFilter(accountid: String,filter: String): LongArray
    @Query("SELECT * FROM sports WHERE rowid NOT IN(:rowidList)")
    fun getNOTFilteredSports(rowidList: LongArray): List<Sports>
    
    @Transaction
    @Query("")
    fun getNOTFilteredSportDriver(accountid: String,filters: List<String>): List<Sports> {
        val rowidlist = ArrayList<Long>()
        for (f in filters) {
             for(currentRowid in  getRowidsForAFilter(accountid,f)) {
                 rowidlist.add(currentRowid)
             }
        }
        return getNOTFilteredSports(rowidlist.toLongArray())
    }
    

    Noting that the @Entity annotated class Sports is:-

    @Entity
    data class Sports(
        @PrimaryKey
        val pk: Long?=null,
        val accountid: String,
        val title: String,
        val date: Long
    )
    

    Now consider the following activity code (note the main thread has been used for brevity):-

    class MainActivity : AppCompatActivity() {
        lateinit var db: CanaryDatabase
        lateinit var dao: AllDAOs
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            db = CanaryDatabase.getInstance(this)
            dao = db.getAllDAOs()
    
            dao.insert(Sports(accountid = "A001", title = "FRED BLOGGS", date = System.currentTimeMillis() / 1000))
            dao.insert(Sports(accountid = "A001", title = "TOM THUMB", date = System.currentTimeMillis() / 1000))
            dao.insert(Sports(accountid = "A001", title = "JANE DOE", date = System.currentTimeMillis() / 1000))
            dao.insert(Sports(accountid = "A001", title = "BOB SMITH", date = System.currentTimeMillis() / 1000))
            dao.insert(Sports(accountid = "A001", title = "JOHN DOE", date = System.currentTimeMillis() / 1000))
    
            val f1 = listOf<String>("OB","LO","UM")
            for (s in dao.getNOTFilteredSportDriver("A001",f1)) {
                Log.d("DBINFO","SPORT pk is ${s.pk} AccountID is ${s.accountid} Date is ${s.date} Title is ${s.title}")
            }
    

    When the above is run then the log contains:-

    D/DBINFO: SPORT pk is 3 AccountID is A001 Date is 1731918959 Title is JANE DOE
    D/DBINFO: SPORT pk is 5 AccountID is A001 Date is 1731918959 Title is JOHN DOE
    

    i.e.