3

我正在尝试为匹配特定日期范围或具有 nil 值的记录创建 Mongoid 查询。这是我的 ruby​​ 代码,它执行我想变成 Mongoid 查询的功能:

class MyModel
  include Mongoid::Document
  field :name
  field :enabled, type: Boolean, default: false
  field :start_date, type: DateTime
  field :end_date, type: DateTime

  def self.active
    documents = where(enabled: true)
    documents = documents.keep_if {|doc| doc.start_date.nil? || doc.start_date <= Date.today}
    documents = documents.keep_if {|doc| doc.end_date.nil? || doc.end_date >= Date.tomorrow}
    documents
  end
end

如何通过将此方法转换为 Mongoid 查询来提高性能?

更新:

这是我用来验证正确行为的 RSpec 测试:

describe '.active' do
  let!(:disabled){ Fabricate(:model, enabled: false, name: 'disabled') }
  let!(:enabled_without_date){ Fabricate(:active_model, name: 'enabled_without_date') }
  let!(:past){ Fabricate(:active_model, start_date: 1.week.ago, end_date: Date.yesterday, name: 'past') }
  let!(:current){ Fabricate(:active_model, start_date: Date.today, end_date: Date.tomorrow, name: 'current') }
  let!(:future){ Fabricate(:active_model, start_date: Date.tomorrow, end_date: 1.week.from_now, name: 'future') }
  
  it 'returns only enabled and within the current time' do
    MyModel.count.should == 5
    models = MyModel.active.to_a
    models.should_not be_empty
    models.should_not include disabled
    models.should_not include past
    models.should_not include future
    models.should include enabled_without_date
    models.should include current
  end
end
4

6 回答 6

3

如果您转换条件:

(start <= X OR start.nil?) AND (end >= Y OR end.nil?)

进入析取形式,你得到:

(start <= X AND end >= Y) OR (start <= X and end.nil?) OR (start.nil? and end >= Y) or (start.nil? and end.nil?)

然后,您可以通过单个 $or 子句表达这一点:

$or: [
  {:start_date.lte => start_date, :end_date.gte => end_date},
  {:start_date => nil, :end_date.gte => end_date},
  {:start_date.lte => start_date, :end_date => nil},
  {:start_date => nil, :end_date => nil},
]

如果两个值都必须是 set 或 nil (也就是说,你不能有一个 set 和一个 nil),这将变得更加简单:

$or: [
  {:start_date.lte => start_date, :end_date.gte => end_date},
  {:start_date => nil},
]

为了满足您的规范,完整的查询将是:

Model.where({
  :enabled => true,
  :$or => [
    {:start_date.lte => Date.today.to_time, :end_date.gte => Date.tomorrow.to_time},
    {:start_date => nil},
  ]
})
于 2013-07-22T19:05:47.227 回答
1

也许这会成功:

where(
  enabled: true,
  {:$or => [{start_date: nil}, {:start_date.lte => Date.today.to_time   }]},
  {:$or => [{end_date:   nil}, {:end_date.gte   => Date.tomorrow.to_time}]}
)
于 2013-07-15T21:30:35.270 回答
1

使用以下代码:

where(:enabled => true, :$or => [{:start_date => nil}, {:start_date =>['$lte' => Date.today]}], :$or => [{:end_date => nil}, {:end_date =>['$gte' =>Date.tomorrow]}])

希望这对您有所帮助。

于 2013-07-19T06:07:46.483 回答
0

到目前为止给出的所有答案对我来说似乎都很好。但是,我将添加另一个在以前的项目中对我有用的语法变体,它还处理了多余的 $or key 场景,这对我来说似乎有点可疑(但无论如何这样做可能完全没问题)。

Document.where({
   '$and' => [
       :enabled => true,
       '$or' => [
           :start_date => nil,
           :start_date => { '$lte' => Date.today.to_time }
       ],
       '$or' => [
           :end_date => nil,
           :end_date => { '$gte' => Date.tomorrow.to_time }
       ]
    ]
})

就进一步的建议而言:您是否也审查了您的规格?(你永远不会知道 ... ;))。还要确保只调试和测试问题的一部分,例如让 :enabled 过滤器自己工作,让日期过滤器自己工作,让 nil 过滤器自己工作,然后尝试再次组合它们 -也许这会让你找到问题的核心。此外,我还看到了指定与 $lte 和 $gte 进行比较的日期的不同变体。我自己成功地提供了一个Time类来进行比较,一定要尝试一下!

于 2013-07-25T11:23:12.227 回答
0

这似乎是 mongodb 驱动程序本身的限制!仅使用 mongodb java shell 我创建了三个记录:

db.so.insert({s:0,e:10})
db.so.insert({s:10,e:20})
db.so.insert({s:20,e:30})

然后尝试了以下查询:

db.so.find({s:{$lte:15}, e:{$gte:15}})  
   ==> finds just the middle record - correct
db.so.find({s:{$lte:15}, $or:[{e:{$exists:false}},{e:{$gte:15}}]})  
   ==> finds just the middle record - correct
db.so.find({$or:[{s:{$exists:false}},{s:{$lte:15}}], $or:[{e:{$exists:false}},{e:{$gte:15}}]})
   ==> finds both the middle and the last record - OOPS
db.so.find({$or:[{s:{$exists:false}},{$and:[{s:{$lte:15}},{e:{$gte:15}}]}]})
   ==> finds just the middle record - correct

我认为这意味着您不能在同一级别使用多个 $or(因为它说您可以嵌套它们)。我将把这个作为一个问题发布在 mongodb 网站上,看看他们是否同意。同时,在您的情况下,第四个查询指出了可能的工作回合,尽管 Chris Heald 已经提出了几乎相同的建议:

Model.where({
    :enabled => true,
    :$or => [
        {:start_date => nil},
        :$and => [
            {:start_date.lte => Date.today.to_time}, 
            {:end_date.gte => Date.tomorrow.to_time}
        ]
    ]
})
于 2013-07-25T16:10:09.290 回答
0

使用

Document.where(
  enabled: true,
  '$or' => [
    { start_date: nil },
    { :start_date.lte => Date.today.to_time }
  ],
  '$or' => [
    { end_date: nil },
    { :end_date.gte => Date.tomorrow.to_time }
  ]
).each do |d|
  puts d.inspect
end

我能够得到

[2] pry(main)> load './documents.rb'
#<Document _id: 51e89c690e21d8ab0d9cf012, enabled: true, start_date: nil, end_date: nil>
#<Document _id: 51e8a2e62147b4bfb5f12c65, enabled: true, start_date: 2012-02-01 05:00:00 UTC, end_date: 2014-02-01 05:00:00 UTC>
#<Document _id: 51e8a4797372723f449765bd, enabled: true, start_date: nil, end_date: 2014-02-01 05:00:00 UTC>
=> true

更新

你说的对。我的回答完全是假的。第二个$or键会覆盖第一个键。但是,即使用(因此使用哈希数组)包装整个事物$and也无济于事 - mongoid 可能会阻止多个$or条件。

值得一提的是,Chris Heald 的解决方案很有效。这是一个完整的证明。运行它rspec mongoid_query.rb

start_date = Date.today
end_date = Date.tomorrow
where(
  enabled: true,
  :$or => [
    {:start_date.lte => start_date, :end_date.gte => end_date},
    {:start_date => nil, :end_date.gte => end_date},
    {:start_date.lte => start_date, :end_date => nil},
    {:start_date => nil, :end_date => nil},
  ]
)
于 2013-07-19T02:30:20.070 回答