Rails4のLEFTOUTER JOIN


80

私は3つのモデルを持っています:

class Student < ActiveRecord::Base
  has_many :student_enrollments, dependent: :destroy
  has_many :courses, through: :student_enrollments
end

class Course < ActiveRecord::Base   
    has_many :student_enrollments, dependent: :destroy
    has_many :students, through: :student_enrollments
end

class StudentEnrollment < ActiveRecord::Base
    belongs_to :student
    belongs_to :course
end

特定の学生に関連付けられているStudentEnrollmentsテーブルに存在しないCoursesテーブルのコースのリストをクエリしたいと思います。

おそらく左結合が進むべき道であることがわかりましたが、railsのjoins()はテーブルのみを引数として受け入れるようです。私が望むことをするだろうと思うSQLクエリは次のとおりです。

SELECT *
FROM Courses c LEFT JOIN StudentEnrollment se ON c.id = se.course_id
WHERE se.id IS NULL AND se.student_id = <SOME_STUDENT_ID_VALUE> and c.active = true

このクエリをRails4の方法で実行するにはどうすればよいですか?

どんな入力でも大歓迎です。


レコードがStudentEnrollmentsに存在しない場合、確かse.student_id = <SOME_STUDENT_ID_VALUE>に不可能でしょうか?
PJSCopeland 2018

回答:


84

join-sqlである文字列を渡すこともできます。例えばjoins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

わかりやすくするために、rails-standardテーブルの命名を使用しますが:

joins("LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id")

2
私の解決策は次のようになりました:query = "LEFT JOIN student_enrollments ONcourses.id = student_enrollments.course_id AND" + "student_enrollments.student_id =#{self.id}" courses = Course.active.joins(query).where(student_enrollments: {id:nil})仕事は終わりますが、私が望むほどRailsではありません。LEFT JOINを実行する.includes()を使用してみましたが、結合時に追加の条件を指定できません。タリンありがとう!
Khanetor 2014年

1
すごい。ねえ、時々私たちはそれを機能させるために私たちがすることをします。それに戻って、将来それをより良くするための時間... :)
Taryn East

1
@TarynEast「それを機能させ、速くし、美しくしなさい。」:)
JoshuaPinter19年

31

Rails 5で左外部結合を行う一般的な方法を探している人がここに来た場合は、この#left_outer_joins関数を使用できます。

マルチ結合の例:

ルビー:

Source.
 select('sources.id', 'count(metrics.id)').
 left_outer_joins(:metrics).
 joins(:port).
 where('ports.auto_delete = ?', true).
 group('sources.id').
 having('count(metrics.id) = 0').
 all

SQL:

SELECT sources.id, count(metrics.id)
  FROM "sources"
  INNER JOIN "ports" ON "ports"."id" = "sources"."port_id"
  LEFT OUTER JOIN "metrics" ON "metrics"."source_id" = "sources"."id"
  WHERE (ports.auto_delete = 't')
  GROUP BY sources.id
  HAVING (count(metrics.id) = 0)
  ORDER BY "sources"."id" ASC

1
おかげで、私はクロスアソシエーションの左外部結合について言及したいと思います、使用left_outer_joins(a: [:b, :c])
2018年

また、あなたはleft_joins略して利用可能であり、同じように振る舞います。例えば。left_joins(:order_reports)
alexventuraio

23

これを行うには、実際には「RailsWay」があります。

あなたはアレルを使うことができますRailsがActiveRecrodsのクエリを作成

私はそれをメソッドでラップして、それをうまく呼び出して、次のような任意の引数を渡すことができるようにします。

class Course < ActiveRecord::Base
  ....
  def left_join_student_enrollments(some_user)
    courses = Course.arel_table
    student_entrollments = StudentEnrollment.arel_table

    enrollments = courses.join(student_enrollments, Arel::Nodes::OuterJoin).
                  on(courses[:id].eq(student_enrollments[:course_id])).
                  join_sources

    joins(enrollments).where(
      student_enrollments: {student_id: some_user.id, id: nil},
      active: true
    )
  end
  ....
