首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計疑難解答

JDBC 分頁查詢陷阱:ORDER BY 參數綁定失效的原因與解決方案

     編輯:程式設計疑難解答

問題現象


// 失效寫法
String sql = "SELECT * FROM goods ORDER BY ? LIMIT ?, ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "price");      // 排序字段
ps.setInt(2, 0);               // offset
ps.setInt(3, 20);              // pageSize
ResultSet rs = ps.executeQuery();
// 結果:始終按默認順序(通常是主鍵)排序,忽略 ORDER BY 參數

根本原因

JDBC PreparedStatement 參數綁定(? 佔位符)僅適用於數據值(DML 值),不適用於 SQL 結構元素:

  • ✅ 列值:WHERE price > ?
  • ✅ 插入值:INSERT INTO t (c) VALUES (?)
  • ❌ 表名:SELECT * FROM ?
  • ❌ 列名:SELECT ? FROM t
  • ❌ 排序字段:ORDER BY ?
  • ❌ 排序方向:ORDER BY price ?
  • ❌ LIMIT 參數:MySQL 5.0.7+ 支持,但部分驅動/數據庫不支持

驅動將 ORDER BY ? 處理為 ORDER BY 'price'(字符串字面量),而非 ORDER BY price(標識符),導致排序無效。

解決方案對比

方案一:字符串拼接(最常用,需防注入)


public List queryGoods(String orderBy, int offset, int limit) {
    // 1. 白名單驗證(核心防注入)
    Set allowedColumns = Set.of("price", "create_time", "sales", "name");
    String safeOrderBy = allowedColumns.contains(orderBy) ? orderBy : "id";
    
    // 2. 方向驗證
    String direction = "ASC".equalsIgnoreCase(direction) ? "ASC" : "DESC";
    
    // 3. 拼接 SQL(結構部分)
    String sql = "SELECT * FROM goods ORDER BY " + safeOrderBy + " " + direction + " LIMIT ?, ?";
    
    // 4. 參數綁定(數據部分)
    try (PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setInt(1, offset);
        ps.setInt(2, limit);
        return executeQuery(ps);
    }
}

方案二:動態構建 PreparedStatement(複雜場景)


public List queryDynamic(Map params) {
    StringBuilder sql = new StringBuilder("SELECT * FROM goods WHERE 1=1");
    List values = new ArrayList<>();
    
    // 動態 WHERE 條件(值綁定)
    if (params.containsKey("category")) {
        sql.append(" AND category = ?");
        values.add(params.get("category"));
    }
    
    // 排序(結構拼接 + 驗證)
    String orderBy = validateColumn((String) params.get("orderBy"), allowedColumns);
    String direction = validateDirection((String) params.get("direction"));
    sql.append(" ORDER BY ").append(orderBy).append(" ").append(direction);
    
    // 分頁(值綁定,MySQL 支持)
    sql.append(" LIMIT ?, ?");
    values.add(params.get("offset"));
    values.add(params.get("limit"));
    
    try (PreparedStatement ps = conn.prepareStatement(sql.toString())) {
        for (int i = 0; i < values.size(); i++) {
            ps.setObject(i + 1, values.get(i));
        }
        return executeQuery(ps);
    }
}


方案三:使用 MyBatis / JPA 等 ORM 框架(推薦生產環境)






List selectGoods(@Param("category") String category,
                        @Param("orderBy") String orderBy,
                        @Param("direction") String direction,
                        @Param("offset") int offset,
                        @Param("limit") int limit);

注意:MyBatis 中 ${} 直接拼接,**必須在 Service 層做白名單驗證**,否則極高風險 SQL 注入。

方案四:游標分頁(大數據量優化)


-- 偏移量分頁深分頁性能差:LIMIT 1000000, 20 掃描 100 萬行
-- 游標分頁:記住上一頁最後一條記錄的排序值
SELECT * FROM goods 
WHERE price > ?  -- 上一頁最後價格
ORDER BY price ASC 
LIMIT 20;

-- 複合排序游標
WHERE (price > ?) OR (price = ? AND id > ?)
ORDER BY price, id
LIMIT 20;

數據庫差異說明

數據庫LIMIT 參數綁定ORDER BY 參數綁定推薦寫法
MySQL 5.0.7+✅ 支持❌ 不支持拼接 ORDER BY,綁定 LIMIT
PostgreSQL✅ 支持❌ 不支持拼接 ORDER BY,綁定 LIMIT/OFFSET
SQL Server❌ 不支持 (OFFSET/FETCH 需常量)❌ 不支持全拼接或動態 SQL
Oracle❌ 不支持 (ROWNUM 需嵌套)❌ 不支持全拼接

安全檢查清單

  1. ☐ 所有拼接入 SQL 的標識符(表名、列名、排序字段)均經過嚴格白名單驗證
  2. ☐ 拒絕用戶直接輸入的字符串進入 SQL 結構部分
  3. ☐ 使用 PreparedStatement 綁定所有數據值
  4. ☐ 記錄執行的 SQL(含綁定值)用於審計
  5. ☐ 考慮使用 JOOQ 等類型安全 SQL 構建器

總結

ORDER BY 字段名、表名、排序方向屬於 SQL 結構,不能用 ? 綁定,必須字符串拼接並嚴格白名單驗證。
LIMIT/OFFSET 參數在 MySQL/PostgreSQL 可綁定,但 SQL Server/Oracle 不支持,跨庫項目建議統一拼接或用 ORM。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved