五張表關係的Mermaid ER圖代碼

若依/RuoYi_分頁

使用説明

  • 關係説明
  • ||--o{ 表示一對多關係,中間表與主表的關聯關係通過該符號體現
  • 最終形成 sys_user(多)↔ sys_user_role(中間表)↔ sys_role(多)、sys_role(多)↔ sys_role_menu(中間表)↔ sys_menu(多)的多對多關聯鏈
    用户表 ↔ 用户角色關聯表:sys_user.user_id = sys_user_role.user_id,一對多關係(一個用户可關聯多個角色)。
    用户角色關聯表 ↔ 角色表:sys_user_role.role_id = sys_role.role_id,多對一關係(多個用户可關聯同一個角色)。
    角色表 ↔ 角色菜單關聯表:sys_role.role_id = sys_role_menu.role_id,一對多關係(一個角色可關聯多個菜單)。
    角色菜單關聯表 ↔ 菜單權限表:sys_role_menu.menu_id = sys_menu.menu_id,多對一關係(多個角色可關聯同一個菜單)。
  • 字段標識PK 標註主鍵,FK 標註外鍵

分頁的實現

@GetMapping("/list")
    public TableDataInfo list(SysLogininfor logininfor)
    {
	// 需要分頁的接口需要加上該函數
        startPage();
        List<SysLogininfor> list = logininforService.selectLogininforList(logininfor);
        return getDataTable(list);
    }

分頁參數加在url上,後端可通過serlvetUtil獲得

PageHelper 分頁核心實現偽代碼(還原 PageInterceptor 工作流程)

PageInterceptor 如何通過實現 MyBatis Interceptor 接口完成分頁功能:

// 1. 核心攔截器類:實現 MyBatis 的 Interceptor 接口
public class PageInterceptor implements Interceptor {
    // 數據庫方言(如 MySQL、Oracle,用於生成對應分頁語法)
    private String dialect;

    // ====================== 接口實現方法1:setProperties - 初始化配置 ======================
    // 讀取配置文件中的分頁屬性(如 dialect=mysql)
    @Override
    public void setProperties(Properties properties) {
        this.dialect = properties.getProperty("helperDialect", "mysql"); // 默認MySQL方言
        // 其他配置:如是否自動統計總條數、分頁參數合理化等
    }

    // ====================== 接口實現方法2:plugin - 生成代理對象 ======================
    // 為目標組件(Executor/StatementHandler)生成動態代理,使攔截邏輯生效
    @Override
    public Object plugin(Object target) {
        // 只對需要攔截的組件進行代理(避免無關組件被包裝)
        if (target instanceof Executor || target instanceof StatementHandler) {
            return Plugin.wrap(target, this); // MyBatis 自帶的代理工具
        }
        return target; // 非目標組件直接返回
    }

    // ====================== 接口實現方法3:intercept - 核心分頁邏輯(重點) ======================
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // ===== 步驟1:獲取分頁參數(從 ThreadLocal 中獲取,由 PageHelper.startPage() 存入) =====
        Page<?> pageParam = PageContext.getLocalPage(); // ThreadLocal 存儲分頁參數
        if (pageParam == null) {
            // 無分頁參數,直接執行原始SQL,不做分頁處理
            return invocation.proceed();
        }

        // ===== 步驟2:獲取原始SQL及執行相關信息 =====
        // 從 invocation 中解析目標對象、方法參數(不同攔截目標解析方式略有差異,此處以 Executor 為例)
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1]; // SQL參數
        String originalSql = getOriginalSql(mappedStatement, parameter); // 獲取原始SQL(如:SELECT * FROM sys_user WHERE status = 0)
        Integer pageNum = pageParam.getPageNum(); // 當前頁碼(如第2頁)
        Integer pageSize = pageParam.getPageSize(); // 每頁條數(如每頁10條)

        // ===== 步驟3:動態改寫SQL,生成分頁SQL =====
        String pageSql = generatePageSql(originalSql, pageNum, pageSize);
        // 示例:MySQL 分頁SQL → SELECT * FROM sys_user WHERE status = 0 LIMIT 10, 10

        // ===== 步驟4:自動生成總條數查詢SQL,統計總記錄數 =====
        if (pageParam.isCount()) { // 是否需要統計總條數(默認true)
            String countSql = generateCountSql(originalSql); // 生成 COUNT SQL:SELECT COUNT(0) FROM (SELECT * FROM sys_user WHERE status = 0) temp
            Long totalCount = executeCountSql(countSql, parameter); // 執行COUNT SQL,獲取總條數
            pageParam.setTotal(totalCount); // 設置總記錄數
            pageParam.setPages(totalCount % pageSize == 0 ? totalCount / pageSize : totalCount / pageSize + 1); // 計算總頁數
        }

        // ===== 步驟5:替換原始SQL,執行分頁查詢 =====
        replaceOriginalSql(mappedStatement, pageSql); // 將改寫後的分頁SQL替換原始SQL
        Object pageResult = invocation.proceed(); // 執行分頁SQL,獲取當前頁數據列表

        // ===== 步驟6:封裝分頁結果,清除ThreadLocal緩存 =====
        pageParam.addAll((List<?>) pageResult); // 將當前頁數據存入分頁對象
        PageContext.clearLocalPage(); // 清除ThreadLocal中的分頁參數,避免線程複用導致錯亂

        // ===== 步驟7:返回分頁結果(最終可包裝為 PageInfo 對象) =====
        return pageParam;
    }

    // ---------------------- 內部輔助方法:生成分頁SQL ----------------------
    private String generatePageSql(String originalSql, Integer pageNum, Integer pageSize) {
        if ("mysql".equalsIgnoreCase(dialect)) {
            // MySQL 分頁:LIMIT (pageNum-1)*pageSize, pageSize
            int offset = (pageNum - 1) * pageSize;
            return originalSql + " LIMIT " + offset + ", " + pageSize;
        } else if ("oracle".equalsIgnoreCase(dialect)) {
            // Oracle 分頁:嵌套ROWNUM查詢
            int start = (pageNum - 1) * pageSize + 1;
            int end = pageNum * pageSize;
            return "SELECT * FROM (SELECT t.*, ROWNUM rn FROM (" + originalSql + ") t WHERE ROWNUM <= " + end + ") WHERE rn >= " + start;
        }
        // 其他數據庫方言(SQL Server、PostgreSQL等)...
        return originalSql;
    }

    // ---------------------- 內部輔助方法:生成總條數查詢SQL ----------------------
    private String generateCountSql(String originalSql) {
        // 去除原始SQL中的 ORDER BY(避免統計總條數時排序影響性能)
        String sqlWithoutOrderBy = originalSql.replaceAll("ORDER BY.*", "");
        return "SELECT COUNT(0) FROM (" + sqlWithoutOrderBy + ") temp_count";
    }

    // ---------------------- 內部輔助方法:執行COUNT SQL,獲取總條數 ----------------------
    private Long executeCountSql(String countSql, Object parameter) {
        // 偽代碼:通過JDBC/MyBatis執行COUNT SQL,返回總記錄數
        PreparedStatement pstmt = getConnection().prepareStatement(countSql);
        setParameters(pstmt, parameter); // 設置SQL參數
        ResultSet rs = pstmt.executeQuery();
        if (rs.next()) {
            return rs.getLong(1);
        }
        return 0L;
    }

    // 其他輔助方法:獲取原始SQL、替換原始SQL...
    private String getOriginalSql(MappedStatement ms, Object parameter) { /* 實現邏輯 */ }
    private void replaceOriginalSql(MappedStatement ms, String newSql) { /* 實現邏輯 */ }
}

