56

The problem

Is it possible construct a query at runtime?


Use case

@Query("SELECT * FROM playlist " +
        "WHERE playlist_title LIKE '% :playlistTitle %' " +
        "GROUP BY playlist_title " +
        "ORDER BY playlist_title " +
        "LIMIT :limit")
 List<IPlaylist> searchPlaylists(String playlistTitle, int limit);

The limit part is optional. That is, it should be able to perform the same query with or without limit.


A more complicated use case

In the previous case, it is possible to make two static queries with and without limit part and appropriate one can be used each time. But sometimes we may have to deal with more complex situations like building a filter.

In that case, unlike the previous example, there will be multiple number of optional parts. For a table of books, we may need to do filtering according to the category the book belongs to, author name, price range, publication date etc. It is almost impossible to make static queries with all combinations of these parts.

Bertram Gilfoyle
  • 9,899
  • 6
  • 42
  • 67
Anderson K
  • 5,445
  • 5
  • 35
  • 50
  • What do you mean by "optional parameter"? Ignoring Room for the moment, how would you implement an optional parameter using traditional Android SQLite APIs? – CommonsWare May 31 '17 at 14:34
  • @CommonsWare I mean that i would like dynamically, set or not the limit parameter for the query. – Anderson K May 31 '17 at 14:37
  • 3
    OK. Ignoring Room for the moment, how would you implement this using `SQLiteDatabase`? If the answer is "I would have two different query strings", then why not use two different `@Query` annotations? Since an annotation can refer to a `static` field (AFAIK), in principle, you can even reduce redundancy by defining the core part of the query string once and referring to it from each annotation. – CommonsWare May 31 '17 at 14:38
  • 1
    I understand you, I'm doing a refactoring and as the room is very new, I'm trying to see if there is a better way to do this, but otherwise I'll create two methods. – Anderson K May 31 '17 at 14:45

10 Answers10

80

Room supports @RawQuery annotation to construct queries at run-time.


Step 1 : Make DAO method

Mark the DAO method with @RawQuery annotation instead of normal @Query.

@Dao
interface BooksDao{
    @RawQuery
    List<Book> getBooks(SupportSQLiteQuery query);
}

Step 2 : Construct the query

Room uses prepared statements for security and compile time verification. Therefore, while constructing queries, we need to store query string and bind parameters separately.

In this example, I use the variable queryString for query string and args for bind parameters.

(Please note that I used text editor to write code. Therefore there may be typo or simple syntax errors. If you find anything please let me know in the comments or edit the post.)

// Query string
String queryString = new String();

// List of bind parameters
List<Object> args = new ArrayList();

boolean containsCondition = false;

// Beginning of query string
queryString += "SELECT * FROM BOOKS";

// Optional parts are added to query string and to args upon here

if(!authorName.isEmpty()){
    queryString += " WHERE";
    queryString += " author_name LIKE ?%";
    args.add(authorName);
    containsCondition = true;
}

if(fromDate!=null){
    
    if (containsCondition) {
        queryString += " AND";
    } else {
        queryString += " WHERE";
        containsCondition = true;
    }

    queryString += " publication_date AFTER ?";
    args.add(fromDate.getTime());
}

if(toDate!=null){
    
    if (containsCondition) {
        queryString += " AND";
    } else {
        queryString += " WHERE";
        containsCondition = true;
    }

    queryString += " publication_date BEFORE ?";
    args.add(toDate.getTime());
}

// End of query string
queryString += ";";

Step 3 : Perform query

SimpleSQLiteQuery query = new SimpleSQLiteQuery(queryString, args.toArray());
List<Book> result = booksDao.getBooks(query);



Notes

  • Like normal Query, RawQuery supports returning raw cursors, entities, POJOs and POJOs with embedded fields
  • RawQuery supports relations
