V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
tongjiann
V2EX  ›  程序员

有一个代码的设计问题,大佬们帮帮我

  •  
  •   tongjiann · 2024-07-26 09:39:00 +08:00 · 5633 次点击
    这是一个创建于 403 天前的主题,其中的信息可能已经有所发展或是发生改变。

    简单介绍一下情况。

    1. 我们是用的是 jdk8+mybatis+MybatisPlus+diboot(小众的 MP 的增强,没看见过可忽略)的后端项目
    2. 每个业务表中都有一个 period(会计期间)字段

    现在有一个 PM 提了一个很容易理解但是很傻逼的 Feature:

    只有管理员开放指定 period 之后,才能对指定 period 的业务表数据进行增删改查(增删改已经做限制,采用下面第二种方法。),否则都不允许(返回空或提示权限不足) 管理员无视上面一条要求

    初步想法

    1. 在所有的查询语句执行前在代码层添加可查询的 period 列表。
      • 比如 select * from table a where id = 'xxx',那就改成 select * from table a where id = 'xxx' and period in (valid_period_list);
    2. 对所有的查询出来的数据进行校验。由于增删改单次进行的对象的数据量较小,所以在增删改操作进行前做一次查询取出所有被查询的数据并判断 period ,这样损耗较小,目前可以接受。但是当查询的时候使用系统方法我认为会严重损耗性能。
      • 比如 select * from table a where id = 'xxx'。在查询出数据之后,在代码层进行一次校验,判断 period 是否合理。

    请问一下大佬们对这个 Feature 有什么比较好的实现经验吗,或者说比较好的 idea

    第 1 条附言  ·  2024-07-26 17:03:01 +08:00
    感谢大家,最终采取了 17L 的建议
    43 条回复    2024-07-27 15:30:58 +08:00
    cnhongwei
        1
    cnhongwei  
       2024-07-26 09:45:51 +08:00
    用 mybatis 不清楚,用 spring security+spring data jpa 有这个查询增加条件和结果集检查功能,但只是看文档中有,没有实践过,不知道对性能有多少影响。但如果用到 spring security 的话,可以看看 spring security 的文档,在 mybatis 中能不能实现。
    Musong
        2
    Musong  
       2024-07-26 09:51:13 +08:00
    我前端啊,后端不懂。有个小小问题,权限是不是应该在查询之前,在代码逻辑中就给拦截了?(我可完全不懂啊,问错了不要笑话我)
    billbur
        3
    billbur  
       2024-07-26 09:53:49 +08:00   ❤️ 2
    第一种就行了,数据库没你想象的那么脆弱,一个是要注意覆盖索引,另一个是分页或者说限制一次能查询的数量
    tongjiann
        4
    tongjiann  
    OP
       2024-07-26 09:56:33 +08:00
    @cnhongwei #1 好的,感谢
    dong568789
        5
    dong568789  
       2024-07-26 10:00:15 +08:00
    我们是加了个 middleware,所有请求经过中间件,都会判断是否要加这个 period 条件判断,如果是就注入到 query 里,然后拼装 sql 的地方,会解析这个 period
    tongjiann
        6
    tongjiann  
    OP
       2024-07-26 10:01:01 +08:00
    @Musong #2 你说的没问题,部分请求中存在这个参数,确实可以在查询之前进行拦截。但是吧,有部分情况是,比如和描述中一样,select * from table a where id = 'xxx'。这个时候我并不知道查询出来的数据的 period 是啥。还得查出在之后再进行校验看看是不是合法数据。不过这个方法可行,确实可以加两层,前面先过滤掉一批,这样可以适当的减轻数据库的压力
    paopjian
        7
    paopjian  
       2024-07-26 10:03:22 +08:00
    我也不懂后端, 这个应该和用户权限校验走一起的逻辑吧, 在用户权限校验处加一个操作权限判断, 这样以后还有其他的新控制字段也可以再加入
    tongjiann
        8
    tongjiann  
    OP
       2024-07-26 10:06:17 +08:00
    @paopjian #7 是的,从代码设计上来说没问题。但是我们目前的系统为了偷懒,允许前端通过构建类似于 SQL 查询条件的方式,直接请求通用接口查询数据库中的数据,可以直接跳过对应字段的校验。但是已经是一坨了,只能往上面再来一坨了
    leejinhong
        9
    leejinhong  
       2024-07-26 10:09:03 +08:00
    在代码层加这种判断有点不太好,如果后期继续加权限逻辑岂不是得改很多。如果有使用 ORM 的话,适当改装一下 ORM ,业务层面通过 ORM 操作,对于本来业务的侵入会比较少。
    theOneMe
        10
    theOneMe  
       2024-07-26 10:09:44 +08:00   ❤️ 1
    1. 逻辑上应该是第一种,第二种的话,分页查询如果查询之后过滤可能出现为空的问题,导致缺页;
    2. 数据量如果百万以下,不用考虑太多,直接操作就行
    tongjiann
        11
    tongjiann  
    OP
       2024-07-26 10:11:54 +08:00
    @dong568789 #5 这个 idea 很好,和我的第一种想法类似,不过你的可能更加完善。我验证一下这个方法在我们代码中的可行性
    tongjiann
        12
    tongjiann  
    OP
       2024-07-26 10:15:29 +08:00
    @leejinhong #9 这个需求是针对系统的需求,和业务无关,最终实现肯定是在一个 ORM 层进行实现,并不是在每个业务实现类中进行操作
    2tongW
        13
    2tongW  
       2024-07-26 10:15:59 +08:00   ❤️ 1
    我也做过类似的需求,就是采用的第一种方法。如果担心性能的话,是不是可以考虑新增一个“是否可操作”的字段,来维护。
    代价就是管理员开放指定 period 的时候需要去批量更新这个字段。
    Jasckcc
        14
    Jasckcc  
       2024-07-26 10:19:22 +08:00
    可考虑使用布隆过滤器或 redis 做前置条件验证。
    needpp
        15
    needpp  
       2024-07-26 10:23:43 +08:00
    新建一个 视图 ,在视图里面查询
    zm8m93Q1e5otOC69
        16
    zm8m93Q1e5otOC69  
       2024-07-26 10:24:05 +08:00 via Android
    Mybatis 插件动态添加 period 字段的筛选
    andy2415
        17
    andy2415  
       2024-07-26 10:31:49 +08:00   ❤️ 1
    之前写的<=的逻辑, 只处理了删改查你可以试试改成 in,
    ```java
    @Slf4j
    public class Demo extends JsqlParserSupport implements InnerInterceptor {

    @Override
    public void beforePrepare(
    StatementHandler sh, Connection connection, Integer transactionTimeout) {
    PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
    MappedStatement ms = mpSh.mappedStatement();
    SqlCommandType commandType = ms.getSqlCommandType();

    if (commandType == SqlCommandType.SELECT || commandType == SqlCommandType.UPDATE || commandType == SqlCommandType.DELETE) {
    mpSh.mPBoundSql().sql(parserMulti(mpSh.mPBoundSql().sql(), null));
    }
    }

    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
    PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
    MinorThanEquals minorThanEquals = getMinorThanEquals();
    Expression where = plainSelect.getWhere();
    plainSelect.setWhere(
    where == null ? minorThanEquals : new AndExpression(where, minorThanEquals));
    }

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
    ....
    }

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
    ....
    }

    @NotNull
    private static MinorThanEquals getMinorThanEquals() {
    MinorThanEquals minorThanEquals = new MinorThanEquals();
    minorThanEquals.setLeftExpression(new Column("column_name"));
    minorThanEquals.setRightExpression(new LongValue(4));
    return minorThanEquals;
    }
    }
    ```
    28Sv0ngQfIE7Yloe
        18
    28Sv0ngQfIE7Yloe  
       2024-07-26 10:32:00 +08:00
    就算是 1 也没必要 select *吧?只要 select count (*)是不是就能满足业务需要了?
    andy2415
        19
    andy2415  
       2024-07-26 10:33:59 +08:00
    @andy2415 #12 另外, mybatisplus 拦截器初始化顺序要 注意添加再分页的后面, 不然分页查询会有问题
    tongjiann
        20
    tongjiann  
    OP
       2024-07-26 10:39:36 +08:00
    @Morii #18 最终的数据要进行后续的操作,如前端展示,那这个时候需要的不只是数据条数了,count 还不够吧
    tongjiann
        21
    tongjiann  
    OP
       2024-07-26 10:41:58 +08:00
    @andy2415 #19 谢谢,还贴了代码
    xibeifeng
        22
    xibeifeng  
       2024-07-26 10:42:55 +08:00   ❤️ 1
    我理解的这个问题本质是数据权限问题,要考虑:1.控制数据权限的字段有哪些,这里是 period ,后续有没有其他字段需要配置,比如多个字段组合,是否可能寻在这种场景 ? 2.这个判断逻辑放在哪里,数据库、后端、前端。3.具体场景可能还有一些其他问题,反正就是跟时空之神做交易
    具体方案的话:1.数据量小,字段固定,直接页面加载的时候,先把 period 缓存一下到本地 2.放后端,写一个切面加上注解,数据库做一张配置表,计算好每个人配置的 period ,然后在查询之前对查询条件做过滤,后续也能支持多字段控制数据权限,也可以通过加上注解实现逻辑可插拔 3.数据库硬编码写死逻辑,或者构建试图
    SoviaPhilo
        23
    SoviaPhilo  
       2024-07-26 10:52:59 +08:00   ❤️ 1
    巧了, 我手上有个项目有类似的需求, 而且也用上面的方式做了统一处理

    然后一年以后我就不再用这玩意儿了。
    一个问题是管理员需要操作全量数据, 意味着要维护 selectIgnore
    另一个问题是出现了相似的相对含义字段,类比一下就是业务增加了第二个 会计期间,而且要基于这个做逻辑
    第三个问题是业务性条件,非显式地填充事实上增加了潜在成本

    考虑到 PO 的视野根本看不了这么远, 直接用你的方案 1 算了
    MaxYang666
        24
    MaxYang666  
       2024-07-26 11:13:07 +08:00
    如果操作的表不是太多的话,方案 1 就可以,如果操作的表数量比较多,还是想办法抽一个中间层出来比较好
    BiChengfei
        25
    BiChengfei  
       2024-07-26 11:36:44 +08:00
    方案 1 ,加个 Mybatis 拦截器就行
    meeop
        26
    meeop  
       2024-07-26 11:45:37 +08:00
    如果 period 只是表示数据是否可查询的话,可以考虑做两张表,一张草稿标,一张在线查询表,在线表都是可查的,草稿表则是没进入发布态的数据,可发布时刻再插入在线表
    NX2023
        27
    NX2023  
       2024-07-26 12:23:18 +08:00 via iPhone
    第一个想到的是 casbin👀
    dongdong12345
        28
    dongdong12345  
       2024-07-26 12:55:54 +08:00
    这个看着和若依的数据权限处理有点像,不过若依的是按部门分数据权限,每个表都有 deptId 字段,可以参考看看。
    原理还是 mybatis 拦截器修改 sql 语句来实现的,这样分页功能不会受到影响。
    dongdong12345
        29
    dongdong12345  
       2024-07-26 12:58:31 +08:00
    http://doc.ruoyi.vip/ruoyi/document/htsc.html#%E6%95%B0%E6%8D%AE%E6%9D%83%E9%99%90
    boqiqita
        30
    boqiqita  
       2024-07-26 13:02:57 +08:00
    补充下数据库里的数据量和 QPS 呗
    M48A1
        31
    M48A1  
       2024-07-26 13:49:00 +08:00 via iPhone
    @needpp 我见过的是根据角色不同使用不同的 function
    tongjiann
        32
    tongjiann  
    OP
       2024-07-26 14:10:50 +08:00
    @boqiqita #30 目前已有总数据量千万吧,月度数据增量大概百万,QPS 不高,可能就月底用一下?属于绩效系统,从外部取数,然后算出绩效,面向的对象是主要还是绩效专员
    ningmengzhensuan
        33
    ningmengzhensuan  
       2024-07-26 14:15:40 +08:00
    第一种的话,你要维护所有的表修改,改动量太大,而且后期你们产品再添加新的类似需求的时候,那你还得改全部的 SQL 查询,第二种确实耗费性能
    你可以把这两个实现聚合起来,用拦截器+注解,新增一个注解,给需要设定权限限制的 SQL 查询添加上
    在拦截器那里添加前置拦截,判断权限,后置处理数据可以再进行特殊判断,添加一些权限、角色配置
    这样的话,对代码的渗透较低,可以扩充变更
    nealHuang
        34
    nealHuang  
       2024-07-26 14:20:34 +08:00
    我们都用 kjqj 来表示 会计期间
    xxmaqzas
        35
    xxmaqzas  
       2024-07-26 14:37:13 +08:00
    查询走视图
    zhazi
        36
    zhazi  
       2024-07-26 14:58:57 +08:00
    MMDeJeVS3GtMVLeu
        37
    MMDeJeVS3GtMVLeu  
       2024-07-26 15:21:49 +08:00
    我是前端,如果对一个请求做前、后的处理,用 axios 有拦截器这个概念。
    搜了一下 sql 也有类似的概念,mybatis 拦截器 https://juejin.cn/post/7116757450274897957

    除非项目特别简单、后期不动了,否则强烈不推荐第一种方式:
    1 、每个地方加,找起来很痛苦
    2 、文档不够好的话,对后面开发的开发来说就是灾难了,因为别人很容易遗漏这个条件
    tongjiann
        38
    tongjiann  
    OP
       2024-07-26 15:41:25 +08:00
    @justyeh #37 我描述的不够准确,首先,肯定不可能在每个业务类中修改代码实现这个功能,最起码也要抽一层出来,尽可能减少与业务的关联性,保证后续新增业务表也不需要修改这里的代码。我最终的实现方案是参照 17L 的方式,写一个拦截器,统一在 SQL 执行前进行拦截,然后判断,如果需要就注入 SQL
    crz
        39
    crz  
       2024-07-26 15:47:45 +08:00
    刚好最近看过,postgres 有 row level security ,数据库层级对访问/操作进行控制,就是对应这种需求的,不知道你们用的数据库有没有类似实现
    tongjiann
        40
    tongjiann  
    OP
       2024-07-26 17:02:33 +08:00
    感谢大家的回复,最终根据 17L 的建议进行了修改
    iMoutai
        41
    iMoutai  
       2024-07-26 18:35:46 +08:00
    MyBatis-Plus 自带的数据权限插件不符合要求吗?
    m1ch3ng
        42
    m1ch3ng  
       2024-07-27 09:48:29 +08:00
    @andy2415 #19 试了下,oracle 分页遇到了一个问题,sql 打印:

    ==> Preparing: SELECT * FROM (SELECT TMP.*, ROWNUM ROW_ID FROM (SELECT id, serial_number, command, sap_no, product_no, voucher_id, voucher_detail_id, voucher_details_id, record_status, remark, created_time, updated_time, pro_factory, batch_no, product_date, expire_date, product_num, pro_unit_cost, rx_flag, operator_name, operate_date, total_amount, price, old_voucher_id, old_voucher_details_id, spp_hsbl, settlement_time FROM POS_INVENTORY_UPLOAD_RECORD WHERE (command = ?)) TMP WHERE ROWNUM <= ?) WHERE ROW_ID > ? AND created_time >= {d '2024-07-26'}
    ==> Parameters: SALE_OR_REFUND_SALE(String), 4(Long), 2(Long)
    <== Total: 0

    其中 created_time >= {d '2024-07-26'} 是我自定义拦截器加的 GreaterThanEquals ,然而 mybatis-plus 在 oracle 场景下把它放在了最外层查询里面,如果想要实现放在 WHERE (command = ? AND created_time >= {d '2024-07-26'}),请问该如何调整?

    P.S. 我配置的自定义拦截器顺序是放在分页插件后面的
    m1ch3ng
        43
    m1ch3ng  
       2024-07-27 15:30:58 +08:00   ❤️ 1
    自己研究了下解决了,代码如下:
    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
    // 默认情况下,直接加到最外层查询的 where 后面
    PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
    if (select.toString().contains("ROWNUM")) {
    // oracle 分页的情况,需要找到最内层的查询,然后加到 where 后面
    plainSelect = getInnerSelect(plainSelect);
    }
    // 给查询添加条件
    GreaterThanEquals greaterThanEquals = getGreaterThanEquals();
    Expression where = plainSelect.getWhere();
    plainSelect.setWhere(where == null ? greaterThanEquals : new AndExpression(where, greaterThanEquals));
    }

    private static PlainSelect getInnerSelect(PlainSelect select) {
    if (select.getFromItem() instanceof SubSelect) {
    PlainSelect fromSelect = (PlainSelect) ((SubSelect) select.getFromItem()).getSelectBody();
    return getInnerSelect(fromSelect);
    }
    return select;
    }

    @NotNull
    private static GreaterThanEquals getGreaterThanEquals() {
    GreaterThanEquals greaterThanEquals = new GreaterThanEquals();
    greaterThanEquals.setLeftExpression(new Column("created_time"));
    greaterThanEquals.setRightExpression(new DateValue("'2024-07-26'"));
    return greaterThanEquals;
    }
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   993 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 19:18 · PVG 03:18 · LAX 12:18 · JFK 15:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.