首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
拉钩
V2EX  ›  Java

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

  •  
  •   V2XEX · 8 天前 · 3155 次点击

    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 指教下

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

    更重要的是,很多复合 SQL 语法你要自己在 Java 端实现一遍那简直就是…
        3
    tomczhen   8 天前
    给钱少、工期短,方案 1 就是最佳实践。

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

    不知道我有没有理解楼主的问题= =我没搞过 JAVA
        19
    MegrezZhu   8 天前
    @V2XEX 直接将 uuid 写入帖子表的话,帖子表里面不也一样会是至多 m × n 个 uuid 吗,顶多减少了帖子 tid 的存储空间,所以我才说不会有数量级上的差距。
    而且考虑删除情况的话,考虑在某个帖子下删除某个用户的阅读记录(呃,为啥会有这个需求,还是我理解错了?),首先就会有 O(n)的查询复杂度。相对地如果是采用访问记录表的话,依靠索引可以近似地达到 O(1)的复杂度。
    如果是删帖带来的删除所有该帖子下的阅读记录的话,方法 1 可能会略有优势,但访问记录表依然可以利用索引高效删除,而且删除操作相对也不多。
        20
    yfl168648   8 天前
    搞个表,类别、用户、未读数,首次用脚本生成此表数据,然后改造读帖子的代码,如果首次读,未读数减一。这样如何?
        21
    barryng67   8 天前 via iPhone
    一般弄个冗余字段存数,自己写逻辑维护,这样效率高点,数据量大也不怕。
        22
    lihongjie0209   8 天前
    如果架构设计足够好, 封装度足够高, 那么在你的概念中都不应该出现 sql 这个东西, 都是细节
        23
    TomVista   8 天前 via iPhone
    对比下 io 成本和计算成本,然后选合适的
        24
    Kiske   8 天前   ♥ 1
    是两个问题: 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.
        25
    V2XEX   8 天前
    @MegrezZhu
    不是啊……
    1 将已读用户写入帖子表意思是把 uuid 写入帖子表的 readers 字段并用逗号分隔,比如像这样:uuid1,uuid2,uuid3
    某个帖子每被浏览一次就更新对应帖子的这个字段
    2 删除是指帖子可能会被删除,而不是删除浏览记录,如果有中间表那对应帖子的所有浏览记录都得删,不知对比将帖子整个删除这是否是个额外的花销(软删除同理)
        26
    MegrezZhu   8 天前
    @V2XEX
    第一点的话,上面的 @Kiske 讲得很好。
    第二点,采用访问记录表的话的确会有额外开销,但我认为这在大部分场景下是完全可以接受的。而如果删除成为瓶颈的话,软删除的方案挺好的。
        27
    V2XEX   8 天前
    @yfl168648 确实是个好思路

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

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

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

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

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

    还有,你自己给自己定的需求,别说过段时间改一次了……可能这一秒跟下一秒是完全不同的两个想法,那直接抄起键盘就开干,但是你摸良心说说这和客户\产品经理给你改的需求是一回事不……
        39
    jlkm2010   6 天前
    打死那个设计表字段的,瞎胡搞
        40
    no1xsyzy   6 天前   ♥ 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 年吧。
        41
    V2XEX   6 天前
    @no1xsyzy 我想了下,感到这确实也是个经验问题。
    开始我假设的情况是“定需求"这个环节能做到最好,那么开发过程中很多看似由程序员造的坑即可避免(当然,在这种需求都给清楚了的情况下,程序员还能犯错那自然是难辞其咎的)。

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

    下次写代码前还是得多花时间去“想”,这样在起步阶段也许会慢,但是把这个工作做了,那项目将会更健壮,容错率也更高。
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   1318 人在线   最高记录 4019   ·  
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.1 · 21ms · UTC 16:57 · PVG 00:57 · LAX 08:57 · JFK 11:57
    ♥ Do have faith in what you're doing.
    沪ICP备16043287号-1