Sử dụng một phạm vi theo mặc định trên một mối quan hệ has_many của Rails


81

Giả sử tôi có các lớp học sau

class SolarSystem < ActiveRecord::Base
  has_many :planets
end

class Planet < ActiveRecord::Base
  scope :life_supporting, where('distance_from_sun > ?', 5).order('diameter ASC')
end

Planetcó phạm vi life_supportingSolarSystem has_many :planets. Tôi muốn xác định mối quan hệ has_many của mình để khi tôi yêu cầu một solar_systemcho tất cả các liên kết planets, life_supportingphạm vi sẽ tự động được áp dụng. Về cơ bản, tôi muốn solar_system.planets == solar_system.planets.life_supporting.

Yêu cầu

  • Tôi không muốn thay đổi scope :life_supportingtrong Planetđể

    default_scope where('distance_from_sun > ?', 5).order('diameter ASC')

  • Tôi cũng muốn ngăn trùng lặp bằng cách không phải thêm vào SolarSystem

    has_many :planets, :conditions => ['distance_from_sun > ?', 5], :order => 'diameter ASC'

Mục tiêu

Tôi muốn có một cái gì đó giống như

has_many :planets, :with_scope => :life_supporting

Chỉnh sửa: Cách làm việc

Như @phoet đã nói, có thể không đạt được phạm vi mặc định bằng ActiveRecord. Tuy nhiên, tôi đã tìm thấy hai cách giải quyết tiềm năng. Cả hai đều ngăn chặn sự trùng lặp. Phương thức đầu tiên, trong khi dài, duy trì khả năng đọc và tính minh bạch rõ ràng, và phương thức thứ hai là phương thức loại trợ giúp với đầu ra là rõ ràng.

class SolarSystem < ActiveRecord::Base
  has_many :planets, :conditions => Planet.life_supporting.where_values,
    :order => Planet.life_supporting.order_values
end

class Planet < ActiveRecord::Base
  scope :life_supporting, where('distance_from_sun > ?', 5).order('diameter ASC')
end

Một giải pháp khác gọn gàng hơn rất nhiều là chỉ cần thêm phương pháp sau vào SolarSystem

def life_supporting_planets
  planets.life_supporting
end

và sử dụng solar_system.life_supporting_planetsbất cứ nơi nào bạn muốn solar_system.planets.

Cả hai đều không trả lời câu hỏi vì vậy tôi chỉ đặt chúng ở đây như một giải pháp thay thế nếu bất kỳ ai khác gặp phải tình huống này.


2
Cách giải quyết bằng cách sử dụng where_vales thực sự là giải pháp tốt nhất hiện có và giá trị một câu trả lời được chấp nhận
Viktor Tron

where_valuescó thể không hoạt động với các điều kiện băm: {:cleared => false}... nó cung cấp một mảng băm mà ActiveRecord không thích. Là một hack, grabbing mục đầu tiên trong mảng hoạt động: Planet.life_supporting.where_values[0]...
Nolan Amy

Tôi thấy tôi phải sử dụng where_astthay vì where_valueshoặc where_values_hashnhư tôi đã sử dụng AREL trong phạm vi trên mô hình kia. Làm việc một điều trị! +1
br3nt

Câu trả lời:


130

Trong Rails 4, Associationscó một scopetham số tùy chọn chấp nhận một lambda được áp dụng cho Relation(xem tài liệu cho ActiveRecord :: Associations :: ClassMethods )

class SolarSystem < ActiveRecord::Base
  has_many :planets, -> { life_supporting }
end

class Planet < ActiveRecord::Base
  scope :life_supporting, -> { where('distance_from_sun > ?', 5).order('diameter ASC') }
end

Trong Rails 3, where_valuescách giải quyết này đôi khi có thể được cải thiện bằng cách sử dụng cách where_values_hashxử lý các phạm vi tốt hơn trong đó các điều kiện được xác định bằng nhiều wherehoặc bằng một hàm băm (không phải trường hợp ở đây).

has_many :planets, conditions: Planet.life_supporting.where_values_hash

Bạn có thể nêu chi tiết các giải pháp rails 3? Các đường ray 4 một là rất sạch sẽ!
Augustin Riedinger

1
Đối với Rails 3, tôi nên đọc nó has_many :planets, conditions: Planet.life_supporting.where_values_hashđể thực thi phạm vi. Đây cũng là vàng để tải háo hức.
thần kinh học

1
Tôi đã phát hiện ra một cách khó khăn where_values_hashkhông hoạt động với văn bản trong đó các mệnh đề, ví dụ: User.where(name: 'joe').where_values_hashsẽ trả về giá trị băm 'điều kiện mong đợi, trong khi User.where('name = ?', 'Joe').where_values_hashthì không. Do đó, ví dụ về các hành tinh có thể sẽ thất bại.
thần kinh học

1
@nerfologist Cảm ơn bạn đã chỉ ra lỗi trong ví dụ mã cuối cùng, tôi đã chỉnh sửa câu trả lời. Nhận xét thứ hai của bạn có lý, tôi nghĩ đó là điều tôi đang ám chỉ trong đoạn cuối của câu trả lời. Vui lòng chỉnh sửa câu trả lời của tôi nếu bạn có thể tìm thấy một cách rõ ràng hơn để giải thích những hạn chế.
user1003545

2
@ GrégoireClermont này không làm việc trong Rails 5
elquimista

16

Trong Rails 5, đoạn mã sau hoạt động tốt ...

  class Order 
    scope :paid, -> { where status: %w[paid refunded] }
  end 

  class Store 
    has_many :paid_orders, -> { paid }, class_name: 'Order'
  end 

1

tôi vừa mới đi sâu vào ActiveRecord và không có vẻ gì là nếu điều này có thể đạt được với việc triển khai hiện tại has_many. bạn có thể chuyển một khối tới :conditionsnhưng điều này bị giới hạn trong việc trả về một băm điều kiện, không phải bất kỳ loại thứ gì.

một cách thực sự đơn giản và minh bạch để đạt được những gì bạn muốn (những gì tôi nghĩ rằng bạn đang cố gắng làm) là áp dụng phạm vi trong thời gian chạy:

  # foo.rb
  def bars
    super.baz
  end

điều này khác xa so với những gì bạn đang yêu cầu, nhưng nó có thể hoạt động;)


Cảm ơn phoet! Điều này sẽ hoạt động, tuy nhiên nó sẽ trông hơi kỳ quặc khi xem xét mã. Tôi nghĩ rằng phản hồi mà tôi sẽ nhận được khi thực hiện điều này là thay vào đó thực hiện việc sao chép trên has_manytờ khai vì nó rõ ràng hơn.
Aaron

từ quan điểm của tôi mã xét tôi sẽ thích tùy chọn này qua sự trùng lặp về điều kiện, vv miễn là bạn cung cấp một thử nghiệm cho những gì nó có nghĩa là để làm, phương pháp này là nhiều hơn nữa DRY và SRP
phoet

Tôi thực sự khuyên bạn không nên sử dụng một phương pháp như thế này khi một liên kết thường được sử dụng. Thay vào đó, tôi sẽ xóa các điều kiện khỏi liên kết & phạm vi được gọi rõ ràng trên liên kết. Nó sẽ dễ bảo trì hơn và rõ ràng hơn trong tương lai.
BM5k

Rất tiếc, chỉ cần nhận ra bài đăng này thực sự cũ. Đã ở đây nghiên cứu một vấn đề tương tự.
BM5k
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.