Given:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
# Referential integrity is for wusses! YOLO!
t.belongs_to :post, null: true, foreign_key: false
t.timestamps
end
end
end
p1 = Post.create!(title: 'Foo')
3.times { p1.comments.create! }
p2 = Post.create!(title: 'Bar')
3.times { p2.comments.create! }
p2.destroy! # orphans the comments
If you do an INNER JOIN on posts you will only get rows with at least one match in the join table:
irb(main):014:0> Comment.joins(:post)
Comment Load (0.3ms) SELECT "comments".* FROM "comments" INNER JOIN "posts" ON "posts"."id" = "comments"."post_id" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Comment id: 1, post_id: 1, created_at: "2021-05-11 08:59:04", updated_at: "2021-05-11 08:59:04">, #<Comment id: 2, post_id: 1, created_at: "2021-05-11 08:59:04", updated_at: "2021-05-11 08:59:04">, #<Comment id: 3, post_id: 1, created_at: "2021-05-11 08:59:04", updated_at: "2021-05-11 08:59:04">]>
This gives you the "non-orphaned" posts.
The opposite is of course an OUTER JOIN:
irb(main):016:0> Comment.left_joins(:post).where(posts: { id: nil })
Comment Load (0.3ms) SELECT "comments".* FROM "comments" LEFT OUTER JOIN "posts" ON "posts"."id" = "comments"."post_id" WHERE "posts"."id" IS NULL LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Comment id: 4, post_id: 2, created_at: "2021-05-11 08:59:26", updated_at: "2021-05-11 08:59:26">, #<Comment id: 5, post_id: 2, created_at: "2021-05-11 08:59:26", updated_at: "2021-05-11 08:59:26">, #<Comment id: 6, post_id: 2, created_at: "2021-05-11 08:59:26", updated_at: "2021-05-11 08:59:26">]>
Rails 6.1 added the .missing
query method which is a shortcut for the above query:
Comment.where.missing(:post)