left join not providing the object properties i want - ruby-on-rails

I'm sure I'm doing something stupid, but my AR code
#comments = Comment.find(:all, :limit => 10,
:joins => "LEFT JOIN `users` ON comments.user_id = users.id",
:select => 'comments.*, users.theme')
Is returning the correct sql:
SELECT comments.*, users.theme from comments LEFT JOIN users on comments.user_id = users.id
and when I put it into a mysql, I get the results I want, but when I try to access #comment.theme (in an each loop on #comments) after the above AR call is made, theme is not there.
So, is there something special I have to do to the Comments model to allow joins to populate the associated columns? I thought that Rails would just add them as properties I could dot-grab.

Try this:
#comments.each do |comment|
puts comment.theme
end
All the fields are in the array #comments, as Object type Comment, and the field names are as used in the select, with no regard to their original source.
Good luck.

Related

ActiveRecord vs SQL with sub select

I found this query easier if done with SQL
select Topics.subject, shortcode, (select count(*) from votes where Votes.Topic_Id = Topics.Id ) as votes from Topics
where url like 'http://test.com%'
ORDER BY votes desc;
Using ActiveRecord, I think there should be a more elegant.. or at least possible way to do it. Any suggestions?
I started with this, which worked, but didn't get to the next steps, instead used:
t = Topic.find(:all, :conditions => "url like 'http://test.com%'")
To get topics with votes:
Topic.where('url like :url', :url => 'http://test.com%').
joins(:votes).
select('topics.*, count(votes.id) as votes')
Note that this will only work in MySql. For PostgreSQL, you need to specify the group clause:
Topic.where('url like :url', :url => 'http://test.com%').
joins(:votes).
group(Topic.column_names.map{|col| "topics.#{col}"}).
select('topics.*, count(votes.id) as votes')

Is it possible to delete_all with inner join conditions?

