I am using my custom ContentProvider to communicate with sqlite database. I would like to display on a list (using ListFragment), data that comes from two tables (with many to many relation). The only solution I can think of for such case is to use rawQuery. And the questions is, if it is a good practice, or should I solve this in some other way?
Example of tables:
Table A: ID, COLUMN_FROM_A
Table B: ID, COLUMN_FROM_B
Joining table AB: ID, FK_ID_A, FK_ID_B
Example of overridden query method in ContentProvider:
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
Cursor cursor = null;
int uriType = URIMatcher.match(uri);
switch (uriType) {
case TABLE_A_URI:
queryBuilder.setTables("TABLE_A");
cursor = queryBuilder.query(databaseHelper.getReadableDatabase(), projection, selection, selectionArgs, null, null, sortOrder);
break;
case TABLE_B_URI:
queryBuilder.setTables("TABLE_B");
cursor = queryBuilder.query(databaseHelper.getReadableDatabase(), projection, selection, selectionArgs, null, null, sortOrder);
break;
case TABLE_JOIN_A_B_URI:
cursor = databaseHelper.getReadableDatabase().rawQuery("select a.COLUMN_FORM_A, b.COLUMN_FROM_B from TABLE_A a, TABLE_B b, TABLE_AB ab where ab.FK_ID_A=a.ID and ab.FK_ID_B=b.ID", null);
break;
default:
throw new IllegalArgumentException("Unknown URI");
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
It's a good and common practice, very appropriate in this case.
I don't foresee any problems, we have used it in many apps.
Related
I would like to display in the Log.d the current SQL query being generated by a CursorLoader in the onCreateLoader method such as:
return new CursorLoader(
getActivity(), // Parent activity context
WifiEntry.CONTENT_URI, // Provider content URI to query
projection, // Columns to include in the resulting Cursor
null, // No selection clause
null, // No selection arguments
WifiEntry.COLUMN_WIFI_NAME + " ASC");
I have looked at the Android docs, and in here, but no success. It is useful to debugging purposes to be able to visualize the SQL string.
Thanks!
UPDATE. I am adding the ContentProvider query method, anybody can answer how to display in the Log the resulting SQL query?
Thanks.
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
// Get readable database
SQLiteDatabase database = mDbHelper.getReadableDatabase();
// This cursor will hold the result of the query
Cursor cursor;
// Figure out if the URI matcher can match the URI to a specific code
int match = sUriMatcher.match(uri);
switch (match)
{
case WIFIS:
// For the WIFIS code, query the wifi table directly with the given
// projection, selection, selection arguments, and sort order. The cursor
// could contain multiple rows of the pets table.
cursor = database.query(WifiEntry.TABLE_NAME, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case WIFI_ID:
// For the WIFI_ID code, extract out the ID from the URI.
// the selection will be "_id=?"
selection = WifiEntry._ID + "=?";
selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
cursor = database.query(WifiEntry.TABLE_NAME, projection, selection, selectionArgs,
null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Cannot query, unknown URI " + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
// Return the cursor
return cursor;
}
i have constructed a basic content provider that stores SMS messages for learning purposes, so far i can read(without selection args), insert, update and delete.
However i have been stumped trying to figure out how to format the selection args for the WHERE clause in my provider:
Basicly my application needs to search for a specific timestamp (in long format) and return its _id
say your database has an entry like this that your trying to access:
2|1|1410293471300|test type 1||testing|0
and the entire database looks like this:
_id|CLIENTTRXID|CREATED_AT|TYPE|MESSAGEPRIO|MESSAGE|ACCEPTED
1|1|1410293471000|test type 1||testing|0
2|1|1410293471300|test type 1||testing|0
3|1|1410293471600|test type 1||testing|0
in sql the query would be
"select _id from alerts where CREATED_AT=1410293471300;"
the code i was hoping would do the equivalent:
//normally i would get the string dynamically but to make it equal to the sql
String date = "1410293471300";
String[] selectionArgs = new String[]{ date };
Cursor cursor = getContext().getContentResolver().query(AlertContract.CONTENT_URI, null, AlertContract.Column.CREATED_AT, selectionArgs, AlertContract.DEFAULT_SORT);
seems to always produce the following error no matter what i try as selectionArgs
Exception caught﹕ Cannot bind argument at index 1 because the index is out of range. The statement has 0 parameters.
here is the query method of my contentprovider:
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables( AlertContract.TABLE);
switch (sURIMatcher.match(uri)) {
case AlertContract.STATUS_DIR:
break;
case AlertContract.STATUS_ITEM:
qb.appendWhere(AlertContract.Column.ID + "=" + uri.getLastPathSegment());
break;
default:
throw new IllegalArgumentException( "illegal uri: " + uri);
}
String orderBy = (TextUtils.isEmpty(sortOrder)) ? AlertContract.DEFAULT_SORT : sortOrder;
SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
//register for uri changes
cursor.setNotificationUri(getContext().getContentResolver(), uri);
Log.d(TAG, "queried records: "+cursor.getCount());
return cursor;
}
Presumably im missing something extremely obvious, and will feel quite silly for having posted this question.
But for the moment i would very much appreciate any help, as i am quite stumped.
It looks like your issue is with your selection, rather than with your selectionArgs per se. The selection should be the whole query after the "where". Here your selection is "CREATED_AT". You need two more items to get it to work:
an =, since you want equality (you can also do other operators, of course)
a ?. This is where your selectionArgument will be inserted (each argument needs a ? in the selection, so there should be the same number of ?s in the selection as selectionArguments.
The end result should be more like "CREATED_AT = ?"
Check out the documentation and this tutorial for more info on how to correctly construct a ContentProvider query.
When you query the content provider, try the following. The selection should be AlertContract.Column.CREATED_AT + "=?"
Cursor cursor = getContext().getContentResolver().query(AlertContract.CONTENT_URI, null, AlertContract.Column.CREATED_AT + "=?", selectionArgs, AlertContract.DEFAULT_SORT);
How do I run this query in a contentprovider?
SELECT * FROM sms_data WHERE number != (SELECT DISTINCT number from test) AND time > ?
First, the query is probably wrong. Instead of number != (...) you likely want number NOT IN (...).
Assuming test is a table in your app and not in the content provider, you can perform the query in two steps:
Subquery SELECT DISTINCT number FROM test. Build a comma-separated string such as '12345','23456','45678' from the results.
Do the content provider query with selection
number NOT IN (<that comma-separated list>) AND time > ?
For a subquery you can use rawQuery() or SQLiteQueryBuilder.
Overriding onQuery method of ContentProvider, I managed to put this bunch of code that works well for me.
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
switch (URI_MATCHER.match(uri)) {
case TABLE1:
Cursor cursor = database.query("sms_data", projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
case TABLE2:
Cursor c = database.query(true, "test", projection, selection, selectionArgs, null, null, sortOrder, null);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
case COMBINED:
String sql = "SELECT " + appendColumns(projection) +
" FROM sms_data WHERE number != IFNULL((SELECT DISTINCT number FROM test), 'abc') AND " + selection;
Cursor cur = database.rawQuery(sql, selectionArgs);
cur.setNotificationUri(getContext().getContentResolver(), uri);
return cur;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri + " Match = " + URI_MATCHER.match(uri));
}
}
Hope this works for you too.
Please help me understand what's happening here.
I have two fragments (A and B) in tabs reading data from different tables, via a CursorLoader and aContentProvider, in a Sqlite-database. With different URIs I can change which table the ContentProvider is quering.
I works as expected when switch between the tabs A and B, unless I switch to B, rotate and switches back to A the wrong cursor is returned. The cursor from fragment B is returned instead of a cursor for fragment A with makes the listView in fragment A to cause a crash. In some way the cursor seems to be reused on a rotation.
Why is this happening and how can I prevent that the wrong cursor is returned?
This is what I have in both fragment A and B. Tried to assing a loader id with no success.
public void onResume() {
super.onResume();
getLoaderManager().restartLoader(mLoaderId, null, this);
}
My ContentProvider looks like this:
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case ALL_NEWS:
queryBuilder.setTables(NewsDb.SQLITE_TABLE);
cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
break;
case SINGLE_PLACE:
queryBuilder.setTables(PlacesDb.SQLITE_TABLE);
String id = uri.getPathSegments().get(1);
queryBuilder.appendWhere(PlacesDb.KEY_ID + "=" + id);
cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
return cursor;
}
When using CursorLoader, data changes are automatically observed, so you should definitely remove the restartLoader call in your onResume(). If implemented properly, you should only have to call initLoader in onActivityCreated of the Fragment.
LoaderManager IDs are only scoped to the Fragment, so you should be using a static constant ID. If the Loaders are handled in the Fragments, themselves, it's perfectly fine for every Fragment to have the same Loader ID (even if they're managed by the same Activity).
So in each Fragment:
private static final int LOADER_ID = 0;
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// ...
getLoaderManager().initLoader(LOADER_ID, null, this);
}
How do I define a GROUP BY query for my CursorLoader?
The two constructors for a CursorLoader I see take either a single Context or a Context, Uri, projection, selection, selectionArgs and sortOrder.
But no groupBy.
(I'm using the support package for a Android 2.3 device)
Not really...
You can define a specific URI to your specific GROUP BY clause.
For example, if you have a table mPersonTable, possibly grouped by gender, you can define the following URIs:
PERSON
PERSON/#
PERSON/GENDER
When querying, switch between your queries so you can add your group by parameter:
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
String groupBy = null;
switch (mUriMatcher.match(uri)) {
case PERSON_ID:
...
break;
case PERSON_GENDER:
groupBy = GENDER_COLUMN;
case PERSON:
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(mPersonTable);
builder.setProjectionMap(mProjectionMap);
return builder.query(db, projection, selection, selectionArgs, groupBy, having, sortOrder, limit);
default:
break;
}
}
In fact, you could pass any sort of parameters to your query
Obs.: Use a UriMatcher to match the uri with your query implementation.
You can add Group by with selection parameter
new CursorLoader(context,URI,
projection,
selection+") GROUP BY (coloum_name",
null,null);
Apparently (and this is a bit embarrassing) the very first line in the documentation clearly states that the CursorLoader queries the ContentResolver to retrieve the Cursor. While the ContentResolver doesn't expose any means to GROUP BY there is, hence, no way the CursorLoader could expose such functionality either.
So the apparent answer to my very own question is: You can't!