• Wir präsentieren Dir heute ein Stellenangebot für einen Frontend-Entwickler Angular / Java in Braunschweig. Hier geht es zur Jobanzeige

Android SQLite Query "WHERE _id in (X, Y, Z)" keine Daten

K

kurztipp

Aktives Mitglied
Hallo,

ich hab eine ArrayList, die mir die IDs von Datensätzen einer Tabelle speichert. Allerdings kann ich diese Daten mit SQLiteDatabase.query(...) nicht (wie gewollt) abrufen:

Java:
String ids = "";
for (int i = 0; i < mIngredientIds.size(); i++) {
	ids = ids.concat(mIngredientIds.get(i));
	if (i < mIngredientIds.size() - 1) {
		ids = ids.concat(", ");
	}
}

log(ids);

String sql = SQLiteQueryBuilder.buildQueryString(false,
		TABLE_NAME, null, COL_ID
				+ " IN (" + ids + ")", null, null, null, null);
log(sql);
log(db.rawQuery(sql, null).getCount());

sql = SQLiteQueryBuilder.buildQueryString(false,
		TABLE_NAME, null, COL_ID
				+ " IN (?)", null, null, null, null);
log(sql);
log(db.rawQuery(sql, new String[] { ids }).getCount());

Cursor cursor = db.query(TABLE_NAME, null,
		COL_ID + " IN (?)", new String[] { ids },
		null, null, null);
log(cursor.getCount());
Die erste Abfrage macht immer, was sie soll und ich erhalte den gewünschten Cursor. Die beiden anderen Abfragen liefern mir den gewünschten Cursor nur, wenn nur eine ID in der ArrayList ist.

Ausgabe bei einer ID in der Liste:
Code:
 2
SELECT * FROM Nutrients WHERE  _id IN (2, 7)
1
SELECT * FROM Nutrients WHERE  _id IN (?)
1
1

Ausgabe bei zwei (und mehr) IDs in der Liste:
Code:
 2, 7
SELECT * FROM Nutrients WHERE  _id IN (2, 7)
2
SELECT * FROM Nutrients WHERE  _id IN (?) //Dass das ? hier nicht ersetzt ist, ist logisch, da das erst im Queryaufruf geschieht.
0
0

Ganz offensichtlich mache ich bei den
Code:
String[] selectionArgs
etwas falsch, ich komme aber nicht dahinter.

Gruß
 
Zuletzt bearbeitet:
dzim

dzim

Top Contributor
schau ich mir am Montag mal an, wenn ich meinen Quellcode für Plain-SQL-Queries wieder vor Augen hab ;-)
 
dzim

dzim

Top Contributor
So. Ich hab gerade mal in meinen Code geschaut. Ich poste einfach mal die query-Methode, ich denke du erkennst dann sofort den Unterschied, der auch bestimmt der Grund ist, warum es bei dir nicht läuft:

Java:
	public static List<Map<String, Object>> sqlQuery(SQLiteDatabase db, String select, String from, String where, String sortOrder, Integer limit,
			Long offset, List<String> whereArgs, Set<String> mapColums) {
		
		if (db == null)
			return null;
		
		if (!db.isReadOnly()) {
			db.beginTransaction();
		}
		
		String query = String.format(Locale.ENGLISH, "SELECT %s FROM %s", select, from);
		if (where != null && !where.isEmpty()) {
			query = query.concat(String.format(Locale.ENGLISH, " WHERE %s", where));
		}
		if (sortOrder != null && !sortOrder.isEmpty()) {
			query = query.concat(String.format(Locale.ENGLISH, " ORDER BY %s", sortOrder));
		}
		if (limit != null && limit > 0) {
			query = query.concat(String.format(Locale.ENGLISH, " LIMIT %d", limit));
			if (offset != null && offset >= 0) {
				query = query.concat(String.format(Locale.ENGLISH, " OFFSET %d", offset));
			}
		}
		
		android.util.Log.v(SpeedTestDBAccess.class.getSimpleName(), query);
		
		List<Map<String, Object>> results = new ArrayList<Map<String, Object>>();
		
		Cursor cursor = db.rawQuery(query, whereArgs == null ? null : whereArgs.toArray(new String[] {}));
		
		if (cursor != null) {
			
			while (cursor.moveToNext()) {
				
				Map<String, Object> result = new HashMap<String, Object>();
				String[] colNames = cursor.getColumnNames();
				
				for (int i = 0; i < colNames.length; i++) {
					
					String colName = colNames[i];
					if (mapColums != null && !mapColums.contains(colName)) {
						continue;
					}
					
					int colIndex = cursor.getColumnIndex(colName);
					
					switch (cursor.getType(colIndex)) {
					case Cursor.FIELD_TYPE_BLOB:
						result.put(colName, cursor.getBlob(colIndex));
						break;
					case Cursor.FIELD_TYPE_FLOAT:
						result.put(colName, cursor.getDouble(colIndex));
						break;
					case Cursor.FIELD_TYPE_INTEGER:
						result.put(colName, cursor.getLong(colIndex));
						break;
					case Cursor.FIELD_TYPE_NULL:
						result.put(colName, null);
						break;
					case Cursor.FIELD_TYPE_STRING:
						result.put(colName, cursor.getString(colIndex));
						break;
					}
				}
				
				results.add(result);
			}
			
			cursor.close();
		}
		
		if (!db.isReadOnly()) {
			db.setTransactionSuccessful();
			db.endTransaction();
		}
		
		return results;
	}

