mongoid ordering problem - sorting

I have been using mongoid for storing data. I have a scenario where I have to sort the parents according the number of dependents they have. My model is simple:
Parent model references_many dependents stored_as => array
Dependent model reference_many parents stored_as => array
I don't want to loop through the parents, find the number of dependants, store them in an array and dump them into my view.
What I need is to sort the parents according to number of dependants with just one query.
Is that possible?
Thanks.

You will have to add a field with the count of the number of dependents and keep it up to date with a callback. Something like:
class MyModel
include Mongoid::Document
references_many :things
field :thing_count, :type => Integer
before_update :set_thing_count
def set_thing_count
self.thing_count = self.things.count
end
end
Then your sort would just be:
MyModel.asc(:thing_count)
Mongoid doesn't provide a good querying mechanism for sorts yet, but adding a denormalized count field is pretty easy, and probably better for performance anyways. Hope this helps!

Related

Handling Arrays of custom objects in Mongoid

I'm having some issues with creating a Mongoid document that includes an array of custom objects.
I my particular case I intend to store an array of BaseDevice objects. The BaseDevice class is already mongified and serializes from/to a plain hash using Mongoid's custom fields support. This works pretty well on single object.
For storing an array of BaseDevice, I've created the following class:
class BaseDeviceArray < Array
class << self
def demongoize(object)
object ? object.map{ |obj| BaseDevice.demongoize obj } : new
end
def evolve(object)
case
when BaseDeviceArray then object.mongoize
else object
end
end
end
def mongoize
self.map(&:mongoize)
end
end
The mongoid document looks like this
class MongoPeriph
include Mongoid::Document
field :devices, type: BaseDeviceArray
end
Let's say some_devices is an array containing two BaseDevice instances.
What happens is the following: when I assign some_devices to the devices fields of the MongoPeriph instance that works correctly.
mp = MongoPeriph.create
mp.devices = some_devices
mp.devices # => [#<BaseDevice:0x007fa84bac0080>,#<BaseDevice:0x007fa84baaff78>]
When try to send push, pop, shift, unshift methods to the devices field within the mongoid document, nothing seems to happen. The changes are not appearing on the mp object. Also when referencing one of the objects by index (i.e. when calling mp.devices[0].some_method) the world does not change.
When popping objects from the array, on every pop a new object is given. This is expected as the deserializer is instantiating a new BaseDevice object for every pop, but the internal field is not updated i.e. the object stays there and one can pop endlessly.
Using the BaseDeviceArray separate from a mongoid document works as expected:
foo = BaseDeviceArray.new
foo << BaseDevice.new
results in an array with a BaseDevice object.
Btw. I found one other approach to this on the net. It is a more generalized way of implementing what I need, but it monkey-patches Mongoid. Something I try to avoid. Moreover that solution seems to have the same issue my approach has.
Issue in your code is that you have #mongoize (instance) method but you actually need ::mongoize (class) method. You never create an instance of BaseDeviceArray thus instance methods are useless.
Here's an example of how I did the ::mongoize method where I actually have in mongo a Hash with a single key with array value. Also I wanted to make the resulting array into a hash with ids as keys for easier lookup.
def demongoize(hash)
return validate_hash(hash)["TestRecord"].each_with_object({}) do |r, m|
rec = TestRecord.new(r)
m[rec.case_id] = rec
end
end
def mongoize(object)
case object
when Array then {"TestRecord" => object.map(&:mongoize)}
when Hash
if object["TestRecord"]
# this gets actually called when doing TestRun.new(hash)
mongoize(demongoize(object))
else
{"TestRecord" => object.values.map(&:mongoize)}
end
else raise("dunno how to convert #{object.class} into records JSON")
end
end
def evolve(object)
# can't see how we want to process this here yet
# docs.mongodb.com/ruby-driver/master/tutorials/6.0.0/mongoid-documents
object
end
I guess op task done long ago but thought somebody may find it useful.

How to get an array of values from a conditional ActiveRecord query with associations?

