How to prevent multiple DB roundtrips for parents of has_many relations - ruby

I'm likely missing something here, but it seems like this is a performance oversight. I first noticed this when looking at the query logs of Errbit and noticing hundreds of queries for the same objects.
It seems that all the children of a has_many relation don't have a reference back to their parent object after being loaded through the relation. i.e. accessing parent.children.map &:parent will get the parent from the DB once for each child instead of being setup with the in-memory copy of parent
Example:
Using a very simple belongs_to / has_many setup:
class Person
include Mongoid::Document
field :name
has_many :posts, :inverse_of => :person
end
class Post
include Mongoid::Document
field :text
belongs_to :person, :inverse_of => :posts
end
Then, in the Rails console, a simple demonstration:
Loading development environment (Rails 3.2.12)
[1] pry(main)> tom = Person.create(:name => 'Tom')
[2] pry(main)> tom.posts.create(:text => 'stuff')
[3] pry(main)> tom.posts.create(:text => 'other stuff')
[4] pry(main)> Person.first.posts.map {|post| post.person.object_id}
=> [50687740, 50719060]
Note that last line, each person reference points to a different ruby object. I'm using ruby's object_id attribute to highlight the fact that these are literally different objects and this requires two round-trips to the database.
Why isn't the parent relation just a reference to the parent object after loading through the has_many relation?

Turns out that this is a known deficiency and adding this feature is planned for version 4.0.
In the mean time, you can drastically reduce duplicated queries and improve performance by enabling the Mongoid Identity Map

Related

Mongoid model with hardcoded data

I have a mongoid model
class MyMongoidModel
include Mongoid::Document
include Mongoid::Timestamps
field :name, :type => String
field :data_id, :type => Integer
has_and_belongs_to_many :the_other_model, :class_name => 'class_name_model'
has_many :model2
def self.all
[
#.... the hardcoded data that will never be changed
]
end
end
it's used by the other model and it uses them as well. However, it contains the data that won't be changed for a very long time, let's say, at all. Thus, I don't want to retrieve it from db, I want it to be hardcoded and, at the same time, I want it acts like a normal mongoid model. Using caching is not what I'm looking for.
I hope you understand what I mean.
How do accomplish it?
There's a great gem called active_hash that provides this functionality for ActiveRecord: defining a fixed set of data as models you can reference/relate to normal models, but have it defined in code and loaded in memory (not stored/retrieved from DB).
https://github.com/zilkey/active_hash
Interestingly, since Mongoid and ActiveRecord both share common ActiveModel basis, you may be able to use active_hash with a Mongoid document now.
For example:
class Country < ActiveHash::Base
self.data = [
{:id => 1, :name => "US"},
{:id => 2, :name => "Canada"}
]
end
class Order
include Mongoid::Document
include Mongoid::Timestamps
has_one :country
end

Querying and sorting embedded document in mongoid

I have three classes
class Post
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :user, :inverse_of => nil
embeds_many :comments, :as => :commentable
field :content, :type => String
end
class Commment
include Mongoid::Document
include Mongoid::Timestamps
belongs_to :user, :inverse_of => nil
embedded_in :commentable, :polymoriphic => true
end
class User
has_many :posts, :dependent => :destroy
field :name, :type => String
end
Whenever the user creates a new comment, I want to compare the contents of it with the latest comment that the user has made. Here is my code that fetches the latest comment by the user:
comments_user=[]
Post.where("comments.user_id" => user.id).
only(:comments).
each {|p| comments_user += p.comments.where(:user_id => user.id).to_a}
latest_comment = comments_user.sort_by{|comment| comment[:updated_at]}.reverse.first
The above code gives me the result but the approach taken is not efficient as I have to traverse through all the posts that the user has commmented to find the latest comment. If any, can anyone provide me a more efficient solution to this problem?
Simply speaking, Isn't there any way I can get all the comments made by this user?
This should fetch the latest user`s comment:
Post.where("comments.user_id" => user.id).order_by(:'comments.updated_at'.desc).limit(1).only(:comments).first
This is standard problem with embedding. It greatly improves some queries ("load post with all its comments"), but makes others non-efficient/impractical ("find latest comment of a user").
I see two options here:
Keep embedding and duplicate data. That is, when user makes a comment, embed this comment to a post document and to the user document. This data duplication has its drawbacks, of course (what if you need to edit comments?);
Stop embedding and start referencing. This means that comment is now a top level entity. You can't quickly load a post with comments, because there are no joins. But other queries are faster now, and there's no data duplication.

Is there a way in MongoMapper to achieve similar behavior as AR's includes method?

Is there a feature equivalent in MongoMapper to this:
class Model < ActiveRecord::Base
belongs_to :x
scope :with_x, includes(:x)
end
When running Model.with_x, this avoids N queries to X.
Is there a similar feature in MongoMapper?
When it's a belongs_to relationship, you can turn on the identity map and run two queries, once for your main documents and then one for all the associated documents. That's the best you can do since Mongo doesn't support joins.
class Comment
include MongoMapper::Document
belongs_to :user
end
class User
include MongoMapper::Document
plugin MongoMapper::Plugins::IdentityMap
end
#comments = my_post.comments # query 1
users = User.find(#comments.map(&:user_id)) # query 2
#comments.each do |comment|
comment.user.name # user pulled from identity map, no query fired
end
(Mongoid has a syntax for eager loading, but it works basically the same way.)

Rails 3 - Active_admin select nested object in many to many

I have two models. Deals and Stores.
I want to add already created Deals when creating a Store.
I want to add already created Sores when creating a Deal.
I am trying to use f.has_many, but I can't make it working.
My relationship is built using has_and_belongs_to_many :deals and has_and_belongs_to_many :stores (in the models)
My store custom form has the following:
f.inputs "Deals" do
f.has_many :deals do |deal|
deal.input :id, :as => :select, :include_blank => false
end
end
I don't know how to add deals to the store.
Any help?
Just checking, but do you have an
accepts_nested_attributes_for :deals #, :allow_destroy => true
declaration in your Store model, and an
accepts_nested_attributes_for :stores
declaration in your Deals model?
You may want to check these two pages:
http://apidock.com/rails/ActiveRecord/NestedAttributes/ClassMethods/accepts_nested_attributes_for
https://github.com/justinfrench/formtastic (search in the page for accepts_nested_attributes_for)

Rails 3 relationship definition problem?

I'm new to Rails 3, i'm creating a web app that use active admin, i get a problem with him, and asked for help inside github plugin, someone told me maybe a relationship definitions.
I really dont know what is that, i have nested elements and in active admin i want to make nested element independent.
but now, im totally lost. what i missed? thanks.
here is my model definition
class Company < ActiveRecord::Base
before_save :getsubdomain
has_attached_file :logo, :styles => { :thumb => '150x150>', :medium => '250x250>', :normal => '350x350>'}
has_many :buildings
accepts_nested_attributes_for :buildings
end
Building model
class Building < ActiveRecord::Base
belongs_to :companies
end
in my db, i have colum company_id in buildings table.
Here the error message i get..
NameError in Admin/buildings#index
Showing /Library/Ruby/Gems/1.8/bundler/gems/active_admin-c3a1ffa98072/app/views/active_admin/resource/index.html.arb where line #1 raised:
uninitialized constant Building::Companies
Rails.root: /Users/username/Sites/myapps
Request
Parameters:
{"order"=>"id_desc"}
Response
Headers:
None
thanks for your help
belongs_to expects a singular name. Try
belongs_to :company

Resources