LEFT OUTER JOIN in Rails 4

前端 未结 12 1696
一整个雨季
一整个雨季 2020-11-28 09:44

I have 3 models:

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


        
相关标签:
12条回答
  • 2020-11-28 10:12

    You can pass a string that is the join-sql too. eg joins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

    Though I'd use rails-standard table naming for clarity:

    joins("LEFT JOIN student_enrollments ON courses.id = student_enrollments.course_id")
    
    0 讨论(0)
  • 2020-11-28 10:15

    It'a join query in Active Model in Rails.

    Please click here for More info about 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
    
    0 讨论(0)
  • 2020-11-28 10:19

    There is actually a "Rails Way" to do this.

    You could use Arel, which is what Rails uses to construct queries for ActiveRecrods

    I would wrap it in method so that you can call it nicely and pass in whatever argument you would like, something like:

    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
    

    There is also the quick (and slightly dirty) way that many use

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

    eager_load works great, it just has the "side effect" of loding models in memory that you might not need (like in your case)
    Please see Rails ActiveRecord::QueryMethods .eager_load
    It does exactly what you are asking in a neat way.

    0 讨论(0)
  • 2020-11-28 10:20

    If anyone came here looking for a generic way to do a left outer join in Rails 5, you can use the #left_outer_joins function.

    Multi-join example:

    Ruby:

    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
    
    0 讨论(0)
  • 2020-11-28 10:24

    See below my original post to this question.

    Since then, I have implemented my own .left_joins() for ActiveRecord v4.0.x (sorry, my app is frozen at this version so I've had no need to port it to other versions):

    In file app/models/concerns/active_record_extensions.rb, put the following:

    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)
    

    Now I can use .left_joins() everywhere I'd normally use .joins().

    ----------------- ORIGINAL POST BELOW -----------------

    If you want OUTER JOINs without all the extra eagerly loaded ActiveRecord objects, use .pluck(:id) after .eager_load() to abort the eager load while preserving the OUTER JOIN. Using .pluck(:id) thwarts eager loading because the column name aliases (items.location AS t1_r9, for example) disappear from the generated query when used (these independently named fields are used to instantiate all the eagerly loaded ActiveRecord objects).

    A disadvantage of this approach is that you then need to run a second query to pull in the desired ActiveRecord objects identified in the first query:

    # 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)
    
    0 讨论(0)
  • 2020-11-28 10:25

    Adding to the answer above, to use includes, if you want an OUTER JOIN without referencing the table in the where (like id being nil) or the reference is in a string you can use references. That would look like this:

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

    or

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

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

    0 讨论(0)
提交回复
热议问题