I'm looking to define a method on one of my objects that will return a just one column of data from all of its child objects so long as another column in the same record meets certain conditions.
For instance if I have two objects
ParentObject
has_many: child_objects
#fields
name (string)
ChildObject
belongs_to: parent_object
#fields
name (string)
whitelisted_at (datetime)
I've read up that I can get a list of all child_object records for a parent_object based on a conditional specified using .where(). For instance in my controller I have code like so:
ParentObject.child_objects.where("whitelisted_at IS NOT NULL")
This gives me an active record associate like so:
#<ActiveRecord::AssociationRelation [
<ChildObject id: 1, name:"Susan", whitelisted_at: "2015-02-18 12:07:37">,
<ChildObject id: 1, name:"Simon", whitelisted_at: "2015-02-18 12:07:37">,
<ChildObject id: 1, name:"Sally", whitelisted_at: "2015-02-18 12:07:37">
]
I was looking how I would then filter through these to return an array of just names. Ideally i'd be able to run this all as a Model method so:
class ChainObject < ActiveRecord::Base
...
def whitelisted_names
#... outputs [Susan, Simon, Sally]
end
end
What would be the most concise and rails-y way of doing this. I thought about doing a .where() then an .each() and having a block method but that seems really cumbersome and I'm sure I'm just missing some smart ActiveRecord or Association method that could pluck an array of values from multiple hashes. I'm pouring over the APIdock but I think the problem is I don't know how to describe what I'm trying to do!
In your parent model you could use where.not and use the pluck method ActiveRecord gives you (props to Stefan - see pluck)
class ParentObject < ActiveRecord::Base
...
def whitelisted_names
child_objects.where.not(whitelisted_at: nil).pluck(:name)
end
end

Inserting models at arbitrary positions in a rails 4 association collection

I have a typical has_many relation between two models, let's say Order and Item.
Also, I am using nested attributes like so:
class Order < ActiveRecord::Base
has_many :items
accepts_nested_attributes_for :items
end
When putting together a nested edit order form, I want to be able to build new items (where an item has a name and a quantity) and insert them at arbitrary positions within the previously saved list of items.
Suppose I have a sorted array of strings listing all_possible_item_names a customer can specify a quantity for.
Until rails 3.2.13, #order.items was a simple array, and I could use ruby's own Array#insert method to insert new items wherever I wanted:
# after loading the original order, this code will build additional items
# and insert them in the nested edit order form with a default qty of 0
all_possible_item_names.each_with_index do |name, pos|
unless #order.items.find { |i| i.name == name }
#order.items.insert pos, Item.new(name: name, qty: 0)
end
end
In rails 4, on the other hand, #order.items is a ActiveRecord::Associations::CollectionProxy, and insert has a different meaning.
How can I accomplish in rails 4 what I used to be able to do in rails 3?
Convert it into array then insert
#order.items.to_a.insert pos, Item.new(name: name, qty: 0)

Find Value in Array of ActiveRecord Objects vs. Find Value from Repeated DB Calls

I would just like to return true, if my Array of Contact(s) (model) contains a Contact with id equal to some value. For example:
#contacts = Contact.all
#someval = "alskjdf"
find_val(#contacts, #someval)
def find_val(contacts, val)
#contact.each do |c|
if c.id == val
return true
end
end
return false
end
I have to do this repeatedly in my app (at most about 100 times for this particular actions), in order to exclude some data from an external API that has a list of contacts. Is this going to be too expensive?
I thought I might be able to do something faster, similar to ActiveRecord find on the array after it's been pulled down from the db, but can't figure that out. Would it be too expensive to call ActiveRecord like this?
Contacts.find_by_id(#someval)
The above line would have to be called hundreds of times... I figure iterating through the array would be less expensive. Thanks!
The best approach would be to index the contacts in a hash, using the contact id as the key after you retrieve them all from the db:
contacts = Contact.all.inject({}) {|hash, contact| hash[contact.id] = contact; hash }
Then you can easily get a contact with contact[id] in a performant way.
One way to reduce the amount of code you have to write to search the array is to open the array class and make a custom instance method:
class Array
def haz_value?(someval)
if self.first.respond_to? :id
self.select { |contact| contact.id == someval }.length > 0
else
false
end
end
end
Then, you can call #contacts.haz_value? #someval. In terms of efficiency, I haven't done a comparison, but both ways use Array's built-in iterators. It would probably be faster to create a stored procedure in your database and call it through ActiveRecord, here is how you can do that.

Order items in MongoDB according to the size of an array with MongoMapper?

I'd like to select a collection of items ordered based on the number of items within an array. Hopefully the following example will clarify my rather poor explanation:
class Thing
include MongoMapper::Document
key :name, String
key :tags, Array
end
I'd like to retrieve all Things ordered from those with the most tags to those with the least. The tags in this example are simply strings within the tags array. Basically I want something which means the same as this (but works):
Thing.all(:order => 'tags.count desc')
Is this possible?
The core server currently doesn't support computing the size of an array and then sorting by that. I think that your best bet for the moment would be to cache the array size on your own, and add an index on that field.
class Thing
include MongoMapper::Document
key :name, String
key :tags, Array
key :tag_size, Integer, :default => 0, :index => true
end
Then just add a callback to your model that updates tag_size on save.
If this is a feature you'd like to see in the core server, you can add a case here:
http://jira.mongodb.org/browse/SERVER

Resources