Bitte bedenke auch, dass ich diese Methode nur für kleine Ergebnismengen verwende, denn die
Code:
List<Map<String, Object>>
-Variante ist nicht gerade Resourcenschonend. Ich habe in unserer App mal testweise mehr als 10000 Ergebnisse gehabt (SpeedTest-Messungen), während ich denke, dass die meisten nie so viele Daten haben werden, ist es für bestimmte Anwendergruppen - wie uns - durchaus möglich. Diese 10000 Ergebnisse (viiiiiiele Details in der DB) kommt unsere Anwendung irgendwann ins schleudern, wenn man es auf älteren Geräten ausführt. Und älter heisst z.B. auch mein gutes altes Galaxy Nexus...

Also verwende diese Methode wirklich nur mit bedacht. Der ContentProvider kann - glaube ich - so was wie Lazy Loading, also Daten erst nach Bedarf holen (wenn man in einer Liste hin scrollst...). Das kann dieser Brute-Force-Ansatz nicht. Ich verwende es primär für Statistiken oder mittels Joins zu holende flache Strukturen aus wenigen Datensätzen (Joins mit dem ContentProvider gehen zwar, finde es aber unhandlich, dass man quasi eigene "Tabellen" dafür definieren muss).

Grüsse,
Daniel

PS: Sag mal bescheid, ob's klappt oder nicht.

PPS: Durch die switch über die Datentypen (
Code:
Cursor.FIELD_*
) ist das ganze API-Level 11

PPPS: Wenn du größere Inserts mit Raw-Queries machen möchtest, verwende am besten immer Transaktionen, das hat mir einen Performance-Boost gegeben.
 
K

kurztipp

Aktives Mitglied
ich denke du erkennst dann sofort den Unterschied, der auch bestimmt der Grund ist, warum es bei dir nicht läuft

Hallo,

leider nein, hilft Dein Code auch nicht weiter. Es liegt nicht daran, dass ich die Liste nicht zum Array o.ä. mache. Die Methode, die die
Code:
whereArgs
einfügen soll, kommt scheinbar nicht mit einem String (aus einem Array ;) ) zurecht, der
Code:
"n, ... m"
enhält, also eine kommaseparierte Aufzählung von Ganzzahlen.

Man erkennt das ganz gut, an der Ausgabe zu folgendem Snippet:
Code:
cursor
ruft die Daten ab, wie gewünscht, ist aber eine "von Hand geschriebene" Abfrage.
Code:
cursor1
ruft beim ersten mal das richtige Element, ab dem zweiten Durchlauf, aber keine Daten mehr ab, wenn die IDs in den
Code:
whereArgs
übergeben werden -- warum auch immer*.
Code:
cursor2
stürzt beim zweiten Durchlauf erwarteterweise ab, da es in
Code:
where
nur ein
Code:
?
gibt,
Code:
whereArgs
aber zwei Elemente enthält**.

* Weil
Code:
WHERE _id IN (n, m)
keine Übereinstimmung ergibt und genau da liegt das Problem, weil ich nicht weiß, warum, denn als
Code:
rawQuery()
funktioniert es ja.

** Genauso viele
Code:
?
in
Code:
where
zu schreiben wie IDs dann einzeln per
Code:
whereArgs
übergeben werden halte ich für wenig zielführend und nicht für im Sinne des Erfinders.

