rails 物件封裝


#1

最近遇到一個問題想請教各位高手:
A、B、C 三個class各自有一個id 欄位和基礎的controller,三者的關係是

  class A < ActiveRecord::Base
    has_many   :Bs
  end
  
  class B < ActiveRecord::Base
    has_many  :Cs
    belongs_to :A
  end
  
  class C < ActiveRecord::Base
    belongs_to :B
  end
  

目標是從C的 show action開始,最後取得其關連的A的所有B class物件的 id(A.Bs的所有id)

請問哪種方法比較好:
1.C 的show action 回傳A的id,再從A的show action回傳其關連的所有B的id
2.在C 的show action 裡回傳C.B.A.Bs的所有id


#2

簡單回,B join A 然後 GROUP BY B 或 A(看誰為基本集合)然後 SELECT 需要的欄位即可,基本上一個 SQL 就解決了,不管如何應該都以 B 為出發點,因為給一個 C 的 id 就可以做完整件事了才是


#3

原始思路是這樣:

先用 C 找到所屬的 B,再用 B 找到所屬的 A,再用 A 找到下面全部的 B。

可是這樣會找來找去很麻煩,一個簡單的省略過程是「B 身上有記錄 A 的 id 了,所以我們直接從 b.a_id 拿到 A 的 id 然後問問看有哪些 B 的 #a_id 也是一樣的就好了」,所以變成這樣:

bs = B.where(a_id: c.b.a_id)

不過上面的 query 會查兩次,一次在 c.b.a_id 這段要先找出 c.b 這筆資料,另一次才是 B.where(...)

根據 @JokerCatz 的做法也可以利用 AR 在找出確實的資料之前不會真的去 query 的特性,改寫成:

b = B.where(id: c.b_id) # 這裏還不會 query
a = A.joins(:bs).where(b: b) # 這也還不會 query
# 上面兩行也可以寫成:
# a = A.joins(bs: :cs).where(b: [c: c])
bs = B.joins(:a).where(a: a) # 其實這裏也不會 query,可是你等一下拿來用的時候就會下 query 了

雖然我是堅持能不寫 SQL 就不寫 SQL 派的(可攜性及其他種種理由),但是這樣好像超級醜……所以

如果你不太會寫 SQL 或是跟我一樣比較偏好 Ruby 風格的寫法的話,介紹你一個好用的工具(推坑):

基本上他就是把 ActiveRecord 擴展成可以用 Ruby DSL 來寫 SQL,上面的解法大概會寫成這樣:

bs = B.joins(:a).where.has{ |b| b.a.in(A.joining{bs.cs}.where.has{ |a| a.b.c == c }) }

這樣子可能會比較好讀懂(吧)


#4

感謝兩位大大的解答


#5

c 裡面快取a_id
Bs = B.where(a_id: c.a_id)
b = c.b
a = c.a


#6

如果要在 DB 裡面多存 c -> a_id 的話,那你變成在修改 c -> b_id 的時候同時要改 a_id 或是修改 c.b -> a_id 的時候要修改 b 下面所有 c 的 a_id,同時維護重複的資料是很不切實際的


#7

下戰帖了嗎?


#8

這個沒有什麼好不好戰帖的好嗎XD

同時維護重複的資料是很不切實際的 這是事實
寫 Rails 的時候就會告訴你要 DRY 跟 KISS,在資料庫維護上也是,你可以用現有資料推論出來的結果就不要花時間、花力氣去額外同步。現在有一種很有趣的資料運算方式叫做 MapReduce 啊,就是讓你只保留最基本需要的資料,剩下的在需要的時候才取出運算結果。


#9

去堅持一堆原則、pattern 或正規化是好事
是說有多好維護 多好擴充 可讀性有多高
但有多少人為了加速去反做正規化
又有多少的專案是過度設計、矯枉過正

bs = B.joins(:a).where.has{ |b| b.a.in(A.joining{bs.cs}.where.has{ |a| a.b.c == c }) }

這樣三個月後你看的懂嗎
這解法這樣要串幾個table、 join幾次、幾個select 又要加幾個 Gem
來解決當初用一個SQL 就解掉的問題
不過小弟是不切實際的想法


#10

哇可是這個跟你上面說的沒有關係啊

