1

Since the Mongoid API did not make the MongoDB $sample operation visible, I had to manually run a query with the Mongo driver, and I don't know what to do with the results.

I have different classes/collections that adhere to some common interface (I did not want to use inheritance for several reasons), and I am trying to render those as a single collection. I have a code that samples from those three classes

entries = [Class1, Class2, Class3].inject([]) do |array, clazz|
  entries << clazz.collection.aggregate([ { '$sample': { size: 10 } } ])    
end

This gives me an array of three different Mongo::Collection::View::Aggregation. I'd like to somehow merge those and be able to instanciate the objects so I can use them in my views (with cells for example)

<%= cell(:profile, collection: entries) %>

Using entries.to_a will return an array of hashes and not an array of (model) objects. I was hoping it would be the case and that I would then use cells builder to handle the rest of subtle differences between the models

builds do |model, options|
    case model
    when Class1; Class1Cell
    when Class2; Class2Cell
    when Class3; Class3Cell
  end

EDIT :

I can actually still use to_a and use the key _type to find the corresponding Constant/Model. Now the newt question, is how to instanciate a model with the hash, that does not return true on new_record?

sample = entries.to_a.first
  instance = Utility.resolve_class(sample[:_type]).new(entry_hash)
  # Problem is...
  instance.new_record? # => returns true, but since it comes from the DB it means it has already been persisted so it should return false.
Cyril Duchon-Doris
  • 12,964
  • 9
  • 77
  • 164
  • 1
    It would seem the most practical thing here would be to do the `to_a` and then re-instantiate each class based on the data. There certainly is no way in which the "server" can return all results in one response, and despite the fact that a sole pipeline stage of `$sample` does not actually "change anything", the usual assumption of drivers is that `.aggregate()` calls "do in fact change data" from what is defined in the model. Hence why they don't return the class types. – Neil Lunn Apr 18 '16 at 01:08
  • @Neil Yes I probably have to do that. Then my next question is how do I instanciate data from a Hash returned by the DB ? I have tried `MyModel.new` but then it is considered as a new record. – Cyril Duchon-Doris Apr 18 '16 at 07:30
  • This is actually a common "ORM/ODM" problem, since people become reliant on the "data classes" when they actually want a "separation" of "normal classes" and data persistence. Some "ORM/ODM" solutions play nicely with this and just let you keep memory instances, but then again some don't. Mongoid appears to fall in the latter category, and since it generally follows ActiveRecord with rails, then that's not that surprising. This is generally why I prefer plain classes with added traits for data persistence. Most other software gets too opinionated. – Neil Lunn Apr 18 '16 at 07:37
  • @NeilLunn thanks for your clarifying comments, however I did a bit of digging and found the `instantiate` method which does the job, as stated in my answer. I thought you might be interested in knowing that, hence the notification ;-) – Jaffa Jul 11 '18 at 11:54

3 Answers3

1

Cells works on any PORO. So the easiest way to achieve what you want is to create a class that represents the data you want within your models files. Just create this as plain ruby class. You can hide your data query method for creating the aggregations and returning a set of classes as a class method.

Something like (you'll want to tidy this up, it is just a hack to get you started):

# some PORO
class Record
   attr_accessor :field_1, :field_2, :field_3

   def self.build
       # your existing code
       entries = [Class1, Class2, Class3].inject([]) do |array, clazz|
        entries << profile_collection.collection.aggregate([ { '$sample': { size: 10 } } ])    
       end

       array_of_objects = []

       # now for each record within the aggregate create an object
       entries.each do |obj|
          new_poro = self.new
          obj.keys.each do |key|
             new_poro.self.instance_variable_set(key, obj[key])
          end
          array_of_objects.push new_poro
       end  
       return array_of_objects 
    end
end


# to run it in your controller
@records_in_objects_for_cells = Record.build

# in your views
<%= cell(:record, collection: records_in_objects_for_cells %>
ABrowne
  • 1,574
  • 1
  • 11
  • 21
  • Hmm isn't that like reinvented the wheel ? The class type is contained in the `_type` key of the Hash so it isn't a problem to find the right class to instanciate the data from the DB, my only problem is that the newly created record returns `true` on `record.new_record?` and that messes up my cells (My cells use the value of `new_record?` to display different markup) – Cyril Duchon-Doris Apr 18 '16 at 21:43
  • It is completely a reinvention. Your issue as you state was never whether you could call new and pass in the record (or that was the assumption). However dependent on the aggregate pipeline created you may not have passed _type as an projected field. Either way, the above merely provides a workaround to the problem in hand, which is that you need a level of abstraction across these 3 collection due the architectural choices you have made. If you could provide more details over your architectural choices on separating them, and what you are trying to achieve I can have a rethink. – ABrowne Apr 18 '16 at 21:56
  • The other potential option would be to create a mongoid class that represents the common parts of it and include a mongoid::override class that you create. The override class you create will override the common get & setters for class enabling you to populate the class from all 3 collections, and even search using the common where/find_by queries. I have done this before, it's more effort than the above, but abstracts the pain of the multiple collections to one. – ABrowne Apr 18 '16 at 22:00
  • I am using `aggregate([ { '$sample': { size: 10 } } ])` because there is no `class.sample` method in the Mongoid API (the $sample was added very recently to MongoDB, and hasn't made it to the drivers apparently). [mongoid-random-document](http://stackoverflow.com/questions/7759250/mongoid-random-document) – Cyril Duchon-Doris Apr 18 '16 at 22:05
1

To answer your edited question, you could just set it to false. The variable is new_record as seen here (http://www.rubydoc.info/github/mongoid/mongoid/Mongoid/Stateful:new_record%3F).

So:

r = MongoRecord.find_by(x:y)
e = r.new(e)
e.new_record?
=> true
e.new_record = false
e.new_record? 
=> false

MongoId uses this flag to know whether it's persisted. It uses the _id to know which record to update if a persistence event occurs.

ABrowne
  • 1,574
  • 1
  • 11
  • 21
  • Oh sorry, I thought the internals of this `new_record?` method was more complicated, I didn't even consider the possibility that it was just an instance variable. Do you think with this it is safe to assume the object will behave exactly as a normally instanciated object ? I was thinking that maybe `on: create` callbacks, or callbacks related to persisting an object for the very first time could still fire. – Cyril Duchon-Doris Apr 18 '16 at 22:40
  • It does. The persistence flag of new_record merely informs the persistence layer on whether to use update or create when saving. I have tested and it's possible to create an a new object from the attributes of one already stored. If you save, it'll create a new one and set a new _id. If you set the new_record to false, and save it updates the existing record. – ABrowne Apr 19 '16 at 08:00
1

The best approach would be to use Mongoid::Document's class method instantiate:

Person.instantiate(document)
# or even
Person.instantiate({firstname: 'John', lastname: 'Doe'})

Or for your example:

entries = [Class1, Class2, Class3].inject([]) do |array, clazz|
  entries << clazz.collection.aggregate([
    { '$sample': { size: 10 } }
  ]).map do |document|
    clazz.instantiate(document)
  end    
end

As stated in the description:

Instantiate a new object, only when loaded from the database or when the attributes have already been typecast.

Moreover, it takes selected_fields as a second parameter which is useful to let it know only the given fields have been loaded from the database.

Jaffa
  • 12,442
  • 4
  • 49
  • 101