[java] PreparedStatement IN 절 대안?

SQL 주입 공격 보안 문제로 인해 여러 값에서 지원되지 않는의 IN인스턴스와 함께 SQL 절 을 사용하는 가장 좋은 해결 방법은 다음 과 java.sql.PreparedStatement같습니다.? 자리 표시자는 값 목록이 아닌 하나의 값을 나타냅니다.

다음 SQL 문을 고려하십시오.

SELECT my_column FROM my_table where search_column IN (?)

사용 preparedStatement.setString( 1, "'A', 'B', 'C'" );은 기본적으로 사용 하지 않는 시도입니다.? 처음부터 .

어떤 해결 방법이 있습니까?



답변

다양한 옵션에 대한 분석과 각각의 장단점이 여기에 있습니다 .

제안 된 옵션은 다음과 같습니다.

  • 를 준비 SELECT my_column FROM my_table WHERE search_column = ?하고 각 값에 대해 실행하고 결과를 클라이언트 측으로 UNION하십시오. 하나의 준비된 진술 만 필요합니다. 느리고 아프다.
  • 준비 SELECT my_column FROM my_table WHERE search_column IN (?,?,?) 하고 실행하십시오. 리스트 크기 당 하나의 준비된 명령문이 필요합니다. 빠르고 명백합니다.
  • 준비 SELECT my_column FROM my_table WHERE search_column = ? ; SELECT my_column FROM my_table WHERE search_column = ? ; ...하고 실행하십시오. [또는 UNION ALL세미콜론 대신 사용 하십시오. –ed] 목록 크기 당 하나의 준비된 명령문이 필요합니다. 엄청나게 느리고, 엄청나게 나빠서, WHERE search_column IN (?,?,?)블로거가 왜 그것을 제안했는지 모르겠습니다.
  • 스토어드 프로 시저를 사용하여 결과 세트를 구성하십시오.
  • N 개의 다른 크기 목록 쿼리를 준비하십시오. 2, 10 및 50 값으로 말하십시오. 6 개의 다른 값으로 IN 목록을 검색하려면 다음과 같이 size-10 쿼리를 채 웁니다 SELECT my_column FROM my_table WHERE search_column IN (1,2,3,4,5,6,6,6,6,6). 적절한 서버는 쿼리를 실행하기 전에 중복 값을 최적화합니다.

그러나 이러한 옵션 중 어느 것도 훌륭하지 않습니다.

이 장소에서 똑같이 제정 한 대안으로 중복되는 질문에 대한 답을 얻었지만 여전히 위대한 질문은 없습니다.

JDBC4와를 지원하는 서버를 사용하는 경우 정답 x = ANY(y)은 다음에 PreparedStatement.setArray설명 된대로 사용 하는 것입니다.

setArray그러나 IN- 목록으로 작업 하는 방법은없는 것 같습니다 .


때때로 SQL 문은 런타임시 (예 : 특성 파일에서)로드되지만 가변 개수의 매개 변수가 필요합니다. 이러한 경우 먼저 쿼리를 정의하십시오.

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

그런 다음 쿼리를로드하십시오. 그런 다음 실행하기 전에 매개 변수 수를 결정하십시오. 매개 변수 개수를 알면 다음을 실행하십시오.

sql = any( sql, count );

예를 들면 다음과 같습니다.

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

JDBC 4 스펙을 통한 배열 전달이 지원되지 않는 특정 데이터베이스의 경우,이 메소드는 느린 = ?것을 더 빠른 IN (?)절 조건 으로 변환하는 것을 용이하게 할 수 있으며, 그런 다음 any메소드 를 호출하여 확장 할 수 있습니다 .


답변

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

또는

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


답변

간단한 방법은 없습니다. 목표가 명령문 캐시 비율을 높게 유지하는 경우 (즉, 모든 매개 변수 수마다 명령문을 작성하지 않는 경우) 다음을 수행 할 수 있습니다.

  1. 몇 가지 (예 : 10) 매개 변수로 명령문을 작성하십시오.

    … 어디에서 (?,?,?,?,?,?,?,?,?,?) …

  2. 모든 실제 매개 변수 바인드

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

  3. 나머지를 NULL로 바인드

    setNull (3, Types.VARCHAR) … setNull (10, Types.VARCHAR)

NULL은 절대로 일치하지 않으므로 SQL 계획 빌더에 의해 최적화됩니다.

List를 DAO 함수에 전달하면 로직을 쉽게 자동화 할 수 있습니다.

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

while( i < MAX_PARAMS ) {
  ps.setNull(i+1,Types.VARCHAR);
  i++;
}


답변