你開心可以用 c.b.a.cs 沒有人會阻止你啊,你也可以用 Arel 組合出 query chain,當然也可以寫 SQL,但我會遵守的最小原則就是能不寫 raw sql 就不要寫,當然你可以有你自己的原則。我只是針對你提出的在 c 裡面多存 a_id 這個想法做討論。

這裏還有一個給 MySQL 用的技巧,建立 View table,他可以幫你把妳下好的 query cache 在 db 裡面,類似 mapreduce 的感念,這樣你就可以不用特別維護 c 到 a 之間的關係,直接拿到 a_id 會比手動存來的更好,不知道這樣有沒有滿足你的要求


#11

其實你也可以在查詢一開始的 c 的時候就拿到 a_id ,只是我是以 c 已查詢為前提回答,不然其實可以用下面的方式查詢 c:

c = C.where(conds).includes(b: :a).first
bs = c.b.a.bs # 這裡只下了一次 query,因為 b a 已經在撈 c 的時候一起撈出來了

這樣也可以達到你說 cache a (在撈 c 的時候發生)
但是很多時候我們可能不會用到 a b 先撈出來浪費資源而已


#12

反正規化是滿常見的作法了,配合ActiveRecord的callback使用維護性也很ok的。


#13

good luck


#14

但是當數據到達一個數量級的時候,你 update 一次要花的時間也會達到另一個數量級……
這裡要考慮的不單單只是程式的維護性,還有在 DB 的效率跟種種原因


#15

反正規化正是考量到數據量級才要做的事情。
把原本需要 join 的東西優化成 select 一次
大部分的情況 select 的次數 >>> update 的次數
如果 update 的次數很頻繁
可以用其他 db 來做支援,如 redis


#16

但是如果你的 db 是 scalable 的,那麼要考慮讀取可以優先於寫入,你可以把讀取的運算分配給子節點,但是寫入的運算必須同步到所有節點上


#17

RDS Scalabe ? 你想說的是讀寫分離? 有實際做過嘛
寧願 Scaling 也不願意做反正規化,這樣在開發上真的有比較快嘛@@


#18

開發上已經很快啦 我覺得當資料多到一定程度之後要考慮的反而是執行效率

話說我們好像已經離題太多了⋯(ry


#19

嘛…我另外開一篇來討論這話題好了,不過我是反正規化的那派的,不管是 big table / mapreduce 或是後期的 nosql 系列的 redis / mongodb 其實都非常推崇反正規化,或是另外一個說法:『用空間換取時間的 cache,而該 cache 必須能 rebuild,或是擁有 timeout』而 cache 通常都是 relation 而非 attr(不過這邊要看實作而定),所以 update 的頻率基本上非常低就是了,然而就算有 update 也比 select 的量省很多很多很多才是

master / slave 的 sync 要看 binlog 原始模式,那個模式很蠢,這邊效能可以略過不記(大概就是把 I/U/D 的 SQL 直接打給 slave ,讓 slave 重新執行一次就完成 sync 了,所有 RDBMS 都這樣搞,而非用差異的 data,因為 update 10k 的 SQL 只要一行,而非需要 sync 10k 的data)所以效能評估時看單一台的即可,此處同 redis的 aof mode

之前處理過很多幾百萬的量,甚至千萬級,schema 沒設計好光 select 都有問題,RDBMS 的 view 只是 select 的快取,它沒真實的 data set,所以很高的程度是需要重跑的哩,然而你不如用 update 一次 rebuild 需要的快取(非全部,只有必要變動的),而非每次 select 想命中 cache buffer(這邊同view),因為這種 cache buffer / view 不太有效用, RDBMS 管理者都會調到很低,先天不足後天不良,類似資料一變動(I/U/D)就必須全丟棄(有join更慘,其一就要丟)且命中機率超低,有和沒有一樣哩

至於堅持不用 SQL…hmm…這不一定是好事,單純如果你做 open source 當然好改(對別人而言),但其實企業開發團隊內限定 mysql / postgresql 時,才能用該 db 的專用語法和調整來做效能的增進,且目前看到一票 open source project 其實都選邊站的(尤其有了 docker 之後…大都選 postgresql …),堅持只能用到 ANSI SQL 系列,對我而言可惜了些就是,舉例而言 ANSI SQL 內沒 JSON 這 data type 可用,但你真的要存取 JSON 時?另外一方面,你不用 NOW() 取到的是 now 嗎?(這 func 也不是 ANSI 的哩,so