Alternatywne klauzule PreparedStatement IN?

343

Jakie są najlepsze obejścia dla używania INklauzuli SQL z instancjami java.sql.PreparedStatement, które nie są obsługiwane dla wielu wartości z powodu problemów związanych z bezpieczeństwem ataku wstrzykiwania SQL: Jeden ?symbol zastępczy reprezentuje jedną wartość, a nie listę wartości.

Rozważ następującą instrukcję SQL:

SELECT my_column FROM my_table where search_column IN (?)

Używanie preparedStatement.setString( 1, "'A', 'B', 'C'" );jest zasadniczo niedziałającą próbą obejścia przyczyn używania ?w pierwszej kolejności.

Jakie obejścia są dostępne?

Chris Mazzola
źródło
1
Oscar, myślę, że dynamiczne generowanie (?,?, ....) jest najprostszym obejściem, jeśli potrzebujesz klauzuli IN, ale zostawiłem to poszczególnym połączeniom, ponieważ wydajność była wystarczająca w moim konkretnym przypadku.
Chris Mazzola,
6
Jedną z zalet przygotowanych instrukcji jest to, że sohuld można raz skompilować pod kątem wydajności. Uczynienie klauzuli in dynamicznej skutecznie neguje przygotowane oświadczenie.
2
W rzeczywistości działa to na MySQL (użycie setObject do ustawienia tablicy String jako wartości parametru). Jakiego DB używasz?
Frans
Oto powiązane pytanie: stackoverflow.com/q/6956025/521799
Lukas Eder

Odpowiedzi:

194

Analiza różnych dostępnych opcji oraz zalety i wady każdej z nich jest dostępna tutaj .

Sugerowane opcje to:

  • Przygotuj SELECT my_column FROM my_table WHERE search_column = ?, uruchom ją dla każdej wartości, a UNION wyniki po stronie klienta. Wymaga tylko jednego przygotowanego wyciągu. Powolny i bolesny.
  • Przygotuj SELECT my_column FROM my_table WHERE search_column IN (?,?,?)i uruchom. Wymaga jednego przygotowanego zestawienia na każdy rozmiar listy. Szybki i oczywisty.
  • Przygotuj SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...i uruchom. [Lub użyj UNION ALLzamiast tych średników. --ed] Wymaga jednego przygotowanego zestawienia na każdy rozmiar listy. Głupio powolne, zdecydowanie gorsze niż WHERE search_column IN (?,?,?), więc nie wiem, dlaczego bloger nawet to zasugerował.
  • Użyj procedury składowanej, aby zbudować zestaw wyników.
  • Przygotuj N różnych zapytań o wielkości listy IN; powiedzmy, z wartościami 2, 10 i 50. Aby wyszukać listę IN z 6 różnymi wartościami, wypełnij zapytanie wielkości 10 tak, aby wyglądało SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). Każdy przyzwoity serwer zoptymalizuje zduplikowane wartości przed uruchomieniem zapytania.

Żadna z tych opcji nie jest jednak świetna.

W tych miejscach udzielono odpowiedzi na duplikaty z równie rozsądnymi alternatywami, ale żadne z nich nie jest super świetne:

Prawidłowa odpowiedź, jeśli korzystasz z JDBC4 i obsługiwanego serwera x = ANY(y), powinna być zgodna z poniższym PreparedStatement.setArrayopisem:

Wydaje się jednak, że nie ma sposobu na setArraypracę z listami IN.


Czasami instrukcje SQL są ładowane w czasie wykonywania (np. Z pliku właściwości), ale wymagają zmiennej liczby parametrów. W takich przypadkach najpierw zdefiniuj zapytanie:

query=SELECT * FROM table t WHERE t.column IN (?)

Następnie załaduj zapytanie. Następnie określ liczbę parametrów przed uruchomieniem. Po znaniu liczby parametrów uruchom:

sql = any( sql, count );

Na przykład:

/**
 * Converts a SQL statement containing exactly one IN clause to an IN clause
 * using multiple comma-delimited parameters.
 *
 * @param sql The SQL statement string with one IN clause.
 * @param params The number of parameters the SQL statement requires.
 * @return The SQL statement with (?) replaced with multiple parameter
 * placeholders.
 */
public static String any(String sql, final int params) {
    // Create a comma-delimited list based on the number of parameters.
    final StringBuilder sb = new StringBuilder(
            new String(new char[params]).replace("\0", "?,")
    );

    // Remove trailing comma.
    sb.setLength(Math.max(sb.length() - 1, 0));

    // For more than 1 parameter, replace the single parameter with
    // multiple parameter placeholders.
    if (sb.length() > 1) {
        sql = sql.replace("(?)", "(" + sb + ")");
    }

    // Return the modified comma-delimited list of parameters.
    return sql;
}

W przypadku niektórych baz danych, w których przekazywanie tablicy za pomocą specyfikacji JDBC 4 nie jest obsługiwane, ta metoda może ułatwić przekształcenie spowolnienia = ?w IN (?)warunek szybszej klauzuli, który można następnie rozwinąć, wywołując anymetodę.

Dónal
źródło
123

Rozwiązanie dla PostgreSQL:

final PreparedStatement statement = connection.prepareStatement(
        "SELECT my_column FROM my_table where search_column = ANY (?)"
);
final String[] values = getValues();
statement.setArray(1, connection.createArrayOf("text", values));
final ResultSet rs = statement.executeQuery();
try {
    while(rs.next()) {
        // do some...
    }
} finally {
    rs.close();
}

lub

