THAM GIA TRÁI NGOÀI trong Rails 4


80

Tôi có 3 mô hình:

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

Tôi muốn truy vấn danh sách các khóa học trong bảng Khóa học, không tồn tại trong bảng StudentEnrollments được liên kết với một sinh viên nhất định.

Tôi thấy rằng có lẽ Kết nối trái là cách để đi, nhưng có vẻ như tham gia () trong đường ray chỉ chấp nhận một bảng làm đối số. Truy vấn SQL mà tôi nghĩ sẽ làm những gì tôi muốn là:

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

Làm cách nào để thực hiện truy vấn này theo cách Rails 4?

Bất kỳ đầu vào nào được đánh giá cao.


Nếu bản ghi không tồn tại trong StudentEnrollments, chắc chắn se.student_id = <SOME_STUDENT_ID_VALUE>sẽ là không thể?
PJSCopeland

Câu trả lời:


84

Bạn cũng có thể chuyển một chuỗi là tham gia-sql. ví dụjoins("LEFT JOIN StudentEnrollment se ON c.id = se.course_id")

Mặc dù tôi sẽ sử dụng cách đặt tên bảng tiêu chuẩn rails để rõ ràng:

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

2
Giải pháp của tôi kết thúc là: query = "LEFT THAM GIA student_enrollments ON Course.id = student_enrollments.course_id VÀ" + "student_enrollments.student_id = # {self.id}" Course = Course.active.joins (query) .where (student_enrollments: {id: nil}) Rails không như tôi muốn, mặc dù nó đã hoàn thành công việc. Tôi đã thử sử dụng .includes (), có LEFT JOIN, nhưng nó không cho phép tôi chỉ định một điều kiện bổ sung khi tham gia. Cảm ơn Taryn!
Khanetor

1
Tuyệt quá. Này, đôi khi chúng ta làm những gì chúng ta làm để nó hoạt động. Thời gian trở lại với nó và làm cho nó tốt hơn trong tương lai ... :)
Taryn Đông

1
@TarynEast "Làm cho nó hoạt động, làm cho nó nhanh chóng, làm cho nó đẹp." :)
Joshua Pinter

31

Nếu bất kỳ ai đến đây tìm kiếm một cách chung để thực hiện phép nối bên ngoài bên trái trong Rails 5, bạn có thể sử dụng #left_outer_joinshàm này.

Ví dụ về đa tham gia:

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

1
Cảm ơn, tôi muốn đề cập đến cho Hội thánh giá bên ngoài trái tham gia, sử dụngleft_outer_joins(a: [:b, :c])
Fangxing

Ngoài ra, bạn có sẵn left_joinsvà hành xử theo cùng một cách. Ví dụ. left_joins(:order_reports)
alexventuraio

23

Thực ra có một "Rails Way" để làm điều này.

Bạn có thể sử dụng Arel , đây là những gì Rails sử dụng để tạo các truy vấn cho ActiveRecrods

Tôi sẽ gói nó trong một phương thức để bạn có thể gọi nó độc đáo và chuyển vào bất kỳ đối số nào bạn muốn, đại loại như:

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

Ngoài ra còn có một cách nhanh chóng (và hơi bẩn) mà nhiều người sử dụng

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

Mong_load hoạt động tuyệt vời, nó chỉ có "tác dụng phụ" của việc ghi các mô hình trong bộ nhớ mà bạn có thể không cần (như trong trường hợp của bạn).
Vui lòng xem Rails ActiveRecord :: QueryMethods .eager_load
Nó thực hiện chính xác những gì bạn đang yêu cầu một cách gọn gàng.


54
Tôi chỉ phải nói rằng tôi không thể tin rằng ActiveRecord vẫn không có hỗ trợ tích hợp cho điều này sau rất nhiều năm. Nó hoàn toàn không thể dò được.
mrbrdo

1
Sooooo khi nào Sequel có thể trở thành ORM mặc định trong Rails?
animatedgif

5
Đường ray không được phình ra. Imo họ đã làm đúng khi họ quyết định lấy đá quý ra mà đã được gói theo mặc định ngay từ đầu. Triết lý là "làm ít nhưng tốt" và "chọn những gì bạn muốn"
Adit Saxena

9
Rails 5 có hỗ trợ THAM GIA LEFT OUTER: blog.bigbinary.com/2016/03/24/…
Murad Yusufov

Để tránh "tác dụng phụ" của háo hức, hãy xem câu trả lời của tôi
textral

13

