Preload associations with an existing scope - ruby-on-rails

I wrote a piece of code to eager load some associations from an already loaded collection:
#articles= Article.find_by_sql("SELECT * FROM articles WHERE blabla")
ActiveRecord::Associations::Preloader.new(#articles, {:comments => {:user => :permissions}}).run
I have a scope defined in my Article class ready to eager load some articles associations at several levels:
class Article << ActiveRecord::Base
[...]
scope :eager_loading_for_comments, includes(:comments => {:user => :permissions})
end
Am I able to use this scope in my first code ? A way like that:
ActiveRecord::Associations::Preloader.new(#articles, :eager_loading_for_comments).run
Or:
ActiveRecord::Associations::Preloader.new(#articles, Article.eager_loading_for_comments).run
Thank you !

Why not just call the scope as you would any other, e.g.
#articles = Article.eager_loading_for_comments
?

How about chaining the where onto the scope:
Article.eager_loading_for_comments.where("blah blah blah")

Related

Rails - ActiveRecord Reputation System scoped query issue

I get the following error whenever I try to execute find_with_reputation or count_with_reputation methods.
ArgumentError: Evaluations of votes must have scope specified
My model is defined as follows:
class Post < ActiveRecord::Base
has_reputation :votes,
:source => :user,
:scopes => [:up, :down]
The error raises when I try to execute for example:
Post.find_with_reputation(:votes, :up)
or
Post.find_with_reputation(:votes, :up, { order: "likes" } )
Unfortunately, the documentation isn't very clear on how to get around this error. It only states that the method should be executed as follows:
ActiveRecord::Base.find_with_reputation(:reputation_name, :scope, :find_options)
On models without scopes ActiveRecord Reputation System works well with methods such as:
User.find_with_reputation(:karma, :all)
Any help will be most appreciated.
I've found the solution. It seems that ActiveRecord Reputation System joins the reputation and scope names on the rs_reputations table. So, in my case, the reputation names for :votes whose scopes could be either :up or :down are named :votes_up and :votes_down, respectively.
Therefore, find_with_reputation or count_with_reputation methods for scoped models need to be built like this:
Post.find_with_reputation(:votes_up, :all, { conditions: ["votes_up > ?", 0] })
instead of:
Post.find_with_reputation(:votes, :up, { conditions: ["votes_up > ?", 0] })
Note that you'll need to add the conditionsoption to get the desired results, otherwise it will bring all the records of the model instead of those whose votes are positive, for example.

Eager loading and map in rails 3

If I wanted to eagerly load a collection in rails and render it in json, I would have to do something like this.
#photos = #event.photos.to_json(:include =>
{:appearances => {:include => :person}}
)
What if I wanted to map this collection? As you can see it's no longer a collection, but a json string. Prior to this necessary eager loading, I was doing the following:
#photos = #event.photos.map{|photo|
photo['some_funky_stuff'] = photo.funky_calculation
photo
}
But, I can't seem to be able to do the two things together:
#event.photos.map{|photo|
photo['some_funky_stuff'] = photo.funky_calculation
photo
}.to_json(:include =>
{:appearances => {:include => :person}}
)
The above does not show 'appearances' ( the eagerly loaded join record )... How do I do these two together? Many thanks!
You may have the term "eager loading" mixed up a little bit. As previous answers have mentioned, you need to use it on the association for it to be eager loaded. However, when you use :include in the to_json call, you will still end up with the same result, no matter if it is eager or not.
But to answer your question, for the to_json method to both include the appearances and the funky_calculation you can combine it with :methods instead. Try it like this:
#photos = #event.photos.to_json(
:include => {:appearances => {:include => :person}},
:methods => [: funky_calculation]
)
And if you want increased performance (eager loading), then use include on the associations as well:
#photos = #event.photos.includes(:appearances => :person).to_json(
:include => {:appearances => {:include => :person}},
:methods => [: funky_calculation]
)
You could eager load using includes after has_many association
#photos = #event.photos.includes(:appearances => [:person]).to_json
You might want to try using joins() or includes() on photos, instead as an option to to_json().
http://guides.rubyonrails.org/active_record_querying.html#using-array-hash-of-named-associations

Really easy Rails Active-Record Associations question

I have 2 models:
class Mission < ActiveRecord::Base
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :missions
end
And I have a complex Mission find statement:
#missions = Mission.send(#view, level).send(#show).search(#search).paginate :page => params[:page], :order => actual_sort, :per_page => 50
I'd like to add to my query the possibility to search for a specific category too.
I tried this but it does not work:
#missions = Mission.send(#view, level).send(#show).send(:category, #category).search(#search).paginate :page => params[:page], :order => actual_sort, :per_page => 50
Rails says that Mission has not a .category method. How would you solve this?
Thanks,
Augusto
OH ... MY ... GOD
Are you sure this is the best way to be doing this?
I don't think people will be able to help you if you don't explain a bit more, but I highly doubt that you couldn't write your statement like so:
#missions = Mission.select("missions.level ...,category.attr ...").where(["missions.level = ? ...", level ...]).includes(:category).where(["categories.field = ?", ...]).paginate(...)
Obviously the elipses (...) mean generally etc.
This is a working example on one of my projects in testing:
i = Item.where("items.name like '%Coupon%'").includes(:category).where(["categories.name = ? ",'Category 2'])
try performing the where selection on category_id

Can I add the limit the result of object return in Ruby on rails?