final PreparedStatement statement = connection.prepareStatement(
        "SELECT my_column FROM my_table " + 
        "where search_column IN (SELECT * FROM unnest(?))"
);
final String[] values = getValues();
statement.setArray(1, connection.createArrayOf("text", values));
final ResultSet rs = statement.executeQuery();
try {
    while(rs.next()) {
        // do some...
    }
} finally {
    rs.close();
}
Boris
źródło
1
wygląda dobrze. jaka część tego kodu jest specyficzna dla PostreSQL? „gdzie search_column = ANY (?)”? lub connection.createArrayOf? albo coś innego?
David Portabella
1
Myślę, że z powodu tej .createArrayOf()części jest bardziej specyficzny dla JDBC4 niż dla PostgreSQL , ale nie jestem pewien, czy ścisła semantyka dla użytkownika Arrayjest zdefiniowana w specyfikacji JDBC.
lvella,
3
Jeśli .createArrayOfnie działa, możesz wykonać własne ręczne tworzenie literału tablicowego, takiego jak String arrayLiteral = "{A,\"B \", C,D}" (zwróć uwagę, że „B” ma spację, podczas gdy C nie), a następnie, statement.setString(1,arrayLiteral)gdzie jest przygotowane wyrażenie ... IN (SELECT UNNEST(?::VARCHAR[]))lub ... IN (SELECT UNNEST(CAST(? AS VARCHAR[]))). (PS: Nie sądzę, aby ANYSELECT
działało
Świetne rozwiązanie! Naprawdę uratował mi dzień. Dla tablicy liczb całkowitych użyłem „int” w pierwszym parametrze createArrayOf () i wygląda dobrze. Ten pierwszy parametr wydaje się specyficzny dla DB, jednak na podstawie dokumentacji.
Emmanuel Touzery
2
To wydaje się najczystszym rozwiązaniem. Jeśli ktoś szuka specyficznej składni HSQLDB: udało mi się ją uruchomić z IN (UNNEST (?))
aureianimus
19

Nie ma prostej drogi AFAIK. Jeśli celem jest utrzymanie wysokiego współczynnika pamięci podręcznej instrukcji (tj. Nie tworzenie instrukcji dla każdej liczby parametrów), możesz wykonać następujące czynności:

  1. utwórz instrukcję z kilkoma (np. 10) parametrami:

    ... GDZIE WEJŚĆ (?,?,?,?,?,?,?,?,?,?) ...

  2. Powiąż wszystkie aktualne parametry

    setString (1, „foo”); setString (2, „bar”);

  3. Powiąż resztę jako NULL

    setNull (3, Typ.VARCHAR) ... setNull (10, Typ.VARCHAR)

NULL nigdy nie pasuje do niczego, więc jest optymalizowany przez konstruktora planu SQL.

Logikę można łatwo zautomatyzować po przekazaniu listy do funkcji DAO:

while( i < param.size() ) {
  ps.setString(i+1,param.get(i));
  i++;
}

while( i < MAX_PARAMS ) {
  ps.setNull(i+1,Types.VARCHAR);
  i++;
}
Vladimir Dyuzhev
źródło
„NULL nigdy nie pasuje do niczego” - czy NULLw zapytaniu pasuje NULLwartość w bazie danych?
Craig McQueen
5
@CraigMcQueen Nie, nie byłoby. Null nie pasuje nawet do null, zgodnie ze standardem ANSI.
Dawood ibn Kareem
Możesz dopasować NULL, używając słowa kluczowego IS NULL. Dobrym sposobem na wykrycie wierszy, które nie istnieją w połączonej tabeli, jest użycie LEWEGO DOŁĄCZENIA wraz z IS NULL. „WYBIERZ a.URL, b.URL Z TABELI_A a LEWA DOŁĄCZ TABELA_B b NA a_A.URL = b_B.URL GDZIE b.URL JEST NULL” Wyświetli wszystkie wiersze w tabeli A, które nie pasują do tabeli B.
Jens Tandstad,
3
Uważaj jednak na to. NOT INi INnie obsługuj wartości null w ten sam sposób. Uruchom to i zobacz, co się stanie: select 'Matched' as did_it_match where 1 not in (5, null); Następnie usuń nulli obserwuj magię.
Brandon,
Lub możesz ustawić wszystkie dodatkowe parametry na wartość dowolnego poprzedniego parametru. Każdy przyzwoity silnik DB je odfiltruje. Więc a IN (1,2,3,3,3,3,3)to samo co a IN (1,2,3). Działa również w NOT INprzeciwieństwie do a NOT IN (1,2,3,null,null,null,null)(który zawsze nie zwraca wierszy, ponieważ any_value != NULLzawsze jest fałszem).
Ruslan Stelmachenko
11

Nieprzyjemnym obejściem, ale z pewnością wykonalnym jest użycie zapytania zagnieżdżonego. Utwórz tymczasową tabelę MYVALUES z kolumną w niej. Wstaw listę wartości do tabeli MYVALUES. Następnie wykonaj

select my_column from my_table where search_column in ( SELECT value FROM MYVALUES )

Brzydka, ale realna alternatywa, jeśli lista wartości jest bardzo duża.

Ta technika ma tę dodatkową zaletę, że potencjalnie lepsze plany zapytań optymalizatora (sprawdź stronę pod kątem wielu wartości, skanuj tabele tylko raz zamiast tego raz na wartość itp.) Może zaoszczędzić na narzutach, jeśli baza danych nie buforuje przygotowanych instrukcji. Twoje „WSTAWKI” musiałyby być wykonane wsadowo, a tabela MYVALUES może wymagać ulepszenia, aby zapewnić minimalne blokowanie lub inne wysokie zabezpieczenia.

James Schek
źródło
Jakie zalety miałoby to w porównaniu z pytaniem o moją wartość w jednej tabeli?
Paul Tomblin,
3
Optymalizator zapytań może zmniejszyć obciążenie we / wy, pobierając wszystkie możliwe dopasowania z załadowanej strony. Skany tabel lub skany indeksu mogą być wykonywane raz zamiast jednej wartości. Narzut związany z wstawianymi wartościami można zmniejszyć za pomocą operacji wsadowych i może być mniejszy niż kilka zapytań.
James Schek
1
wygląda dobrze, ale mogą występować problemy z współbieżnością. czy specyfikacja jdbc zawiera sposób na utworzenie tymczasowej anonimowej tabeli w pamięci? lub coś w tym rodzaju, jeśli to możliwe, nie jest specyficzne dla dostawcy jdbc?
David Portabella
9

Ograniczenia operatora in () są źródłem wszelkiego zła.

Działa w trywialnych przypadkach i można go rozszerzyć o „automatyczne generowanie przygotowanego zestawienia”, jednak zawsze ma swoje ograniczenia.

  • jeśli tworzysz instrukcję ze zmienną liczbą parametrów, spowoduje to, że sql narzuci narzut przy każdym wywołaniu
  • na wielu platformach liczba parametrów operatora in () jest ograniczona
  • na wszystkich platformach całkowity rozmiar tekstu SQL jest ograniczony, co uniemożliwia przesłanie 2000 symboli zastępczych dla parametrów
  • wysyłanie zmiennych wiązania 1000-10k nie jest możliwe, ponieważ sterownik JDBC ma swoje ograniczenia

Podejście in () może być wystarczające w niektórych przypadkach, ale nie jest odporne na rakiety :)

