Rails includes nested models with condition


#1

Model 之間的關係是

Comment belongs_to Product
Product has_many Product_infos

但是以下的 where condition 卻一直噴Error @@

@comments = Comment.order(id: :desc).includes([product: :product_infos])
@comments = @comments.where('products.product_infos.name like ?', "%#{params[:search]}%")

Error

ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column ‘products.product_infos.name’ in ‘where clause’: SELECT comments.* FROM comments WHERE (products.product_infos.name like ‘123’) AND (products.product_infos.name like ‘%123123%’) ORDER BY comments.id DESC LIMIT 1

我 try 了以下是可以work的

Comment.order(id: :desc).includes([product: :product_infos]).where(products: {product_infos: {id: 333}})

但是因為需求需要用到 Like, 不知道為何換成字串格式就狂噴


#2

… 嘛 … 當你只有 Rails 時,你只剩下 Rails 了 …

首先需求分析,你為何要 LIKE 指令?你想達到怎樣的目的?

再來是否要做搜尋系統?而資料未來筆數預計多少呢?太多是否改 Sphinx / Elasticsearch?因為這種 DB 操作基本上都只能 full table scan,效能都非常糟糕,除非你有 slave DB 或單純弄類似 tag system 之類的

你的錯誤並非不了解 Rails,而是不了解 RDBMS 怎樣操作哩,知道 RDBMS 怎樣操作,應該就不會下出這樣的 Rails 語法才是(嘛,不過我和你相反 … 以前只有 RDBMS 能玩,所以反而不知道 Rails 該怎樣寫才好||)

這題的 schema 應該是

Comment => Product <= ProductInfo    (可以用箭頭方向看出一對多關係)

而其中 ProductInfo 和 Comment 都有 product_id,所以先找出右邊的,再找出左邊的即可,不過這種多對多關聯很容易爆炸就是了||||,而你可以逆向來找(單純我比較愛 LEFT JOIN,思維簡單些),簡單的範例(未測)

SELECT c.* , pit.name AS pit_name FROM comments AS c INNER JOIN product_infos AS pit ON pit.name LIKE "%#{PATTERN}%" AND pit.product_id = c.product_id

RDBMS 概念過了後才去組成 Rails 的語法 … 完整語法會類似

@comments = Comment.select('c.* , pit.name AS pit_name').from('comments AS c').joins("INNER JOIN product_infos AS pit ON pit.name LIKE ? AND pit.product_id = c.product_id" , "%#{params[:pattern]}%")

#因為包裝成 Comment 所以有 comment 的所有欄位,而需要 ProductInfo 就要自己加,類似
@comments.first.pit_name

不過以上都未測,自己玩看看唄?

不過另外一方面,上面的語法是照你的思維推的,我應該會寫成類似這樣的鬼才是

SELECT * FROM comments WHERE product_id IN (
  SELECT DISTINCT product_id FROM product_infos WHERE name LIKE "%#{PATTERN}%"
)

這種格式說不定會好點也清楚點,先找出所有的 product_id 後才去找 comments,缺點是數量太多 SQL 會爆炸唄( product_id 過多) … 換成 Rails 語法寬鬆點會類似

@product_ids = ProductInfo.where(`name LIKE ?` , "%#{params[:pattern]}%").select('DISTINCT product_id').to_a.map{|i|i.product_id}
raise 'no data' if @product_ids.empty?
@comments = Comment.where("product_id IN (#{@product_ids.join(',')})")

(故意不寫成單一 SQL … 分頁會好做些,但一樣的 product_id 過多 SQL 一樣會過長),anyway 多對多關聯都是硬傷,這種需求太多我應該都會改成一對多唄,類似 ProductInfo 乾脆弄成 serialize 丟在 Product 內,或是單純的一對一關聯存 text 自己寫 obj 也好就是,但最漂亮應該還是過 search engine,以上


#3

感謝JC, 我也有寫出他的SQL指令, 但就是覺得有攏長
這是接案的專案, 所以他原本的架構有點難改就是XD
而這個搜尋也是放在 admin 後台的, 所以暫時沒有考慮要改架構

後來找到一個方法解決了, 加上 .references(:product_infos)

完整的寫法是

Comment.order(id: :desc)
       .includes([product: :product_infos])
       .where('product_infos.name like ?', "%#{params[:search]}%")
       .references(:product_infos)