For Example, I want to know the User have many posts. So, I can get back post using this :
#user.posts
but I don't want to get all the posts back. I would like to limite the result, for example, top ten created, or may be sorted by some column. How can I do so? Thank you.
You can always make a generic scope to handle the limit, such as putting this in an initializer:
class ActiveRecord::Base
named_scope :limit, lambda { |*limit| {
:limit => limit[0] || 10,
:offset => limit[1]
}}
end
This makes limiting queries easy:
# Default is limited to 10
#user.posts.limit
# Pass in a specific limit
#user.posts.limit(25)
# Pass in a specific limit and offset
#user.posts.limit(25, 25)
For something more robust, you might want to investigate will_paginate.
Try this:
#user.posts(:limit => 10, :order => "created_at DESC")
http://guides.rubyonrails.org/active_record_querying.html
You should take a look at the options available for the has_many association.
You could try something like this:
class User < ActiveRecord::Base
has_many :posts
has_many :top_ten_posts, { :class_name => "Post", :order => :some_rating_column, :limit => 10 }
end

Rails ActiveRecord - Best way to perform an include?

I have three models:
class Book < ActiveRecord::Base
has_many :collections
has_many :users, :through => :collections
end
class User < ActiveRecord::Base
has_many :collections
has_many :books, :through => :collections
end
class Collection < ActiveRecord::Base
belongs_to :book
belongs_to :user
end
I'm trying to display a list of the books and have a link to either add or remove from the user's collection. I can't quite figure out the best syntax to do this.
For example, if I do the following:
Controller
class BooksController < ApplicationController
def index
#books = Book.all
end
end
View
...
<% if book.users.include?(current_user) %>
...
or obviously the inverse...
...
<% if current_user.books.include?(book) %>
...
Then queries are sent for each book to check on that include? which is wasteful. I was thinking of adding the users or collections to the :include on the Book.all, but I'm not sure this is the best way. Effectively all I need is the book object and just a boolean column of whether or not the current user has the book in their collection, but I'm not sure how to forumlate the query in order to do that.
Thanks in advance for your help.
-Damien
I have created a gem(select_extra_columns) for returning join/calculated/aggregate columns in a ActiveRecord finders. Using this gem, you will be able to get the book details and the flag indicating if the current user has the book in one query.
In your User model register the select_extra_columns feature.
class Book < ActiveRecord::Base
select_extra_columns
has_many :collections
has_many :users, :through => :collections
end
Now in your controller add this line:
#books = Book.all(
:select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user",
:extra_columns => {:belongs_to_user => :boolean},
:joins => "LEFT OUTER JOIN collections
ON book.id = collections.book_id AND
collections.user_id = #{current_user.id}"
)
Now in your view you can do the following.
book.belongs_to_user?
You're going to to want 2 SQL queries, and O(1) based lookups (probably irrelevant, but it's the principle) to check if they have the book.
The initial calls.
#books = Book.all
#user = User.find(params[:id], :include => :collections)
Next, you're going to want to write the books the user has into a hash for constant time lookup (if people won't ever have many books, just doing an array.include? is fine).
#user_has_books = Hash.new
#user.collections.each{|c|#user_has_books[c.book_id] = true}
And on the display end:
#books.each do |book|
has_book = #user_has_books.has_key?(book.id)
end
I'd err away from caching the book_ids on the user object, simply because going this route can have some funny and unexpected consequences if you ever start serializing your user objects for whatever reason (i.e. memcached or a queue).
Edit: Loading intermediary collection instead of double loading books.
Essentially you need to make one call to get the book information and the Boolean flag indicating if the current user has the book. ActiveRecord finders doesn't allow you to return the join results from another table. We work around this problem by doing a trick.
In your Book model add this method.
def self.extended_book
self.columns # load the column definition
#extended_user ||= self.clone.tap do |klass|
klass.columns << (klass.columns_hash["belongs_to_user"] =
ActiveRecord::ConnectionAdapters::Column.new(
"belongs_to_user", false, "boolean"))
end # add a dummy column to the cloned class
end
In your controller use the following code:
#books = Book.extended_book.all(
:select => "books.*, IF(collections.id, 1, 0) AS belongs_to_user",
:joins => "LEFT OUTER JOIN collections
ON book.id = collections.book_id AND
collections.user_id = #{current_user.id}"
)
Now in your view you can do the following.
book.belongs_to_user?
Explanation:
In the extended_book method you are creating a copy of Book class and adding a dummy column belongs_to_user to the hash. During the query extra join column is not rejected as it exists in the columns_hash. You should use the extended_book only for querying.
If you use it for CRUD operations DB will throw error.
I would first create an instance method in the User model that 'caches' the all the Book ID's in his collection:
def book_ids
#book_ids ||= self.books.all(:select => "id").map(&:id)
end
This will only execute the SQL query once per controller request. Then create another instance method on the User model that takes a book_id as a parameter and checks to see if its included in his book collection.
def has_book?(book_id)
book_ids.include?(book_id)
end
Then while you iterate through the books:
<% if current_user.has_book?(book.id) %>
Only 2 SQL queries for that controller request :)
Use exists? on the association as it is direct SQL call. The association array is NOT loaded to perform these checks.
books.users.exists?(current_user)
This is the SQL executed by Rails.
SELECT `users`.id FROM `users`
INNER JOIN `collections` ON `users`.id = `collections`.user_id
WHERE (`users`.`id` = 2) AND ((`collections`.book_id = 1)) LIMIT 1
In the above SQL current_user id = 2 and book id is 1
current_user.books.exists?(book)
This is the SQL executed by Rails.
SELECT `books`.id FROM `books`
INNER JOIN `collections` ON `books`.id = `collections`.book_id
WHERE (`books`.`id` = 3) AND ((`collections`.user_id = 4)) LIMIT 1
In the above SQL current_user id = 4 and book id is 3
For more details, refer to the documentation of the exists? method in a :has_many association.
Edit: I have included additional information to validate my answer.

Resources