Rakietowym rozwiązaniem jest przekazanie dowolnej liczby parametrów w osobnym wywołaniu (na przykład przez przekazanie zbioru parametrów), a następnie wyświetlenie (lub w jakikolwiek inny sposób) reprezentacji ich w języku SQL i użycie tam, gdzie jest kryteria

Wariant brutalnej siły jest tutaj http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

Jednak jeśli możesz używać PL / SQL, ten bałagan może stać się całkiem czysty.

function getCustomers(in_customerIdList clob) return sys_refcursor is 
begin
    aux_in_list.parse(in_customerIdList);
    open res for
        select * 
        from   customer c,
               in_list v
        where  c.customer_id=v.token;
    return res;
end;

Następnie możesz przekazać dowolną liczbę identyfikatorów klientów oddzielonych przecinkami w parametrze i:

  • nie otrzyma opóźnienia analizy, ponieważ SQL dla select jest stabilny
  • brak złożoności funkcji potokowych - to tylko jedno zapytanie
  • SQL używa prostego łączenia zamiast operatora IN, co jest dość szybkie
  • w końcu jest to dobra zasada, aby nie uderzać bazy danych zwykłym select lub DML, ponieważ jest to Oracle, która oferuje o wiele więcej lat niż MySQL lub podobne proste silniki baz danych. PL / SQL pozwala skutecznie ukryć model magazynu przed modelem domeny aplikacji.

Sztuką jest tutaj:

  • potrzebujemy wywołania, które akceptuje długi ciąg i przechowujemy w miejscu, w którym sesja db może uzyskać do niego dostęp (np. prosta zmienna pakietu lub dbms_session.set_context)
  • potrzebujemy widoku, który może przetworzyć to na wiersze
  • a następnie masz widok zawierający identyfikatory, o które pytasz, więc wszystko, czego potrzebujesz, to proste dołączenie do tabeli, której dotyczy zapytanie.

Widok wygląda następująco:

create or replace view in_list
as
select
    trim( substr (txt,
          instr (txt, ',', 1, level  ) + 1,
          instr (txt, ',', 1, level+1)
             - instr (txt, ',', 1, level) -1 ) ) as token
    from (select ','||aux_in_list.getpayload||',' txt from dual)
connect by level <= length(aux_in_list.getpayload)-length(replace(aux_in_list.getpayload,',',''))+1

gdzie aux_in_list.getpayload odnosi się do oryginalnego ciągu wejściowego.


Możliwym podejściem byłoby przekazywanie tablic pl / sql (obsługiwanych tylko przez Oracle), jednak nie można ich używać w czystym SQL, dlatego zawsze potrzebny jest krok konwersji. Konwersji nie można wykonać w języku SQL, więc przecież przekazywanie clob ze wszystkimi parametrami w ciągu i konwertowanie go w widoku jest najbardziej wydajnym rozwiązaniem.

Gee Bee
źródło
6

Oto jak rozwiązałem to we własnej aplikacji. Idealnie byłoby użyć StringBuilder zamiast + dla Strings.

    String inParenthesis = "(?";
    for(int i = 1;i < myList.size();i++) {
      inParenthesis += ", ?";
    }
    inParenthesis += ")";

    try(PreparedStatement statement = SQLite.connection.prepareStatement(
        String.format("UPDATE table SET value='WINNER' WHERE startTime=? AND name=? AND traderIdx=? AND someValue IN %s", inParenthesis))) {
      int x = 1;
      statement.setLong(x++, race.startTime);
      statement.setString(x++, race.name);
      statement.setInt(x++, traderIdx);

      for(String str : race.betFair.winners) {
        statement.setString(x++, str);
      }

      int effected = statement.executeUpdate();
    }

Zastosowanie zmiennej takiej jak x powyżej zamiast konkretnych liczb bardzo pomaga, jeśli zdecydujesz się zmienić zapytanie w późniejszym czasie.

