【问题标题】:Combine multiple scope or where queries with OR使用 OR 组合多个范围或 where 查询
【发布时间】:2014-05-30 15:06:02
【问题描述】:

我如何以这样的方式获取 arel 组件,我可以执行以下操作:

queries = []
queries << MyModel.some_scope.get_the_arel_component
queries << MyModel.some_scope_with_param("Dave").get_the_arel_component
queries << MyModel.where(:something => 'blah').get_the_arel_component
queries << MyModel.some_scope_with_join_and_merge.get_arel_component
# etc ... (may be any number of queries) 

# join each query with OR
combined_query = nil
queries.each do |query|
  combined_query ||= query
  combined_query = combined_query.or(q)
end

# run the query so it just works
MyModel.where(combined_query)

我在接受类似问题的答案时遇到了一些问题。

假设我有这样的课程:

class Patient
  has_one :account

  scope :older_than, ->(date) { where(arel_table[:dob].lt(date)) }
  scope :with_gender, ->(gender) { where(:gender => gender) }
  scope :with_name_like, ->(name) { where("lower(name) LIKE ?", name.downcase) }
  scope :in_arrears, -> { joins(:account).merge( Account.in_arrears ) } 
end

目标是将任何范围或 where 子句与 OR 结合起来。

One way 将是 Patient.with_name_like("Susan") | Patient.with_name_like("Dave")。这似乎单独运行每个单独的查询,而不是组合成一个查询。我已经排除了这个解决方案。

Another method 仅在某些情况下有效:

# this fails because `where_values` for the `with_name_like` scope returns a string
sues = Patient.with_name_like("Susan").where_values.reduce(:and)
daves = Patient.with_name_like("Dave").where_values.reduce(:and)
Patient.where(sues.or(daves))

# this works as `where_values` returns an `Arel::Nodes::Equality` object
ages = Patient.older_than(7.years.ago).where_values.reduce(:and)
males = Patients.with_gender('M').where_values.reduce(:and)
Patient.where(ages.or(males))

# this fails as `in_arrears` scope requires a joins
of_age = Patient.older_than(18.years.ago).where_values.reduce(:and)
arrears = Patients.in_arrears.where_values.reduce(:and)
Patient.where(of_age.or(arrears)) # doesn't work as no join on accounts
Patient.join(:account).where(of_age.or(arrears)) # does work as we have our join

总而言之,当where 被传递一个字符串或查询需要连接时,就会出现 ORing 查询的问题。

我很确定where 会将传递给它的任何内容转换为 arel 对象,这只是访问正确的部分并以正确的方式重新组合它们的问题。我只是还没弄好。

答案最好只使用 ActiveRecord 和 AREL,而不是第三方库。

【问题讨论】:

  • 你似乎已经弄清楚了。 Or-ing 很难。我没有答案,只有一些想法。因为你总是需要去where_values,所以你失去了所需的连接和like 运算符。因此,一种解决方案是直接使用 arel 运算符,但这意味着:不对现有范围进行 or-ing。另外:你是如何点赞的。但是恕我直言,您清楚地记录了与 arel 进行或运算的局限性。就我个人而言,我只是避免使用“OR”,或者如果出于性能原因确实需要它,我会编写一个自定义查询。
  • @nathanvda,感谢 cmets 和建议。我不介意直接与 arel 运营商合作,但我无法对它们进行试验,也不知道它们是如何组合在一起的。只是为了了解更多背景知识,我正在尝试对模型进行高级搜索,用户可以在该模型中选择查找与所有 (AND) 或任何 (OR) 可用选项匹配的记录。
  • 实际上,amazing website 的 Dan Shultz 建议使用 in 运算符。我想它会像MyModel.where(:id =&gt; MyModel.select(:id).some_scope)。所以在我的第一个示例中,循环将变为queries.each {|q| combined_query.where(:id =&gt; q)}。我还没有尝试过,但它似乎可以工作。
  • 嗯,实际上考虑了我的最后一条评论,这仍然等同于对每个范围进行 AND 运算。此外,即使在同一张桌子上,它也会对属性进行新的选择。也许我需要更多地思考如何正确应用他的建议。