I need to delete a lot of records at once and I need to do so based on a condition in another model that is related by a "belongs_to" relationship. I know I can loop through each checking for the condition, but this takes forever with my large record set because for each "belongs_to" it makes a separate query.
Here is an example. I have a "Product" model that "belongs_to" an "Artist" and lets say that artist has a property "is_disabled".
If I want to delete all products that belong to disabled artists, I would like to be able to do something like:
Product.delete_all(:joins => :artist, :conditions => ["artists.is_disabled = ?", true])
Is this possible? I have done this directly in SQL before, but not sure if it is possible to do through rails.
The problem is that delete_all discards all the join information (and rightly so). What you want to do is capture that as an inner select.
If you're using Rails 3 you can create a scope that will give you what you want:
class Product < ActiveRecord::Base
scope :with_disabled_artist, lambda {
where("product_id IN (#{select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})")
}
end
You query call then becomes
Product.with_disabled_artist.delete_all
You can also use the same query inline but that's not very elegant (or self-documenting):
Product.where("product_id IN (#{Product.select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})").delete_all
In Rails 4 (I tested on 4.2) you can almost do how OP originally wanted
Application.joins(:vacancy).where(vacancies: {status: 'draft'}).delete_all
will give
DELETE FROM `applications` WHERE `applications`.`id` IN (SELECT id FROM (SELECT `applications`.`id` FROM `applications` INNER JOIN `vacancies` ON `vacancies`.`id` = `applications`.`vacancy_id` WHERE `vacancies`.`status` = 'draft') __active_record_temp)
If you are using Rails 2 you can't do the above. An alternative is to use a joins clause in a find method and call delete on each item.
TellerLocationWidget.find(:all, :joins => [:widget, :teller_location],
:conditions => {:widgets => {:alt_id => params['alt_id']},
:retailer_locations => {:id => #teller_location.id}}).each do |loc|
loc.delete
end

:select with find_in_batches in rails

How can I include a :select clause with find_in_batches. The following throws an error " Mysql::Error: Unknown column 'users.id' in 'field list': .
Post.find_in_batches(:batch_size => 100, :select => "users.id, users.name, categories.name, posts.id", :include => [:user, :category]) do |group|
#stuff with group
end
So, if you're considering using find_in_batches it probably means you have a lot of records to go through and you very well might only want select fields to be returned to you from the DB.
In Rails 3/4 you can chain find_in_batches with any other type ActiveRecord::Relation method (or at least, most... I have not tested all of them personally).
This is probably what you're looking for
User.select(:id).find_in_batches(:batch_size => 100) do |group|
# do something with group...
# like print all the ids
puts group.map(&:id)
end
If you try this in the console it generates SQL like this...
SELECT id FROM `users` WHERE (`users`.`id` > 895846) ORDER BY `users`.`id` ASC LIMIT 100
See more info here: http://api.rubyonrails.org/classes/ActiveRecord/Batches.html
Your life with Rails will be much easier if you just retrieve all of the fields for each model queried, like so:
Post.find_in_batches(:batch_size => 100, :include => [:user, :category]) do |post|
u = post.user
c = post.category
# do stuff
end
A trimmed select list, as in your question, provides a limited DB performance improvement, but in most cases not enough to be worth the clunkier code.

How do I Order on common attribute of two models in the DB?

If i have two tables Books, CDs with corresponding models.
I want to display to the user a list of books and CDs. I also want to be able to sort this list on common attributes (release date, genre, price, etc.). I also have basic filtering on the common attributes.
The list will be large so I will be using pagination in manage the load.
items = []
items << CD.all(:limit => 20, :page => params[:page], :order => "genre ASC")
items << Book.all(:limit => 20, :page => params[:page], :order => "genre ASC")
re_sort(items,"genre ASC")
Right now I am doing two queries concatenating them and then sorting them. This is very inefficient. Also this breaks down when I use paging and filtering. If I am on page 2 of how do I know what page of each table individual table I am really on? There is no way to determine this information without getting all items from each table.
I have though that if I create a new Class called items that has a one to one relationship with either a Book or CD and do something like
Item.all(:limit => 20, :page => params[:page], :include => [:books, :cds], :order => "genre ASC")
However this gives back an ambiguous error. So can only be refined as
Item.all(:limit => 20, :page => params[:page], :include => [:books, :cds], :order => "books.genre ASC")
And does not interleave the books and CDs in a way that I want.
Any suggestions.
The Item model idea will work, but you are going to have to pull out all the common attributes and store them in Item. Then update all you forms to store those specific values in the new table. This way, adding a different media type later would be easier.
Update after comment:
What about a union? Do find_by_sql and hand-craft the SQL. It won't be simple, but your DB scheme isn't simple. So do something like this:
class Item < ActiveModel::Base
attr_reader :title, :genre, :media_type, ...
def self.search(options = {})
# parse options for search criteria, sorting, page, etc.
# will leave that for you :)
sql = <<-SQL
(SELECT id, title, genre, 'cd' AS media_type
FROM cds
WHERE ???
ORDER BY ???
LIMIT ???
) UNION
(SELECT id, title, genre, 'book' AS media_type
FROM books
WHERE ???
ORDER BY ???
LIMIT ???
)
SQL
items = find_by_sql(sql)
end
end
untested
Or something to that effect. Basically build the item rows on the fly (you will need a blank items table). The media_type column is so you know how to create the links when displaying the results.
Or...
Depending on how often the data changes you could, gasp, duplicate the needed data into the items table with a rake task on a schedule.
You say you can't change how books and CDs are stored in the database, but could you add an items view? Do you have any control over the database at all?
CREATE VIEW items
(id, type, title, genre, created_at, updated_at)
AS
SELECT b.id, 'Book', b.title, b.genre, b.created_at, b.updated_at
FROM books b
UNION
SELECT c.id, 'CD', c.title, c.genre, c.created_at, c.updated_at
FROM cds c;
You can paginate on a results array, so leave pagination out of the invidual model queries, and add it to your results array:
re_sort(items,"genre ASC").paginate(:page => params[:page], :per_page => items_per_page)

Reversed has_many in Rails

Let's say I have models: User and Item and relation many-to-many between them. How to get Users who have exactly(no more) Items with the defined attributes i.e. Users who have Items with colors = ['red', 'black'].
Of course I can do something like this:
User.all :joins => [:items, :items], :conditions => {:"items.color" => "red", :"items_users.color" => 'black'}
But for more attributes it's going to be quite cumbersome.
I can do also:
User.all(:conditions => ["items.color in (?), ['red', 'black']], :include => :items)
But this one returns also users with items having colors = ['red', 'black', 'blue', 'etc']
So the only solution is to get all and to sort with ruby syntax ? How to do it in one SQL query or Rails AR syntax ?
The way which optimizes programmer time and readability, in my opinion:
#get all users who have items which are both red and black but no other colors
candidate_users = User.all(:include => :items)
candidate_users.reject! do |candidate|
candidate.items.map {|item| item.color}.sort != ['black', 'red']
end
If you are expecting to be looping through a metric truckload of users there, then you'll need to SQL it up. Warning: SQL is not my bag, baby: test before use.
select users.*, items.* FROM users
INNER JOIN items_users ON (items_users.user_id = users.id)
INNER JOIN items ON (items_users.item_id = items.id)
GROUP BY users.id HAVING COUNT(DISTINCT items.color) = 2
What I think that evil mess does:
1) Grabs every user/item combination
2) Winnows down to users who have items of exactly 2 distinct colors
Which means you'll need to:
candidate_users.reject! do |candidate|
candidate.items.map {|item| item.color}.sort != ['black', 'red']
end
You can probably eliminate the need for the ruby here totally but the SQL is going to get seven flavors of ugly. (Cross joins, oh my...)

Resources