m.sabouri
źródło
5

Nigdy tego nie próbowałem, ale czy .setArray () zrobiłby to, czego szukasz?

Aktualizacja : najwyraźniej nie. Wydaje się, że setArray działa tylko z java.sql.Array, który pochodzi z kolumny ARRAY, która została pobrana z poprzedniego zapytania, lub podzapytania z kolumną ARRAY.

Paul Tomblin
źródło
4
Nie działa ze wszystkimi bazami danych, ale jest to „poprawne” podejście.
skaffman
Masz na myśli wszystkich kierowców. Niektórzy kierowcy mają zastrzeżone odpowiedniki tego standardu z lat (ubiegłego wieku?). Innym sposobem jest wpuszczenie partii wartości do tabeli tymczasowej, ale nie wszystkie bazy danych obsługują to ...
Tom Hawtin - wprowadzono
java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/… Według Sun zawartość tablicy [zwykle] pozostaje po stronie serwera i jest pobierana w razie potrzeby. PreparedStatement.setArray () może odesłać tablicę z poprzedniego zestawu wyników, nie tworząc nowej tablicy po stronie klienta.
Chris Mazzola,
5

Moje obejście:

create or replace type split_tbl as table of varchar(32767);
/

create or replace function split
(
  p_list varchar2,
  p_del varchar2 := ','
) return split_tbl pipelined
is
  l_idx    pls_integer;
  l_list    varchar2(32767) := p_list;
  l_value    varchar2(32767);
begin
  loop
    l_idx := instr(l_list,p_del);
    if l_idx > 0 then
      pipe row(substr(l_list,1,l_idx-1));
      l_list := substr(l_list,l_idx+length(p_del));
    else
      pipe row(l_list);
      exit;
    end if;
  end loop;
  return;
end split;
/

Teraz możesz użyć jednej zmiennej, aby uzyskać pewne wartości w tabeli:

select * from table(split('one,two,three'))
  one
  two
  three

select * from TABLE1 where COL1 in (select * from table(split('value1,value2')))
  value1 AAA
  value2 BBB

Tak przygotowane oświadczenie może być:

  "select * from TABLE where COL in (select * from table(split(?)))"

Pozdrowienia,

Javier Ibanez

Javier Ibanez
źródło
To jest PL / SQL, tak. Nie będzie działać w innych bazach danych. Zauważ, że ta implementacja ma ograniczenie parametrów wejściowych - całkowita długość jest ograniczona do 32k znaków - oraz ograniczenie wydajności, ponieważ wywołanie funkcji potokowej powoduje przełączanie kontekstu między silnikami PL / SQL i SQL Oracle.
Gee Bee
3

Podejrzewam, że można (przy użyciu podstawowej manipulacji ciągiem) wygenerować ciąg zapytania w PreparedStatementcelu uzyskania liczby ?pasującej do liczby elementów na liście.

Oczywiście, jeśli to robisz, jesteś o krok od wygenerowania giganta powiązanego ORw zapytaniu, ale bez odpowiedniej liczby ?w ciągu zapytania nie widzę, jak inaczej możesz to obejść.

Adam Bellaire
źródło
Nie jest to dla mnie rozwiązanie, ponieważ chcę wysłać inną liczbę? za każdym razem, gdy dzwonię do ps. Ale nie sądzę, żebym tego nie rozważał. : P
Chris Mazzola,
4
Kolejny hack: możesz użyć dużej liczby symboli zastępczych parametrów - tyle ile najdłuższa lista wartości, którą masz - a jeśli twoja lista wartości jest krótsza, możesz powtórzyć wartości: ... GDZIE pole wyszukiwania W (? ,?,?,?,?,?,?,?), a następnie podaj wartości: A, B, C, D, A, B, C, D
Bill Karwin,
1
Ale ogólnie faworyzuję rozwiązanie Adama: generowanie kodu SQL dynamicznie i łączenie? symbole zastępcze, aby dopasować liczbę wartości, które musisz przekazać.
Bill Karwin,
Bill, to rozwiązanie jest wykonalne, jeśli nie chcę ponownie użyć PreparedStatement. Innym rozwiązaniem jest wielokrotne wywołanie pojedynczego parametru i zebranie wyników po stronie klienta. Prawdopodobnie bardziej wydajne byłoby zbudowanie / wykonanie nowej instrukcji z niestandardową liczbą? za każdym razem.
Chris Mazzola,
3

Możesz użyć metody setArray, jak wspomniano w tym javadoc :

PreparedStatement statement = connection.prepareStatement("Select * from emp where field in (?)");
Array array = statement.getConnection().createArrayOf("VARCHAR", new Object[]{"E1", "E2","E3"});
statement.setArray(1, array);
ResultSet rs = statement.executeQuery();
Panky031
źródło
2
nie jest to obsługiwane przez wszystkie sterowniki, jeśli funkcja nie jest obsługiwana, otrzymasz wyjątek SQLFeatureNotSupportedException
nienazwany
Niestety mój sterownik go nie obsługuje
EdXX
3

Możesz użyć Collections.nCopiesdo wygenerowania zbioru symboli zastępczych i dołączenia ich za pomocą String.join:

List<String> params = getParams();
String placeHolders = String.join(",", Collections.nCopies(params.size(), "?"));
String sql = "select * from your_table where some_column in (" + placeHolders + ")";
try (   Connection connection = getConnection();
        PreparedStatement ps = connection.prepareStatement(sql)) {
    int i = 1;
    for (String param : params) {
        ps.setString(i++, param);
    }
    /*
     * Execute query/do stuff
     */
}
Gurwinder Singh
źródło
Wydaje się być najlepszym rozwiązaniem do tej pory, gdy używa się Oracle JDBC ...
jansohn
2