// ====================== 輔助類:ThreadLocal 存儲分頁參數 ======================
class PageContext {
    // ThreadLocal 保證多線程環境下分頁參數隔離
    private static final ThreadLocal<Page<?>> PAGE_THREAD_LOCAL = new ThreadLocal<>();

    // 設置分頁參數(由 PageHelper.startPage(pageNum, pageSize) 調用)
    public static void setLocalPage(Page<?> page) {
        PAGE_THREAD_LOCAL.set(page);
    }

    // 獲取分頁參數
    public static Page<?> getLocalPage() {
        return PAGE_THREAD_LOCAL.get();
    }

    // 僅清空當前線程的分頁參數
    public static void clearLocalPage() {
        PAGE_THREAD_LOCAL.remove();
    }
}

// ====================== 分頁參數載體:Page 類(存儲分頁相關信息) ======================
class Page<T> extends ArrayList<T> {
    private Integer pageNum; // 當前頁碼
    private Integer pageSize; // 每頁條數
    private Long total; // 總記錄數
    private Integer pages; // 總頁數
    private boolean count = true; // 是否統計總條數

    // getter/setter 省略
}

// ====================== 外部調用入口:PageHelper 靜態方法 ======================
class PageHelper {
    // 開發者調用該方法設置分頁參數,本質是將參數存入 ThreadLocal
    public static <T> Page<T> startPage(Integer pageNum, Integer pageSize) {
        Page<T> page = new Page<>(pageNum, pageSize);
        PageContext.setLocalPage(page);
        return page;
    }
}

偽代碼核心流程總結

  1. 參數存入:開發者調用 PageHelper.startPage(pageNum, pageSize),分頁參數存入 ThreadLocal,保證多線程隔離;
  2. 攔截觸發:MyBatis 執行 SQL 時,PageInterceptor 攔截目標組件,從 ThreadLocal 獲取分頁參數;
  3. SQL改寫:根據數據庫方言,動態生成分頁 SQL(如 MySQL 的 LIMIT)和總條數統計 SQL;
  4. 執行查詢:替換原始 SQL,執行分頁查詢和總條數查詢,封裝分頁結果;
  5. 清理緩存:執行完畢後清除 ThreadLocal 中的分頁參數,避免線程複用問題;
  6. 返回結果:返回包含當前頁數據、總條數、總頁數的分頁對象,可進一步包裝為 PageInfo 供業務使用。

加油啦!加油鴨,衝鴨!!!