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.
Related
Using Rails 3.2. My models are nested in such a way:
Review => Reviewable (Country or Shop)
Country => CountryDay => Shop => Photos
Shop => Photos
I have the following:
#reviews = #user.reviews.includes(:user, :reviewable)
Usually we can include nested polymorphic in such a way:
# this will return errors because :shop is not found in the model Shop (:reviewable is actually :shop)
#reviews = #user.reviews.includes(:user, :reviewable => [:shop])
# this will return errors because :photos is not directly associated to Country
#reviews = #user.reviews.includes(:user, :reviewable => :photos)
There are many other variants. How do I work around it to have the ActiveRecord includes the correct model based on its association?
I think I had the same problem just now. I got an ActiveRecord::EagerLoadPolymorphicError when trying to load associations. My solution was to put together a custom join statement which I've put into a scope for easier usage.
Untested, but this may get you going:
class Review < ActiveRecord::Base
scope :eagerly_loaded, -> {
joins('
INNER JOIN shops
ON (reviews.reviewable_type = "Shop" AND reviews.reviewable_id = shops.id)
INNER JOIN countries
ON (reviews.reviewable_type = "Country" reviews.reviewable_id = countries.id)
')
}
end
You then use #user.reviews.eagerly_loaded. Make sure to use a matching .group()-statement to only get the ones you need.
I fetched all users from the database based on city name.
Here is my code:
#othertask = User.find(:all, :conditions => { :city => params[:city]})
#othertask.each do |o|
#other_tasks = Micropost.where(:user_id => o.id).all
end
My problem is when loop gets completed, #other_task holds only last record value.
Is it possible to append all ids record in one variable?
You should be using a join for something like this, rather than looping and making N additional queries, one for each user. As you now have it, your code is first getting all users with a given city attribute value, then for each user you are again querying the DB to get a micropost (Micropost.where(:user_id => o.id)). That is extremely inefficient.
You are searching for all microposts which have a user whose city is params[:city], correct? Then there is no need to first find all users, instead query the microposts table directly:
#posts = Micropost.joins(:user).where('users.city' => params[:city])
This will find you all posts whose user has a city attribute which equals params[:city].
p.s. I would strongly recommend reading the Ruby on Rails guide on ActiveRecord associations for more details on how to use associations effectively.
you can do it by following way
#othertask = User.find(:all, :conditions => { :city => params[:city]})
#other_tasks = Array.new
#othertask.each do |o|
#other_tasks << Micropost.where(:user_id => o.id).all
end
Here is the updated code:
#othertask = User.find_all_by_city(params[:city])
#other_tasks = Array.new
#othertask.each do |o|
#other_tasks << Micropost.find_all_by_user_id(o.id)
end
You are only getting the last record because of using '=' operator, instead you need to use '<<' operator in ruby which will append the incoming records in to the array specified.
:)
Try:
User model:
has_many :microposts
Micropost model:
belongs_to :user
Query
#Microposts = Micropost.joins(:user).where('users.city' => params[:city])
Example
class User
has_many :tickets
end
I want to create association which contains logic of count tickets of user and use it in includes (user has_one ticket_count)
Users.includes(:tickets_count)
I tried
has_one :tickets_count, :select => "COUNT(*) as tickets_count,tickets.user_id " ,:class_name => 'Ticket', :group => "tickets.user_id", :readonly => true
User.includes(:tickets_count)
ArgumentError: Unknown key: group
In this case association query in include should use count with group by ...
How can I implement this using rails?
Update
I can't change table structure
I want AR generate 1 query for collection of users with includes
Update2
I know SQL an I know how to select this with joins, but my question is now like "How to get data" . My question is about building association which I can use in includes. Thanks
Update3
I tried create association created like user has_one ticket_count , but
looks like has_one doesn't support association extensions
has_one doesn't support :group option
has_one doesn't support finder_sql
Try this:
class User
has_one :tickets_count, :class_name => 'Ticket',
:select => "user_id, tickets_count",
:finder_sql => '
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
WHERE b.user_id = #{id}
GROUP BY b.user_id
'
end
Edit:
It looks like the has_one association does not support the finder_sql option.
You can easily achieve what you want by using a combination of scope/class methods
class User < ActiveRecord::Base
def self.include_ticket_counts
joins(
%{
LEFT OUTER JOIN (
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
GROUP BY b.user_id
) a ON a.user_id = users.id
}
).select("users.*, COALESCE(a.tickets_count, 0) AS tickets_count")
end
end
Now
User.include_ticket_counts.where(:id => [1,2,3]).each do |user|
p user.tickets_count
end
This solution has performance implications if you have millions of rows in the tickets table. You should consider filtering the JOIN result set by providing WHERE to the inner query.
You can simply use for a particular user:
user.tickets.count
Or if you want this value automatically cached by Rails.
Declare a counter_cache => true option in the other side of the association
class ticket
belongs_to :user, :counter_cache => true
end
You also need a column in you user table named tickets_count.
With this each time you add a new tickets to a user rails will update this column so when you ftech your user record you can simply accs this column to get the ticket count without additional query.
Not pretty, but it works:
users = User.joins("LEFT JOIN tickets ON users.id = tickets.user_id").select("users.*, count(tickets.id) as ticket_count").group("users.id")
users.first.ticket_count
What about adding a method in the User model that does the query?
You wouldn't be modifying the table structure, or you can't modify that either?
How about adding a subselect scope to ApplicationRecord:
scope :subselect,
lambda { |aggregate_fn, as:, from:|
query = self.klass
.select(aggregate_fn)
.from("#{self.table_name} _#{self.table_name}")
.where("_#{self.table_name}.id = #{self.table_name}.id")
.joins(from)
select("(#{query.to_sql}) AS #{as}")
}
Then, one might use the following query:
users = User.select('users.*').subselect('COUNT(*)', as: :tickets_count, from: :tickets)
users.first.ticket_count
# => 5
I am trying to get a list, and I will use books as an example.
class Book < ActiveRecord::Base
belongs_to :type
has_and_belongs_to_many :genres
end
class Genre < ActiveRecord::Base
has_and_belongs_to_many :books
end
So in this example I want to show a list of all Genres, but it the first column should be the type. So, if say a genre is "Space", the types could be "Non-fiction" and "Fiction", and it would show:
Type Genre
Fiction Space
Non-fiction Space
The Genre table has only "id", "name", and "description", the join table genres_books has "genre_id" and "book_id", and the Book table has "type_id" and "id". I am having trouble getting this to work however.
I know the sql code I would need which would be:
SELECT distinct genres.name, books.type_id FROM `genres` INNER JOIN genres_books ON genres.id = genres_books.genre_id INNER JOIN books ON genres_books.book_id = books.id order by genres.name
and I found I could do
#genre = Genre.all
#genre.each do |genre|
#type = genre.book.find(:all, :select => 'type_id', :group => 'type_id')
#type.each do |type|
and this would let me see the type along with each genre and print them out, but I couldn't really work with them all at once. I think what would be ideal is if at the Genre.all statement I could somehow group them there so I can keep the genre/type combinations together and work with them further down the road. I was trying to do something along the lines of:
#genres = Genre.find(:all, :include => :books, :select => 'DISTINCT genres.name, genres.description, books.product_id', :conditions => [Genre.book_id = :books.id, Book.genres.id = :genres.id] )
But at this point I am running around in circles and not getting anywhere. Do I need to be using has_many :through?
The following examples use your models, defined above. You should use scopes to push associations back into the model (alternately you can just define class methods on the model). This helps keep your record-fetching calls in check and helps you stick within the Law of Demeter.
Get a list of Books, eagerly loading each book's Type and Genres, without conditions:
def Book < ActiveRecord::Base
scope :with_types_and_genres, include(:type, :genres)
end
#books = Book.with_types_and_genres #=> [ * a bunch of book objects * ]
Once you have that, if I understand your goal, you can just do some in-Ruby grouping to corral your Books into the structure that you need to pass to your view.
#books_by_type = #books.group_by { |book| book.type }
# or the same line, more concisely
#books_by_type = #books.group_by &:type
#books_by_type.each_pair do |type, book|
puts "#{book.genre.name} by #{book.author} (#{type.name})"
end
I have the following models. Users have UserActions, and one possible UserAction can be a ContactAction (UserAction is a polymorphism). There are other actions like LoginAction etc. So
class User < AR::Base
has_many :contact_requests, :class_name => "ContactAction"
has_many :user_actions
has_many_polymorphs :user_actionables, :from => [:contact_actions, ...], :through => :user_actions
end
class UserAction < AR::Base
belongs_to :user
belongs_to :user_actionable, :polymorphic => true
end
class ContactAction < AR::Base
belongs_to :user
named_scope :pending, ...
named_scope :active, ...
end
The idea is that a ContactAction joins two users (with other consequences within the app) and always has a receiving and a sending end. At the same time, a ContactAction can have different states, e.g. expired, pending, etc.
I can say #user.contact_actions.pending or #user.contact_requests.expired to list all pending / expired requests a user has sent or received. This works fine.
What I would now like is a way to join both types of ContactAction. I.e. #user.contact_actions_or_requests. I tried the following:
class User
def contact_actions_or_requests
self.contact_actions + self.contact_requests
end
# or
has_many :contact_actions_or_requests, :finder_sql => ..., :counter_sql => ...
end
but all of these have the problem that it is not possible to use additional finders or named_scopes on top of the association, e.g. #user.contact_actions_or_requests.find(...) or #user.contact_actions_or_requests.expired.
Basically, I need a way to express a 1:n association which has two different paths. One is User -> ContactAction.user_id, the other is User -> UserAction.user_id -> UserAction.user_actionable_id -> ContactAction.id. And then join the results (ContactActions) in one single list for further processing with named_scopes and/or finders.
Since I need this association in literally dozens of places, it would be a major hassle to write (and maintain!) custom SQL for every case.
I would prefer to solve this in Rails, but I am also open to other suggestions (e.g. a PostgreSQL 8.3 procedure or something simliar). The important thing is that in the end, I can use Rails's convenience functions like with any other association, and more importantly, also nest them.
Any ideas would be very much appreciated.
Thank you!
To provide a sort-of answer to my own question:
I will probably solve this using a database view and add appropriate associations as needed. For the above, I can
use the SQL in finder_sql to create the view,
name it "contact_actions_or_requests",
modify the SELECT clause to add a user_id column,
add a app/models/ContactActionsOrRequests.rb,
and then add "has_many :contact_actions_or_requests" to user.rb.
I don't know how I'll handle updating records yet - this seems not to be possible with a view - but maybe this is a first start.
The method you are looking for is merge. If you have two ActiveRecord::Relations, r1 and r2, you can call r1.merge(r2) to get a new ActiveRecord::Relation object that combines the two.
If this will work for you depends largely on how your scopes are set up and if you can change them to produce a meaningful result. Let's look at a few examples:
Suppose you have a Page model. It has the normal created_at and updated_at attributes, so we could have scopes like:
:updated -> { where('created_at != updated_at') }
:not_updated -> { where('created_at = updated_at') }
If you pull this out of the database you'll get:
r1 = Page.updated # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at)
r2 = Page.not_updated # SELECT `pages`.* FROM `pages` WHERE (created_at = updated_at)
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE (created_at != updated_at) AND (created_at = updated_at)
=> []
So it did combine the two relations, but not in a meaningful way. Another one:
r1 = Page.where( :name => "Test1" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test1'
r2 = Page.where( :name => "Test2" ) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
r1.merge(r2) # SELECT `pages`.* FROM `pages` WHERE `pages`.`name` = 'Test2'
So, it might work for you, but maybe not, depending on your situation.
Another, and recommended, way of doing this is to create a new scope on you model:
class ContactAction < AR::Base
belongs_to :user
scope :pending, ...
scope :active, ...
scope :actions_and_requests, pending.active # Combine existing logic
scope :actions_and_requests, -> { ... } # Or, write a new scope with custom logic
end
That combines the different traits you want to collect in one query ...