Associations return Array instead of Query?

129 views
Skip to first unread message

brandonweiss

unread,
Apr 23, 2011, 1:20:02 PM4/23/11
to MongoMapper
I'm new to MongoMapper (coming from Mongoid), so forgive my ignorance
if this is by design, but I would have expected an association to be
able to be further queried upon. So for instance:

Product.first.releases.where(something)

That fails for me because releases returns an Array, instead of a
Query. I can't tell if this is intentional, or if I'm doing something
wrong? And if it's intentional, what's the right way of doing what I'm
trying to do?

Jon Kern

unread,
Apr 24, 2011, 8:48:06 AM4/24/11
to mongo...@googlegroups.com
look at http://mongomapper.com documentation

you can also see some examples here: http://technicaldebt.com/?cat=30

jon
blog: http://technicaldebt.com
twitter: http://twitter.com/JonKernPA


brandonweiss said the following on 4/23/11 1:20 PM:

brandonweiss

unread,
Apr 24, 2011, 12:23:11 PM4/24/11
to MongoMapper
While I appreciate your answer, that really didn't answer my question.
I realize this might sound like an obvious, inane question, but I've
already read the documentation and I checked out your site as well,
and while it's entirely possible I overlooked it, I can't seem to find
my answer anywhere.

All I need to know is, are associations supposed to return Arrays when
called? Because I would have assumed they'd return Queries. And if
they do return Arrays, how are you supposed to further query on
associations?


On Apr 24, 7:48 am, Jon Kern <jonker...@gmail.com> wrote:
> look athttp://mongomapper.comdocumentation

Jamie Orchard-Hays

unread,
Apr 24, 2011, 3:12:12 PM4/24/11
to mongo...@googlegroups.com
It returns an Array that has been enhanced with other methods and can be enhanced by you in your models.

class Substance
include MongoMapper::Document

many :datapoints, :order => "name", :extend => DatapointsExtensions
end

Here, I've put my extensions in a module:

module DatapointsExtensions
def energy
find_all{ |d| d.name =~ /^energy_/ }
end
def env
find_all{ |d| d.name =~ /^env_/ }
end
def hum
find_all{ |d| d.name =~ /^hum_/ }
end
.....
end

You declare them right on the many declaration:

many :datapoints, :order => "name", do
def energy
find_all{ |d| d.name =~ /^energy_/ }
end
def env
find_all{ |d| d.name =~ /^env_/ }
end
def hum
find_all{ |d| d.name =~ /^hum_/ }
end
.....
end

You'll have to dig in to the source/docs to find all the available methods.

Jamie

> --
> You received this message because you are subscribed to the Google
> Groups "MongoMapper" group.
> For more options, visit this group at
> http://groups.google.com/group/mongomapper?hl=en?hl=en

Jon Kern

unread,
Apr 24, 2011, 10:45:56 PM4/24/11
to mongo...@googlegroups.com
ok, here you go...

>> Patient.where(:last_name=>/john/i).class
=> Plucky::Query
>> Patient.where(:last_name=>/john/i).all.class
=> Array
>> Patient.where(:last_name=>/john/i).all.count
=> 1
>> Patient.where(:last_name=>/john/i).first.class
=> Patient
>> Patient.sort(:created_at.desc).first.class
=> Patient

Since you didn't present any model code, I am going to assume "releases"
is a scope.

class Encounter
include MongoMapper::Document
...
# Associations :::::::::::::::::::::::::::::::::::::::::::::::::::::
many :events, :limit => 30, :order => 'msg_timestamp desc' do
...
def images
where(:type =>
[EventConstants::EventType.to_text(EventConstants::EventType::IMAGE)]).order(:created_at.desc).all
end

def charts
where(:type =>
[EventConstants::EventType.to_text(EventConstants::EventType::ED_SUMMARY)],
:file_version.in => ["P", "F"]).order(:created_at.desc).all
end

def admits
all(:type =>
[EventConstants::EventType.to_text(EventConstants::EventType::ADMIT)])
end
end
...
end

