惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 小纸条

ruoyiai 启动指南 反向传播 numpy的使用 B 和 B+树 红黑树 梯度下降法 博弈论 离散化 AcWing 907. 区间覆盖 AcWing 906. 区间分组 AcWing 908 最大不相交区间数量 AcWing 905. 区间选点 AcWing 104. 货仓选址 动态规划经典题 窗口函数 1226. 哲学家进餐 1195. 交替打印字符串 1117. H2O 生成 1116. 打印零与奇偶数 关联子查询
ruoyi-vue
小纸条 · 2025-12-23 · via 博客园 - 小纸条

五张表关系的Mermaid ER图代码

erDiagram %% 用户信息表 sys_user { bigint(20) user_id PK "用户ID(主键)" bigint(20) dept_id "部门ID" varchar(30) user_name "用户账号" varchar(30) nick_name "用户昵称" varchar(2) user_type "用户类型(00系统用户)" varchar(50) email "用户邮箱" varchar(11) phonenumber "手机号码" char(1) sex "用户性别(0男 1女 2未知)" varchar(100) avatar "头像地址" varchar(100) password "密码" char(1) status "帐号状态(0正常 1停用)" char(1) del_flag "删除标志(0代表存在 2代表删除)" varchar(128) login_ip "最后登录IP" datetime login_date "最后登录时间" varchar(64) create_by "创建者" datetime create_time "创建时间" varchar(64) update_by "更新者" datetime update_time "更新时间" varchar(500) remark "备注" } %% 用户角色关联表(中间表) sys_user_role { bigint(20) user_id FK "用户ID(联合主键/外键)" bigint(20) role_id FK "角色ID(联合主键/外键)" } %% 角色信息表 sys_role { bigint(20) role_id PK "角色ID(主键)" varchar(30) role_name "角色名称" varchar(100) role_key "角色权限字符串" int(4) role_sort "显示顺序" char(1) data_scope "数据范围(1:全部 2:自定 3:本部门 4:本部门及以下)" tinyint(1) menu_check_strictly "菜单树选择项是否关联显示" tinyint(1) dept_check_strictly "部门树选择项是否关联显示" char(1) status "角色状态(0正常 1停用)" char(1) del_flag "删除标志(0代表存在 2代表删除)" varchar(64) create_by "创建者" datetime create_time "创建时间" varchar(64) update_by "更新者" datetime update_time "更新时间" varchar(500) remark "备注" } %% 角色菜单关联表(中间表) sys_role_menu { bigint(20) role_id FK "角色ID(联合主键/外键)" bigint(20) menu_id FK "菜单ID(联合主键/外键)" } %% 菜单权限表 sys_menu { bigint(20) menu_id PK "菜单ID(主键)" varchar(50) menu_name "菜单名称" bigint(20) parent_id "父菜单ID" int(4) order_num "显示顺序" varchar(200) path "路由地址" varchar(255) component "组件路径" varchar(255) query "路由参数" int(1) is_frame "是否为外链(0是 1否)" int(1) is_cache "是否缓存(0缓存 1不缓存)" char(1) menu_type "菜单类型(M目录 C菜单 F按钮)" char(1) visible "菜单状态(0显示 1隐藏)" char(1) status "菜单状态(0正常 1停用)" varchar(100) perms "权限标识" varchar(100) icon "菜单图标" varchar(64) create_by "创建者" datetime create_time "创建时间" varchar(64) update_by "更新者" datetime update_time "更新时间" varchar(500) remark "备注" } %% 定义关联关系 %% 用户与角色:多对多,通过sys_user_role关联 sys_user ||--o{ sys_user_role : "关联" sys_role ||--o{ sys_user_role : "关联" %% 角色与菜单:多对多,通过sys_role_menu关联 sys_role ||--o{ sys_role_menu : "关联" sys_menu ||--o{ sys_role_menu : "关联"

使用说明

  • 关系说明
    • ||--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 供业务使用。