Records ohne Assoziationen finden

Wenn in relationalen Datenbanken nach Einträgen gesucht wird, die keine assoziierten Objekte haben, dann ist ein LEFT JOIN notwendig. Denn ein INNER JOIN würde genau die Gesuchten ausschließen.

Das Rails Beispiel

In einem Shop-System können Nutzer Bestellungen machen.
Die Models:

rails g model User name:string
rails g model Order user:references

mit den Beziehungen:

class Order < ApplicationRecord
  belongs_to :user
end

class User < ApplicationRecord
  has_many :orders
end

Die Anforderung ist, alle User zu finden, die keine Order haben.

ARel Left Join

Bis Ruby on Rails 5, konnte nur mit der Flexibilität von ARel eine adäquate Lösung erreicht werden.
Aber zum Einen ist ARel nicht jedermanns Sache und zum Anderen war es auch immer eine Notlösung für ein eher grundsätzliches Problem:

class User < ApplicationRecord
  has_many :orders

 def self.without_order
    orders            = reflect_on_association(:orders)
    order_arel        = orders.klass.arel_table
    user_primary_key  = arel_table[primary_key]
    order_foreign_key = order_arel[orders.foreign_key]
    orders_left_join  = arel_table.join(order_arel, Arel::Nodes::OuterJoin)
                                  .on(user_primary_key.eq order_foreign_key)
                                  .join_sources
    joins(orders_left_join)
      .where(orders.table_name => { orders.klass.primary_key => nil} )
  end
end

Bei dieser Lösung wird aus den ARel Objekten der beiden Models ein Arel::Nodes::OuterJoin erzeugt. Für die Join Bedingung werden dabei die Assoziationsattribute der gesetzten orders Reflection benutzt. Schließlich wird aus dem ARel Node Objekt das eigentliche Join Fragment generiert und an joins übergeben.
Die Selektionsbedingung sorgt dafür, dass nur die Einträge gefunden werden, die mit keinen Orders joinen konnten.
Der Aufruf:

User.without_order

generiert das erwartete SQL:

SELECT "users".*
  FROM "users"
  LEFT OUTER JOIN "orders" ON "users"."id" = "orders"."user_id"
  WHERE "orders"."id" IS NULL

ActiveRecord Left Join

Ab ActiveRecord 5, gibt es aber auch endlich das #left_joins:

User.left_joins(:orders).where(orders: { id: nil } )

mit dem das gleiche SQL generiert wird.

Getested

Ein Test:

require 'rails_helper'

RSpec.describe User, type: :model do
  subject { User.create! }
  before { User.create! orders: Array(Order.new) }

  describe '.without_orders' do
    it 'returns all users without any order' do
      expect(User.without_order).to match_array(subject)
    end
  end

  describe '.left_joins' do
    it 'returns all users without any order' do
      expect(User.left_joins(:orders).where(orders: { id: nil } )).to match_array(subject)
    end
  end
end

beweist, dass beide Varianten tatsächlich das selbe Resultat liefern:

Finished in 0.95742 seconds (files took 2.1 seconds to load)
2 examples, 0 failures