Oto kompletne rozwiązanie w Javie, aby stworzyć dla Ciebie przygotowaną instrukcję:

/*usage:

Util u = new Util(500); //500 items per bracket. 
String sqlBefore  = "select * from myTable where (";
List<Integer> values = new ArrayList<Integer>(Arrays.asList(1,2,4,5)); 
string sqlAfter = ") and foo = 'bar'"; 

PreparedStatement ps = u.prepareStatements(sqlBefore, values, sqlAfter, connection, "someId");
*/



import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class Util {

    private int numValuesInClause;

    public Util(int numValuesInClause) {
        super();
        this.numValuesInClause = numValuesInClause;
    }

    public int getNumValuesInClause() {
        return numValuesInClause;
    }

    public void setNumValuesInClause(int numValuesInClause) {
        this.numValuesInClause = numValuesInClause;
    }

    /** Split a given list into a list of lists for the given size of numValuesInClause*/
    public List<List<Integer>> splitList(
            List<Integer> values) {


        List<List<Integer>> newList = new ArrayList<List<Integer>>(); 
        while (values.size() > numValuesInClause) {
            List<Integer> sublist = values.subList(0,numValuesInClause);
            List<Integer> values2 = values.subList(numValuesInClause, values.size());   
            values = values2; 

            newList.add( sublist);
        }
        newList.add(values);

        return newList;
    }

    /**
     * Generates a series of split out in clause statements. 
     * @param sqlBefore ""select * from dual where ("
     * @param values [1,2,3,4,5,6,7,8,9,10]
     * @param "sqlAfter ) and id = 5"
     * @return "select * from dual where (id in (1,2,3) or id in (4,5,6) or id in (7,8,9) or id in (10)"
     */
    public String genInClauseSql(String sqlBefore, List<Integer> values,
            String sqlAfter, String identifier) 
    {
        List<List<Integer>> newLists = splitList(values);
        String stmt = sqlBefore;

        /* now generate the in clause for each list */
        int j = 0; /* keep track of list:newLists index */
        for (List<Integer> list : newLists) {
            stmt = stmt + identifier +" in (";
            StringBuilder innerBuilder = new StringBuilder();

            for (int i = 0; i < list.size(); i++) {
                innerBuilder.append("?,");
            }



            String inClause = innerBuilder.deleteCharAt(
                    innerBuilder.length() - 1).toString();

            stmt = stmt + inClause;
            stmt = stmt + ")";


            if (++j < newLists.size()) {
                stmt = stmt + " OR ";
            }

        }

        stmt = stmt + sqlAfter;
        return stmt;
    }

    /**
     * Method to convert your SQL and a list of ID into a safe prepared
     * statements
     * 
     * @throws SQLException
     */
    public PreparedStatement prepareStatements(String sqlBefore,
            ArrayList<Integer> values, String sqlAfter, Connection c, String identifier)
            throws SQLException {

        /* First split our potentially big list into lots of lists */
        String stmt = genInClauseSql(sqlBefore, values, sqlAfter, identifier);
        PreparedStatement ps = c.prepareStatement(stmt);

        int i = 1;
        for (int val : values)
        {

            ps.setInt(i++, val);

        }
        return ps;

    }

}
dwjohnston
źródło
2

Wiosna umożliwia przekazywanie pliku java.util.Lists do obiektu NamedParameterJdbcTemplate , który automatyzuje generowanie (?,?,?, ...,?) Odpowiednio do liczby argumentów.

W przypadku Oracle ten post na blogu omawia użycie oracle.sql.ARRAY (Connection.createArrayOf nie działa z Oracle). W tym celu musisz zmodyfikować instrukcję SQL:

SELECT my_column FROM my_table where search_column IN (select COLUMN_VALUE from table(?))

Funkcja tabeli wyroczni przekształca przekazaną tablicę w tabelę o wartości użytecznej w INinstrukcji.

Hans-Peter Störr
źródło
1

spróbuj użyć funkcji instr?

select my_column from my_table where  instr(?, ','||search_column||',') > 0

następnie

ps.setString(1, ",A,B,C,"); 

Wprawdzie jest to trochę brudny hack, ale zmniejsza możliwości zastrzyku sql. W każdym razie działa w wyroczni.

stjohnroe
źródło
Aha, i wiem, że nie będzie używać indeksów
stjohnroe
nie działałoby to w przypadku niektórych ciągów, na przykład, jeśli ciąg zawiera „,”.
David Portabella
1

Sormula obsługuje operator SQL IN, umożliwiając dostarczenie obiektu java.util.Collection jako parametru. Tworzy przygotowane oświadczenie z? dla każdego z elementów kolekcji. Zobacz przykład 4 (w przykładzie SQL jest komentarzem wyjaśniającym, co zostało utworzone, ale nie jest używane przez Sormulę).

Jeff Miller
źródło
1

zamiast używać

SELECT my_column FROM my_table where search_column IN (?)

użyj instrukcji Sql jako

select id, name from users where id in (?, ?, ?)

i

preparedStatement.setString( 1, 'A');
preparedStatement.setString( 2,'B');
preparedStatement.setString( 3, 'C');

lub użyj procedury składowanej byłoby to najlepsze rozwiązanie, ponieważ instrukcje SQL będą kompilowane i przechowywane na serwerze DataBase

kapil das
źródło
1