end

多くの人が使用する迅速な(そして少し汚れた)方法もあります

Course.eager_load(:students).where(
    student_enrollments: {student_id: some_user.id, id: nil}, 
    active: true
)

eager_loadはうまく機能し、必要のないモデルをメモリにロードするという「副作用」があります(あなたの場合のように)。RailsActiveRecord
:: QueryMethodsを参照してください。eager_load
それはあなたが求めていることをきちんと実行します。


54
何年も経っても、ActiveRecordがまだこれをサポートしていないとは信じられません。それは完全に計り知れません。
mrbrdo 2015年

1
SequelがRailsのデフォルトのORMになるのはいつですか?
animatedgif

5
レールが肥大化してはいけません。そもそもデフォルトでバンドルされている宝石を抽出することにしたとき、彼らはそれを正しく理解しました。哲学は「少ないが良いことをする」と「あなたが望むものを選ぶ」です
Adit Saxena 2016

9
レール5は、LEFT OUTERをサポートしていますが、JOIN:blog.bigbinary.com/2016/03/24/...
ミュラドYusufov

eager_loadの「副作用」を回避するには、私の回答を参照してください
textral 2017年

13

組み合わせincludeswhere、ActiveRecordは舞台裏でLEFT OUTER JOINを実行します(これにより、通常の2つのクエリのセットが生成される場所はありません)。

したがって、次のようなことができます。

Course.includes(:student_enrollments).where(student_enrollments: { course_id: nil })

ここのドキュメント:http//guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations


12

上記の回答に加えて、を使用するincludesには、whereのテーブルを参照せずにOUTER JOINが必要な場合(idがnilの場合など)、または参照が文字列内にある場合に使用できますreferences。これは次のようになります。

Course.includes(:student_enrollments).references(:student_enrollments)

または

Course.includes(:student_enrollments).references(:student_enrollments).where('student_enrollments.id = ?', nil)

http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-references


これは深くネストされたリレーションに対して機能しますか、それともリレーションはクエリ対象のモデルから直接ハングアップする必要がありますか?前者の例は見当たらないようです。
DPS

大好きです!ジャストは交換していたjoinsことでincludesあり、それはトリックをしました。
RaphaMex

8

クエリは次のように実行します。

Course.joins('LEFT JOIN student_enrollment on courses.id = student_enrollment.course_id')
      .where(active: true, student_enrollments: { student_id: SOME_VALUE, id: nil })

7

これは古い質問であり、古いスレッドであることを私は知っていますが、Rails5では簡単に行うことができます

Course.left_outer_joins(:student_enrollments)

問題は、特にRails4.2を対象としています。
VoLTEの


4

私はかなり前からこの種の問題に苦しんでいて、それを解決するために何かをすることに決めました。この問題に対処するGistを公開しました:https//gist.github.com/nerde/b867cd87d580e97549f2

コードに生のSQLを記述せずに、ArelTableを使用して左結合を動的に構築する小さなARハックを作成しました。

class ActiveRecord::Base
  # Does a left join through an association. Usage:
  #
  #     Book.left_join(:category)
  #     # SELECT "books".* FROM "books"
  #     # LEFT OUTER JOIN "categories"
  #     # ON "books"."category_id" = "categories"."id"
  #
  # It also works through association's associations, like `joins` does:
  #
  #     Book.left_join(category: :master_category)
  def self.left_join(*columns)
    _do_left_join columns.compact.flatten
  end

  private

  def self._do_left_join(column, this = self) # :nodoc:
    collection = self
    if column.is_a? Array
      column.each do |col|
        collection = collection._do_left_join(col, this)
      end
    elsif column.is_a? Hash
      column.each do |key, value|
        assoc = this.reflect_on_association(key)
        raise "#{this} has no association: #{key}." unless assoc
        collection = collection._left_join(assoc)
        collection = collection._do_left_join value, assoc.klass
      end
    else
      assoc = this.reflect_on_association(column)
      raise "#{this} has no association: #{column}." unless assoc
      collection = collection._left_join(assoc)
    end
    collection
  end

  def self._left_join(assoc) # :nodoc:
    source = assoc.active_record.arel_table
    pk = assoc.association_primary_key.to_sym
    joins source.join(assoc.klass.arel_table,
      Arel::Nodes::OuterJoin).on(source[assoc.foreign_key].eq(
        assoc.klass.arel_table[pk])).join_sources
  end
