For performance reason, I use as often as possible the only() keyword when writing up a mongoid query in order to specify the fields I want to load.
The usual suspect, is for instance when I want a user's email of all my admins only for display purposes.
I would write:
User.where(:groups => :admins).only(:email).each do |u|
puts u.email
end
I do this because my user model are quite full of a lot of data that I can gladly ignore when listing a bunch of emails.
However, now let imagine, that my users are referenced via a Project model, so that for each project I can do: project.user. Thanks to mongoid's lazy loading, my object user will only get instantiated (and queried from the DB) when I call upon the reference.
But what if I want to list all the email of the owner of all admin project for instance ?
I would write this:
Project.where(:admin_type => true).each do |p|
puts p.user.email
end
The major problem here is that doing this, I load the entire user object for each projects, and if there are lots of project matching the query that could get pretty heavy. So how do I load only the emails ?
I could do this:
User.where(:_id => p.user_id).only(:email).first.email
But this obviously defeat the purpose of the nice syntax of simply doing:
p.user.email
I wish I could write something like: p.user.only(:email).email, but I can't. Any ideas ?
Alex
Answer from creator of Mongoid. It's not possible yet. It's been added as feature request.
I think you need to denormalize here. First of all, read A Note on Denormalization.
You can implement denormalization by self using mongoid events or use great mongoid_denormalize gem. It pretty straight and after implementing it you could use p.user_email or something in your queries.
Related
I am working on a rails application and i have 3 different user types. These users are potentially very different, so i created models for each of them. Now, they should be able to login thru a single form. So basically i want to say something like 'find_by_email("some_email")', but search over all three tables. It seems, though, that Rails expect you to call 'find_by' with a specific model, like Admin.find_by(). Any suggestions?
Try something like this and assuming that that the email is unique across all the tables
[Model1, Model2, Model3].each do |model|
break if model.find_by_email("email#email.com").present?
end
Hopefully this is early in your development, but the current structure may not be the best possible route. How many different columns are NOT shared by each of the user types? You may want to use a "user role" system, and have that simply be an extra column on your user table.
Than, you can use something like CanCan to manage those roles and what/where they may access.
I've got three nested models: user has many plates and plate has many fruits. I also have a current_user helper method that runs in the before filter to provide authentication. So when I get to my controller, I already have my user object. How can I load all the user's plates and fruits at once?
In other words, I'd like to do something like:
#plates = current_user.plates(include: :fruits)
How can I achieve this?
I'm using Rails 3.1.3.
You will probably want to use the provided #includes method on your relation. DO NOT USE #all unless you intend to immediately work through the records, it will immediately defeat many forms of caching.
Perhaps something like: #plates = current_user.plates.includes(:fruits)
Unfortunately, there are portions of the Rails API that are not as well documented as they should be. I would recommend checking out the following resources if you have any further questions about the Rails query interface:
Query Interface Guide
ActiveRecord::Relation Walkthrough (screencast)
The query interface is possibly the most difficult part of the Rails stack to keep up with, especially with the changes made with Rails 3.0 and 3.1.
You can do
ActiveRecord::Associations::Preloader.new([current_user], :plates => :fruit).run
To eager load associations after current_user was loased. The second argument can be anything you would normally pass to includes: a symbol, an array of symbols, a hash etc
#plates = current_user.plates.all(:include => :fruits)
should do it.
I have a number of custom find_by_sql queries in Rails. I would like to use eager loading with them but there doesn't seem to be a good way to do this.
I have seen the eager_custom.rb file floating around and it doesn't seem to work with Rails now. It appear Rails does eager loading differently now, using 2 queries (the regular query plus a query where the 'id IN' the ids from the first query), instead of the single join query used in the past.
My question is if I do a custom SQL query, then do 'id IN' query, is there a way to add back associated objects into the initial query results?
For example I have topics loaded with find_by_sql, then I find topic images where the topic id is in the topics ids, is there a way to add the images manually back to the topics?
Thanks
As you noticed, in Rails 2.1 a new kind of eager/pre-loading was introduced which uses multiple queries with id IN (...). This method is usually faster, especially when there are multiple associations being pre-loaded. You can use this functionality manually with find_by_sql by using the preload_associations class method inherited from ActiveRecord (not recommended). For example:
class Person
def self.find_a_special_group
people = find_by_sql("...")
preload_associations(people, [:jobs, :addresses])
return people
end
end
The preload_associations method is protected, so you must call it from within the class, and it takes (1) an array of objects, (2) an array, hash, or symbol of associations (same format as find's :include option), and (3) an options hash. See the documentation for the ActiveRecord::AssociationPreload::ClassMethods module for more details.
However, having said all of that, this technique is certainly undesirable as the Rails documentation discourages programmers from using preload_associations directly. Are you sure you have to use find_by_sql? Are you sure you know all of the options find takes? (:select, :from, :joins, :group, :having, etc) I'm not saying you don't need find_by_sql, but it might be worth a few minutes to make sure.
Normally rails loads data with a :select => "*" from the database. I know I could change this. But I'd like to go another way: always only select "id" and load the attributes automatically later on when they are needed.
Example:
accessing user.description should if it's been loaded yet. if not, trigger a "SELECT description FROM users WHERE id=#{self.id}" and set it.
Anyone know if there's a rails plugin which does this? Or how to implement it?
Thanks,
Corin
I wrote a Rails plugins that does exactly this lazy_columns. Notice that a much better approach is to create a new model with the big columns you want to load on demand (since Rails load related objects lazily by default).
A quick google search turned up this, but I'm with glongman. I can't imagine what kind of performance issue would require this...
It is wise to just refactor your main "huge blob" fields into a separate model (like BookBody) which is usually not needed when operating with models in bulk. Alternatively, you can use the :select option on finders
BookWithHugeBlobOfText.find(:first, :select=>"only,small,columns")
Records selected that way will be readonly since the Rails philosophy says (and rightfully so!) that you need to have all data on the model to validate it. Lazy loading would be a nice to have, but as it stands now I would discourage you from using monkeypatch plugins for this.
Another option would be to create a SQL view which would only contain the lightweight fields and run your ops from there.
I've come across an oddity in ActiveRecord's #relationship_ids method (that's added automatically when you declare 'has_many'), which saves immediately for existing records, which is causing me some issues, and I wonder if anyone had any useful advice.
I'm running Rails 2.3.5.
Consider this simple scenario, where an article has_many tags, say:
a = Article.first
a.name = "New Name" # No save yet
a.author_id = 1 # No save yet
a.tag_ids = [1,2,3] # These changes are saved to the database
# immediately, even if I don't subsequently
# call 'a.save'
This seems surprising to me. It's specifically causing problems whilst trying to build a preview facility - I want to update a bunch of attributes and then preview the article without saving it - but in this instance the tag changes do get saved, even though no other fields do.
(Of possible relevance is that if 'a' is a new article, rather than an existing one, things behave as I'd expect - nothing is saved until I call 'a.save')
I have a fairly nasty workaround - I can override the tag_ids= method in my model to instead populate an instance variable, and actually save the related models in a before_save callback.
But I'd love to know of a simpler way than me having to do this for every model with a has_many relationship I'd like to create a preview facility for.
Does anyone have any fixes/workarounds/general advice? Thanks!
There's a reason things are this way. It's called foreign keys. In a has many relationship, the information that links to the model that has many is stored outside of that model as a foreign key.
As in Articles, has many tags. The information that links a tag to an article is stored either in the tags table or in a join table. When you call save on an article you're only saving the article.
Active record modifies those other records immediately. Except in the case where you're working with a new article that hasn't been saved yet. Rails will delay creating/updating the associated records if it doesn't know which id to place in the foreign key.
However, if you're modifying existing records, the solution you've decided on is really all that you can do. There's an even uglier hack using accepts_nested_attributes_for, but it's really not worth the effort.
If you're looking to add this behaviour to many models but not all models, you might want to consider writing a simple plugin to redefine the assigment the method you need and add the call back in a single class method call. Have a look at the source of something like acts_as_audited to see how it's done.
If you're looking to add this behaviour to all models, you can probably write a wrapper for has_many to do that.