Natknąłem się na szereg ograniczeń związanych z przygotowanym oświadczeniem:

  1. Przygotowane instrukcje są buforowane tylko w tej samej sesji (Postgres), więc naprawdę będzie działać tylko z pulą połączeń
  2. Wiele różnych przygotowanych instrukcji zaproponowanych przez @BalusC może spowodować przepełnienie pamięci podręcznej, a wcześniej buforowane instrukcje zostaną usunięte
  3. Zapytanie należy zoptymalizować i użyć wskaźników. Brzmi oczywisto, jednak np. Instrukcja ANY (ARRAY ...) zaproponowana przez @Boris w jednej z najlepszych odpowiedzi nie może używać indeksów, a zapytanie będzie wolne, mimo buforowania
  4. Przygotowana instrukcja buforuje również plan zapytania, a rzeczywiste wartości dowolnych parametrów określonych w instrukcji są niedostępne.

Spośród proponowanych rozwiązań wybrałbym takie, które nie zmniejsza wydajności zapytań i sprawia, że ​​mniej jest zapytań. To będzie numer 4 (grupowanie kilku zapytań) z linku @Don lub określenie wartości NULL dla niepotrzebnych „?” znaki zaproponowane przez @Vladimir Dyuzhev

Alexander
źródło
1

Właśnie opracowałem dla tego opcję specyficzną dla PostgreSQL. Jest to trochę hack i ma swoje zalety i wady oraz ograniczenia, ale wydaje się, że działa i nie ogranicza się do konkretnego języka programowania, platformy lub sterownika PG.

Sztuczka polega oczywiście na znalezieniu sposobu na przekazanie zbioru wartości o dowolnej długości jako jednego parametru i spowodowanie, aby db rozpoznał je jako wiele wartości. Rozwiązaniem, które pracuję, jest skonstruowanie łańcucha rozdzielanego z wartości w kolekcji, przekazanie go jako jednego parametru i użycie string_to_array () z wymaganym rzutowaniem dla PostgreSQL, aby właściwie z niego skorzystać.

Więc jeśli chcesz wyszukać „foo”, „bla” i „abc”, możesz połączyć je razem w jeden ciąg znaków: „foo, bla, abc”. Oto prosty SQL:

select column from table
where search_column = any (string_to_array('foo,blah,abc', ',')::text[]);

Oczywiście zmieniłbyś jawną rzutowanie na cokolwiek, co chcesz, aby wynikowa tablica wartości była - int, tekst, uuid itp. A ponieważ funkcja przyjmuje jedną wartość ciągu (lub dwie, jak sądzę, jeśli chcesz dostosować separator) również), możesz przekazać go jako parametr w przygotowanej instrukcji:

select column from table
where search_column = any (string_to_array($1, ',')::text[]);

Jest to nawet wystarczająco elastyczne, aby obsługiwać takie rzeczy jak porównania LIKE:

select column from table
where search_column like any (string_to_array('foo%,blah%,abc%', ',')::text[]);

Ponownie, bez wątpienia jest to hack, ale działa i pozwala nadal korzystać ze wstępnie skompilowanych przygotowanych instrukcji, które przyjmują * ahem * dyskretne parametry, z towarzyszącymi korzyściami bezpieczeństwa i (być może) wydajnością. Czy jest to wskazane i faktycznie skuteczne? Oczywiście to zależy, ponieważ parsowanie ciągów i być może przesyłanie odbywa się jeszcze przed uruchomieniem zapytania. Jeśli spodziewasz się wysłać trzy, pięć, kilkadziesiąt wartości, na pewno jest to w porządku. Kilka tysięcy? Tak, może nie tak bardzo. YMMV, obowiązują ograniczenia i wyłączenia, bez gwarancji wyraźnej lub dorozumianej.

Ale to działa.

Joel Fouse
źródło
0

Dla kompletności: tak długo, jak zestaw wartości nie jest zbyt duży, można również po prostu utworzyć łańcuch typu instrukcja

... WHERE tab.col = ? OR tab.col = ? OR tab.col = ?

który możesz następnie przekazać do przygotowania (), a następnie użyć setXXX () w pętli, aby ustawić wszystkie wartości. Wygląda to na szczęście, ale wiele „dużych” systemów komercyjnych rutynowo robi takie rzeczy, dopóki nie osiągną limitów specyficznych dla DB, takich jak 32 KB (tak myślę) dla instrukcji w Oracle.

Oczywiście musisz upewnić się, że zestaw nigdy nie będzie nieuzasadniony duży, ani nie wychwytuj błędów, jeśli tak jest.

Carl Smotricz
źródło
Tak, masz rację. Moim celem w tym przypadku było ponowne użycie PreparedStatement z inną liczbą elementów za każdym razem.
Chris Mazzola,
3
Użycie „LUB” zaciemni zamiar. Trzymaj się „IN”, ponieważ jest łatwiejszy do odczytania, a cel jest bardziej wyraźny. Jedynym powodem zmiany jest to, że plany zapytań były inne.
James Schek
0