标签: ruby-on-rails arel


【解决方案1】:

既然你愿意使用第三方库,Ransack 怎么样?

它有一个非常健壮的实现,允许各种和和或条件组合,并且与相关模型一起工作也很好。

对于像您这样的用例,其中有一些预定义的查询/范围,我希望用户能够从中选择并运行它们的 or 组合,我使用 ransack 的开箱即用实现,然后继续在视图级别,我使用 javascript 插入隐藏字段,其值将导致控制器中预期的结构化参数哈希洗劫。

您的所有作用域都可以使用 ransack 助手在视图中轻松定义。您的代码应如下所示:

控制器

def index
  @q = Patient.search(params[:q])
  @patients = @q.result(distinct: true)
end

查看

<%= search_form_for @q do |f| %>
  <%= f.label :older_than %>
  <%= f.date_field :dob_lt %>
  <%= f.label :with_gender %>
  <%= f.text_field :gender_eq %>
  <%= f.label :with_name_like %>
  <%= f.text_field :name_cont %>
  <%= f.label :in_arrears_exceeding %>
  <%= f.text_field :accounts_total_due_gte %>
  <%= f.submit %>
<% end %>

此外,如果您想对andingoring 进行更多控制,请查看使用ransack 的complex search form builder 示例。

【讨论】:

  • 感谢您的回答。 Ransack,看起来很有前途(complex search demo),但是,它目前不支持作用域(#61#156)。虽然看起来解决方案正在慢慢开发中。 charliehere 建议了一个可能的解决方法。
  • @br3nt 是的,它不支持范围,但老实说,在使用 ransack 时,我发现它们几乎没有用处。此外,使用 ransack 为我们在界面上提供了极大的灵活性,我们有一个用于amount 的 txtbox,例如,我们允许用户输入 '4000' 或 '500 - 1000' 所有这些都被放置到隐藏字段中用于搜查搜索表。此外,它允许的复杂分组意味着您可以创建自己的分组,然后静态化生成的表单查询并保存类似的搜索。
  • 是的,这就是它的灵活性,这就是为什么我将其标记为正确答案的原因,因为我认为它对大多数人都有帮助。我没有太多机会玩它,但不幸的是,我认为它不适合我的特殊需求,尽管我还没有一个像样的外观来了解创建自定义标准有多少控制。我仍在调查其他一些有希望的线索,并希望在一周内发布我自己的答案。
  • 只是为了您的兴趣,这里有一个指向我的用例示例的要点的链接:gist.github.com/br3nt/d249e1935d3e7432fa4a
【解决方案2】:

我在之前的一个项目中曾处理过类似的问题。要求是找到一组志愿者来记录匹配一组标准,如电子邮件、位置、学习流等。对我有用的解决方案是定义细粒度范围并编写我自己的查询构建器,如下所示:

class MatchMaker
  # Scopes
  #   Volunteer => [ * - 'q' is mandatory, # - 'q' is optional, ** - 's', 'e' are mandatory ]
  #     active  - activation_state is 'active'
  #     scribes - type is 'scribe'
  #     readers - type is 'reader'
  #     located - located near (Geocoder)
  #     *by_name  - name like 'q'
  #     *by_email - email like 'q'
  #     educated - has education and title is not null
  #     any_stream - has education stream and is not null
  #     *streams - has education stream in 'q'
  #     #stream - has education stream like 'q'
  #     #education - has education and title like 'q'
  #     *level - education level (title) is 'q'
  #     *level_lt - education level (title) is < 'q'
  #     *level_lteq - education level (title) is <= 'q'
  #     *marks_lt - has education and marks obtained < 'q'
  #     *marks_lteq - has education and marks obtained <= 'q'
  #     *marks_gt - has education and marks obtained > 'q'
  #     *marks_gteq - has education and marks obtained >= 'q'
  #     *knows - knows language 'q'
  #     *reads - knows and reads language 'q'
  #     *writes - knows and writes language 'q'
  #     *not_engaged_on - doesn't have any volunteering engagements on 'q'
  #     **not_engaged_between - doesn't have any volunteering engagements betwee 'q' & 'q'
  #     #skyped - has skype id and is not null
  def search(scope, criteria)
    scope = scope.constantize.scoped

    criteria, singular = singular(criteria)
    singular.each do |k|
        scope = scope.send(k.to_sym)
    end

    if criteria.has_key?(:not_engaged_between)
      multi = criteria.select { |k, v| k.eql?(:not_engaged_between) }
      criteria.delete(:not_engaged_between)

      attrs = multi.values.flatten
      scope = scope.send(:not_engaged_between, attrs[0], attrs[1])
    end

    build(criteria).each do |k, v|
        scope = scope.send(k.to_sym, v)
    end

    scope.includes(:account).limit(Configuration.service_requests['limit']).all
  end

  def build(params)
    rejects = ['utf8', 'authenticity_token', 'action']
    required = ['by_name', 'by_email', 'by_mobile', 'streams', 'marks_lt', 'marks_lteq', 'marks_gt', 
      'marks_gteq', 'knows', 'reads', 'writes', 'not_engaged_on', 'located', 'excluding', 
      'level', 'level_lt', 'level_lteq']
    optional = ['stream', 'education']

    params.delete_if { |k, v| rejects.include?(k) }
    params.delete_if { |k, v| required.include?(k) && v.blank? }
    params.each { |k, v| params.delete(k) if optional.include?(k.to_s) && v.blank? }

    params
  end

  def singular(params)
    pattrs   = params.dup
    singular = ['active', 'scribes', 'readers', 'educated', 'any_stream', 'skyped']
    original = []

    pattrs.each { |k, v| original << k && pattrs.delete(k) if singular.include?(k.to_s) }

    [pattrs, original]
  end
end

表格应该是这样的:

...

<%= f.input :paper ... %>

<%= f.input :writes ... %>

<%= f.input :exam_date ... %>

<%= f.time_select :start_time, { :combined => true, ... } %>

<%= f.time_select :end_time, { :combined => true, ... } %>

<fieldset>
  <legend>Education criteria</legend>

  <%= f.input :streams, :as => :check_boxes, 
    :collection => ..., 
    :input_html => { :title => 'The stream(s) from which the scribe can be taken' } %>

  <%= f.input :education, :as => :select, 
    :collection => ...,
    :input_html => { :class => 'input-large', :title => configatron.scribe_request.labels[:education]}, :label => configatron.scribe_request.labels[:education] %>

  <%= f.input :marks_lteq, :label => configatron.scribe_request.labels[:marks_lteq], 
    :wrapper => :append do %>
    <%= f.input_field :marks_lteq, :title => "Marks", :class => 'input-mini' %>
    <%= content_tag :span, "%", :class => "add-on" ... %>
  <% end %> 
</fieldset>

...

最后

# Start building search criteria
criteria = service_request.attributes
...
# do cleanup of criteria
MatchMaker.new.search('<Klass>', criteria)

这在过去对我很有效。希望这会引导您朝着正确的方向解决您面临的问题。一切顺利。

【讨论】:

  • 这允许搜索一个或多个条件(可选或强制),其中记录需要匹配提供的所有条件。我正在寻找的也是匹配任何标准的能力。那是至少一个范围,但不一定是所有范围。例如,任何符合教育水平 > 5 或标记 >= 95 的记录。我认为这不会成功,但如果我错了,请告诉我。
  • 这是需要以某种方式进行 ORed 的行:scope = scope.send(k.to_sym, v)
  • 确实,恕我直言,这是对所有选定范围的 AND。
猜你喜欢
  • 2016-09-23
  • 2021-04-29
  • 2022-01-01
  • 2016-04-29
  • 2023-04-08
  • 1970-01-01
  • 1970-01-01
  • 2012-09-20
  • 1970-01-01
相关资源
最近更新 更多