How to union two different Mongoid Criteria - ruby

I have the following scopes defined in my model:
scope :upcoming, -> { where(:start_time.gt => Time.now).asc(:start_time) }
scope :in_progress, -> {
now = Time.now
where(:start_time.lte => now).where(:end_time.gte => now).asc(:start_time)
}
I want to create another scope that combines the results of both of those scopes called current. I tried something like this:
scope :current, -> { self.in_progress | self.upcoming }
But this just ends up treating them both like arrays and concatenating them. The problem with this is that when I try to call my scope with Model.current, I get the following error message:
NoMethodError: undefined method `as_conditions' for #<Array:0xaceb008>
This is because it converted the Mongoid Criteria object to an array, but I don't want that. I want the object to stay as a Mongoid Criteria object.
What I really want is the union of the in_progress set and the upcoming set.
Any ideas? Thanks.

You can try to compose your criteria using Mongoid's query methods and dereferencing into the criteria's selector, but I wouldn't necessarily recommend this -- see below for an example. I second the recommendation to craft your third scope. Remember that these scopes correspond to db queries that you want to be efficient, so it is probably worth your time to examine and understand the resulting and underlying MongoDB queries that are generated.
Model
class Episode
include Mongoid::Document
field :name, type: String
field :start_time, type: Time
field :end_time, type: Time
scope :upcoming, -> { where(:start_time.gt => Time.now).asc(:start_time) }
scope :in_progress, -> {
now = Time.now
where(:start_time.lte => now).where(:end_time.gte => now).asc(:start_time)
}
scope :current, -> { any_of([upcoming.selector, in_progress.selector]) }
scope :current_simpler, -> { where(:end_time.gte => Time.now) }
end
Test
require 'test_helper'
class EpisodeTest < ActiveSupport::TestCase
def setup
Episode.delete_all
end
test "scope composition" do
#p Episode.in_progress
#p Episode.upcoming
#p Episode.current
#p Episode.current_simpler
in_progress_name = 'In Progress'
upcoming_name = 'Upcoming'
Episode.create(:name => in_progress_name, :start_time => Time.now, :end_time => 1.hour.from_now)
Episode.create(:name => upcoming_name, :start_time => 1.hour.from_now, :end_time => 2.hours.from_now)
assert_equal([in_progress_name], Episode.in_progress.to_a.map(&:name))
assert_equal([upcoming_name], Episode.upcoming.to_a.map(&:name))
assert_equal([in_progress_name, upcoming_name], Episode.current.to_a.map(&:name))
assert_equal([in_progress_name, upcoming_name], Episode.current_simpler.to_a.map(&:name))
end
end

You have to map your Array back to a Mongoid::Criteria.
Any array of yours can be translated to a criteria with any_in:
scope :has_data, -> { any_in(:_id => all.select{ |record| record.data.size > 0 }.map{ |r| r.id }) }
So, something like this should do the trick: (untested)
scope :current, -> { any_in(:_id => (self.in_progress + self.upcoming).map{ |r| r.id }) }
I hope there exists better solutions, but this solves the equation at least.

Related

Eager load self - join table recursively

I have got a model
class MyModel < ActiveRecord::Base
belongs_to :parent, :class_name => 'MyModel'
has_many :children, :class_name => 'MyModel', :foreign_key => 'parent_id'
end
And I want to grab all objects that have no parent and get them with all the children. In another words I want all parents and their children and children of their children and so on. So it has to be eager loaded recursively.
If I do
#result = MyModel.includes(:children)
.where('parent IS ?', nil)
I get only parents with the one level of children. But I dont get children of children. Is it possible to get all of them recursively? And is it possible to albo add counting the number of children for each parent?
I'd recommend you use a nested set gem like awesome_nested_set. That will give you a lot of functionality to help with handling sets to similar objects, and includes the facility to store children count.
You could recursively preload a set amount of self-joins by passing in a nested hash
#result = MyModel.includes(
children: {
children: {
children: {
children: :children
}
}
}
).where('parent IS ?', nil)
Or better yet generate this hash dynamically
This is untested (basically pseudo code)
scope :recursivly_include_children, -> (max_depth) {
includes(generate_nested_children_hash(max_depth))
}
def self.generate_nested_children_hash(current_hash = {}, level = 0, remaining_levels)
if remaining_levels > 0
// add one level to the current_hash here
generate_nested_children_hash(current_hash, level+1, remaining_levels-1)
else
current_hash
end
end
Then in the controller to get a maximum of 10 levels of preloaded children:
#result = MyModel.recursivly_include_children(10).where('parent IS ?', nil)

Ruby DataMapper::ImmutableError

get '/watch/:id' do |id|
#results = Twitchtvst.all( :fields => [:Twitchtv ],
:conditions => { :user_id => "#{id}" }
)
#p #results.inspect
#results.each do |result|
puts result.id
end
erb :mystream
end
I get this error message immutable resource cannot be lazy loaded. How do I fix this?
The Error message is:
DataMapper::ImmutableError at /watch/1
Immutable resource cannot be lazy loaded
According to the official documentation:
Note that if you don't include the primary key in the selected columns, you will not be able to modify the returned resources because DataMapper cannot know how to persist them. DataMapper will raise DataMapper::ImmutableError if you're trying to do so nevertheless.
I know that you are not modifying anything here but I think that the same rule applies for lazy loading. So I will suggest to try it like that:
#results = Twitchtvst.all( :fields => [:Twitchtv, :id],
:conditions => { :user_id => "#{id}" }
) ode here
Note the id as an additional field.

Why does my ActiveRecord scope with `merge` return an array?

I have a scope on my Contract model that uses merge and returns an array, not an ActiveRecord::Relation as I would like.
Yes, I've seen it said that "It is an ActiveRecord::Relation, but Rails is intentionally lying to you". But in this case:
The scope uses merge
it only works if it's the last scope in the chain
The object it returns says it's of class Array
The object it returns has nothing about ActiveRecord in its ancestors
Calling ActiveRecord::Relation methods like scoped on the return value raises raises NoMethodError: undefined method 'scoped' for []:Array.
The scope is on Contract and looks something like
scope :hourly, scoped.merge(Division.find_by_name!('Hourly').contracts)
Why is this returning an array? Can I get it to return an ActiveRecord::Relation?
Ref comments above. I gave this a go with a dummy relationship which I expect you have with Division and Contract.
# app/models/contract.rb
scope :hourly,
select: 'distinct contracts.*',
joins: :divisions,
conditions: {
"divisions.name" => 'Hourly'
},
order: :id
contracts = Contracts.hourly
# => [#<Contract id: 1>, #<Contract id: 2>]
contracts.class
# => #<ActiveRecord::Relation>
contracts.scoped.class
# => #<ActiveRecord::Relation>
contracts.arel
# => #<Arel::SelectManager:0x007fab629f7e90>
contracts.to_a
# => [#<Contract id: 1>, #<Contract id: 2>]
contracts.to_sql
# => SELECT distinct contracts.* FROM `contracts` INNER JOIN `divisions` ON `divisions`.`contract_id` = `contracts`.`id` WHERE `divisions`.`name` = 'Hourly' ORDER BY id
Let me know if this is what you were looking for...

How to get raw Mongo data results from a MongoMapper Plucky::Query object?

Let's say we have a MongoDB collection called "images", and a MongoMapper-powered application with a corresponding "Image" model. If we set up a MongoMapper query using this model, we see that it is of type Plucky::Query and returns results of type Image:
>> Image.where(:file_type => 'image/jpeg').class
=> Plucky::Query
>> Image.where(:file_type => 'image/jpeg').first.class
=> Image
We can run the corresponding query directly on the Mongo adapter, mostly bypassing MongoMapper, by accessing the MongoMapper.connection. If we do it this way, the query is of type Mongo::Cursor and returns raw data results of type BSON::OrderedHash:
>> MongoMapper.connection.db(dbname).collection('images').find({ :file_type => 'image/jpeg' }).class
=> Mongo::Cursor
>> MongoMapper.connection.db(dbname).collection('images').find({ :file_type => 'image/jpeg' }).first.class
=> BSON::OrderedHash
The question is, is there a way to take a Plucky::Query like above and convert it to (or retrieve from it) a basic, non-extended Mongo::Cursor object?
At first I thought I found a solution with find_each, which does actually take a Plucky::Query and return a Mongo::Cursor:
>> Image.where(:file_type => 'image/jpeg').find_each.class
=> Mongo::Cursor
But it turns out this Mongo::Cursor is somehow extended or otherwise different from the above one because it still returns Image objects instead of BSON::OrderHash objects:
>> Image.where(:file_type => 'image/jpeg').find_each.first.class
=> Image
Update: I can't simply bypass MongoMapper query magic altogether like I did in the second case because I need to access features of MongoMapper (specifically named scopes) to build up the query, so what I end up with is a Plucky::Query. But then I want the results to be plain data objects, not models, because all I need is data and I don't want the overhead of model instantiation.
If you drop to the driver, the transformer is nil by default:
1.9.3p194 :003 > Image.collection.find({ :file_type => 'image/jpeg' }, { :limit => 1 }).first.class
=> BSON::OrderedHash
MongoMapper achieves the conversion by setting a "transformer" lambda on the plucky query. You can see this in the MongoMapper source code:
def query(options={})
query = Plucky::Query.new(collection, :transformer => transformer)
...
end
...
def transformer
#transformer ||= lambda { |doc| load(doc) }
end
So after each mongo document retrieval, this Plucky::Query runs the transformation that loads the model. Looking at the Plucky source code we see that there is a simple setter method [] we can use to disable this. So this is the solution:
plucky_query = Image.where(:file_type => 'image/jpeg')
plucky_query.first.class
# => Image
plucky_query[:transformer] = nil
plucky_query.first.class
# => BSON::OrderedHash
If you don't mind monkey-patching you can encapsulate like so:
module Plucky
class Query
def raw_data
self[:transformer] = nil
self
end
end
end
Then you could simply write:
Image.where(:file_type => 'image/jpeg').raw_data.first.class
# => BSON::OrderedHash

How to construct the 2d structure in a dynamic fashion

I iterate through all cars and its supported attributes (many attributes per car) to create a structure like this, how do I do this in a dynamic fashion.
cars = {
"honda" => {'color' => 'blue', 'type' => 'sedan'}.
"nissan" => {'color' => 'yellow', 'type' => 'sports'}.
...
}
cars.each do |car|
car_attrs = ...
car_attrs.each do |attr|
??? How to construct the above structure
end
end
Your question is not very clear... But i guess this is what you want:
cars = {}
options = {}
options['color'] = 'blue'
...
cars['honda'] = options
Is that what you were looking for?
It sounds like you may be asking for a way to create a 2-dimensional hash without having to explicitly create each child hash. One way to accomplish that is by specifying the default object created for a hash key.
# When we create the cars hash, we tell it to create a new Hash
# for undefined keys
cars = Hash.new { |hash, key| hash[key] = Hash.new }
# We can then assign values two-levels deep as follows
cars["honda"]["color"] = "blue"
cars["honda"]["type"] = "sedan"
cars["nissan"]["color"] = "yellow"
cars["nissan"]["type"] = "sports"
# But be careful not to check for nil using the [] operator
# because a default hash is now created when using it
puts "Found a Toyota" if cars["toyota"]
# The correct way to check would be
puts "Really found a Toyota" if cars.has_key? "toyota"
Many client libraries assume that the [] operator returns a nil default, so make sure other code doesn't depend on that behavior before using this solution. Good luck!
Assuming you are using something similar to ActiveRecord (but easy to modify if you are not):
cars_info = Hash[cars.map { |car| [car.name, car.attributes] }

Resources