Podążając za pomysłem Adama. Ułóż przygotowaną instrukcję, wybierając opcję moja_kolumna z mojej_tabeli, w której kolumna_wyszukiwania w (#) Utwórz ciąg x i wypełnij go liczbą „?,?,?” w zależności od listy wartości Następnie zmień # w zapytaniu dla nowego ciągu x i wypełnij


źródło
0

Wygeneruj ciąg zapytania w PreparedStatement, aby liczba? Pasowała do liczby elementów na liście. Oto przykład:

public void myQuery(List<String> items, int other) {
  ...
  String q4in = generateQsForIn(items.size());
  String sql = "select * from stuff where foo in ( " + q4in + " ) and bar = ?";
  PreparedStatement ps = connection.prepareStatement(sql);
  int i = 1;
  for (String item : items) {
    ps.setString(i++, item);
  }
  ps.setInt(i++, other);
  ResultSet rs = ps.executeQuery();
  ...
}

private String generateQsForIn(int numQs) {
    String items = "";
    for (int i = 0; i < numQs; i++) {
        if (i != 0) items += ", ";
        items += "?";
    }
    return items;
}
neu242
źródło
5
Nie trzeba już używać StringBuilder. Kompilator konwertuje znaki + na StringBuilder.append (), więc nie ma żadnego spadku wydajności. Spróbuj sam :)
neu242,
5
@ neu242: O tak, kompilator używa StringBuilder. Ale nie tak, jak myślisz. Podczas dekompilacji generateQsForInwidać, że dla każdej iteracji pętli przydzielane są dwa nowe StringBuilderi toStringsą one wywoływane dla każdego z nich. StringBuilderOptymalizacja łapie tylko takie rzeczy "x" + i+ "y" + j, ale nie wykracza poza jednym wyrażeniu.
AH
@ neu242 Nie możesz użyć ps.setObject(1,items)zamiast iteracji po liście, a następnie ustawienia paramteres?
Neha Choudhary,
0

Istnieją różne alternatywne podejścia, których możemy użyć dla klauzuli IN w PreparedStatement.

  1. Korzystanie z pojedynczych zapytań - najniższa wydajność i duże zużycie zasobów
  2. Korzystanie z StoredProcedure - najszybszy, ale specyficzny dla bazy danych
  3. Tworzenie dynamicznego zapytania dla PreparedStatement - Dobra wydajność, ale nie korzysta z buforowania, a PreparedStatement jest rekompilowany za każdym razem.
  4. Użyj NULL w zapytaniach PreparedStatement - Optymalna wydajność, działa świetnie, gdy znasz limit argumentów klauzuli IN. Jeśli nie ma limitu, możesz wykonywać zapytania wsadowo. Przykładowy fragment kodu to;

        int i = 1;
        for(; i <=ids.length; i++){
            ps.setInt(i, ids[i-1]);
        }
    
        //set null for remaining ones
        for(; i<=PARAM_SIZE;i++){
            ps.setNull(i, java.sql.Types.INTEGER);
        }

Możesz sprawdzić więcej szczegółów na temat tych alternatywnych podejść tutaj .

Pankaj
źródło
„Tworzenie dynamicznego zapytania dla PreparedStatement - Dobra wydajność, ale nie korzysta z buforowania, a PreparedStatement jest ponownie kompilowany za każdym razem”. buforowanie i unikanie ponownych kompilacji sprawia, że ​​przygotowane polecenie działa dobrze. Dlatego nie zgadzam się z twoim roszczeniem. Zapobiegnie to jednak iniekcji SQL, ponieważ ograniczasz konkatenowane / dynamiczne dane wejściowe do przecinka.
Brandon,
Zgadzam się z tobą, jednak „Dobra wydajność” tutaj jest dla tego konkretnego scenariusza. Lepsze wyniki niż podejście 1, jednak podejście 2 jest najszybsze.
Pankaj,
0

W niektórych sytuacjach wyrażenie regularne może pomóc. Oto przykład, który sprawdziłem na Oracle i działa.

select * from my_table where REGEXP_LIKE (search_column, 'value1|value2')

Ale ma to wiele wad:

  1. Każda zastosowana kolumna powinna zostać przekonwertowana na varchar / char, przynajmniej pośrednio.
  2. Trzeba uważać na znaki specjalne.
  3. Może spowolnić działanie - w moim przypadku wersja IN używa skanowania indeksu i zakresu, a wersja REGEXP wykonuje pełne skanowanie.
Wasilij
źródło
0

Po przeanalizowaniu różnych rozwiązań na różnych forach i nie znalezieniu dobrego rozwiązania, uważam, że poniższy hack, który wymyśliłem, jest najłatwiejszy do naśladowania i kodowania:

Przykład: Załóżmy, że masz wiele parametrów do przekazania w klauzuli „IN”. Wystarczy umieścić fikcyjny ciąg w klauzuli „IN”, powiedzmy, „PARAM” oznacza listę parametrów, które będą przychodzić w miejsce tego fikcyjnego ciągu.

    select * from TABLE_A where ATTR IN (PARAM);

Możesz zebrać wszystkie parametry w jedną zmienną String w kodzie Java. Można to zrobić w następujący sposób:

    String param1 = "X";
    String param2 = "Y";
    String param1 = param1.append(",").append(param2);

W naszym przypadku możesz dołączyć wszystkie parametry oddzielone przecinkami do jednej zmiennej String, „param1”.

Po zebraniu wszystkich parametrów w jednym ciągu możesz po prostu zastąpić tekst fikcyjny w zapytaniu, tj. „PARAM” w tym przypadku parametrem String, tj. Param1. Oto, co musisz zrobić:

    String query = query.replaceFirst("PARAM",param1); where we have the value of query as 

    query = "select * from TABLE_A where ATTR IN (PARAM)";

Możesz teraz wykonać zapytanie za pomocą metody executeQuery (). Upewnij się tylko, że w zapytaniu nigdzie nie ma słowa „PARAM”. Możesz użyć kombinacji znaków specjalnych i alfabetów zamiast słowa „PARAM”, aby mieć pewność, że takie słowo nie pojawi się w zapytaniu. Mam nadzieję, że masz rozwiązanie.

Uwaga: Chociaż nie jest to przygotowane zapytanie, wykonuje ono pracę, którą chciałem wykonać w moim kodzie.