Java:
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {

	// Check if the user selected an ingredient which is already selected
	if (!mIdList.contains(String.valueOf(id))) {
		mIdList.add(String.valueOf(id));
	}

	// Create the _id selectionArgs
	String ids = "";
	for (int i = 0; i < mIdList.size(); i++) {
		ids = ids.concat(mIdList.get(i));
		if (i < mIdList.size() - 1) {
			ids = ids.concat(", ");
		}
	}
	log("IDs: " + ids);

	String sql = SQLiteQueryBuilder.buildQueryString(false,
			TABLE_NAME, null, COL_ID
					+ " IN (" + ids + ")", null, null, null, null);
	log("RawSQL: " + sql);
	Cursor cursor = db.rawQuery(sql, null);
	log("RawSQLCursor.count(): " + cursor.getCount());

	Cursor cursor1 = db.query(TABLE_NAME, null,
			COL_ID + " IN (?)", new String[] { ids },
			null, null, null);
	log("SQLDatabase1.query().getCount(): " + cursor1.getCount());

	Cursor cursor2 = db.query(TABLE_NAME, null,
			COL_ID + " IN (?)",
			mIdList.toArray(new String[] {}), null, null, null);
	log("SQLDatabase2.query().getCount(): " + cursor2.getCount());

	updateListView(cursor);
}

Java:
IDs: 2
RawSQL: SELECT * FROM Nutrients WHERE  _id IN (2)
RawSQLCursor.count(): 1
SQLDatabase1.query().getCount(): 1
SQLDatabase2.query().getCount(): 1
***** Change cursor *****
IDs: 2, 4
RawSQL: SELECT * FROM Nutrients WHERE  _id IN (2, 4)
RawSQLCursor.count(): 2
SQLDatabase1.query().getCount(): 0
Shutting down VM
thread exiting with uncaught exception (group=0x41465700)
FATAL EXCEPTION: main
java.lang.IllegalArgumentException: Cannot bind argument at index 2 because the index is out of range. The statement has 1 parameters.

Ich hoffe, es ist klar, was ich meine.

[EDIT]Es liegt scheinbar an der Methode, die
Code:
whereArgs
übersetzt, wenn where nämlich
Code:
_id in ?
(statt
Code:
_id in (?)
) ist und man
Code:
"(n,...m)"
(statt
Code:
"n,...m"
) übergibt, stürzt das ganze nämlich auch mit einer SQLiteException ab.[/EDIT]

Gruß
 
Zuletzt bearbeitet:
dzim

dzim

Top Contributor
Leider werde ich gerade aus deiner Beschreibung ansonsten nicht ganz schlau - zu viele kleine Brocken an Informationen überlagern sich ;-)

Das, was mir aufgefallen beim Betrachten deines Codes aufgefallen ist, ist das mal
Code:
rawQuery
und mal nur
Code:
query
verwendet wird. Ich verwende nur rawQuery, weil das eben wirklich reines SQL ist, während bei
Code:
query
Android noch irgendetwas herumbastelt. Aber das ist nicht das Problem denke ich.

Dein Problem kommt wahrscheinlich eher von SQL, als von Android.
Fakt ist: Wenn du in JDBC oder eben hier die Daten dynamisch an die Query binden möchtest, musst du ein
Code:
?
als Platzhalter setzen. Immer. Das ist gewollt und tatsächlich im Sinne des Erfinders. Allerdings ist es in JDBC etwas klarer, weil man hier das Binding der Platzhalter an konkrete Daten per Index - also per Position in der Query - vornimmt. In Android geschieht das halt über die absolute Position im Array.
Der Hintergrund ist (jedenfalls in JDBC) dass du die Query nur einmal schreiben (also die "gewollten" Daten einsetzen) musst Und dann nur noch die Platzhalter ersetzt. Die Query kann dann schon mal vorbereitet - "prepared" (ich glaube sogar wirklich schon DB-seitig!) - werden und das ist meist effizienter als die Brute-Force-Methode, bei der du dich auch noch um die Maskierung von Strings u.s.w. kümmern müsstest.
Zugegeben: In dem Rahmen, in dem wir uns bewegen, spielt das meist keine Rolle, aber sei's drum.

Die Klammern in einer WHERE IN-Klausel weg zu lassen ist übrigens ein No-Go - das ist kein konformes SQL und kann nicht funktionieren, es sei denn die DB erlaubt explizit etwas anderes - ich würde mich aber immer an Standards halten, dann ist deine Query zumindest Theoretisch auf allen SQL-DBs ausführbar (wenn das Schema übereinstimmt).

Wichtig ist übrigens auch zu wissen, dass SQLite intern (glaube ich) nur mit Strings arbeitet (bitte überprüfen). Daher auch immer String-Arrays als Argumentenliste für
Code:
?
-Platzhalter. Es könnte also sein, dass dein "ich verwende nur ein ? und ersetze es"-Ansatz daran scheitert, dass du die IDs nicht als Strings maskierst (sprich Hochkomma (
Code:
'
) um jedes setzt:
Code:
'1', '2'
) - auch nur ne Vermutung.
 
Anzeige

Neue Themen


Oben