불쾌한 해결 방법이지만 확실하게 가능한 것은 중첩 쿼리를 사용하는 것입니다. 열이 포함 된 임시 테이블 MYVALUES를 작성하십시오. MYVALUES 테이블에 값 목록을 삽입하십시오. 그런 다음 실행

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

추악하지만 값 목록이 매우 큰 경우 가능한 대안입니다.

이 기술은 데이터베이스가 준비된 명령문을 캐시하지 않으면 옵티마이 저의 잠재적으로 더 나은 쿼리 계획 (여러 값에 대한 페이지 확인, 값당 한 번에 한 번만 테이블 스캔 등)의 오버 헤드를 줄일 수있는 이점이 있습니다. “INSERTS”를 일괄 처리해야하고 잠금 또는 최소 오버 헤드 방지 기능을 최소화하기 위해 MYVALUES 테이블을 조정해야 할 수도 있습니다.


답변

in () 연산자의 한계는 모든 악의 근원입니다.

사소한 경우에는 작동하며 “준비된 명령문의 자동 생성”으로 확장 할 수 있지만 항상 한계가 있습니다.

  • 가변 개수의 매개 변수로 명령문을 작성하는 경우 각 호출에서 SQL 구문 분석 오버 헤드가 발생합니다.
  • 많은 플랫폼에서 in () 연산자의 매개 변수 수는 제한되어 있습니다.
  • 모든 플랫폼에서 총 SQL 텍스트 크기가 제한되어 인 매개 변수에 대해 2000 자리 표시자를 보낼 수 없습니다.
  • JDBC 드라이버에 한계가 있으므로 1000-10k의 바인드 변수를 보내는 것은 불가능합니다.

in () 접근 방식은 어떤 경우에는 충분하지만 로켓 증거는 아닙니다. 🙂

로켓 방지 솔루션은 임의의 수의 매개 변수를 별도의 호출로 전달하는 것입니다 (예 : 매개 변수의 clob를 전달하여). 기준.

무차별 대입 변형은 여기 http://tkyte.blogspot.hu/2006/06/varying-in-lists.html

그러나 PL / SQL을 사용할 수 있다면이 혼란은 매우 깔끔해질 수 있습니다.

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;

그런 다음 매개 변수에서 임의의 수의 쉼표로 구분 된 고객 ID를 전달할 수 있습니다.

  • select에 대한 SQL이 안정적이므로 구문 분석 지연이 없습니다.
  • 파이프 라인 함수 복잡성 없음-단 하나의 쿼리
  • SQL은 IN 연산자 대신 간단한 조인을 사용하고 있습니다.
  • 결국, MySQL이나 유사한 단순한 데이터베이스 엔진보다 수년이 넘는 Oracle을 제공하기 때문에 Oracle은 일반 선택 또는 DML로 데이터베이스에 충돌 하지 않는 것이 좋습니다 . PL / SQL을 사용하면 효과적인 방식으로 애플리케이션 도메인 모델에서 스토리지 모델을 숨길 수 있습니다.

트릭은 다음과 같습니다.

  • 긴 문자열을 받아들이고 db 세션이 액세스 할 수있는 위치 (예 : 간단한 패키지 변수 또는 dbms_session.set_context)를 저장하는 호출이 필요합니다.
  • 그런 다음 행을 파싱 할 수있는 뷰가 필요합니다.
  • 그런 다음 쿼리하는 ID가 포함 된보기가 있으므로 쿼리 된 테이블에 간단히 조인하면됩니다.

보기는 다음과 같습니다.

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

여기서 aux_in_list.getpayload는 원래 입력 문자열을 나타냅니다.


가능한 접근 방법은 pl / sql 배열 (Oracle에서만 지원)을 전달하는 것이지만 순수 SQL에서는 사용할 수 없으므로 변환 단계가 항상 필요합니다. SQL에서는 변환을 수행 할 수 없으므로 결국 문자열에 모든 매개 변수가있는 clob를 전달하고 뷰를 사용하여 변환하는 것이 가장 효율적인 솔루션입니다.


답변

내 응용 프로그램에서 해결하는 방법은 다음과 같습니다. 이상적으로는 + for Strings 대신 StringBuilder를 사용해야합니다.

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

구체적인 숫자 대신 위의 x와 같은 변수를 사용하면 나중에 쿼리를 변경하기로 결정한 경우 많은 도움이됩니다.


답변

나는 그것을 시도한 적이 없지만 .setArray () 당신이 찾고있는 것을 할 것입니까?

업데이트 : 분명히 아닙니다. setArray는 이전 쿼리에서 검색 한 ARRAY 열 또는 ARRAY 열이있는 하위 쿼리에서 가져온 java.sql.Array에서만 작동하는 것 같습니다.