IN 子句和占位符

我尝试在 Android 中执行以下 SQL 查询:

    String names = "'name1', 'name2";   // in the code this is dynamically generated


String query = "SELECT * FROM table WHERE name IN (?)";
Cursor cursor = mDb.rawQuery(query, new String[]{names});

但是,Android 不会用正确的值替换问号。我可以这样做,但是,这并不能防止 SQL 注入:

    String query = "SELECT * FROM table WHERE name IN (" + names + ")";
Cursor cursor = mDb.rawQuery(query, null);

我怎样才能避免这个问题并且能够使用 IN 子句?

51392 次浏览

Sadly there's no way of doing that (obviously 'name1', 'name2' is not a single value and can therefore not be used in a prepared statement).

So you will have to lower your sights (e.g. by creating very specific, not reusable queries like WHERE name IN (?, ?, ?)) or not using stored procedures and try to prevent SQL injections with some other techniques...

A string of the form "?, ?, ..., ?" can be a dynamically created string and safely put into the original SQL query (because it is a restricted form that does not contain external data) and then the placeholders can be used as normal.

Consider a function String makePlaceholders(int len) which returns len question-marks separated with commas, then:

String[] names = { "name1", "name2" }; // do whatever is needed first
String query = "SELECT * FROM table"
+ " WHERE name IN (" + makePlaceholders(names.length) + ")";
Cursor cursor = mDb.rawQuery(query, names);

Just make sure to pass exactly as many values as places. The default maximum limit of host parameters in SQLite is 999 - at least in a normal build, not sure about Android :)


Here is one implementation:

String makePlaceholders(int len) {
if (len < 1) {
// It will lead to an invalid query anyway ..
throw new RuntimeException("No placeholders");
} else {
StringBuilder sb = new StringBuilder(len * 2 - 1);
sb.append("?");
for (int i = 1; i < len; i++) {
sb.append(",?");
}
return sb.toString();
}
}

Actually you could use android's native way of querying instead of rawQuery:

public int updateContactsByServerIds(ArrayList<Integer> serverIds, final long groupId) {
final int serverIdsCount = serverIds.size()-1; // 0 for one and only id, -1 if empty list
final StringBuilder ids = new StringBuilder("");
if (serverIdsCount>0) // ambiguous "if" but -1 leads to endless cycle
for (int i = 0; i < serverIdsCount; i++)
ids.append(String.valueOf(serverIds.get(i))).append(",");
// add last (or one and only) id without comma
ids.append(String.valueOf(serverIds.get(serverIdsCount))); //-1 throws exception
// remove last comma
Log.i(this,"whereIdsList: "+ids);
final String whereClause = Tables.Contacts.USER_ID + " IN ("+ids+")";


final ContentValues args = new ContentValues();
args.put(Tables.Contacts.GROUP_ID, groupId);


int numberOfRowsAffected = 0;
SQLiteDatabase db = dbAdapter.getWritableDatabase());
try {
numberOfRowsAffected = db.update(Tables.Contacts.TABLE_NAME, args, whereClause, null);
} catch (Exception e) {
e.printStackTrace();
}
dbAdapter.closeWritableDB();




Log.d(TAG, "updateContactsByServerIds() numberOfRowsAffected: " + numberOfRowsAffected);


return numberOfRowsAffected;
}

You can use TextUtils.join(",", parameters) to take advantage of sqlite binding parameters, where parameters is a list with "?" placeholders and the result string is something like "?,?,..,?".

Here is a little example:

Set<Integer> positionsSet = membersListCursorAdapter.getCurrentCheckedPosition();
List<String> ids = new ArrayList<>();
List<String> parameters = new ArrayList<>();
for (Integer position : positionsSet) {
ids.add(String.valueOf(membersListCursorAdapter.getItemId(position)));
parameters.add("?");
}
getActivity().getContentResolver().delete(
SharedUserTable.CONTENT_URI,
SharedUserTable._ID + " in (" + TextUtils.join(",", parameters) + ")",
ids.toArray(new String[ids.size()])
);

Short example, based on answer of user166390:

public Cursor selectRowsByCodes(String[] codes) {
try {
SQLiteDatabase db = getReadableDatabase();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();


String[] sqlSelect = {COLUMN_NAME_ID, COLUMN_NAME_CODE, COLUMN_NAME_NAME, COLUMN_NAME_PURPOSE, COLUMN_NAME_STATUS};
String sqlTables = "Enumbers";


qb.setTables(sqlTables);


Cursor c = qb.query(db, sqlSelect, COLUMN_NAME_CODE+" IN (" +
TextUtils.join(",", Collections.nCopies(codes.length, "?")) +
")", codes,
null, null, null);
c.moveToFirst();
return c;
} catch (Exception e) {
Log.e(this.getClass().getCanonicalName(), e.getMessage() + e.getStackTrace().toString());
}
return null;
}

As suggest in accepted answer but without using custom function to generate comma-separated '?'. Please check code below.

String[] names = { "name1", "name2" }; // do whatever is needed first
String query = "SELECT * FROM table"
+ " WHERE name IN (" + TextUtils.join(",", Collections.nCopies(names.length, "?"))  + ")";
Cursor cursor = mDb.rawQuery(query, names);

This is not Valid

String subQuery = "SELECT _id FROM tnl_partofspeech where part_of_speech = 'noun'";
Cursor cursor = SQLDataBase.rawQuery(
"SELECT * FROM table_main where part_of_speech_id IN (" +
"?" +
")",
new String[]{subQuery}););

This is Valid

String subQuery = "SELECT _id FROM tbl_partofspeech where part_of_speech = 'noun'";
Cursor cursor = SQLDataBase.rawQuery(
"SELECT * FROM table_main where part_of_speech_id IN (" +
subQuery +
")",
null);

Using ContentResolver

String subQuery = "SELECT _id FROM tbl_partofspeech where part_of_speech = 'noun' ";


final String[] selectionArgs = new String[]{"1","2"};
final String selection = "_id IN ( ?,? )) AND part_of_speech_id IN (( " + subQuery + ") ";
SQLiteDatabase SQLDataBase = DataBaseManage.getReadableDatabase(this);


SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables("tableName");


Cursor cursor =  queryBuilder.query(SQLDataBase, null, selection, selectionArgs, null,
null, null);

In Kotlin you can use joinToString

val query = "SELECT * FROM table WHERE name IN (${names.joinToString(separator = ",") { "?" }})"
val cursor = mDb.rawQuery(query, names.toTypedArray())

I use the Stream API for this:

final String[] args = Stream.of("some","data","for","args").toArray(String[]::new);
final String placeholders = Stream.generate(() -> "?").limit(args.length).collect(Collectors.joining(","));
final String selection = String.format("SELECT * FROM table WHERE name IN(%s)", placeholders);


db.rawQuery(selection, args);