I'm working on an android app that uses sugarORM. I want to get a multiple items that match the ids in a list.
However when i call
findWithQuery(A.class, "SELECT * FROM <table> WHERE <column> in (?)", "1,2,3")
I always get an empty list(although I double checked the query with SQLite DB Browser and it worked).
Splitting this query into multiple findById seems inefficient. Any thoughts on getting WHERE IN to work using SugarORM?
After more attempts I found that there is a problem with replacing the placeholders.
Switching from:
findWithQuery(A.class, "SELECT * FROM <table> WHERE <column> in (?)", "1,2,3")
To:
findWithQuery(A.class, "SELECT * FROM <table> WHERE <column> in (1,2,3)", null)
fixes the issue.
The issue is that SQLite escapes the arguments "1,2,3" and turns it into a single value that is then used to replace the single ? placeholder in your query string. The correct way to supply multiple arguments would be to have a placeholder for every individual argument. Your original line of code would then have to change to:
findWithQuery(A.class, "SELECT * FROM <table> WHERE <column> in (?,?,?)", new String[] { "1","2","3" })
You later pointed out that the number of arguments is dynamic. This you can easily be accomplished by generating the query (the where clause in particular) at runtime based on the arguments that you want to query for.
For the most general case, it only takes a few lines of code to do so:
final String[] args = new String[] { /* ... */ };
final String query = "SELECT * FROM <table> WHERE <column> in " +
"(" + TextUtils.join(",", Collections.nCopies(args.length, "?")) + ")";
final List<A> result = A.findWithQuery(A.class, query, args);
(note that you could take a shortcut and inject the arguments directly into the query string - instead of using placeholders - but then you'll loose SQLite's built-in escaping, so I decided against that)
All that's left to do is to generate a String[] out of your arguments. A simple helper method like this should cover most scenarios:
static String[] toStringArray(Object... args) {
final String[] array = new String[args.length];
for (int i = 0; i < args.length; i++) array[i] = args[i].toString();
return array;
}
You'll probably want to add some null checks in there and potentially set up a few overloads if you plan on using primitive arrays as arguments.
Disclaimer: I typed everything straight into the browser, so no guarantees that everything works and the first try. :)
Related
I have been able to achieve a basic search capability in Android Room + FTS with the following query as an example in my Dao:
#Query("SELECT * FROM Conversation JOIN ConversationFts ON Conversation.id == ConversationFts.id WHERE ConversationFts.title LIKE :text GROUP BY Conversation.id")
public abstract DataSource.Factory<Integer, Conversation> search(String text);
Where the text is passed along between percentage characters, as such %lorem%.
This example works perfectly for the search of a single word and I want to expand this to be able to search for one or more words with the condition that they do not need to be in the order they are entered, but they must have a match for all the words. This means this has to be an AND and not OR situation.
I've found a few examples of SQL queries, but they require a specific query tailored for each case, for example: LIKE text AND LIKE another AND LIKE, etc... which is not a solution.
How can this be achieved? And to make it clear, I am not looking for a raw query solution, I want to stick to Room as much as possible, otherwise, I'd rather just use this as it is than to resort to raw queries.
EDIT: Adding an example per request
I search for do the results returned include all matches that contain do in the title, even if it is a partial match
I search for do and test the results returned include all matches that contain do and test, but in no specific order, even if they are partial matches.
However, if just one of them cannot be found in the text then it will not be returned in the results. For example, if do is found, but test is not then it will not be part of the results.
I think there is no way to do that except creating a raw query dynamically. You can write another method, something like this:
public abstract class ConversationDao {
public DataSource.Factory<Integer, Conversation> search(String text) {
StringBuilder builder = new StringBuilder();
String[] words = text.split("\\s+");
if (words.length > 0) {
builder.append(" WHERE ");
}
for (int i = 0; i < words.length; i++) {
builder.append("ConversationFts.title LIKE %").append(words[i]).append("%");
if (i < words.length - 1) {
builder.append(" AND ");
}
}
SupportSQLiteQuery query = new SimpleSQLiteQuery(
"SELECT * FROM Conversation JOIN ConversationFts ON Conversation.id == ConversationFts.id"
+ builder.toString()
+ " GROUP BY Conversation.id"
);
return search(query);
}
#RawQuery
public abstract DataSource.Factory<Integer, Conversation> search(SupportSQLiteQuery query);
}
I have a rather big query that is returning data when executed outside android while returning nothing when executed within android.
I split the query in several pieces and determined that the union was ok.
I tried on a smaller set of data with the same behavior.
I've tested with different hardware and API versions.
I'm using the rawQuery method with constant values.
http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#rawQuery(java.lang.String, java.lang.String[])
This query was meant to replace a FULL OUTER JOIN which is not currently supported.
SELECT IFNULL(stype, gtype) AS type, IFNULL(sdate, gdate) AS date, IFNULL(sroute, groute) AS route FROM (
SELECT sp.type AS stype, sp.date AS sdate, -1 AS gtype, gp.date AS gdate, sp.route AS sroute, gp.route AS groute
FROM Sensor_Point AS sp LEFT JOIN GPS_Point AS gp ON gp._id IS NULL AND sp.sent=0 AND sp.route=gp.route AND sp.route=1
UNION ALL
SELECT sp.type AS stype, sp.date AS sdate, -1 AS gtype, gp.date AS gdate, sp.route AS sroute, gp.route AS groute
FROM GPS_Point AS gp LEFT JOIN Sensor_Point AS sp ON sp._id IS NULL AND gp.sent=0 AND sp.route=gp.route AND gp.route=1
) WHERE route=1 ORDER BY date ASC LIMIT 255
Any hints would be greatly appreciated.
Update:
Look's like the problem is finally with the query parameters, if I set it this way:
String[] args = new String[3];
args[0] = args[1] = args[2] = "1";
Cursor data dataBase.rawQuery(SELECT_POINTS, args);
It doesn't work, while it works when hardcoding values directly in the query.
Cursor data = dataBase.rawQuery(SELECT_POINTS, null);
In the Android database API, all query parameters are strings.
(This is a horrible design mistake.)
Your query corresponds to:
... AND sp.route='1'
Try to convert the parameter strings back into a number like this:
... AND sp.route = CAST(? AS INT)
or just put the number directly into the query string.
I have a rather big query that is returning data when executed outside android while returning nothing when executed within android.
I split the query in several pieces and determined that the union was ok.
I tried on a smaller set of data with the same behavior.
I've tested with different hardware and API versions.
I'm using the rawQuery method with constant values.
http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#rawQuery(java.lang.String, java.lang.String[])
This query was meant to replace a FULL OUTER JOIN which is not currently supported.
SELECT IFNULL(stype, gtype) AS type, IFNULL(sdate, gdate) AS date, IFNULL(sroute, groute) AS route FROM (
SELECT sp.type AS stype, sp.date AS sdate, -1 AS gtype, gp.date AS gdate, sp.route AS sroute, gp.route AS groute
FROM Sensor_Point AS sp LEFT JOIN GPS_Point AS gp ON gp._id IS NULL AND sp.sent=0 AND sp.route=gp.route AND sp.route=1
UNION ALL
SELECT sp.type AS stype, sp.date AS sdate, -1 AS gtype, gp.date AS gdate, sp.route AS sroute, gp.route AS groute
FROM GPS_Point AS gp LEFT JOIN Sensor_Point AS sp ON sp._id IS NULL AND gp.sent=0 AND sp.route=gp.route AND gp.route=1
) WHERE route=1 ORDER BY date ASC LIMIT 255
Any hints would be greatly appreciated.
Update:
Look's like the problem is finally with the query parameters, if I set it this way:
String[] args = new String[3];
args[0] = args[1] = args[2] = "1";
Cursor data dataBase.rawQuery(SELECT_POINTS, args);
It doesn't work, while it works when hardcoding values directly in the query.
Cursor data = dataBase.rawQuery(SELECT_POINTS, null);
In the Android database API, all query parameters are strings.
(This is a horrible design mistake.)
Your query corresponds to:
... AND sp.route='1'
Try to convert the parameter strings back into a number like this:
... AND sp.route = CAST(? AS INT)
or just put the number directly into the query string.
I'm tring to make join in two tables and get all columns in both, I did this:
QueryBuilder<A, Integer> aQb = aDao.queryBuilder();
QueryBuilder<B, Integer> bQb = bDao.queryBuilder();
aQb.join(bQb).prepare();
This equates to:
SELECT 'A'.* FROM A INNER JOIN B WHERE A.id = B.id;
But I want:
SELECT * FROM A INNER JOIN B WHERE A.id = B.id;
Other problem is when taking order by a field of B, like:
aQb.orderBy(B.COLUMN, true);
I get an error saying "no table column B".
When you are using the QueryBuilder, it is expecting to return B objects. They cannot contain all of the fields from A in B. It will not flesh out foreign sub-fields if that is what you mean. That feature has not crossed the lite barrier for ORMLite.
Ordering on join-table is also not supported. You can certainly add the bQb.orderBy(B.COLUMN, true) but I don't think that will do what you want.
You can certainly use raw-queries for this although it is not optimal.
Actually, I managed to do it without writing my whole query as raw query. This way, I didn't need to replace my query builder codes (which is pretty complicated). To achieve that, I followed the following steps:
(Assuming I have two tables, my_table and my_join_table and their daos, I want to order my query on my_table by the column order_column_1 of the my_join_table)
1- Joined two query builders & used QueryBuilder.selectRaw(String... columns) method to include the original table's + the columns I want to use in foreign sort. Example:
QueryBuilder<MyJoinTable, MyJoinPK> myJoinQueryBuilder = myJoinDao.queryBuilder();
QueryBuilder<MyTable, MyPK> myQueryBuilder = myDao.queryBuilder().join(myJoinQueryBuilder).selectRaw("`my_table`.*", "`my_join_table`.`order_column` as `order_column_1`");
2- Included my order by clauses like this:
myQueryBuilder.orderByRaw("`order_column_1` ASC");
3- After setting all the select columns & order by clauses, it's time to prepare the statement:
String statement = myQueryBuilder.prepare().getStatement();
4- Get the table info from the dao:
TableInfo tableInfo = ((BaseDaoImpl) myDao).getTableInfo();
5- Created my custom column-to-object mapper which just ignores the unknown column names. We avoid the mapping error of our custon columns (order_column_1 in this case) by doing this. Example:
RawRowMapper<MyTable> mapper = new UnknownColumnIgnoringGenericRowMapper<>(tableInfo);
6- Query the table for the results:
GenericRawResults<MyTable> results = activityDao.queryRaw(statement, mapper);
7- Finally, convert the generic raw results to list:
List<MyTable> myObjects = new ArrayList<>();
for (MyTable myObject : results) {
myObjects.add(myObject);
}
Here's the custom row mapper I created by modifying (just swallowed the exception) com.j256.ormlite.stmt.RawRowMapperImpl to avoid the unknown column mapping errors. You can copy&paste this into your project:
import com.j256.ormlite.dao.RawRowMapper;
import com.j256.ormlite.field.FieldType;
import com.j256.ormlite.table.TableInfo;
import java.sql.SQLException;
public class UnknownColumnIgnoringGenericRowMapper<T, ID> implements RawRowMapper<T> {
private final TableInfo<T, ID> tableInfo;
public UnknownColumnIgnoringGenericRowMapper(TableInfo<T, ID> tableInfo) {
this.tableInfo = tableInfo;
}
public T mapRow(String[] columnNames, String[] resultColumns) throws SQLException {
// create our object
T rowObj = tableInfo.createObject();
for (int i = 0; i < columnNames.length; i++) {
// sanity check, prolly will never happen but let's be careful out there
if (i >= resultColumns.length) {
continue;
}
try {
// run through and convert each field
FieldType fieldType = tableInfo.getFieldTypeByColumnName(columnNames[i]);
Object fieldObj = fieldType.convertStringToJavaField(resultColumns[i], i);
// assign it to the row object
fieldType.assignField(rowObj, fieldObj, false, null);
} catch (IllegalArgumentException e) {
// log this or do whatever you want
}
}
return rowObj;
}
}
It's pretty hacky & seems like overkill for this operation but I definitely needed it and this method worked well.
I'm using the QueryBuilder to construct the inner SQL that later is used in a raw SQL to avoid escaping invalid characters manually.
SelectArg friendsIN = new SelectArg(friendsUsernames);
QueryBuilder<MyObject, Integer> qb = myObjectDao.queryBuilder();
qb.selectRaw("username", "MAX(time) AS latestTime").groupBy("username").where()
.in("username", friendsIN);
String innerSelect = pq.getStatement();
friendsUsernames is defined as ArrayList<String>.
Then I use the innerSelect to build the outer select:
String select = "SELECT w.id FROM (" + innerSelect +") AS x INNER JOIN myObject AS w on w.username = x.username AND w.time = x.latestTime";
GenericRawResults<String[]> results = myObjectDao.queryRaw(select);
But, as expected, the innerString has '?' and when I call queryRaw on myObjectDao I don't get any result. I tried to give friendsUsername as an array to queryRaw:
GenericRawResults<String[]> results =
myObjectrDao.queryRaw(select,
friendsUsernames.toArray(new String[friendsUsernames.size()]));
But I get the following error:
android.database.sqlite.SQLiteBindOrColumnIndexOutOfRangeException:
bind or column index out of range: handle 0x17a22e8
Any suggestions on how to accomplish this kind of queries with OrmLite?
Yeah that's not going to work. There is only one ? in your query and yet you are trying to pass in an array of user-names. There must be a 1-to-1 correspondence between the number of ? SQL arguments and the number of arguments passed to the queryRaw(...) method exactly.
If the friendsUsernames is a fixed size then you should be able to do something like the following which will generate SQL something like "in (?, ?, ?, ?)":
List<SelectArg> friendsInList = new ArrayList<SelectArg>();
for (int i = 0; i < NUM_FRIENDS; i++) {
// it doesn't matter what the value is since you just want the ?
fieldsInList.add(new SelectArg());
}
...in("name", friendsInList);
However if the list of names is dynamic then you are going to have to do this on the fly since, again, the number of ? must match the number of arguments passed to the queryRaw(...) method exactly.