Rails JOIN TABLES - ruby-on-rails

I have a following SQL QUERY:
SELECT articles.name, articles.price, users.zipcode FROM articles INNER JOIN users ON users.id = articles.user_id WHERE vectors ## to_tsquery('crime') ORDER BY articles.price ASC
And I Would like to write it inside of a find method from an ActiveRecord Class named Articles (Articles belongs_to user). Basically i wanna search for Articles and access the zipcode propertie from the User (user has_many Articles)
I wrote the following version, but i'm not sure that its working because in the response i dont receive any information about the user zipcode.
a = Article.find(:all,:conditions=>"vectors ## to_tsquery('crime')",:joins= >:user,:order=>:price,:include=>:user)
But i have no idea how to access the zipcode information. how can I access this information ? this is the right approach ?
Regards,
Victor

If you've coupled Articles and Users like you say above, this should be pretty easy:
#articles = Article.find(:all, :conditions => "…", :include => :user)
Then, in your view, you can do:
<ul>
<% for each article in #articles do %>
<li><%= article.user.zipcode %></li>
</ul>
<% end %>
This works because Rails creates a property for the parent object (User) in the model (Article) - you can read more about that in the API docs. This even works without the "include" key above, but leaving it out would mean a database query in every step of the loop.

Related

Print what is not in a join table

I have a join table created from 2 other tables.
Say one model is called cat, another is called request, and the join table is called catrequest (this table would have cat_id and request_id)
How would I print all of the cats not intersected in the join table i.e. all of the cats NOT requested using rails. I saw some DB based answers, but I am looking for a rails solution using ruby code.
I get how to print a cat that belongs to a request i.e.:
<% #requests.each do |request| %>
<% request.cats.each do |cat| %>
<%= cat.name %>
<% end %>
but I don't understand how to do the reverse of this.
To get a list of cats that have never been requested you'd go with:
Cat.includes(:cat_requests).where(cat_requests: { id: nil })
# or, if `cat_requests` table does not have primary key (id):
Cat.includes(:cat_requests).where(cat_requests: { cat_id: nil })
The above assumes you have the corresponding association:
class Cat
has_many :cat_requests
end
It sounds like what you need is an outer join, and then to thin out the cats rows that don't have corresponding data for the requests? If that's the case, you might consider using Arel. It supports an outer join and can probably be used to get what you're looking for. Here is a link to a guide that has a lot of helpful information on Arel:
http://jpospisil.com/2014/06/16/the-definitive-guide-to-arel-the-sql-manager-for-ruby.html
Search the page for "The More the Merrier" section which is where joins are discussed.

Rails 4 grouping duplicates