# For a given encounter
enc=Encounter.find('4dadad188951a20727000160')
>> enc.events.images.count
=> 7
>> enc.events.images.class
=> Array
>> enc.events.images.first
=> #<Event created_at: Tue, 19 Apr 2011 15:41:15 UTC +00:00,
file_version: nil, title: "Image", updated_at: Tue, 19 Apr 2011 15:41:15
UTC +00:00, file_location: "4DBrainPerfusion-6.jpg", msg_timestamp: Tue,
12 Apr 2011 19:17:00 UTC +00:00, _id:
BSON::ObjectId('4dadad1b8951a2072700016f'), type: "Image", encounter_id:
BSON::ObjectId('4dadad188951a20727000160')>


brandonweiss said the following on 4/24/11 12:23 PM:

brandonweiss

unread,
Apr 24, 2011, 11:03:09 PM4/24/11
to MongoMapper
But that's just Ruby. Right? You're getting an array from the
association and then calling enumerable methods on it.

Maybe I'm not being clear, but what I can't figure out how to do is
run actual Mongo queries on an association. Assuming a Product embeds
many Releases, like this:

Product.first.releases.where(:released_at.gt => 5.days.ago)

Yes, if I wanted to do this in Ruby I could, but it's way faster to
actually run a Mongo query. How do I do that?

Jon Kern

unread,
Apr 24, 2011, 11:05:20 PM4/24/11
to mongo...@googlegroups.com
And to reply a bit further to your original post... Let's break it down,
one bit at a time:

Product.first # Class. This would be an instance of Product
Product.first.releases # Array. This would be a return value of an
array, assuming Product <>----> * Release
Product.first.releases.where(something) # Array. This doesn't change the
above... merely adds a restrictive query clause

I am not sure why, but for me it seems more logical to start my clauses
with the where, and narrow them down further, or modify them... It is
much more "loose" than say a SQL SELECT query that requires things in
proper order... I would tend to write my queries in more or less this
fashion:

ModelClass.where(some criteria).[sort | order | another where clause |
fields | limit].[all | first | paginate]

I can picture one of those "man page" or SQL style of fancy ways to show
you how you can construct a mongomapper query given all the combinations
of options for each "position" in the query...

My advice is to make the query look as "natural" as possible in terms of
how you might read it aloud.

Product.releases.where(:major.gt => 1).sort(:minor.desc).first # Get the
latest 1.x release

(And, if the releases where clause query is common, you can create a
named scope.)


brandonweiss said the following on 4/23/11 1:20 PM:

Jon Kern

unread,
Apr 24, 2011, 11:25:42 PM4/24/11
to mongo...@googlegroups.com
i think somewhere in my code, I showed that (http://technicaldebt.com/?p=906)

you have two choices:
1) on the fly
2) as an Association Extension

either way, the same code:

Product.releases.where(:released_at.gt => 5.days.ago)

OR:

class Product
 ...
 many :releases
   def recent
     where(:released_at.gt => 5.days.ago).all
    end
 end
  ...
end
brandonweiss said the following on 4/24/11 11:03 PM:

brandonweiss

unread,
Apr 24, 2011, 11:41:32 PM4/24/11
to MongoMapper
OK, so firstly, thanks! I really appreciate the lengthy examples. And
also let me apologize for not showing code initially; it probably
would have been easier to understand that way. Here's the abbreviated/
relevant portions:

class Product
include MongoMapper::Document

many :releases
end

class Release
include MongoMapper::EmbeddedDocument

key :released_at, Date

embedded_in :product
end


OK. So I've got a Product model which embeds many Releases. Now it
doesn't matter what I do, if I try to chain any query off of the
releases association I get an error.

