I can't wrap my head around how to accomplish this and hoping that someone will be able to help - I am sure this will be something simple!
I'm trying to implement a "tag cloud" from scratch in my rails app, (fairly similar to the episode as posted on railscasts.com) with the added complexity that I only want to show tags in my "cloud", that relate to the results that are returned. For example, let's say that in the index view, the user has filtered the results. My desire is to only show tags from the filtered result set.
I have two models that contain a HABTM association between them:
class Article < ActiveRecord::Base
has_and_belongs_to_many :tags
...
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :articles
...
end
In my Article controller, I have an #articles variable which gives me the results that show in my index action. For the purpose of this explanation, let's say it's:
#article = Article.all
What I thought I'd be able to do is to call #article.tags, hoping it would return the associated tags for all articles, within the results. I thought I'd be able to then group and iterate through them, showing them in the cloud. However, this throws an "undefined method error".
I've been playing in the rails console and found that if I run:
>> #articles = Article.find(1)
>> #articles.tags
Then all tags associated with that article are returned to me. Why can't you call that on .all?
If I was to use SQL directly I'd do something like:
SELECT name, COUNT(*)
FROM tags INNER JOIN articles_tags on ... INNER JOIN articles on...
WHERE // filtered results
GROUP BY name
I guess that's a simplified equivalent of what I'm trying to do but using the rails-query-lingo.
Any ideas?
You can get the Tags that have Articles by:
Tag.joins(:articles)
Same applies for atricles that have tags.
Article.joins(:tags)
You may prefer using has_many through instead of habtm, that gives you more control over the join table, check this question
Related
I'm trying to add the ability to filter posts by keywords. I have it working if a post only includes one keyword, but it doesn't work when a post has multiple keywords. I think I understand why it doesn't work, but I can't figure out how to fix it.
This is the method in the controller:
KEYWORDS = ["authorlife", "lostlegacies", "ashiftinshadows", "snippets", "artwork", "random"]
def filter
filtered_posts = Post.all
KEYWORDS.each do |keyword|
if params[keyword] == true
filtered_posts = filtered_posts.where(keywords: keyword)
end
end
render json: filtered_posts
end
In the case where a post only has one keyword I think it works because my query is basically checking if the keywords for a post match the keyword in the loop. So if I only have keyword, let's say "keyword1" in the post and the keyword it's looking for is "keyword1" then it works. But if I have two keywords then it's trying to match "keyword1" but only finding posts that have "keyword1 keyword2" and because the second keyword is there it doesn't match. I'm not sure if I should be doing something with includes? or ILIKE. I've tried a couple different things and still can't figure this out.
The actual problem here is that you're most likely storing the keywords wrong. The only way using LIKE will actually fix the problem is if you're storing the keywords in a comma separated string or something simular. Storing multiple values in a column like that violates first normal form and you're really just introducing bugs in your application and hurting performance by using pattern matching. An example of these bugs is that LIKE %foo% will match foo_bar and foo_bar_baz, ooops.
A slightly better solution would be to use a native array or JSON/JSONB type column.
But the best solution would be to use a normalization table and join table to avoid duplicating the keywords.
First generate the model and join table
rails g model keyword text:string:unique
rails g migration create_posts_keywords_join_table posts keywords
Then setup the assocations:
class Keyword < ApplicationRecord
has_and_belongs_to_many :posts
end
class Post < ApplicationRecord
has_and_belongs_to_many :keywords
end
This will let you filter posts by keyword by doing a LEFT JOIN:
Post.joins(:keywords)
.where(keywords: { text: keywords })
I changed my query to this:
filtered_posts = filtered_posts.where("keywords like ?", "%#{keyword}%")
And it's working now. Not sure if there's a better way to do this.
If you want to get all the Posts that have a keyword in the KEYWORDS array, ActiveRecord can handle the array for you:
Post.where(keyword: KEYWORDS)
#this generates the sql query:
# select posts.* from posts where posts.keyword in ('authorlife','lostlegacies', ...etc)
class Post
has_many :commments
end
class Comment
belongs_to :post
end
I wish to display a list of posts ordered by date of post creation (submitted_at). I also want some post xyz to appear at the top if it has some new comment posted and yet to be reviewed by moderator. We will determine this by a boolean attribute/field at comments level (moderated = 1/0)
I tried
Posts.join(:comments)
.distinct
.order("submitted_at DESC, comments.moderated")
but this excludes posts that have no comments and results aren't sorted as expected. I am sure that we can do this at ruby level, but looking for a way to do this using AR.
For the join, use this:
Posts.join("LEFT JOIN comments ON comments.post_id = posts.id")
Which will include the ones with no comments.
Your sorting seems to suggest you want a count of moderated comments, in which case, try this:
.order("submitted_at DESC, COUNT(comments.moderated)")
Although, you may need to use group in some way too.
I have a Track table and a Section table. A track has many sections. Sections are connected to their respective task by the Section's :track_id, which corresponds to the Track's :id attribute.
<% #track = Track.find(params[:id]) %>
<% #sections = Section.find_by_track_id(#track.id) %>
In the code above I'm trying to find multiple sections that share the same :track_id attribute, but find_by_track_id() only returns the first. What's the best way to get all of them?
Thanks!
If your tracks and sections are related in this way, then the best way to relate them is by using the methods that come automatically from Rails' associations.
in this case, I expect in your model files, you have the following:
class Track < ActiveRecord::Base
has_many :sections
end
class Section < ActiveRecord::Base
belongs_to :track
end
Then you can get the sections for a track like this:
#track = Track.find(params[:id])
#sections = #track.sections
You're looking for where, which finds all records where a specific set of conditions are met.
#sections = Section.where(track_id: #track.id)
This is unrelated to your question, but you should set #sections and #track in your controller. As it seems like you're new to Rails, I'd highly recommend reading through the Rails Guides. They will help you immensely on your journey.
EDIT: I was solving for the general question of "Find multiple database objects by attribute in Rails?", which is how to find multiple database objects in the general case. #TarynEast's method is the way to go to find all of the sections for a track, or more generally, all of the objects that belong to the desired object. For the specific case you're asking for above, go with #TarynEast's solution.
Association
To extend Taryn East's answer, you need to look into ActiveRecord Associations.
In your model, if you have the following has_many relationship:
#app/models/track.rb
Class Track < ActiveRecord::Base
has_many :sections
end
#app/models/section.rb
Class Section < ActiveRecord::Base
belongs_to :track
end
This will set up a relational database association between your tracks and sections datatables.
--
Associative Data
The magic of Rails comes into play here
When you call the "parent" object, you'll be able to locate it using its primary key (typically the ID). The magic happens when Rails automatically uses this primary_key as a foreign_key of the child model - allowing you to call all its data as an append to the parent object:
#track = Track.find params[:id] #-> find single Track by primary key
#sections = #track.sections #-> automagically finds sections using the track primary key
This means if you call the following, it will work exactly how you want:
#sections.each do |section|
section.name
end
Where
Finally, if you wanted to look up more than one record at a time, you should identify which ActiveRecord method you should use:
find is to locate a single record by id
finy_by key: "value" is to locate a single record by your defined key/column
where is to return multiple items using your own conditions
So to answer your base line question, you'll want to use where:
#sections = Section.where track_id: params[:id]
This is not the right answer, but it should help you
<% #sections=#track.sections%>
Use find when you are looking for one specific element identified by it's id.
Model.find is using the primary key column. Therefore there is always exactly one or no result.
So basically I have two classes, Book and Author. Books can have multiple authors and authors can have multiple books. Books have the following default scope.
default_scope :order => "publish_at DESC"
On the Author show page I want to list all the books associated with that author so I say the following...
#author = Author.find(params[:id])
#books = #author.books
All is good so far. The author#show page lists all books belonging to that author ordered by publication date.
I'm also working on a gem that is able to sort by the popularity of a book.
#books = #author.books.sort_by_popularity
The problem is that whenever it tries to sort, the default_scope always gets in the way. And if I try to unscope it before it will get rid of the author relation and return every book in the database. For example
#books = #author.books.unscoped.sort_by_popularity # returns all books in database
I'm wondering if I can use the ActiveRelation except() method
to do something like this (which seems like it should work but it doesn't. It ignores order, just not when it is a default_scope order)
def sort_by_popularity
self.except(:order).do_some_joining_magic.order('popularity ASC')
# |------------| |---------------------|
end
Any ideas as to why this doesn't work? Any ideas on how to get this to work a different way? I know I can just get rid of the default_scope but I'm wondering if there another way to do this.
You should be able to use reorder to completely replace the existing ORDER BY:
reorder(*args)
Replaces any existing order defined on the relation with the specified order.
So something like this:
def self.sort_by_popularity
scoped.do_some_joining_magic.reorder('popularity ASC')
end
And I think you want to use a class method for that and scoped instead of self but I don't know the whole context so maybe I'm wrong.
I don't know why except doesn't work. The default_scope seems to get applied at the end (sort of) rather than the beginning but I haven't looked into it that much.
You can do it without losing default_scope or other ordering
#books.order_values.prepend 'popularity ASC'
models:
#StatusMessage model
class StatusMessage < ActiveRecord::Base
belongs_to :users
default_scope :order => "created_at DESC"
end
#User Model
class User < ActiveRecord::Base
has_many :status_messages
end
In controller I want to join these two tables and get fields from both table. for example I want email field from User and status field from StatusMessage. When I use :
#status = User.joins(:status_messages)
Or
#status = User.includes(:status_messages)
It gives me only the user table data.
How can I implement this requirement?
You need to use includes here. It preloads data so you won't have another SQL query when you do #user.status_messages.
And yes you can't really see the effect: you need to check your logs.
First of all, I don't think it is possible (and reasonable) what you want to do. The reason for that is that the relation between User and StatusMessage is 1:n, that means that each user could have 0 to n status messages. How should these multitudes of attributes be included in your user model?
I think that the method joints in class ActiceRecord has a different meaning, it is part of the query interface. See the question LEFT OUTER joins in Rails 3
There are similar questions on the net, here is what I have found that matches most:
Ruby on Rails: How to join two tables: Includes (translated for your example) in the user a primary_status_message, which is then materialized in the query for the user. But it is held in one attribute, and to access the attributes of the status_message, you have to do something like that: #user.primary_status_message.status
When you use #status = User.includes(:status_messages) then rails eagerley loads the data of all the tables.
My point is when you use this User.includes(:status_messages) it will loads the data of status_messages also but shows only users table data then if you want first user status_messages then you have to #user.first.status_messages