Kết hợp includeswheredẫn đến ActiveRecord thực hiện LEFT OUTER JOIN đằng sau hậu trường (không có nơi mà điều này sẽ tạo ra tập hợp bình thường của hai truy vấn).

Vì vậy, bạn có thể làm điều gì đó như:

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

Tài liệu tại đây: http://guides.rubyonrails.org/active_record_querying.html#specify-conditions-on-eager-loaded-associations


12

Thêm vào câu trả lời ở trên, để sử dụng includes, nếu bạn muốn tham chiếu OUTER JOIN mà không tham chiếu đến bảng trong nơi (như id là nil) hoặc tham chiếu nằm trong một chuỗi mà bạn có thể sử dụng references. Nó sẽ trông như thế này:

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

hoặc là

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

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


Điều này sẽ hoạt động cho một mối quan hệ lồng nhau sâu sắc hay mối quan hệ cần phải treo trực tiếp khỏi mô hình đang được truy vấn? Tôi dường như không thể tìm thấy bất kỳ ví dụ nào về cái trước đây.
dps

Yêu nó! Chỉ cần phải thay thế joinsbằng includesvà nó đã làm các thủ thuật.
RaphaMex

8

Bạn sẽ thực hiện truy vấn như sau:

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

Tôi biết rằng đây là một câu hỏi cũ và một chủ đề cũ nhưng trong Rails 5, bạn chỉ có thể làm

Course.left_outer_joins(:student_enrollments)

Câu hỏi đang nhắm mục tiêu cụ thể vào Rails 4.2.
Volte

6

Bạn có thể sử dụng gem left_joins , left_joinsphương thức backports từ Rails 5 cho Rails 4 và 3.

Course.left_joins(:student_enrollments)
      .where('student_enrollments.id' => nil)

4

Tôi đã vật lộn với loại vấn đề này trong một thời gian khá dài và quyết định làm gì đó để giải quyết nó một lần và mãi mãi. Tôi đã xuất bản Gist giải quyết vấn đề này: https://gist.github.com/nerde/b867cd87d580e97549f2

Tôi đã tạo một bản hack AR nhỏ sử dụng Arel Table để tạo động các phép nối bên trái cho bạn mà không cần phải viết SQL thô trong mã của bạn:

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

Hy vọng nó giúp.


4

Xem bên dưới bài viết ban đầu của tôi cho câu hỏi này.

Kể từ đó, tôi đã triển khai .left_joins()ActiveRecord v4.0.x của riêng mình (xin lỗi, ứng dụng của tôi bị đóng băng ở phiên bản này nên tôi không cần phải chuyển nó sang các phiên bản khác):

Trong hồ sơ app/models/concerns/active_record_extensions.rb, hãy ghi những điều sau:

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)

Bây giờ tôi có thể sử dụng .left_joins()ở mọi nơi tôi thường sử dụng .joins().

----------------- BÀI ĐĂNG GỐC DƯỚI ĐÂY -----------------

Nếu bạn muốn OUTER JOIN mà không có tất cả các đối tượng ActiveRecord được tải thêm một cách háo hức, hãy sử dụng .pluck(:id)after .eager_load()để hủy bỏ tải mong muốn trong khi vẫn bảo toàn OUTER JOIN. Sử dụng .pluck(:id)ngăn chặn tải nhanh vì bí danh tên cột ( items.location AS t1_r9ví dụ) biến mất khỏi truy vấn được tạo khi được sử dụng (các trường được đặt tên độc lập này được sử dụng để khởi tạo tất cả các đối tượng ActiveRecord được tải nhanh).

Một nhược điểm của phương pháp này là sau đó bạn cần chạy truy vấn thứ hai để lấy các đối tượng ActiveRecord mong muốn được xác định trong truy vấn đầu tiên:

# 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)

Hay đấy.
dps

+1 nhưng bạn có thể cải thiện nhiều hơn một chút và sử dụng select(:id)thay vì pluck(:id)và ngăn hiện thực hóa truy vấn bên trong và để tất cả vào cơ sở dữ liệu.
Andre Figueedlyo

3

Đó là một truy vấn tham gia trong Mô hình Hoạt động trong Rails.

Vui lòng nhấp vào đây để biết thêm thông tin về Định dạng Truy vấn Mô hình Hoạt động .

@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
Tốt hơn là thêm một số giải thích cho câu trả lời của bạn đã đăng.
Bhushan Kawadkar

3

Sử dụng Squeel :

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

2
Squeel là một thư viện không được hỗ trợ, không được khuyến nghị
iNulty
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.