Product.first.releases.where(:released_at.gt => Time.now)
NoMethodError: undefined method `where' for #<Array:0x00000102bc37e0>

This is not what I'd expect, but it makes sense. Obviously there's no
where method on Array. But what I couldn't figure out is if I'm doing
something retarded, or if this is a weird bug. Because according to
the code you just posted, this should work, right?

Jon Kern

unread,
Apr 25, 2011, 1:20:04 AM4/25/11
to mongo...@googlegroups.com
hmmm the first thing I did was comment this out:
    # embedded_in :product
However, it works with it in or out... Therefore, I would leave it out.

Here's what I did and it worked... https://github.com/JonKernPA/mongo_examples/tree/master/product_release

  it "should allow querying by age" do
    list = Product.all(:conditions => {"releases.released_at" => {"$lt" => 5.days.ago}})
    list. size.should == 2
    list = Product.all(:conditions => {"releases.released_at" => {"$lt" => 25.days.ago}})
    list.count.should == 0
  end
I think this can be further refined and better sent to the various classes... But any times I will use a test to figure out what I will need in my code. But now it is late :=\
brandonweiss said the following on 4/24/11 11:41 PM:

brandonweiss

unread,
Apr 25, 2011, 2:04:27 AM4/25/11
to MongoMapper
embedded_in is necessary in order to move back up the tree (e.g.
release.product).

Well, the example you wrote does not have the same effect as what I
wrote. You're now querying on the association, but you're returning
Product objects. The example code I wrote (if it worked), would return
Release objects.

On Apr 25, 12:20 am, Jon Kern <jonker...@gmail.com> wrote:
> hmmm the first thing I did was comment this out:
>     # embedded_in :product
> However, it works with it in or out... Therefore, I would leave it out.
> Here's what I did and it worked...https://github.com/JonKernPA/mongo_examples/tree/master/product_release  it"should allow querying by age"do
>
>     list=Product.all(:conditions=>{"releases.released_at"=>{"$lt"=>5.days.ago}})
>
>     list.size.should==2
>
>     list=Product.all(:conditions=>{"releases.released_at"=>{"$lt"=>25.days.ago}})
>
>     list.count.should==0
>
>   endI think this can be further refined and better sent to the various classes... But any times I will use a test to figure out what I will need in my code. But now it is late :=\jon blog:http://technicaldebt.comtwitter:http://twitter.com/JonKernPA
> brandonweiss said the following on 4/24/11 11:41 PM:OK, so firstly, thanks! I really appreciate the lengthy examples. And also let me apologize for not showing code initially; it probably would have been easier to understand that way. Here's the abbreviated/ relevant portions: class Product include MongoMapper::Document many :releases end class Release include MongoMapper::EmbeddedDocument key :released_at, Date embedded_in :product end OK. So I've got a Product model which embeds many Releases. Now it doesn't matter what I do, if I try to chain any query off of the releases association I get an error. Product.first.releases.where(:released_at.gt => Time.now) NoMethodError: undefined method `where' for #<Array:0x00000102bc37e0> This is not what I'd expect, but it makes sense. Obviously there's no where method on Array. But what I couldn't figure out is if I'm doing something retarded, or if this is a weird bug. Because according to the code you just posted, this should work, right? On Apr 24, 10:05 pm, Jon Kern<jonker...@gmail.com>wrote:And to reply a bit further to your original post... Let's break it down, one bit at a time: Product.first # Class. This would be an instance of Product Product.first.releases # Array. This would be a return value of an array, assuming Product <>----> * Release Product.first.releases.where(something) # Array. This doesn't change the above... merely adds a restrictive query clause I am not sure why, but for me it seems more logical to start my clauses with the where, and narrow them down further, or modify them... It is much more "loose" than say a SQL SELECT query that requires things in proper order... I would tend to write my queries in more or less this fashion: ModelClass.where(some criteria).[sort | order | another where clause | fields | limit].[all | first | paginate] I can picture one of those "man page" or SQL style of fancy ways to show you how you can construct a mongomapper query given all the combinations of options for each "position" in the query... My advice is to make the query look as "natural" as possible in terms of how you might read it aloud. Product.releases.where(:major.gt => 1).sort(:minor.desc).first # Get the latest 1.x release (And, if the releases where clause query is common, you can create a named scope.) jon blog:http://technicaldebt.comtwitter:http://twitter.com/JonKernPAbrandonweiss said the following on 4/23/11 1:20 PM:I'm new to MongoMapper (coming from Mongoid), so forgive my ignorance if this is by design, but I would have expected an association to be able to be further queried upon. So for instance:Product.first.releases.where(something)That fails for me because releases returns an Array, instead of a Query. I can't tell if this is intentional, or if I'm doing something wrong? And if it's intentional, what's the right way of doing what I'm trying to do?

Brian Hempel

