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 指教下
1
raphael008 2018-12-08 20:01:14 +08:00
方案 1 放 manager 层里
|
2
xy90321 2018-12-08 20:04:23 +08:00 via iPhone
除非你用的数据库性能很差或者不提供类似功能语法的支持,否则我看不出全拿出来有什么好处。特别是你的数据量稍微大一点的时候,那已经不是蛋疼而是蛋碎了。(前提是如果内存和磁盘还没爆炸的话)
更重要的是,很多复合 SQL 语法你要自己在 Java 端实现一遍那简直就是… |
3
tomczhen 2018-12-08 20:08:40 +08:00
给钱少、工期短,方案 1 就是最佳实践。
给钱多、工期足,方案 2 就是最佳实践,顺便弄点高大上的技术词汇,什么分布式、中间件都给整上。 |
4
Inside 2018-12-08 20:09:30 +08:00
帖子已读用户放到数组里,execute me ?对关系的理解和认识本身就有问题。
这种认识直接导致了方案一这种瞎搞的方案。 |
8
liuhuansir 2018-12-08 20:32:52 +08:00
应该再加一张已读帖子与用户多对多的表,用帖子总数减去已读数得到结果,一个业余后端的建议
|
9
MegrezZhu 2018-12-08 20:33:24 +08:00
首先既然是 SQL 数据库的话,这样设计表是有问题的…应该抽出一个用户已读帖子的表( userid+postid ),然后做一些外键 /索引,这样直接通过 SQL 查询的时候数据库能对查询做优化,不用读取全部数据。第一种方法在数据量大的时候基本不现实。
可以去看看数据库范式,挺经典的理论。 |
10
xiangyuecn 2018-12-08 20:37:31 +08:00
最佳实践是根据实际情况合理搭配和选择。。。算了,还是先把那个设计这个表结构的打死了再谈后面的吧,哈哈
|
11
ruandao 2018-12-08 20:45:56 +08:00
这个要考虑 数据库的 IO 成本和计算量
|
12
houyujiangjun 2018-12-08 20:51:20 +08:00
这是一个领域模型驱动的问题.
|
14
V2XEX OP @xiangyuecn 等我意识到这么设计表有多蠢的时候就会扇自己两巴掌
|
15
MegrezZhu 2018-12-08 21:26:28 +08:00
@V2XEX 已有的帖子表 post 里面本来就存了每个帖子的已读用户,把它抽出来并不会增加多少负担。数据量级是没有变化的。
|
16
V2XEX OP @MegrezZhu 有 n 个帖子,m 个用户,那么这张中间表至多就会有 m × n 条记录,以后每新发一个帖子至多会增加 m 条记录。还需要考虑删除情况……这样的开销对于原来直接将用户 uuid 写入帖子表某个字段来说不知道哪个更优?
|
17
chanchan 2018-12-08 21:55:26 +08:00
我的习惯是 2
|
18
azzwacb9001 2018-12-08 22:25:34 +08:00
好问题。我是一个菜鸟,但我觉得方案 2 是比较合理的方案。如果不考虑具体的场景,那我觉得这个问题可以这么看:
如果从数据库中取出来的数据,没有在中间层进行二次加工的需求,那就使用方案 2 ;如果一些从数据库中用比较复杂的 SQL 语句取出来的数据,还可能二次加工或者供多方使用,那就用方案 1. 不知道我有没有理解楼主的问题= =我没搞过 JAVA |
19
MegrezZhu 2018-12-08 22:31:58 +08:00
@V2XEX 直接将 uuid 写入帖子表的话,帖子表里面不也一样会是至多 m × n 个 uuid 吗,顶多减少了帖子 tid 的存储空间,所以我才说不会有数量级上的差距。
而且考虑删除情况的话,考虑在某个帖子下删除某个用户的阅读记录(呃,为啥会有这个需求,还是我理解错了?),首先就会有 O(n)的查询复杂度。相对地如果是采用访问记录表的话,依靠索引可以近似地达到 O(1)的复杂度。 如果是删帖带来的删除所有该帖子下的阅读记录的话,方法 1 可能会略有优势,但访问记录表依然可以利用索引高效删除,而且删除操作相对也不多。 |
20
yfl168648 2018-12-08 22:34:44 +08:00
搞个表,类别、用户、未读数,首次用脚本生成此表数据,然后改造读帖子的代码,如果首次读,未读数减一。这样如何?
|
21
barryng67 2018-12-08 22:38:19 +08:00 via iPhone
一般弄个冗余字段存数,自己写逻辑维护,这样效率高点,数据量大也不怕。
|
22
lihongjie0209 2018-12-08 22:41:32 +08:00
如果架构设计足够好, 封装度足够高, 那么在你的概念中都不应该出现 sql 这个东西, 都是细节
|
23
TomVista 2018-12-08 22:45:50 +08:00 via iPhone
对比下 io 成本和计算成本,然后选合适的
|
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. |
25
V2XEX OP @MegrezZhu
不是啊…… 1 将已读用户写入帖子表意思是把 uuid 写入帖子表的 readers 字段并用逗号分隔,比如像这样:uuid1,uuid2,uuid3 某个帖子每被浏览一次就更新对应帖子的这个字段 2 删除是指帖子可能会被删除,而不是删除浏览记录,如果有中间表那对应帖子的所有浏览记录都得删,不知对比将帖子整个删除这是否是个额外的花销(软删除同理) |
26
MegrezZhu 2018-12-08 23:10:00 +08:00
|
27
V2XEX OP @yfl168648 确实是个好思路
@Kiske 1、单就现在的简单需求(真的不考虑后续维护)来说,两种做法哪种更优? 2、个人感觉 find in set 不如 like 啊,因为前者的“分组”操作是一个开销,我已用 uuid 储存(非自增 id ),不会出现误查的情况 。不知 MySQL 的 like 查询是否有短路机制。 3、不瞒你说,我想在在搞的东西需求简单,还真想过把对象全转 json 存数据库,但考虑到数据库操作 json 肯定要经过解析这一步,每条数据都解析一遍开销略大,罢了。你讲的维护的事情涉及到东西很多,有时候不是程序员的水平不行,迫不得已写垃圾代码谁也没办法(每天都有新需求,每天都要改需求,你懂的)。 4、关于项目分层,我觉得 mvcs 的分层好像和“面向对象”的思想有些出入,本想在本帖一并讨论,但又感觉两者非同类问题。不日我将另发一帖讨论。 |
28
akira 2018-12-08 23:28:06 +08:00
用户日活一百左右的话,用这个方案没问题
|
29
no1xsyzy 2018-12-09 00:40:06 +08:00
@V2XEX
我不太清楚各个数据库实现上有什么区别,但字符串应该是顺序存储在一块内的吧。 也就是说在删除后肯定会产生不规则形状的洞。这些洞要被有效利用上肯定还是要移动其他数据的。 #27 垃圾代码问题,只能说水平问题。 我之前自己有空瞎写的东西,基本上对标到 8 小时也就是每天有新需求和改需求。 然后工作得很好,有几个月没管。 之后突然想要重构,包括扩展接口形状。 结果发现模块化做得很好,就算零注释零文档,重构也没花多少功夫,尽管已经完全不记得上游 API 和代码思路了。 然后重构完还没完做新的接口又丢在那没管。 |
30
no1xsyzy 2018-12-09 00:44:09 +08:00
@V2XEX MVCS 对应的思想是 reactive 吧,更接近消息机制,或者说面向数据流。
我重新发现过轮子圆形好,所以还是挺熟悉的。 |
31
hhhsuan 2018-12-09 01:44:24 +08:00 via Android
看了各位大佬的回答懵圈了,未读数不就是总数减去已读数吗?总数很容易获取,已读数每次读新帖加 1 就行了,这不是很简单。
|
32
mornlight 2018-12-09 01:55:24 +08:00
not like 要遍历所有这个 type 的 post 记录,post 越多耗时越长。没救了,重新设计存储方案。
问题出在 readers 字段,既想一个 string 存储所有已读又想对每个已读的 id 做业务,不科学。 |
33
wenzhoou 2018-12-09 06:21:46 +08:00 via Android
歪个楼。只有我觉得用户用 UUID 是不对的吗?你不觉得 UUID 太长了吗。
|
35
V2XEX OP @no1xsyzy 发现模块化做得很好是什么鬼。我说的改需求是:开始只要你打印一个 hello world,后来要你打印十次,再后来要你根据我输入的次数打印并且还要附带我输入的内容……这种的改需求你能在一开始就预料到了?
如果一定要说面对频繁更改的需求,并在开始写代码前就能预料到客户想法并写出条理清楚、结构清晰,可维护性高的代码如此简单的话,我想“扫码改需求”这种事情就不会成为程序员们所调侃(单自己做的 toy project 不在我说的范围内,产生需求和解决需求都是自己,没有什么东西在约束和评价,与实际多数人都在从事的开发工作不是一回事) |
36
fox0001 2018-12-09 20:37:06 +08:00 via Android 1
我一般选择类似方案 2 的做法。但数据库设计肯定是采用关系表,已读表存放用户 id 和帖子 id。
如果帖子数量很大的话,而且查询又频繁,就考虑弄个缓存,记录用户未读帖子分类和数量,再弄个队列延时更新之类。 至于代码的安排,就是 1 ) controller 接收查询条件,调用 service 方法并返回结果 2 ) service 查询接口,检验数据,处理业务逻辑,数据查询调用 dao 的查询方法 3 ) dao 查询接口,相关查询语句,即与数据库的交互都写在这里,查询结果封装成对象返回 |
37
no1xsyzy 2018-12-09 20:39:14 +08:00
@V2XEX 自底向上编程,请。
——当有一次写出的代码明明和需求不符但运行得很好有感。 如果你从打印一个 hello world 开始就是库+胶水代码,那么打印 10 次也不那么难,循环特定次数也不过是把 10 变成输入项,附带输入内容也可以随手写个 format。 |
38
V2XEX OP @no1xsyzy 我只是举个例子而已……那以后我还要加其他东西呢?你势必要写其他的方法、类,把可重用的东西抽象,这个谁都知道。
如果你开始就知道要接收用户输入按需打印,那你大可以规划一个输入模块,一个计算打印内容的模块,一个打印模块等,代码不仅井井有条、漂漂亮亮,还利于维护拓展,这就是你说的“模块划分很好”了,但是在开始做的时候没人告诉这些,加上时间紧任务重,我想是个正常人都直接写个 system.out.print (“ hello world ”),以后改什么直接在上面加,这样久而久之垃圾代码就出现了… 还有,你自己给自己定的需求,别说过段时间改一次了……可能这一秒跟下一秒是完全不同的两个想法,那直接抄起键盘就开干,但是你摸良心说说这和客户\产品经理给你改的需求是一回事不…… |
39
jlkm2010 2018-12-10 10:52:20 +08:00
打死那个设计表字段的,瞎胡搞
|
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 年吧。 |
41
V2XEX OP @no1xsyzy 我想了下,感到这确实也是个经验问题。
开始我假设的情况是“定需求"这个环节能做到最好,那么开发过程中很多看似由程序员造的坑即可避免(当然,在这种需求都给清楚了的情况下,程序员还能犯错那自然是难辞其咎的)。 然而我假设的这种条件是苛刻的,作为一个开发者自然不能去要求其他人(甚至是上司、客户),把“定需求”这个环境做得完美,在实现需求的过程中要做的工作也不止”按图画画“这么简单,有经验的人听到别人说了 1+1 自己马上能想到 10+10 甚至开始为 10x10 做准备了,我在写代码前确实“想”得少了,就自身来说还是个经验问题。 下次写代码前还是得多花时间去“想”,这样在起步阶段也许会慢,但是把这个工作做了,那项目将会更健壮,容错率也更高。 |
42
dezhou9 2018-12-23 12:52:41 +08:00 via Android
放到哪里都不对,小项目没那么多人做后端和 db,大项目复杂逻辑不该用 MySQL 了。
|