bnsk
źródło
0

Dla kompletności i ponieważ nie widziałem, żeby ktokolwiek inny to sugerował:

Przed wdrożeniem którejkolwiek z powyższych skomplikowanych sugestii zastanów się, czy wstrzyknięcie SQL rzeczywiście stanowi problem w twoim scenariuszu.

W wielu przypadkach wartość podana do IN (...) jest listą identyfikatorów, które zostały wygenerowane w taki sposób, że możesz mieć pewność, że żaden zastrzyk nie jest możliwy ... (np. Wyniki poprzedniego wyboru some_id z some_table gdzie jakiś warunek.)

W takim przypadku możesz po prostu połączyć tę wartość i nie korzystać z usług lub przygotowanej instrukcji lub użyć ich do innych parametrów tego zapytania.

query="select f1,f2 from t1 where f3=? and f2 in (" + sListOfIds + ");";
epeleg
źródło
0

PreparedStatement nie zapewnia dobrego sposobu radzenia sobie z klauzulą ​​SQL IN. Na http://www.javaranch.com/journal/200510/Journal200510.jsp#a2 „Nie można zastąpić rzeczy, które mają stać się częścią instrukcji SQL. Jest to konieczne, ponieważ jeśli sam SQL może się zmienić, właściwość sterownik nie może wstępnie skompilować instrukcji. Ma także ładny efekt uboczny, uniemożliwiając ataki polegające na wstrzyknięciu SQL. ” Skończyło się na następującym podejściu:

String query = "SELECT my_column FROM my_table where search_column IN ($searchColumns)";
query = query.replace("$searchColumns", "'A', 'B', 'C'");
Statement stmt = connection.createStatement();
boolean hasResults = stmt.execute(query);
do {
    if (hasResults)
        return stmt.getResultSet();

    hasResults = stmt.getMoreResults();

} while (hasResults || stmt.getUpdateCount() != -1);
Pedram Bashiri
źródło
0

SetArray to najlepsze rozwiązanie, ale nie jest dostępne dla wielu starszych sterowników. W java8 można zastosować następujące obejście

String baseQuery ="SELECT my_column FROM my_table where search_column IN (%s)"

String markersString = inputArray.stream().map(e -> "?").collect(joining(","));
String sqlQuery = String.format(baseSQL, markersString);

//Now create Prepared Statement and use loop to Set entries
int index=1;

for (String input : inputArray) {
     preparedStatement.setString(index++, input);
}

To rozwiązanie jest lepsze niż inne brzydkie rozwiązania pętli while, w których ciąg zapytania jest budowany przez iteracje ręczne

Raheel
źródło
0

To zadziałało dla mnie (psuedocode):

public class SqlHelper
{
    public static final ArrayList<String>platformList = new ArrayList<>(Arrays.asList("iOS","Android","Windows","Mac"));

    public static final String testQuery = "select * from devices where platform_nm in (:PLATFORM_NAME)";
}

określić wiązanie:

public class Test extends NamedParameterJdbcDaoSupport
public List<SampleModelClass> runQuery()
{
    //define rowMapper to insert in object of SampleClass
    final Map<String,Object> map = new HashMap<>();
    map.put("PLATFORM_LIST",DeviceDataSyncQueryConstants.platformList);
    return getNamedParameterJdbcTemplate().query(SqlHelper.testQuery, map, rowMapper)
}
Nikita Shah
źródło
0

Mój przykład dla baz danych SQLite i Oracle.

Pierwsza pętla For służy do tworzenia obiektów PreparedStatement.

Druga pętla For służy do podawania wartości parametrów parametrów PreparedStatement.

List<String> roles = Arrays.asList("role1","role2","role3");
Map<String, String> menu = getMenu(conn, roles);

public static Map<String, String> getMenu(Connection conn, List<String> roles ) {
    Map<String, String> menu = new LinkedHashMap<String, String>();

    PreparedStatement stmt;
    ResultSet rset;
    String sql;
    try {
        if (roles == null) {
            throw new Exception();
        }
        int size = roles.size();
        if (size == 0) {
            throw new Exception("empty list");
        }
        StringBuilder sb = new StringBuilder();
        sb.append("select page_controller, page_name from pages "
                + " where page_controller in (");
        for (int i = 0; i < size; i++) {
            sb.append("?,");
        }
        sb.setLength(sb.length() - 1);
        sb.append(") order by page_id");
        sql = sb.toString();
        stmt = conn.prepareStatement(sql);
        for (int i = 0; i < size; i++) {
            stmt.setString(i + 1, roles.get(i));
        }
        rset = stmt.executeQuery();
        while (rset.next()) {
            menu.put(rset.getString(1), rset.getString(2));
        }

        conn.close();
    } catch (Exception ex) {
        logger.info(ex.toString());
        try {
            conn.close();
        } catch (SQLException e) {
        }
        return menu;
    }
    return menu;
}
Vstavskyi
źródło
-3

Moje obejście (JavaScript)

    var s1 = " SELECT "

 + "FROM   table t "

 + "  where t.field in ";

  var s3 = '(';

  for(var i =0;i<searchTerms.length;i++)
  {
    if(i+1 == searchTerms.length)
    {
     s3  = s3+'?)';
    }
    else
    {
        s3  = s3+'?, ' ;
    }
   }
    var query = s1+s3;

    var pstmt = connection.prepareStatement(query);

     for(var i =0;i<searchTerms.length;i++)
    {
        pstmt.setString(i+1, searchTerms[i]);
    }

SearchTerms to tablica zawierająca dane wejściowe / klucze / pola itp

smooth_smoothie
źródło