I wasn't sure how to ask this question. I am new to Ruby on Rails and am still figuring out how to piece everything together.
I have books, orders, products and users. Within a specific view, I would like to display one product_id per user — most users will have the same product_id multiple times.
Here's my book view.
<% #orders.each do |order| %>
<% order.product.books.each do |book| %>
<%= link_to book.title %>
<% end %>
<% end %>
In my book controller I have
def index
#orders = Order.select(:product_id).distinct
#books = Book.page params[:page]
end
All of this works well (view shows one product_id per order product_id, even if there are duplicates) until I call order.created_at which gives a missing attribute error. I know this error is because I am only requesting :product_id on order, but when I add :created_at (#orders = Order.select(:product_id, :created_at).distinct) to the select query I get duplicate :product_ids.
What am I missing?
Well, you have to think about what that would look like. When you are selecting rows by a distinct product_id you are discarding all other order rows that have the same product_id. But when you add created_at back into the mix your resulting record set will include the same product_id because the distinct is operating against the tuple of (product_id, created_at) instead of just the product_id.
Your original #orders isn't actually all of your orders, it's just a subset because it chose a unique one per product_id.
I find it helpful to append to_sql to the end of a query to see what the actual SQL coming out of ActiveRecord is doing:
> Order.select(:product_id).distinct.to_sql
=> SELECT DISTINCT "orders"."product_id" FROM "orders"
> Order.select(:product_id, :created_at).distinct.to_sql
=> SELECT DISTINCT "orders"."product_id", "orders"."created_at" FROM "orders"
Try running those in the database and you'll see what data ActiveRecord is using to construct ActiveRecord instances and realize that ActiveRecord is just taking whatever rows come back in the data and manufacturing objects based on that.

How to filter association_ids for an ActiveRecord model?

In a domain like this:
class User
has_many :posts
has_many :topics, :through => :posts
end
class Post
belongs_to :user
belongs_to :topic
end
class Topic
has_many :posts
end
I can read all the Topic ids through user.topic_ids but I can't see a way to apply filtering conditions to this method, since it returns an Array instead of a ActiveRecord::Relation.
The problem is, given a User and an existing set of Topics, marking the ones for which there is a post by the user. I am currently doing something like this:
def mark_topics_with_post(user, topics)
# only returns the ids of the topics for which this user has a post
topic_ids = user.topic_ids
topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
end
But this loads all the topic ids regardless of the input set. Ideally, I'd like to do something like
def mark_topics_with_post(user, topics)
# only returns the topics where user has a post within the subset of interest
topic_ids = user.topic_ids.where(:id=>topics.map(&:id))
topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
end
But the only thing I can do concretely is
def mark_topics_with_post(user, topics)
# needlessly create Post objects only to unwrap them later
topic_ids = user.posts.where(:topic_id=>topics.map(&:id)).select(:topic_id).map(&:topic_id)
topics.each {|t| t[:has_post]=topic_ids.include(t.id)}
end
Is there a better way?
Is it possible to have something like select_values on a association or scope?
FWIW, I'm on rails 3.0.x, but I'd be curious about 3.1 too.
Why am I doing this?
Basically, I have a result page for a semi-complex search (which happens based on the Topic data only), and I want to mark the results (Topics) as stuff on which the user has interacted (wrote a Post).
So yeah, there is another option which would be doing a join [Topic,Post] so that the results come out as marked or not from the search, but this would destroy my ability to cache the Topic query (the query, even without the join, is more expensive than fetching only the ids for the user)
Notice the approaches outlined above do work, they just feel suboptimal.
I think that your second solution is almost the optimal one (from the point of view of the queries involved), at least with respect to the one you'd like to use.
user.topic_ids generates the query:
SELECT `topics`.id FROM `topics`
INNER JOIN `posts` ON `topics`.`id` = `posts`.`topic_id`
WHERE `posts`.`user_id` = 1
if user.topic_ids.where(:id=>topics.map(&:id)) was possible it would have generated this:
SELECT topics.id FROM `topics`
INNER JOIN `posts` ON `topics`.`id` = `posts`.`topic_id`
WHERE `posts`.`user_id` = 1 AND `topics`.`id` IN (...)
this is exactly the same query that is generated doing: user.topics.select("topics.id").where(:id=>topics.map(&:id))
while user.posts.select(:topic_id).where(:topic_id=>topics.map(&:id)) generates the following query:
SELECT topic_id FROM `posts`
WHERE `posts`.`user_id` = 1 AND `posts`.`topic_id` IN (...)
which one of the two is more efficient depends on the data in the actual tables and indices defined (and which db is used).
If the topic ids list for the user is long and has topics repeated many times, it may make sense to group by topic id at the query level:
user.posts.select(:topic_id).group(:topic_id).where(:topic_id=>topics.map(&:id))
Suppose your Topic model has a column named id you can do something like this
Topic.select(:id).join(:posts).where("posts.user_id = ?", user_id)
This will run only one query against your database and will give you all the topics ids that have posts for a given user_id

Order Users with most Songs Desc

Hopefully a simple question. I have several models, two of them, :users and :songs, interact to get data from the database.
A User has_many :songs.
I'm trying to find the users with the most songs in the USER INDEX action, i.e list the 10 users with the most songs, user with the most songs at the top, descending.
So far I have in the users_controller in index;
#users = User.all
And so far all I can do in the view, users/index;
<% #users.each do |user| %>
<%= user.name %><%= user.songs.count %>
<% end %>
Works, it counts the songs but how do I order by users with most songs?
I have been looking at sort_by inside the block and I guess it could be done by listing all songs and grouping them by the user, but I feel that is not efficient enough. -as you can see I'm not an advanced developer.
Please tell me what you guys think and the answer to my solution if possible. The thing that is confusing me is that I am ordering a list generating from a table by data generated by another table. Must be simple but I haven't done it before so I can't get my head around it.
Thanks in Advance.
You should use something like this:
User.includes(:songs).order{|x| x.songs.size}
This does everything in one query so it should be more efficient than for example
User.all.sort{|x| user.songs.size}
Which would perform a query for each user
How about
User.all(:limit => 10, :order => "(select count(user_id) from songs where user_id = users.id) DESC")

ActiveRecord and SELECT AS SQL statements

I am developing in Rails an app where I would like to rank a list of users based on their current points. The table looks like this: user_id:string, points:integer.
Since I can't figure out how to do this "The Rails Way", I've written the following SQL code:
self.find_by_sql ['SELECT t1.user_id, t1.points, COUNT(t2.points) as user_rank FROM registrations as t1, registrations as t2 WHERE t1.points <= t2.points OR (t1.points = t2.points AND t1.user_id = t2.user_id) GROUP BY t1.user_id, t1.points ORDER BY t1.points DESC, t1.user_id DESC']
The thing is this: the only way to access the aliased column "user_rank" is by doing ranking[0].user_rank, which brinks me lots of headaches if I wanted to easily display the resulting table.
Is there a better option?
how about:
#ranked_users = User.all :order => 'users.points'
then in your view you can say
<% #ranked_users.each_with_index do |user, index| %>
<%= "User ##{index}, #{user.name} with #{user.points} points %>
<% end %>
if for some reason you need to keep that numeric index in the database, you'll need to add an after_save callback to update the full list of users whenever the # of points anyone has changes. You might look into using the acts_as_list plugin to help out with that, or that might be total overkill.
Try adding user_rank to your model.
class User < ActiveRecord::Base
def rank
#determine rank based on self.points (switch statement returning a rank name?)
end
end
Then you can access it with #user.rank.
What if you did:
SELECT t1.user_id, COUNT(t1.points)
FROM registrations t1
GROUP BY t1.user_id
ORDER BY COUNT(t1.points) DESC
If you want to get all rails-y, then do
cool_users = self.find_by_sql ['(sql above)']
cool_users.each do |cool_user|
puts "#{cool_user[0]} scores #{cool_user[1]}"
end

Resources