4

如何封装一组操作(其中大部分本质上是 .where(...))并将其应用于不同的模型,以使某些模型可能无法实现某些操作并应返回空集合。(非必要代码跳过)

我设计的(但不满意):

class ActivityFinder

  def self.find(query, limit = nil)
    activities = get_activities(query)
    activities = activities.sort_by(&:created_at).reverse // some-kind of merge-sort
    activities = activities.slice(0, limit) if limit.present?
    activities
  end

  private

  def self.get_activities(query)
    activities = []
    activities += query.apply(ModelA)
    activities += query.apply(ModelB)
    activities += query.apply(ModelC)
    activities
  end
end

class ActivityQuery

  def created_before(time)
    @created_before = time
    self
  end

  def created_after(time)
    @created_after = time
    self
  end

  def apply(activity)
    activity = activity.where("#{activity.table_name}.created_at < ?", @created_before) if @created_before.present?
    activity = activity.where("#{activity.table_name}.created_at >= ?", @created_after) if @created_after.present?
    // more operations, not all of them are suported by ModelC
  rescue NoMethodError
    return []
  end
end

用法

query = ActivityQuery.new.created_before(last_time).with_hash(...)
activities = ActivityFinder.find(query)

我不喜欢的:

  • NoMethodError 的救援
  • 如果不同的模型对字段有不同的名称,则必须将其作为case查询中的语句处理,以这种方式将查询对象与每个模型耦合

所以我正在寻找更好实施的建议

更新

问题是我想传递从 ActiveModel 获得的任何对象,例如 ActiveRecord::Relation,所以我不能只用方法定义模块(并在需要时覆盖)并将其包含在我正在使用的模型中. 问题更多的是为干净的设计指明正确的方向,而不是关于实现细节,我会以某种方式弄清楚

4

4 回答 4

2

为避免紧密耦合,请将模型特定代码放入模型中:

class ModelA < ActiveRecord::Base
  scope :created_before, ->(timestamp) { where("created_at < ?", timestamp) }
  scope :created_after, ->(timestamp) { where("created_at >= ?", timestamp) }
  scope :with_hash, ->(hash) { ... }
end

class ModelB < ActiveRecord::Base
  scope :created_before, ->(timestamp) { where("other_column < ?", timestamp) }
  scope :created_after, ->(timestamp) { where("other_column >= ?", timestamp) }
  scope :with_hash, where('false') # empty, chain-able collection
end

现在你有了一个一致的接口,你可以针对它进行编程:

class ActivityQuery

  def apply(activity)
    activity = activity.scoped
    activity = activity.created_before(@created_before) if @created_before.present?
    activity = activity.created_after(@created_after) if @created_after.present?
    activity = activity.with_hash(@hash) if @hash.present?
    activity
  end

end

无需拯救NoMethodError,也无需再摆弄模型的实现细节。

于 2013-04-18T14:34:28.747 回答
1

您可以使用 ActiveRecord::Base#column_names

class Goodstuff < ActiveRecord::Base
  def self.find_x_greater_than_y(x,y)
    if (scope_attributes + column_names).inlcude?(x)
      where('x > y')
    else
      []
    end
  end
end
于 2013-04-18T12:19:53.337 回答
1

如果你想封装你的查询对象,我编写了一个微型库,它可以非常简单地将复杂的查询逻辑移到你的模型和控制器之外。

https://github.com/ElMassimo/queryable

它负责使您的范围可链接,并委托方法如 each 并映射到实际查询。

您可以创建一个基础对象,在其中定义可用操作(或救援NoMethodError),然后在需要修改或更改任何范围时扩展查询对象。

ActivityFinder.find { |activities|
  activities.created_before(last_time).with_hash(...)
}

class ActivityQuery
  include Queryable::ActiveRecord

  # You can define the scopes as simple methods, which you can override in
  # subclasses.
  def created_before(time)
    where("created_at < ?", time)
  end

  # Rails-style scope syntax is also available
  scope(:created_after) { |time| where("created_at >= ?", time) }

  # You can add more methods to the query interface that just return self,
  # or none and then implement them in the more specific query subclasses.
  def with_hash(*args, &block)
    none
  end
end

class ModelAQuery < ActivityQuery
  # You can specify the collection that the query object works with by default.
  queryable ModelA

  # You can override a method defined in the superclass, since the scopes are
  # plain methods you could also share them with mixins.
  def created_before(time)
    where("measured_at < ?", time)
  end

  def with_hash(hash)
    where("timestamp = ?", hash[:timestamp])
  end

  # ...
end

# To solve that particular case you could pass a block to the find method
# and build the query.
class ActivityFinder
  ACTIVITIES = [ModelAQuery, ModelBQuery, ...]

  def self.find(limit=nil, &block)
    activities = get_activities(&block)
    # ...
  end

  def self.get_activities
    ACTIVITIES.map { |query_class| yield query_class.new }.flatten
  end
end
于 2014-04-13T18:17:00.860 回答
0

最初的想法是创建一些代理对象并将方法执行推迟到实际应用。我用更传统的方法做到了,每个操作都创建一个标准对象(在我的例子中,它称为 QueryOperation)并在稍后阶段应用。仍然存在相同的功能问题,但代码更清晰

class ActivityQuery

  def initialize
    @operations = []
    @operations.push(QueryOperationOrderBy.new('created_at', 'DESC'))
  end

  def for_user(user)
    @user = user
    self
  end

  def with_hash(hash)
    @operations.push(QueryOperationFindByHash.new(hash)) unless hash.blank?
    self
  end

  def limit(limit)
    @operations.push(QueryOperationLimit.new(limit))
    self
  end

  def created_before(time)
    @operations.push(QueryOperationLessThan.new('created_at', time))
    self
  end

  def created_after(time)
    @operations.push(QueryOperationGreaterThan.new('created_at', time))
    self
  end

  def apply(activity)
    @operations.each do |operation|
      activity = operation.apply(activity)
    end

    activity
  rescue NoMethodError
    return []
  end
end
于 2013-04-22T13:24:50.400 回答