CRUD 工程师提问:最佳实践是把逻辑放在数据库中还是后端代码中 ? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2XEX
V2EX    Java

CRUD 工程师提问:最佳实践是把逻辑放在数据库中还是后端代码中 ?

  •  
  •   V2XEX 2018-12-08 19:57:52 +08:00 7498 次点击
    这是一个创建于 2552 天前的主题,其中的信息可能已经有所发展或是发生改变。

    crud 搞久了,最近坐下来想些问题就发现脑子有点乱。 假如现在有这么一个按类别查询某用户未读帖子数量场景:

    有 2 张表
    用户表 user,字段: 用户唯一标识符:uuid
    帖子表 post,字段: 帖子类别:type ; 帖子已读用户:readers (用户每打开一个帖子就往这个字段写入用户 uuid,并以逗号分隔)

    Java 代码中有对应的实体类,orm 用 Spring Data Jpa。

    现在需要按类别查询某用户未读帖子数量,有两个方案:
    1、直接查出所有帖子的类型和已读用户字段,然后用 Java8 的 Stream.filter、Collectors.groupingBy 来过滤、分类,直接给前端一个返回一个 Map (体现了 orm 的思想……)
    2、用包含 couting、not like、group by 等关键字的 sql 直接查出结果,直接给前端返回一个 Set<map>。

    如果使用方案 1,那么项目这部分 Java 代码应该放在哪里( service or controller )?项目结构应该怎么划分呢?

    虽然问题很小,不知道这算是钻牛角尖不……请有经验的 Ver 指教下

    42 条回复    2018-12-23 12:52:41 +08:00
    raphael008
        1
    raphael008  
       2018-12-08 20:01:14 +08:00
    方案 1 放 manager 层里
    xy90321
        2
    xy90321  
       2018-12-08 20:04:23 +08:00 via iPhone
    除非你用的数据库性能很差或者不提供类似功能语法的支持,否则我看不出全拿出来有什么好处。特别是你的数据量稍微大一点的时候,那已经不是蛋疼而是蛋碎了。(前提是如果内存和磁盘还没爆炸的话)

    更重要的是,很多复合 SQL 语法你要自己在 Java 端实现一遍那简直就是…
    tomczhen
        3
    tomczhen  
       2018-12-08 20:08:40 +08:00
    给钱少、工期短,方案 1 就是最佳实践。

    给钱多、工期足,方案 2 就是最佳实践,顺便弄点高大上的技术词汇,什么分布式、中间件都给整上。
    Inside
        4
    Inside  
       2018-12-08 20:09:30 +08:00
    帖子已读用户放到数组里,execute me ?对关系的理解和认识本身就有问题。
    这种认识直接导致了方案一这种瞎搞的方案。
    tomczhen
        5
    tomczhen  
       2018-12-08 20:09:41 +08:00
    @tomczhen 说反了......
    V2XEX
        6
    V2XEX  
    OP
       2018-12-08 20:17:59 +08:00
    @xy90321 其实就算不全拿出来,统计的根本逻辑也是在数据库中实现了,两个做法的区别就是统计的逻辑放在哪里
    V2XEX
        7
    V2XEX  
    OP
       2018-12-08 20:18:45 +08:00
    @Inside 那就这个功能来说,数据库应该如何设计呢?请指教……
    liuhuansir
        8
    liuhuansir  
       2018-12-08 20:32:52 +08:00
    应该再加一张已读帖子与用户多对多的表,用帖子总数减去已读数得到结果,一个业余后端的建议
    MegrezZhu
        9
    MegrezZhu  
       2018-12-08 20:33:24 +08:00
    首先既然是 SQL 数据库的话,这样设计表是有问题的…应该抽出一个用户已读帖子的表( userid+postid ),然后做一些外键 /索引,这样直接通过 SQL 查询的时候数据库能对查询做优化,不用读取全部数据。第一种方法在数据量大的时候基本不现实。
    可以去看看数据库范式,挺经典的理论。
    xiangyuecn
        10
    xiangyuecn  
       2018-12-08 20:37:31 +08:00
    最佳实践是根据实际情况合理搭配和选择。。。算了,还是先把那个设计这个表结构的打死了再谈后面的吧,哈哈
    ruandao
        11
    ruandao  
       2018-12-08 20:45:56 +08:00
    这个要考虑 数据库的 IO 成本和计算量
    houyujiangjun
        12
    houyujiangjun  
       2018-12-08 20:51:20 +08:00
    这是一个领域模型驱动的问题.
    V2XEX
        13
    V2XEX  
    OP
       2018-12-08 21:21:41 +08:00
    @MegrezZhu 但是这么设计的话这张中间表的记录数很容易就是几何级数增长啊,而且当有删除需求时这张表将承载更多任务
    V2XEX
        14
    V2XEX  
    OP
       2018-12-08 21:22:50 +08:00
    @xiangyuecn 等我意识到这么设计表有多蠢的时候就会扇自己两巴掌
    MegrezZhu
        15
    MegrezZhu  
       2018-12-08 21:26:28 +08:00
    @V2XEX 已有的帖子表 post 里面本来就存了每个帖子的已读用户,把它抽出来并不会增加多少负担。数据量级是没有变化的。
    V2XEX
        16
    V2XEX  
    OP
       2018-12-08 21:46:35 +08:00
    @MegrezZhu 有 n 个帖子,m 个用户,那么这张中间表至多就会有 m × n 条记录,以后每新发一个帖子至多会增加 m 条记录。还需要考虑删除情况……这样的开销对于原来直接将用户 uuid 写入帖子表某个字段来说不知道哪个更优?
    chanchan
        17
    chanchan  
       2018-12-08 21:55:26 +08:00
    我的习惯是 2
    azzwacb9001
        18
    azzwacb9001  
       2018-12-08 22:25:34 +08:00
    好问题。我是一个菜鸟,但我觉得方案 2 是比较合理的方案。如果不考虑具体的场景,那我觉得这个问题可以这么看:
    如果从数据库中取出来的数据,没有在中间层进行二次加工的需求,那就使用方案 2 ;如果一些从数据库中用比较复杂的 SQL 语句取出来的数据,还可能二次加工或者供多方使用,那就用方案 1.

    不知道我有没有理解楼主的问题= =我没搞过 JAVA
    MegrezZhu
        19
    MegrezZhu  
       2018-12-08 22:31:58 +08:00
    @V2XEX 直接将 uuid 写入帖子表的话,帖子表里面不也一样会是至多 m × n 个 uuid 吗,顶多减少了帖子 tid 的存储空间,所以我才说不会有数量级上的差距。
    而且考虑删除情况的话,考虑在某个帖子下删除某个用户的阅读记录(呃,为啥会有这个需求,还是我理解错了?),首先就会有 O(n)的查询复杂度。相对地如果是采用访问记录表的话,依靠索引可以近似地达到 O(1)的复杂度。
    如果是删帖带来的删除所有该帖子下的阅读记录的话,方法 1 可能会略有优势,但访问记录表依然可以利用索引高效删除,而且删除操作相对也不多。
    yfl168648
        20
    yfl168648  
       2018-12-08 22:34:44 +08:00
    搞个表,类别、用户、未读数,首次用脚本生成此表数据,然后改造读帖子的代码,如果首次读,未读数减一。这样如何?
    barryng67
        21
    barryng67  
       2018-12-08 22:38:19 +08:00 via iPhone
    一般弄个冗余字段存数,自己写逻辑维护,这样效率高点,数据量大也不怕。
    lihongjie0209
        22
    lihongjie0209  
       2018-12-08 22:41:32 +08:00
    如果架构设计足够好, 封装度足够高, 那么在你的概念中都不应该出现 sql 这个东西, 都是细节
    TomVista
        23
    TomVista  
       2018-12-08 22:45:50 +08:00 via iPhone
    对比下 io 成本和计算成本,然后选合适的
    Kiske
        24
    Kiske  
       2018-12-08 22:50:25 +08:00   2
    是两个问题: 1. readers 字段该不该这么设计. 2.逻辑代码的存放位置.

    1. readers 这个字段, 逗号隔开虽然违反了数据库设计的第一范式,但现在的需求比较简单,只是简单的查出来.

    好处是: 这样做很省事, 不用格外建表, 以后想查询用户是否已读, 用 FIND_IN_SET()就好了.
    坏处是: 就怕以后再复杂点, 让你用这个字段排序和筛选, 就只能用代码写.

    你们根本想象不到以后有多复杂, 因为没法关联查询, 要先拿着这堆用户 ID 去查出来用户, 查出来发现没法分页, 因为还要跨表按热度排序, 你只能手动分页, 而且不是物理分页, lambda 还没法 debug, 别人一接手, 根本维护不动.我写过, 从那以后每次遇到逗号隔开的字符串都有阴影.这就不是关系型数据库应该出现的东西, 真要存逗号隔开的字符串, 干脆对象全都转 json 算了, 字段都不用建.

    2. 逻辑代码想又少又易读, 有非常非常多的地方要注意, 但是存放位置一定要放在 service 层.
    因为 controller 层没有事务啊, controller 确实可以加 @Transactional, 但这样做还分什么层, 直接在 controller 里写 sql 多省事, 以前公司搞了个新框架, 我去一看, controller 里全是拼接 sql 的, 还没防注入,一堆干了十年,五年的人怎么能架构出这种东西,

    所以项目结构应该划分不是那么简单, 既想方便快捷, 又想易于扩展, 很难同时做到, 就算规定好了, 以后也会有人不按规矩来, 有 code review 也挡不住 for 循环里嵌套 for 循环 insert.
    V2XEX
        25
    V2XEX  
    OP
       2018-12-08 22:53:23 +08:00
    @MegrezZhu
    不是啊……
    1 将已读用户写入帖子表意思是把 uuid 写入帖子表的 readers 字段并用逗号分隔,比如像这样:uuid1,uuid2,uuid3
    某个帖子每被浏览一次就更新对应帖子的这个字段
    2 删除是指帖子可能会被删除,而不是删除浏览记录,如果有中间表那对应帖子的所有浏览记录都得删,不知对比将帖子整个删除这是否是个额外的花销(软删除同理)
    MegrezZhu
        26
    MegrezZhu  
       2018-12-08 23:10:00 +08:00
    @V2XEX
    第一点的话,上面的 @Kiske 讲得很好。
    第二点,采用访问记录表的话的确会有额外开销,但我认为这在大部分场景下是完全可以接受的。而如果删除成为瓶颈的话,软删除的方案挺好的。
    V2XEX
        27
    V2XEX  
    OP
       2018-12-08 23:16:52 +08:00
    @yfl168648 确实是个好思路

    @Kiske
    1、单就现在的简单需求(真的不考虑后续维护)来说,两种做法哪种更优?
    2、个人感觉 find in set 不如 like 啊,因为前者的“分组”操作是一个开销,我已用 uuid 储存(非自增 id ),不会出现误查的情况 。不知 MySQL 的 like 查询是否有短路机制。
    3、不瞒你说,我想在在搞的东西需求简单,还真想过把对象全转 json 存数据库,但考虑到数据库操作 json 肯定要经过解析这一步,每条数据都解析一遍开销略大,罢了。你讲的维护的事情涉及到东西很多,有时候不是程序员的水平不行,迫不得已写垃圾代码谁也没办法(每天都有新需求,每天都要改需求,你懂的)。
    4、关于项目分层,我觉得 mvcs 的分层好像和“面向对象”的思想有些出入,本想在本帖一并讨论,但又感觉两者非同类问题。不日我将另发一帖讨论。
    akira
        28
    akira  
       2018-12-08 23:28:06 +08:00
    用户日活一百左右的话,用这个方案没问题
    no1xsyzy
        29
    no1xsyzy  
       2018-12-09 00:40:06 +08:00
    @V2XEX
    我不太清楚各个数据库实现上有什么区别,但字符串应该是顺序存储在一块内的吧。
    也就是说在删除后肯定会产生不规则形状的洞。这些洞要被有效利用上肯定还是要移动其他数据的。

    #27
    垃圾代码问题,只能说水平问题。
    我之前自己有空瞎写的东西,基本上对标到 8 小时也就是每天有新需求和改需求。
    然后工作得很好,有几个月没管。
    之后突然想要重构,包括扩展接口形状。
    结果发现模块化做得很好,就算零注释零文档,重构也没花多少功夫,尽管已经完全不记得上游 API 和代码思路了。
    然后重构完还没完做新的接口又丢在那没管。
    no1xsyzy
        30
    no1xsyzy  
       2018-12-09 00:44:09 +08:00
    @V2XEX MVCS 对应的思想是 reactive 吧,更接近消息机制,或者说面向数据流。
    我重新发现过轮子圆形好,所以还是挺熟悉的。
    hhhsuan
        31
    hhhsuan  
       2018-12-09 01:44:24 +08:00 via Android
    看了各位大佬的回答懵圈了,未读数不就是总数减去已读数吗?总数很容易获取,已读数每次读新帖加 1 就行了,这不是很简单。
    mornlight
        32
    mornlight  
       2018-12-09 01:55:24 +08:00
    not like 要遍历所有这个 type 的 post 记录,post 越多耗时越长。没救了,重新设计存储方案。
    问题出在 readers 字段,既想一个 string 存储所有已读又想对每个已读的 id 做业务,不科学。
    wenzhoou
        33
    wenzhoou  
       2018-12-09 06:21:46 +08:00 via Android
    歪个楼。只有我觉得用户用 UUID 是不对的吗?你不觉得 UUID 太长了吗。
    MegrezZhu
        34
    MegrezZhu  
       2018-12-09 13:29:33 +08:00
    @hhhsuan
    如果需要考虑删帖的话,就还是要维护用户已读帖子的列表的。
    V2XEX
        35
    V2XEX  
    OP
       2018-12-09 18:56:16 +08:00 via Android
    @no1xsyzy 发现模块化做得很好是什么鬼。我说的改需求是:开始只要你打印一个 hello world,后来要你打印十次,再后来要你根据我输入的次数打印并且还要附带我输入的内容……这种的改需求你能在一开始就预料到了?

    如果一定要说面对频繁更改的需求,并在开始写代码前就能预料到客户想法并写出条理清楚、结构清晰,可维护性高的代码如此简单的话,我想“扫码改需求”这种事情就不会成为程序员们所调侃(单自己做的 toy project 不在我说的范围内,产生需求和解决需求都是自己,没有什么东西在约束和评价,与实际多数人都在从事的开发工作不是一回事)
    fox0001
        36
    fox0001  
       2018-12-09 20:37:06 +08:00 via Android   1
    我一般选择类似方案 2 的做法。但数据库设计肯定是采用关系表,已读表存放用户 id 和帖子 id。

    如果帖子数量很大的话,而且查询又频繁,就考虑弄个缓存,记录用户未读帖子分类和数量,再弄个队列延时更新之类。

    至于代码的安排,就是
    1 ) controller 接收查询条件,调用 service 方法并返回结果
    2 ) service 查询接口,检验数据,处理业务逻辑,数据查询调用 dao 的查询方法
    3 ) dao 查询接口,相关查询语句,即与数据库的交互都写在这里,查询结果封装成对象返回
    no1xsyzy
        37
    no1xsyzy  
       2018-12-09 20:39:14 +08:00
    @V2XEX 自底向上编程,请。
    当有一次写出的代码明明和需求不符但运行得很好有感。
    如果你从打印一个 hello world 开始就是库+胶水代码,那么打印 10 次也不那么难,循环特定次数也不过是把 10 变成输入项,附带输入内容也可以随手写个 format。
    V2XEX
        38
    V2XEX  
    OP
       2018-12-09 20:59:52 +08:00 via Android
    @no1xsyzy 我只是举个例子而已……那以后我还要加其他东西呢?你势必要写其他的方法、类,把可重用的东西抽象,这个谁都知道。
    如果你开始就知道要接收用户输入按需打印,那你大可以规划一个输入模块,一个计算打印内容的模块,一个打印模块等,代码不仅井井有条、漂漂亮亮,还利于维护拓展,这就是你说的“模块划分很好”了,但是在开始做的时候没人告诉这些,加上时间紧任务重,我想是个正常人都直接写个 system.out.print (“ hello world ”),以后改什么直接在上面加,这样久而久之垃圾代码就出现了…

    还有,你自己给自己定的需求,别说过段时间改一次了……可能这一秒跟下一秒是完全不同的两个想法,那直接抄起键盘就开干,但是你摸良心说说这和客户\产品经理给你改的需求是一回事不……
    jlkm2010
        39
    jlkm2010  
       2018-12-10 10:52:20 +08:00
    打死那个设计表字段的,瞎胡搞
    no1xsyzy
        40
    no1xsyzy  
       2018-12-10 13:50:59 +08:00   1
    @V2XEX
    > 以后改什么直接在上面加
    这就是问题
    我举的例子是没有 print 函数的情况,那我会先写个 string->None 的 print 函数出来
    要加个数字就弄一个 int->string 的 format 函数,第二个参数来了依照来源做 fetcher 然后套进 format 里。
    然后主函数就变成了 print(format(fetcher1(), fetcher2(), fetcher3))
    主函数从来不写长,而且因为上述嵌套函数过多,我很想能够 (fetcher1, fetcher2, fetcher3)|f[_()]|format|print 这样写。

    我想说的是,作为基础能力,在比较微小且直接的问题上能够很快地抽象
    为什么一跑到巨大而间接的问题就失去了这种能力?
    这说明你的思路从开始就是一团乱麻,小问题上的抽象只是见过这种抽象所以能做。
    这就好像说数学题:数字变了变就不会做 vs 数字变了模式没变还会做 vs 数字变了导致模式变了还可能会做。

    > 当有一次写出的代码明明和需求不符但运行得很好有感。
    那次改需求,结果我听完把原需求和新需求都实现了,API 形状拓展但保留兼容,按需调用,并且因此导致其实需求没传达清楚但能用。
    具体来说,改的时候,告诉我一个 API 需要验证文件 sha3 (来决定是否更新),但其实验证的是 sha384。然而我直接把接口变成 {origname}.{type}(比如 foo.exe.sha384sum ),直接丢过去正常用了,后来说到其实是 sha384 才知道有错。框架也就用了不到一个月,基本上一个函数查 5 次文档,但 API 感觉在那,我能怎么办?
    大概有运气的成分,但能碰到这运气也是有对 API 形状的直觉所致。

    可能主要是因为我从犯中二病开始就一直纠结于这些事,到系统学习编程(高中 NOIP )之前已经想了大概 5 年吧。
    V2XEX
        41
    V2XEX  
    OP
       2018-12-10 20:53:07 +08:00
    @no1xsyzy 我想了下,感到这确实也是个经验问题。
    开始我假设的情况是“定需求"这个环节能做到最好,那么开发过程中很多看似由程序员造的坑即可避免(当然,在这种需求都给清楚了的情况下,程序员还能犯错那自然是难辞其咎的)。

    然而我假设的这种条件是苛刻的,作为一个开发者自然不能去要求其他人(甚至是上司、客户),把“定需求”这个环境做得完美,在实现需求的过程中要做的工作也不止”按图画画“这么简单,有经验的人听到别人说了 1+1 自己马上能想到 10+10 甚至开始为 10x10 做准备了,我在写代码前确实“想”得少了,就自身来说还是个经验问题。

    下次写代码前还是得多花时间去“想”,这样在起步阶段也许会慢,但是把这个工作做了,那项目将会更健壮,容错率也更高。
    dezhou9
        42
    dezhou9  
       2018-12-23 12:52:41 +08:00 via Android
    放到哪里都不对,小项目没那么多人做后端和 db,大项目复杂逻辑不该用 MySQL 了。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     983 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 78ms UTC 22:38 PVG 06:38 LAX 14:38 JFK 17:38
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86