Cláusula IN e marcadores de posição

104

Estou tentando fazer a seguinte consulta SQL no Android:

    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});

No entanto, o Android não substitui o ponto de interrogação pelos valores corretos. Eu poderia fazer o seguinte, no entanto, isso não protege contra injeção de SQL:

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

Como posso contornar esse problema e ser capaz de usar a cláusula IN?

usuario
fonte

Respostas:

188

Uma string do formulário "?, ?, ..., ?"pode ser uma string criada dinamicamente e colocada com segurança na consulta SQL original (porque é um formulário restrito que não contém dados externos) e os marcadores de posição podem ser usados ​​normalmente.

Considere uma função String makePlaceholders(int len)que retorna pontos de leninterrogação separados por vírgulas, então:

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);

Apenas certifique-se de passar exatamente tantos valores quanto casas. O limite máximo padrão de parâmetros de host no SQLite é 999 - pelo menos em uma compilação normal, não tenho certeza sobre o Android :)

Boa codificação.


Aqui está uma implementação:

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();
    }
}

fonte
8
Sim, esta é a (única) maneira de usar consultas IN () parametrizadas no SQLite e em praticamente qualquer outro banco de dados SQL.
Larry Lustig
Usando este método, aumentei o ContentProvider que usei e no método query () adicionei lógica para testar a presença: "IN?" e, se encontrado, conta a ocorrência de "?" na seleção original, em comparação com o comprimento dos argumentos passados, monta um "?,?, ...?" para a diferença e substitui o original "IN?" com a coleção de pontos de interrogação gerada. Isso torna a lógica disponível quase global e para meu uso ela parece estar funcionando bem. Eu tive que adicionar algum provisionamento especial para filtrar listas IN vazias, nesses casos, o "IN?" é substituído por "1" por enquanto.
SandWyrm
3
A coisa boba sobre isso é, claro, que se você for fazer sua própria String com N pontos de interrogação, você também pode apenas codificar os dados diretamente. Supondo que esteja higienizado.
Christopher
16
Este todo makePlaceholderspoderia ser substituído por TextUtils.join(",", Collections.nCopies(len, "?")). Menos prolixo.
Konrad Morawski
1
Caused by: android.database.sqlite.SQLiteException: near ",": syntax error (code 1): , while compiling: SELECT url FROM tasks WHERE url=?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
Iman Marashi
9

Pequeno exemplo, com base na resposta do usuário166390:

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;
}
Yuliia Ashomok
fonte
5

Infelizmente, não há como fazer isso (obviamente 'name1', 'name2'não é um valor único e, portanto, não pode ser usado em uma instrução preparada).

Portanto, você terá que diminuir sua mira (por exemplo, criando consultas muito específicas e não reutilizáveis, como WHERE name IN (?, ?, ?)) ou não usar procedimentos armazenados e tentar evitar injeções de SQL com algumas outras técnicas ...

florian h
fonte
4
Você realmente pode, com um pouco de trabalho, construir uma consulta IN parametrizada. Veja a resposta de pst, abaixo. A consulta resultante é parametrizada e segura para injeção.
Larry Lustig
@LarryLustig a qual resposta você está se referindo com a resposta "pst"? Foi excluído? Esta parece ser a única ocorrência de pst aqui ...
Stefan Haustein
1
@StefanHaustein " resposta do pst " (usuário removido).
user4157124
5

Como sugerido na resposta aceita, mas sem usar a função personalizada para gerar '?' Separado por vírgula. Por favor, verifique o código abaixo.

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);
Kalpesh Gohel
fonte
1

Você pode usar TextUtils.join(",", parameters)para tirar proveito dos parâmetros de vinculação do sqlite, onde parametersé uma lista com "?"espaços reservados e a string de resultado é algo como "?,?,..,?".

Aqui está um pequeno exemplo:

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()])
);
epool
fonte
E se um dos "parâmetros" precisar ser nulo?
desenvolvedor Android
0

Na verdade, você pode usar a forma nativa de consultar do Android em vez de 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;
}
Stan
fonte
0

Isto não é válido

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}););

Isto é válido

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);

Usando 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);
Vahe Gharibyan
fonte
0

Em Kotlin, você pode usar joinToString

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

Eu uso a StreamAPI para isso:

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);
PPpartisan
fonte