ActiveRecord merge method

Published on:

圖片來源

之前查資料找到這篇 Rails where() clause on model association?

原來在查詢條件包含關聯表欄位的時候還有 merge 的方法可以用

之前都在 joins 裡面硬刻 SQL query...

merge 可以合併查詢子句,要做依賴關聯表條件的查詢時非常有用

ActiveRecord::SpawnMethods#merge API Dock 的例子來說:「想要找出至少有一筆非垃圾留言的文章」

首先建立一個新 rails 專案

$ rails new test_active_record_merge
$ cd test_active_record_merge

新增 Migragion file

$ bundle exec rails new start

編輯 Migragion file

class Start < ActiveRecord::Migration[5.0]
  def change
    create_table :posts do |t|
      t.boolean :published, default: false
    end

    create_table :comments do |t|
      t.boolean :spam, default: false
      t.references :post, index: true, foreign_key: true
    end
  end
end

執行 migrate

$ bundle exec rake db:migrate

新增 Model file

# app/models/post.rb

class Post < ApplicationRecord
  has_many :comments
end

# app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :post
end

在 Terminal 中執行 rails c 建立測試資料

Comment.create(spam: true, post: Post.create(published: true))
Comment.create(spam: true, post: Post.create(published: false))
Comment.create(spam: false, post: Post.create(published: true)) #符合條件

Comment.create(spam: false, post: Post.create(published: false))
Post.create(published: true)
Post.create(published: false)

查詢「至少有一筆非垃圾留言的文章」

Post.where(published: true).joins(:comments).merge(Comment.where(spam: false))

# 相當於

# Post.where(published: true).joins('INNER JOIN comments ON comments.post_id = posts.id').where('comments.spam = ?', false)

結果找到一篇文章

  Post Load (0.7ms)  SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "posts"."published" = ? AND "comments"."spam" = ?  [["published", true], ["spam", false]]
=> #<ActiveRecord::Relation [#<Post id: 3, published: true>]>

但以上測試資料文章都只有一個留言,如果為符合條件的文章再加上一個非垃圾留言後再查詢

Comment.create(spam: false, post: posts.first)
posts = Post.where(published: true).joins(:comments).merge( Comment.where(spam: false))

會發現找到兩筆結果,但卻是重複的文章

這是因為 SQL INNER JOIN 的關係,如果我們再為該文章加上一筆新的非垃圾留言,就會變成三筆重複的文章

  Post Load (0.1ms)  SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "posts"."published" = ? AND "comments"."spam" = ?  [["published", true], ["spam", false]]
=> #<ActiveRecord::Relation [#<Post id: 3, published: true>, #<Post id: 3, published: true>]>

這時候可以加上 distinct 來排除重複的資料

Post.where(published: true).joins(:comments).merge(Comment.where(spam: false)).distinct

結果

  Post Load (0.2ms)  SELECT DISTINCT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE "posts"."published" = ? AND "comments"."spam" = ?  [["published", true], ["spam", false]]
=> #<ActiveRecord::Relation [#<Post id: 3, published: true>]>

參考

Rails where() clause on model association?
ActiveRecord::SpawnMethods#merge API Dock
ActiveRecord::SpawnMethods#joins API Dock

Comments

comments powered by Disqus