V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tongjiann
V2EX  ›  程序员

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

  •  
  •   tongjiann · 118 天前 · 4980 次点击
    这是一个创建于 118 天前的主题,其中的信息可能已经有所发展或是发生改变。

    简单介绍一下情况。

    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 条附言  ·  118 天前
    感谢大家,最终采取了 17L 的建议
    43 条回复    2024-07-27 15:30:58 +08:00
    cnhongwei
        1
    cnhongwei  
       118 天前
    用 mybatis 不清楚,用 spring security+spring data jpa 有这个查询增加条件和结果集检查功能,但只是看文档中有,没有实践过,不知道对性能有多少影响。但如果用到 spring security 的话,可以看看 spring security 的文档,在 mybatis 中能不能实现。
    Musong
        2
    Musong  
       118 天前
    我前端啊,后端不懂。有个小小问题,权限是不是应该在查询之前,在代码逻辑中就给拦截了?(我可完全不懂啊,问错了不要笑话我)
    billbur
        3
    billbur  
       118 天前   ❤️ 2
    第一种就行了,数据库没你想象的那么脆弱,一个是要注意覆盖索引,另一个是分页或者说限制一次能查询的数量
    tongjiann
        4
    tongjiann  
    OP
       118 天前
    @cnhongwei #1 好的,感谢
    dong568789
        5
    dong568789  
       118 天前
    我们是加了个 middleware,所有请求经过中间件,都会判断是否要加这个 period 条件判断,如果是就注入到 query 里,然后拼装 sql 的地方,会解析这个 period
    tongjiann
        6
    tongjiann  
    OP
       118 天前
    @Musong #2 你说的没问题,部分请求中存在这个参数,确实可以在查询之前进行拦截。但是吧,有部分情况是,比如和描述中一样,select * from table a where id = 'xxx'。这个时候我并不知道查询出来的数据的 period 是啥。还得查出在之后再进行校验看看是不是合法数据。不过这个方法可行,确实可以加两层,前面先过滤掉一批,这样可以适当的减轻数据库的压力
    paopjian
        7
    paopjian  
       118 天前
    我也不懂后端, 这个应该和用户权限校验走一起的逻辑吧, 在用户权限校验处加一个操作权限判断, 这样以后还有其他的新控制字段也可以再加入
    tongjiann
        8
    tongjiann  
    OP
       118 天前
    @paopjian #7 是的,从代码设计上来说没问题。但是我们目前的系统为了偷懒,允许前端通过构建类似于 SQL 查询条件的方式,直接请求通用接口查询数据库中的数据,可以直接跳过对应字段的校验。但是已经是一坨了,只能往上面再来一坨了
    leejinhong
        9
    leejinhong  
       118 天前
    在代码层加这种判断有点不太好,如果后期继续加权限逻辑岂不是得改很多。如果有使用 ORM 的话,适当改装一下 ORM ,业务层面通过 ORM 操作,对于本来业务的侵入会比较少。
    theOneMe
        10
    theOneMe  
       118 天前   ❤️ 1
    1. 逻辑上应该是第一种,第二种的话,分页查询如果查询之后过滤可能出现为空的问题,导致缺页;
    2. 数据量如果百万以下,不用考虑太多,直接操作就行
    tongjiann
        11
    tongjiann  
    OP
       118 天前
    @dong568789 #5 这个 idea 很好,和我的第一种想法类似,不过你的可能更加完善。我验证一下这个方法在我们代码中的可行性
    tongjiann
        12
    tongjiann  
    OP
       118 天前
    @leejinhong #9 这个需求是针对系统的需求,和业务无关,最终实现肯定是在一个 ORM 层进行实现,并不是在每个业务实现类中进行操作
    2tongW
        13
    2tongW  
       118 天前   ❤️ 1
    我也做过类似的需求,就是采用的第一种方法。如果担心性能的话,是不是可以考虑新增一个“是否可操作”的字段,来维护。
    代价就是管理员开放指定 period 的时候需要去批量更新这个字段。
    Jasckcc
        14
    Jasckcc  
       118 天前
    可考虑使用布隆过滤器或 redis 做前置条件验证。
    needpp
        15
    needpp  
       118 天前
    新建一个 视图 ,在视图里面查询
    uselessVisitor
        16
    uselessVisitor  
       118 天前 via Android
    Mybatis 插件动态添加 period 字段的筛选
    andy2415
        17
    andy2415  
       118 天前   ❤️ 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  
       118 天前
    就算是 1 也没必要 select *吧?只要 select count (*)是不是就能满足业务需要了?
    andy2415
        19
    andy2415  
       118 天前
    @andy2415 #12 另外, mybatisplus 拦截器初始化顺序要 注意添加再分页的后面, 不然分页查询会有问题
    tongjiann
        20
    tongjiann  
    OP
       118 天前
    @Morii #18 最终的数据要进行后续的操作,如前端展示,那这个时候需要的不只是数据条数了,count 还不够吧
    tongjiann
        21
    tongjiann  
    OP
       118 天前
    @andy2415 #19 谢谢,还贴了代码
    xibeifeng
        22
    xibeifeng  
       118 天前   ❤️ 1
    我理解的这个问题本质是数据权限问题,要考虑:1.控制数据权限的字段有哪些,这里是 period ,后续有没有其他字段需要配置,比如多个字段组合,是否可能寻在这种场景 ? 2.这个判断逻辑放在哪里,数据库、后端、前端。3.具体场景可能还有一些其他问题,反正就是跟时空之神做交易
    具体方案的话:1.数据量小,字段固定,直接页面加载的时候,先把 period 缓存一下到本地 2.放后端,写一个切面加上注解,数据库做一张配置表,计算好每个人配置的 period ,然后在查询之前对查询条件做过滤,后续也能支持多字段控制数据权限,也可以通过加上注解实现逻辑可插拔 3.数据库硬编码写死逻辑,或者构建试图
    SoviaPhilo
        23
    SoviaPhilo  
       118 天前   ❤️ 1
    巧了, 我手上有个项目有类似的需求, 而且也用上面的方式做了统一处理

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

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

    除非项目特别简单、后期不动了,否则强烈不推荐第一种方式:
    1 、每个地方加,找起来很痛苦
    2 、文档不够好的话,对后面开发的开发来说就是灾难了,因为别人很容易遗漏这个条件
    tongjiann
        38
    tongjiann  
    OP
       118 天前
    @justyeh #37 我描述的不够准确,首先,肯定不可能在每个业务类中修改代码实现这个功能,最起码也要抽一层出来,尽可能减少与业务的关联性,保证后续新增业务表也不需要修改这里的代码。我最终的实现方案是参照 17L 的方式,写一个拦截器,统一在 SQL 执行前进行拦截,然后判断,如果需要就注入 SQL
    crz
        39
    crz  
       118 天前
    刚好最近看过,postgres 有 row level security ,数据库层级对访问/操作进行控制,就是对应这种需求的,不知道你们用的数据库有没有类似实现
    tongjiann
        40
    tongjiann  
    OP
       118 天前
    感谢大家的回复,最终根据 17L 的建议进行了修改
    iMoutai
        41
    iMoutai  
       118 天前
    MyBatis-Plus 自带的数据权限插件不符合要求吗?
    m1ch3ng
        42
    m1ch3ng  
       117 天前
    @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  
       117 天前   ❤️ 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   ·   实用小工具   ·   2992 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 14:11 · PVG 22:11 · LAX 06:11 · JFK 09:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.