Bertram Gilfoyle
  • 9,899
  • 6
  • 42
  • 67
  • 2
    Is @RawQuery subject to risk of SQL injection attack and therefore a security risk? – AJW Feb 02 '19 at 03:18
  • 2
    No. It is not plain text query as the name make us think. It uses bind variables internally. [SimpleSQLiteQuery](https://developer.android.com/reference/android/arch/persistence/db/SimpleSQLiteQuery), to be more precise. – Bertram Gilfoyle Feb 03 '19 at 19:35
  • 2
    I want to suggest some classes that make this job easier. I look forward to your feedback. [UpdateRawQueryBuilder](https://bitbucket.org/tim4dev/workspace/snippets/4nb4eg) [SimpleQueryBuilder](https://bitbucket.org/tim4dev/workspace/snippets/zndzeb) – tim4dev Jul 23 '20 at 09:32
  • @tim4dev Thanks. Not tested but looks good at first glance. – Bertram Gilfoyle Jul 23 '20 at 20:24
  • 6
    This should be the accepted answer as it solves the original question. – svprdga Oct 16 '20 at 17:59
  • Isn't it possible to do this: `@Query(":query") suspend fun selectByQuery(query: String): List` ? – Leonardo Sibela Mar 29 '23 at 04:10
25

In my experience (short) using Room that's not possible, and not because of being a Room limitation but, as implicitly commented by @CommonsWare, a limitation on SQLite. You need two queries, and therefore two methods in your DAO.

I would have something like:

@Query("SELECT * FROM playlist " +
    "WHERE playlist_title LIKE '% :playlistTitle %' " +
    "GROUP BY playlist_title " +
    "ORDER BY playlist_title " +
    "LIMIT :limit")
List<IPlaylist> searchPlaylists(String playlistTitle, int limit);

@Query("SELECT * FROM playlist " +
    "WHERE playlist_title LIKE '% :playlistTitle %' " +
    "GROUP BY playlist_title " +
    "ORDER BY playlist_title ")
List<IPlaylist> searchPlaylists(String playlistTitle);

Then somewhere else you do the bypass:

if (limit.isPresent()) {
   return playlistDao.searchPlaylists(title, limit.get());
} else {
   return playlistDao.searchPlaylists(title);
}

That's the best option I can think of at the moment.

Ramesh R
  • 7,009
  • 4
  • 25
  • 38
Juanky Soriano
  • 283
  • 3
  • 3
17

Instead of writing multiple query i refer pass negative value to limit clause. Because if there is change in query i have to update the both query which is more error prone.

Official doc -> If the LIMIT expression evaluates to a negative value, then there is no upper bound on the number of rows returned. you can find it here https://sqlite.org/lang_select.html and read the limit clause section.

So I would do somthing like this,

@Query("SELECT * FROM playlist " +
    "WHERE playlist_title LIKE '% :playlistTitle %' " +
    "GROUP BY playlist_title " +
    "ORDER BY playlist_title " +
    "LIMIT :limit")
List<IPlaylist> searchPlaylists(String playlistTitle, int limit);

and pass negative when you don't want to apply filter.

return playlistDao.searchPlaylists(title, limit.isPresent() ? limit.get() : -1)

It's working in my case.

Updated [21 Dec 2018]

In case If you are using kotlin use default value.

@JvmOverloads
@Query("SELECT * FROM playlist " +
        "WHERE playlist_title LIKE '% :playlistTitle %' " +
        "GROUP BY playlist_title " +
        "ORDER BY playlist_title " +
        "LIMIT :limit")
fun searchPlaylists(playlistTitle: String, limit: Int = -1): List<IPlaylist>

@JvmOverloads to make it compatiable with Java. It generate two separate methods for Java.

Moinkhan
  • 12,732
  • 5
  • 48
  • 65
8

Use SupportSQLiteQuery.

https://developer.android.com/reference/android/arch/persistence/db/SupportSQLiteQuery

Latest release 1.1.1 now uses SupportSQLiteQuery.

A query with typed bindings. It is better to use this API instead of rawQuery(String, String[]) because it allows binding type safe parameters.

@Dao
     interface RawDao {
         @RawQuery(observedEntities = User.class)
         LiveData<List<User>> getUsers(SupportSQLiteQuery query);
     }

Usage:

     LiveData<List<User>> liveUsers = rawDao.getUsers( new 
SimpleSQLiteQuery("SELECT * FROM User ORDER BY name DESC"));

Update your gradle to 1.1.1

implementation 'android.arch.persistence.room:runtime:1.1.1'
implementation 'android.arch.lifecycle:extensions:1.1.1'
annotationProcessor "android.arch.persistence.room:compiler:1.1.1"

Note: if you upgrade to 1.1.1, and are using String instead of SupportSQLiteQuery,

you will get the error:

RawQuery does not allow passing a string anymore. Please use android.arch.persistence.db.SupportSQLiteQuery.

Using SupportSQLiteQuery as above will solve the problem.

Note: Make sure you pass in SupportSQLiteQuery query parameter or you will get this error:

RawQuery methods should have 1 and only 1 parameter with type String or SupportSQLiteQuery

live-love
  • 48,840
  • 22
  • 240
  • 204
  • 1
    Is @RawQuery subject to risk of SQL injection attack and therefore a security risk? – AJW Feb 02 '19 at 03:16
  • 1
    @AJW No if used with SupportSQLiteQuery, the arguments are validated before proceeding as per the documentation of [SupportSQLiteQuery](https://developer.android.com/reference/android/arch/persistence/db/SupportSQLiteQuery.html). Consequently SimpleSQLiteQuery is safe because it's a subclass of SupportSQLiteQuery – MatPag Feb 21 '19 at 16:02
  • 1
    @live-love using SupportSQLiteQuery you mention above "...it is better to use this API instead of rawQuery(String, String[]) because it allows binding type safe parameters." What do you mean "type safe parameters"? Meaning there is risk of using a stand-alone RawQuery whereas that risk is removed using SupportSQLLiteQuery? – AJW Sep 07 '19 at 01:22
  • android.arch is deprecated – G. Ciardini Apr 06 '22 at 13:57
7

There is no something like optional parameter in Room, but there is a @RawQuery annotation where you can pass query as a String so you can build your SQL query in the runtime. I think this will work for you.

Here is the example from the Offical documentation:

@Dao
 interface RawDao {
     @RawQuery
     User getUser(String query);
 }

And here is how you can use it:

User user = rawDao.getUser("SELECT * FROM User WHERE id = 3 LIMIT 1");

Important: RawQuery methods must return a non-void type

Important: This is available in Room 1.1.0-alpha3

Mladen Rakonjac
  • 9,562
  • 7
  • 42
  • 55
4

Make it more simple. I'll show you example using where clause using two variable. Do it like this:

@Query("SELECT * FROM Student WHERE stdName1= :myname AND stdId1=:myid")
List<Student> fetchAllData(String myname,int myid);

stdName1 and stdId1 are column names

Ramesh R
  • 7,009
  • 4
  • 25
  • 38
Syed Danish Haider
  • 1,334
  • 11
  • 15
1

For Kotlin-Room-ViewModel

@Query("SELECT * FROM schedule_info_table where schedule_month = :monthValue ORDER BY schedule_date_time ASC")
fun getThisMonthSchedules(monthValue: Int): Flow<List<SchedulesInformation>>
Guru raj
  • 806
  • 11
  • 10
0

@Anderson K & @Juanky Soriano, I agree with @CommonsWare,

There are some Limitations in Room Library, then also We can write a fully dynamic query on Room Database by using the @query() of Support SQLite Database

String mQuery = "SELECT * FROM foobar WHERE columnName1 IN ('value_1','value_2') and columnName2 In('value_3','value_4')";

AppDatabase appDatabase = Room.databaseBuilder(getApplicationContext(),
        AppDatabase.class, "database-name").build();

Cursor mCursor = AppDatabase.getAppDatabase(context).getOpenHelper().getReadableDatabase().query(myQuery);

Now you can convert cursor row-wise data to your POJO class.

Ramesh R
  • 7,009
  • 4
  • 25
  • 38
TejaDroid
  • 6,561
  • 4
  • 31
  • 38
0
@Query("select * from task where state = :states and sentdate between :fromdate and :todate")
  List<Task> getFilterAll(String states, String fromdate, String todate);

Here we need to use column name state. Whenever the need to achieve a custom query just pass the value through the parameter from the activity or fragment will get into the interface we will apply inside the query. like the example in the above code (:fromdate, :todate)

Colon is a must. Which parameter are you will going to use inside the query we will mention before starting with the : symbol.

Ramesh R
  • 7,009
  • 4
  • 25
  • 38
Mahendren Mahisha
  • 1,072
  • 11
  • 9
-2

Raw query is deprecated as per the documentation

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
vinay shetty
  • 895
  • 1
  • 9
  • 15
  • 2
    The android.arch Architecture Components packages are deprecated, [RawQuery is available in androidx](https://developer.android.com/reference/androidx/room/RawQuery) – Zac Feb 26 '21 at 18:40