Klauzula IN i symbole zastępcze


104

Próbuję wykonać następujące zapytanie SQL w systemie 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});

Jednak Android nie zastępuje znaku zapytania prawidłowymi wartościami. Mógłbym zrobić co następuje, jednak to nie chroni przed wstrzyknięciem SQL:

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

Jak mogę obejść ten problem i móc skorzystać z klauzuli IN?

Odpowiedzi:


188

Ciąg formularza "?, ?, ..., ?" może być dynamicznie utworzonym ciągiem i bezpiecznie umieścić go w oryginalnym zapytaniu SQL (ponieważ jest to forma zastrzeżona, która nie zawiera danych zewnętrznych), a następnie symbole zastępcze mogą być używane normalnie.

Rozważmy funkcję, String makePlaceholders(int len)która zwraca lenznaki zapytania oddzielone przecinkami, a następnie:

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

Po prostu upewnij się, że przekazujesz dokładnie tyle wartości, ile miejsc. Domyślny maksymalny limit parametrów hosta w SQLite to 999 - przynajmniej w normalnej kompilacji, nie jestem pewien co do Androida :)

Miłego kodowania.


Oto jedna implementacja:

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

8
Tak, jest to (jedyny) sposób używania sparametryzowanych zapytań IN () w SQLite i prawie każdej innej bazie danych SQL.
Larry Lustig,

Korzystając z tej metody, rozszerzyłem ContentProvider, którego użyłem, iw metodzie query () dodałem logikę do testowania obecności: „IN?” a jeśli zostanie znaleziony, czy liczy wystąpienie znaku „?” w pierwotnej selekcji, w porównaniu z długością przekazanych argumentów, tworzy znak „?,?, ...?” za różnicę i zastępuje oryginalne „IN?” z wygenerowaną kolekcją znaków zapytania. To sprawia, że ​​logika jest dostępna prawie globalnie i wydaje się, że działa dobrze do moich zastosowań. Musiałem dodać jakąś specjalną obsługę administracyjną, aby odfiltrować puste listy IN, w takich przypadkach „IN?” na razie jest zastępowane przez „1”.
SandWyrm

3
Głupią rzeczą w tym jest oczywiście to, że jeśli zamierzasz stworzyć swój własny łańcuch znaków ze znakami zapytania N, równie dobrze możesz po prostu zakodować dane bezpośrednio. Zakładając, że jest odkażony.
Christopher,

16
Całość makePlaceholdersmożna by zastąpić TextUtils.join(",", Collections.nCopies(len, "?")). Mniej gadatliwy.
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

Krótki przykład na podstawie odpowiedzi użytkownika166390:

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

5

Niestety nie 'name1', 'name2'da się tego zrobić (oczywiście nie jest to pojedyncza wartość i dlatego nie można jej użyć w przygotowanym zestawieniu).

Będziesz więc musiał obniżyć swoje cele (np. Tworząc bardzo szczegółowe zapytania nienadające się do ponownego użycia, takie jak WHERE name IN (?, ?, ?)) lub nie używać procedur składowanych i próbować zapobiegać iniekcjom SQL za pomocą innych technik ...


4
W rzeczywistości możesz, przy odrobinie pracy, zbudować sparametryzowane zapytanie IN. Zobacz odpowiedź pst poniżej. Wynikowe zapytanie jest sparametryzowane i bezpieczne dla iniekcji.
Larry Lustig,

@LarryLustig do której odpowiedzi odnosisz się z odpowiedzią „pst”? Czy został usunięty? Wydaje się, że jest to jedyne wystąpienie pst tutaj ...
Stefan Haustein

1
@StefanHaustein " odpowiedź pst " (użytkownik usunięty).
user4157124

5

Jak sugeruje zaakceptowana odpowiedź, ale bez używania funkcji niestandardowej do generowania znaku „?” Oddzielonego przecinkami. Sprawdź kod poniżej.

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

1

Możesz użyć, TextUtils.join(",", parameters)aby skorzystać z parametrów wiązania sqlite, gdzie parametersjest lista z "?"symbolami zastępczymi, a ciąg wynikowy jest podobny do "?,?,..,?".

Oto mały przykład:

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

Co się stanie, jeśli jeden z „parametrów” musi być pusty?
programista Androida

0

Właściwie możesz użyć natywnego sposobu wykonywania zapytań Androida zamiast 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;
}

0

To nie jest ważne

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

To jest ważne

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

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

0

W Kotlinie możesz użyć joinToString

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

0

Używam StreamAPI do tego:

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);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.