jasl #1 · 2018年06月21日
不得不说,Sam Saffron 和 Mike Perham 频频对 Ruby 还有 Rails core team 就性能问题发难,而且有理有据,已经成功引起了重视。
另外我忘了在哪看到的他有提到 Discourse 会出钱赞助 AR 6 的重构,有钱事情应该就好办多了...
2楼 已删除
ksec #3 · 2018年06月21日 对
jasl
回复
有錢事情應該就好辦多了
我反而觉得这个不是钱的问题, 原因是根本没有人肯去处理. 世界上最难解决的问题就是人们不觉得这是一个问题. 至少 DHH 不觉得是.
jasl #4 · 2018年06月21日 对
ksec
回复
DHH 这种架构师角色的人,不会去关注那么细的工作,开源社区,谁行谁上
很多事情都需要社区推动的,比如 Shopify、Discourse 这种大厂去贡献,Basecamp 说白了还是体量太小,很多场景他们没需求自然也没有意识去做
ksec #5 ·
2018年06月21日 对
jasl
回复
感动,很多事情都需要社區推動的,比如 Shopify、Discourse 這種大廠去貢獻,Basecamp 說白了還是體量太小,很多場景他們沒需求自然也沒有意識去做
难得有人有相同想法. 我经常在想怎么能够让 Shopify , Discourse, Cookpad, Github, AirBnB, Bloomberg, 每边找一两个人一同商讨. ( 其实 Apple Music 也是
Ruby Rails, 不过相信要他们派人简直难过登天 )
jasl #6 · 2018年06月21日 对
ksec
回复
你看 5.2 的发行注记,已经提到了是 Shopify 引入的 Bootsnap 加速应用启动,还有 SamSaffron 和 Mike Perham 他们“逼宫” Ruby team 引入一些有利于常驻服务场景的优化,可能之后 Ruby 会引入单独的编译参数把这些优化开启,类似 Java Hotspot VM 分服务器版本和标准版本一样。
CRuby 发展不如 NodeJS 其实道理也很简单,就是没钱... Ruby Together 算是一个社区组织改善公共基础设施的,但是那些开发也要吃饭,要赞助到一定规模(按月出钱)才能考虑全职去做事情。NodeJS 依赖 V8,Chrome 保持话语权的关键组件,Google 投人投钱去改进 V8,Node 自然坐享其成
nouse #7 · 2018年06月21日
一个裸sql就能代替ar?标题党啊
hooopo #8 · 2018年06月22日
这个确实很有用,之前用Rails做项目遇到统计需求很尴尬,用AR来做特别难,一般最终结果就写成了sql+rails的合体,导致可维护性特别差。
本质上是统计需求和ORM并不是同一个维度上的事情,ORM解决的是表和对象之间的关系,而统计需求其实是面向主题的。 另外一个问题是ORM做统计的话经常会产生N+1查询。所以统计需求我一般都直接纯sql的方式来写,不使用任何AR相关API,例如:
AR.connection.execute(<<-SQL)
/* 按SKU分组,计算出出仓数量 */
WITH shipment_items_by_sku AS (
SELECT product_variant_id, SUM(quantity) AS ship_count
FROM shipment_items
WHERE deleted_at IS NULL AND company_id = 1
GROUP BY 1
),
/* 按SKU分组,计算出订单数量、最大金额等 */
order_items_by_sku AS (
SELECT pv.company_id,
oi.product_variant_id,
sum(oi.quantity) AS order_count,
sum(oi.sum_price) AS sum_money,
max(oi.price) AS max_money,
min(oi.price) AS min_money,
avg(oi.price) AS avg_money
FROM order_items AS oi
INNER JOIN product_variants AS pv ON pv.id = oi.product_variant_id
WHERE pv.deleted_at IS NULL AND oi.deleted_at IS NULL AND oi.company_id = 1
GROUP BY 1,2
ORDER BY 3 DESC
LIMIT 10
)
SELECT order_items_by_sku.*,
coalesce(sibs.ship_count, 0) AS ship_count
FROM order_items_by_sku
LEFT JOIN shipment_items_by_sku AS sibs ON order_items_by_sku.product_variant_id = sibs.product_variant_id
ORDER BY order_items_by_sku.order_count DESC
SQL
但这么写的话带来一个问题,用户传参的地方需要自己去防止sql注入,不过mini sql的出现解决了sql注入问题(使用的prepared statment),我觉得mini sql当然不是去和ORM比,其实是弥补了ORM在统计和ETL领域的不足。看了一下mini sql在discourse的应用,也是用来做统计相关:
darkbaby123 #9 · 2018年06月23日
@hooopo 用 find_by_sql 可以对付注入,就是不大好看,必须用某个具体的 model 来调用该方法。加上结果是该 model 的实例加上从 SELECT 部分动态生成的属性就非常奇怪了。这个问题可以通过建立一个专门的 stat model 来解决,避免尴尬。另外 sanitize_sql_xxx 的那批方法应该也可以解决注入问题(没试过),不过用起来就非常麻烦了。
虽然统计不是 ORM 需要解决的问题,一个是 OLAP 一个是 OLTP 。但有时候两者都需要共用一些查询条件和表关联,而且有时候相似的统计 SQL 也会有些代码碎片是重复的,这时候就又回到如何重用的问题上来了。query builder 本身用在构造 SQL 上是合适的,问题是 AR 的 query builder 抽象程度还不够,经常写成了大批的 SQL 片段,也就是你说的 SQL + Rails 的方式。
hooopo #10 · 2018年06月24日 对
darkbaby123
回复
Yes.
对于简单场景,用ORM来做reporting也是可行的。对复杂场景,不能说AR抽象程度不够,本质上每个gem有自己的解决的领域。
# Sam: complicated reports are really hard in AR
代码重复问题,其实这就是一个tradeoff,ORM里的方法对于reporting来说,理论上确实可以共用,但实现上是很难共用,硬要共用的话,必然是写成N+1的方式,之前做过一个项目,reporting部分就是这种思路写的,数据量还不大的时候一个页面就可以达到几十秒。
如果reporting部分想共用的话,其实需要写UDF和view,试过几次效果还不错。
刚看了这篇文章,mini sql的用途写的非常清晰了:meta.discourse.org/t/new-offic…
# Sam: complicated reports are really hard in AR
builder = DB.build <<-SQL
SELECT ftl.url,
COALESCE(ft.title, ftl.title) AS title,
ftl.link_topic_id,
ftl.reflection,
ftl.internal,
ftl.domain,
MIN(ftl.user_id) AS user_id,
SUM(clicks) AS clicks
FROM topic_links AS ftl
LEFT JOIN topics AS ft ON ftl.link_topic_id = ft.id
LEFT JOIN categories AS c ON c.id = ft.category_id
/*where*/
GROUP BY ftl.url, ft.title, ftl.title, ftl.link_topic_id, ftl.reflection, ftl.internal, ftl.domain
ORDER BY clicks DESC, count(*) DESC
LIMIT 50
SQL
builder.where('ftl.topic_id = :topic_id', topic_id: topic_id)
darkbaby123 #11 · 2018年06月24日 对
hooopo
回复
所以呢我说的其实是 query builder 的抽象能力。因为大部分 ORM 都有个 query builder 因此它也算是 ORM 的一部分。如果完全没有抽象的话对查询的共用部分就只能拼接 SQL 字符串了。mini_sql 的 builder.where 看起来已经够用了。我再举个 Ecto (Elixir 语言的 data wrapper)的例子,实现 Sam 文中提到的完整 SQL :
from(
ftl in TopicLink,
left_join: ft in assoc(ftl, :link_topic),
left_join: c in assoc(ft, :category),
group_by: [ftl.url, ft.title, ftl.title, ftl.link_topic_id, ftl.reflection, ftl.internal, ftl.domain],
order_by: [desc: sum(ftl.clicks), desc: count(ftl.id)],
limit: 50,
select: %{
url: ftl.url,
title: coalesce(ft.title, ftl.title),
link_topic_id: ftl.link_topic_id,
reflection: ftl.reflection,
internal: ftl.internal,
domain: ftl.domain,
user_id: min(ftl.user_id),
clicks: sum(clicks)
}
)
|> where([ftl, ...], ftl.topic_id == ^topic_id)
|> where([_, ft, ...], is_nil(ft.deleted_at))
|> where([ftl, ...], not (ilike(ftl.url, "%.png") or ilike(ftl.url, "%.jpg") or ilike(ftl.url, "%.gif")))
|> where([_, ft, ...], coalesce(ft.archetype, "regular") != ^Archetype.private_message)
|> secure_category(guardian.secure_category_ids)
|> Repo.all()
拿它举例并不是抬杠或者吹 Ecto 如何,我相信以 Ruby 的灵活性可以做到同样的事情(只是感觉没多少人觉得它很重要)。只是说明语言层面提供的抽象可以用来更好的组织和重用代码。这点我个人还是觉得很重要的。毕竟 SQL 作为声明式的语言不善于表达过程式的代码,而生成 query 的过程恰好是过程式的,应用层的语言擅长此道。
另外拿我最近做的一个统计做例子:
query_1 =
FunnelItem
|> filter_by_subject(subject_id)
|> filter_by_type(:fam)
|> score_btw(3, 4)
query_2 =
FunnelItem
|> filter_by_subject(subject_id)
|> filter_by_type(:ove)
|> score_btw(6, 7)
query_3 =
FunnelItem
|> filter_by_subject(subject_id)
|> filter_by_type(:puc)
|> score_eq(4)
[query_1, query_2, query_3]
|> join_queries()
|> stat_by_filters(filter_key, filter_value_ids)
上面的例子把部分 where 条件抽成了共用的函数,再自定义了一些函数来组合查询,比如 join_queries :
def join_queries(queries) do
Enum.reduce(queries, fn query, acc ->
from(
[f1, ...] in acc,
join: ft in ^query,
on: ft.sample_id == f1.sample_id and ft.subject_id == f1.subject_id
)
end)
end