unread,
Apr 25, 2011, 8:53:51 AM4/25/11
to mongo...@googlegroups.com
I think part of MongoMapper's philosophy is to always work with root documents. So when you save, the whole root document is saved, when you query, a whole root document is returned. Consequently, embedded documents are only loaded from the database by loading their parent. So, embedded associations always return arrays of embedded documents, not queries--the embedded docs were loaded in Ruby when the parent was loaded, why go back to the database? Embedded docs can then be managed with Ruby's enumerable, which unless you have a gadzillion embedded docs will probably be faster than Mongo (but I haven't benchmarked it).

To query for the products who have a release later than a certain date, you can do this:

Product.where( :"releases.released_at".gt => Time.now )

To get the actual release date you need, you do use Ruby's enumerable:

my_product.releases.select { |release| release.released_at > ... }

Wrapping it all together, if you want all releases after a certain date:

Product.where( :"releases.released_at".gt => Time.now ).map(&:releases).flatten.select { |release| release.released_at > Date.today }

So that's a bit convoluted, but that's why the recommendation is to "only embedded when the embedded documents will *always* be shown in the context of their parent". And, ah, the Time.now vs. Date.today thing is a side effect of MongoMapper providing a "Date" type while Mongo only has time...Date may be deprecated consequently, see: https://github.com/jnunemaker/mongomapper/issues/249

Peace,
Brian

Jon Kern

unread,
Apr 25, 2011, 9:33:33 AM4/25/11
to mongo...@googlegroups.com
Brian, thanks for the alternate query style...
Brandon, I added it to the example:
list = Product.where( :"releases.released_at".lt => 6.days.ago
).map(&:releases).flatten.select { |release| release.released_at <
6.days.ago }

And to underscore Brian's point... If you need to do heavy duty querying
and manipulation on a child association, consider NOT making it embedded.


Brian Hempel said the following on 4/25/11 8:53 AM:

Jon Kern

unread,
Apr 25, 2011, 9:38:00 AM4/25/11
to mongo...@googlegroups.com
Brandon,

in "words" what are you trying to query with your non-functioning query?
Product.first.releases.where(:released_at.gt => Time.now)

Are you trying to find the releases newer than some time for the first
product document in the database?


brandonweiss said the following on 4/24/11 11:41 PM:

Jamie Orchard-Hays

unread,
Apr 25, 2011, 10:55:12 AM4/25/11
to mongo...@googlegroups.com
Glad you posted this example. I never thought about prefixing a string with a colon to make it a Symbol so you could get the dot extension syntax back. Nice, even if a bit clunky to look at. I'd been reverting to regular Mongo syntax when reaching into a doc's keys:

Product where("releases.released_at" => {"$gt" => Time.now})

Definitely a slap the forehead moment.

brandonweiss

unread,
Apr 25, 2011, 12:14:25 PM4/25/11
to MongoMapper
Right, I get that philosophy, I've been using Mongo for a while now.
But in all the other ODM's I've used it's been further possible to
query an association. For instance, if we take my scenario, I need to
loop through all the products, and for each product, output a small
subset of the releases (not all of them). In other ODMs I could add a
where query on the association just like you can on the root
collection/model. In MongoMapper, that doesn't work. This thread was
an attempt to find out why. But unfortunately the answers I got ranged
from "you can" all the way to "you can't". Which made it unclear
whether I was experiencing a bug or not.

But, I actually have the answer now. It's pretty stupid, because I've
done so much querying with the raw Mongo driver that I can't believe I
never made the connection. So the short answer is it's impossible to
run an actual query on an embedded association. Which most of you (and
I) already know. If you'd asked me to do it in the raw Mongo driver I
would have told you it's impossible. But I've been using ActiveRecord
for so long that I never really thought about how the ODMs like
Mongoid do it. What I didn't realize is that they aren't. They're just
preserving the interface for consistency and dropping down to Ruby
enumerable methods behind the scenes. So when you do
Product.first.releases.where(something) it's just doing a select. So
the disconnect happened when I switched to MongoMapper. MongoMapper is
just being more honest about it and returning array, thus removing the
query interface on the embedded association because it doesn't really
exist anyways.

Sorry about the confusion. I appreciate everyone's help!
Reply all
Reply to author
Forward
0 new messages