Klausa IN dan placeholder

104

Saya mencoba melakukan kueri SQL berikut di 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});

Namun, Android tidak mengganti tanda tanya dengan nilai yang benar. Saya dapat melakukan hal berikut, namun, ini tidak melindungi dari injeksi SQL:

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

Bagaimana cara mengatasi masalah ini dan dapat menggunakan klausa IN?

Nick
sumber

Jawaban:

188

String formulir "?, ?, ..., ?"dapat berupa string yang dibuat secara dinamis dan dengan aman dimasukkan ke dalam kueri SQL asli (karena ini adalah formulir terbatas yang tidak berisi data eksternal) dan kemudian placeholder dapat digunakan seperti biasa.

Pertimbangkan fungsi String makePlaceholders(int len)yang mengembalikan lentanda tanya yang dipisahkan dengan koma, lalu:

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

Pastikan untuk meneruskan nilai sebanyak tempat. Batas maksimum default parameter host di SQLite adalah 999 - setidaknya dalam build normal, tidak yakin tentang Android :)

Selamat membuat kode.


Inilah salah satu implementasinya:

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

sumber
8
Ya, ini adalah satu-satunya cara untuk menggunakan kueri parameterized IN () di SQLite dan hampir semua database SQL lainnya.
Larry Lustig
Dengan menggunakan metode ini, saya menambah ContentProvider yang saya gunakan dan dalam metode query () menambahkan logika untuk menguji keberadaan: "IN?" dan jika ditemukan, apakah hitungan kemunculan "?" dalam pemilihan awal, dibandingkan dengan panjang argumen yang diberikan, menyusun "?,?, ...?" untuk perbedaan dan menggantikan aslinya "IN?" dengan koleksi tanda tanya yang dihasilkan. Ini membuat logika tersedia hampir secara global dan untuk penggunaan saya tampaknya berfungsi dengan baik. Saya memang harus menambahkan beberapa penyediaan khusus untuk memfilter daftar IN yang kosong, dalam kasus tersebut, "IN?" diganti dengan "1" untuk saat ini.
SandWyrm
3
Hal konyol tentang ini adalah, tentu saja, jika Anda akan membuat String sendiri dengan tanda tanya N, Anda sebaiknya menyandikan data secara langsung. Dengan asumsi itu sudah dibersihkan.
Christopher
16
Keseluruhan ini makePlaceholdersbisa diganti dengan TextUtils.join(",", Collections.nCopies(len, "?")). Lebih sedikit bertele-tele.
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

Contoh singkat, berdasarkan jawaban pengguna166390:

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

Sayangnya tidak ada cara untuk melakukan itu (jelas 'name1', 'name2'bukan nilai tunggal dan karena itu tidak dapat digunakan dalam pernyataan yang disiapkan).

Jadi, Anda harus menurunkan pandangan Anda (misalnya dengan membuat kueri yang sangat spesifik, tidak dapat digunakan kembali seperti WHERE name IN (?, ?, ?)) atau tidak menggunakan prosedur tersimpan dan mencoba mencegah injeksi SQL dengan beberapa teknik lain ...

florian h
sumber
4
Anda sebenarnya dapat, dengan sedikit kerja, membuat kueri IN berparameter. Lihat jawaban pst di bawah ini. Kueri yang dihasilkan diparameterisasi dan aman injeksi.
Larry Lustig
@LarryLustig jawaban mana yang Anda maksud dengan jawaban "pst"? Apakah sudah dihapus? Ini tampaknya menjadi satu-satunya kejadian pst di sini ...
Stefan Haustein
1
@StefanHaustein " jawaban pst " (pengguna dihapus).
pengguna4157124
5

Seperti yang disarankan dalam jawaban yang diterima tetapi tanpa menggunakan fungsi kustom untuk menghasilkan '?' Yang dipisahkan koma. Silakan periksa kode di bawah ini.

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

Anda dapat menggunakan TextUtils.join(",", parameters)untuk memanfaatkan parameter pengikatan sqlite, di mana parametersdaftar dengan "?"placeholder dan string hasil seperti itu "?,?,..,?".

Ini adalah contoh kecilnya:

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
sumber
Bagaimana jika salah satu "parameter" harus nol?
Pengembang android
0

Sebenarnya Anda bisa menggunakan cara asli android untuk membuat kueri sebagai ganti 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
sumber
0

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

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

Menggunakan 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
sumber
0

Di Kotlin, Anda dapat menggunakan joinToString

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

Saya menggunakan StreamAPI untuk ini:

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
sumber