IN cláusula y marcadores de posición

104

Estoy intentando hacer la siguiente consulta SQL dentro de 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});

Sin embargo, Android no reemplaza el signo de interrogación con los valores correctos. Podría hacer lo siguiente, sin embargo, esto no protege contra la inyección de SQL:

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

¿Cómo puedo solucionar este problema y poder utilizar la cláusula IN?

Mella
fuente

Respuestas:

188

Una cadena del formulario "?, ?, ..., ?"puede ser una cadena creada dinámicamente y colocarse de forma segura en la consulta SQL original (porque es una forma restringida que no contiene datos externos) y luego los marcadores de posición se pueden usar normalmente.

Considere una función String makePlaceholders(int len)que devuelve lensignos de interrogación separados por comas, luego:

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

Solo asegúrese de pasar exactamente tantos valores como lugares. El límite máximo predeterminado de parámetros de host en SQLite es 999, al menos en una compilación normal, no estoy seguro de Android :)

Codificación feliz.


Aquí hay una implementación:

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

fuente
8
Sí, esta es la (única) forma de usar consultas IN () parametrizadas en SQLite y prácticamente cualquier otra base de datos SQL.
Larry Lustig
Usando este método, aumenté el ContentProvider que usé y en el método query () agregué lógica para probar la presencia: "IN?" y si se encuentra, se cuenta la aparición de "?" en la selección original, en comparación con la longitud de los argumentos pasados, ensambla un "?,?, ...?" para la diferencia y reemplaza el original "IN?" con la colección de signos de interrogación generada. Esto hace que la lógica esté disponible casi globalmente y para mis usos parece estar funcionando bien. Tuve que agregar algún aprovisionamiento especial para filtrar listas IN vacías, en esos casos, el "IN?" se reemplaza por "1" por ahora.
SandWyrm
3
Lo tonto de esto es, por supuesto, que si va a hacer su propia cadena con N signos de interrogación, también podría codificar los datos directamente. Suponiendo que esté desinfectado.
Christopher
16
Este conjunto makePlaceholderspodría reemplazarse por TextUtils.join(",", Collections.nCopies(len, "?")). Menos prolijo.
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

Ejemplo corto, basado en la respuesta del usuario 166390:

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
fuente
5

Lamentablemente, no hay forma de hacerlo (obviamente, 'name1', 'name2'no es un valor único y, por lo tanto, no se puede utilizar en una declaración preparada).

Por lo tanto, tendrá que bajar la vista (por ejemplo, creando consultas muy específicas, no reutilizables como WHERE name IN (?, ?, ?)) o no usar procedimientos almacenados e intentar evitar las inyecciones de SQL con algunas otras técnicas ...

florian h
fuente
4
De hecho, puede, con un poco de trabajo, crear una consulta IN parametrizada. Vea la respuesta de pst, a continuación. La consulta resultante está parametrizada y segura para inyecciones.
Larry Lustig
@LarryLustig ¿a qué respuesta te refieres con la respuesta "pst"? ¿Fue eliminado? Esta parece ser la única aparición de pst aquí ...
Stefan Haustein
1
@StefanHaustein " respuesta de pst " (usuario eliminado).
user4157124
5

Como se sugiere en la respuesta aceptada pero sin usar la función personalizada para generar '?' Separados por comas. Por favor verifique el código a continuación.

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
fuente
1

Puede usar TextUtils.join(",", parameters)para aprovechar los parámetros de enlace de sqlite, donde parametershay una lista con "?"marcadores de posición y la cadena de resultado es algo así como "?,?,..,?".

Aquí hay un pequeño ejemplo:

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
fuente
¿Qué pasa si uno de los "parámetros" necesita ser nulo?
desarrollador de Android
0

En realidad, podría usar la forma nativa de consulta de Android en lugar 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
fuente
0

Esto no es 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}););

Esto es 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
fuente
0

En Kotlin puedes usar joinToString

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

Utilizo la StreamAPI para esto:

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