end

それが役に立てば幸い。


4

この質問に対する私の元の投稿を以下に示します。

それ以来、.left_joins()ActiveRecord v4.0.x用に独自の実装を行っています(申し訳ありませんが、私のアプリはこのバージョンでフリーズしているため、他のバージョンに移植する必要はありませんでした)。

ファイルapp/models/concerns/active_record_extensions.rbに、次のように入力します。

module ActiveRecordBaseExtensions
    extend ActiveSupport::Concern

    def left_joins(*args)
        self.class.left_joins(args)
    end

    module ClassMethods
        def left_joins(*args)
            all.left_joins(args)
        end
    end
end

module ActiveRecordRelationExtensions
    extend ActiveSupport::Concern

    # a #left_joins implementation for Rails 4.0 (WARNING: this uses Rails 4.0 internals
    # and so probably only works for Rails 4.0; it'll probably need to be modified if
    # upgrading to a new Rails version, and will be obsolete in Rails 5 since it has its
    # own #left_joins implementation)
    def left_joins(*args)
        eager_load(args).construct_relation_for_association_calculations
    end
end

ActiveRecord::Base.send(:include, ActiveRecordBaseExtensions)
ActiveRecord::Relation.send(:include, ActiveRecordRelationExtensions)

これで.left_joins()、通常使用する場所ならどこでも使用できます.joins()

-----------------以下の元の投稿-----------------

余分に熱心にロードされたActiveRecordオブジェクトをすべて含まないOUTERJOINが必要な場合は、.pluck(:id)after.eager_load()を使用して、OUTERJOINを保持しながら熱心なロードを中止します。.pluck(:id)列名がエイリアスであるため、積極的な読み込みを阻止します(items.location AS t1_r9たとえば)を使用すると、生成されたクエリから消えるます(これらの独立した名前のフィールドは、積極的に読み込まれるすべてのActiveRecordオブジェクトをインスタンス化するために使用されます)。

このアプローチの欠点は、最初のクエリで識別された目的のActiveRecordオブジェクトを取得するために、2番目のクエリを実行する必要があることです。

# first query
idents = Course
    .eager_load(:students)  # eager load for OUTER JOIN
    .where(
        student_enrollments: {student_id: some_user.id, id: nil}, 
        active: true
    )
    .distinct
    .pluck(:id)  # abort eager loading but preserve OUTER JOIN

# second query
Course.where(id: idents)

これは面白い。
DPS

+1ですが、もう少し改善して、内部クエリを具体化するselect(:id)代わりに使用pluck(:id)して、すべてをデータベースに任せることができます。
アンドレフィ

3

これは、Railsのアクティブモデルの結合クエリです。

Active Model Query Formatの詳細については、ここをクリックしてください

@course= Course.joins("LEFT OUTER JOIN StudentEnrollment 
     ON StudentEnrollment .id = Courses.user_id").
     where("StudentEnrollment .id IS NULL AND StudentEnrollment .student_id = 
    <SOME_STUDENT_ID_VALUE> and Courses.active = true").select

3
投稿された回答に説明を追加することをお勧めします。
Bhushan Kawadkar 2014年

3

Squeelを使用する:

Person.joins{articles.inner}
Person.joins{articles.outer}

2
Squeelはサポートされていないライブラリであり、推奨されません
iNulty 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.