I have two models has_and_belong_to_many to each other:
class Post < ActiveRecord::Base
has_and_belongs_to_many :tags, :join_table => :tags_posts
end
class Tag < ActiveRecord::Base
has_and_belongs_to_many :posts, :join_table => :tags_posts
end
And then I have a query:
#tags = Tag.where(name: ["tag1","tag2","tag3"])
I want to get all the unique posts that have these tags, so I write this ugly code:
#posts = []
#tags.each do |tag|
#posts += tag.posts
end
#posts.uniq!
I guess it's inefficient, and since #posts is an array rather than a "ActiveRecord::Relation", I can't use all the class methods on it directly.
Is there a better way to achieve this?
#tags = Tag.where(name: %w(tag1 tag2 tag3)).includes(:posts)
should do the trick (%w() creates an array of strings, so no difference). It loads all posts with the tags in one or two queries, whatever is more efficient in AR's view. And then you can do
#posts = #tags.map(&:posts).flatten.uniq
to get the posts. map calls posts method on every tag and returns an array with those posts, but since you have an array of arrays, you want to flatten it.
You could join the two tables using includes and query on the Post model rather than Tag so that you get unique posts.
Post.includes(:tags).where(:tags => { name: %w(tag1 tag2 tag3) })
Related
This sounds kind of complicated from the title, but it really shouldn't be. I'm using Rails for an API, and have an endpoint which accepts an array of items. I want to return all records where the records that belong to it match at least one of the elements in the array.
For example, say I'm making a blog, and I have Posts and Tags.
I might request something like:
GET http://localhost:3000/api/v1/posts_by_tag?tags=news,weather,life
then in routes.rb
get '/posts_by_tag' => 'posts#index_by_tag'
and in posts_controller.rb
def index_by_tag
tags = params[:tags].split(',')
#posts = Post.where( any element in Post.tags matches any element in tags )
render json: #posts.to_json, status: :ok
end
In the hypothetical example above, I would want to return all posts which had a tags for any of "News", "Weather", or "Life".
I'm coming from node, and haven't been spending much time with Rails recently. But this feels like a situation where there is a very well-defined Rails/ActiveRecord way of doing this.
I assume you have a model Tag, which is joined to Post by a model named, say, PostTagging.
class PostTagging < ActiveRecord::Base
belongs_to :post
belongs_to :tag
end
class Post < ActiveRecord::Base
has_many :post_taggings
has_many :tags, through: :post_taggings
end
Hence, to get all posts with that tag all you need to do is to join Tag to Post and search for that Tag model:
Post.joins(:tags).where(tags: { name: tags })
You are allowed to do that joins(:tags) because you specified a relation inside Post and tags inside where is an array of tag names. Easy-peasy!
Post.where(tags: tags) =>
SELECT `posts`.*
FROM `posts`
WHERE `posts`.`tags`
IN ('News', 'Weather', 'Life')
I need to make and query with has_many relation model.
I have following tables. Articles has has_many relation to tags.
class Article < ActiveRecord::Base
has_many :tags
end
class Tag < ActiveRecord::Base
belongs_to :article
end
For example, I would like to get articles contains "BBQ" and "Pork"
I have following code but it return articles contains "BBQ" or "Pork"
tags = ["BBQ", "Pork"] # number of items could be any
Article.joins(:tags).where(tags: { tag_name: tags } )
Edit:
We don't know how many items in tags.
tag_name field is String.
It's not that pretty but you could try
Article.joins(:tags).where(tags: { tag_name: "BBQ" } ).where(tags: {tag_name: "Pork"})
If you need something more reusable you can create a scope such as "by_tag_name":
Article.rb
scope :by_tag_name, ->(name) { where(tags: {tag_name: name} }
And use it simply like:
Article.joins(:tags).by_tag_name("BBQ").by_tag_name("Pork")
If tags is of unknown size:
#articles_with_tags = Article.joins(:tags)
#assume tags is a regular ruby array ["Pork", "Soy Sausages"]
tags.each do |tag|
#articles_with_tags = #articles_with_tags.by_tag_name(tag)
end
I would first get all the tag records that are BBQ like so:
Tag.where(tag_name:"BBQ")
Then from those, I would add all the tag records that have tag_name Pork:
Tag.where(tag_name:"BBQ") + Tag.where(tag_name:"Pork")
Then in order to the the articles that they belong to, I would take the combined array and append .articles.
So my final search would be:
(Tag.where(tag_name:"BBQ") + Tag.where(tag_name:"Pork")).articles
Note on database structure
My guess is that you want to have two tables, one for all the tags used on your site, and one for the articles. You want each article to possibly have multiple tags. If thats the case, then it might make more sense to make a join table instead of just having the two, because that would mean that a tag could belong to multiple articles and and multiple articles could belong to a tag. To do that, you would add a table:
create_table :article_tags do |i|
i.integer :article_id
i.integer :tag_id
end
In that case, you could have a simpler query.
I have a Post model combined with gem Acts_as_Taggable_on.
I would like to display all posts with all their tags, but the tags should be sorted by number of their use (number of posts tagged with certain tag).
To do that I looped through ActiveRecord_Relation and did a sort on Tags column:
def index
temp_posts = Post.all.order('updated_at DESC')
temp_posts.each_with_index do |temp_post, index|
temp_posts[index].tags = temp_post.tags.sort_by {|tag| -tag.taggings_count}
end
#show = temp_posts.first.tags.sort_by {|tag| -tag.taggings_count} # according to this control output it should work
#posts = temp_posts
end
When looking through the control output #show, the tags are sorted as required, but they are not saved into the temp_posts variable. The output is thus unsorted.
What can I do to 'save' the changes I made in the loop?
Since you have Tag#taggings_count, you can order your associations by it. I don't know if this will conflict with what ActsAsTaggable is up to, but this is what it would look like in vanilla Rails. Perhaps ActsAsTaggable has some options to accomplish the same thing.
class Post < ActiveRecord::Base
has_many :taggings
has_many :tags, through: :taggings, -> { order(taggings_count: :desc) }
end
For more details, see Scopes for has_many.
If you don't want the order applied globally, andrykonchin's idea is a good one. Write a Post#sorted_tags method and you can access it on the Post when you want it. Memoizing it into an instance variable will prevent extra database queries.
class Post < ActiveRecord::Base
def sorted_tags
#sorted_tags ||= tags.sort_by(&:taggings_count).reverse
end
end
The problem was in the end only with using an invalid variable for saving the sorted tags.
Acts as Taggable on uses variable tag_list to store the tags associated with the Tag model. Instead I have wrongly used variable tags.
The full correct version of my code:
def index
temp_posts = Post.all.order('updated_at DESC')
temp_posts.each_with_index do |temp_post, index|
// CHANGE: temp_posts[index].tags => temp_posts[index].tag_list
temp_posts[index].tag_list = temp_post.tags.sort_by {|tag| -tag.taggings_count}
end
#posts = temp_posts
end
I have 3 models with "1 to n" associations, like this
Client --1 to n--> Category --1 to n--> Item
In one page, I need to display a list of Items along with their Categories. This page is subject to 3 level of filtering:
Client filtering: I know the client id (I'll use 'id=2' in this example)
Category name: dynamic filter set by the user
Item name: dynamic filter set by the user
And I'm getting more and more confused with ActiveRecord Associations stuff
In my ItemsController#index, I tried this:
categories = Client.find(2).categories
.where('name LIKE ?', "%#{params[:filter_categories]}%")
#items = categories.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
The second line raises a NoMethodError undefined method 'items' for ActiveRecord::Relation. I understand that the first line returns a Relation object, but I cannot find a way to continue from here and get the list of Items linked to this list of Categories.
I also started to extract the list of categories ids returned by the first line, in order to use them in the where clause of the second line, but while writing the code I found it inelegant and thought there may be a better way to do it. Any help would be very appreciated. Thanks
models/client.rb
class Client < ActiveRecord::Base
has_many :categories
has_many :items, through: :categories
...
end
models/category.rb
class Category < ActiveRecord::Base
belongs_to :client
has_many :items
...
end
model/item.rb
class Item < ActiveRecord::Base
belongs_to :category
has_one :client, through: :category
...
end
You can only call .items on a category object, not on a collection. This would work:
#items = categories.first.items
.where('name LIKE ?', "%#{params[:filter_items]}%")
To get what you want, you can do the following:
#items = Item
.where('category_id IN (?) AND name LIKE ?', categories, "%#{params[:filter_items]}%")
Assuming that in the end you are only interested in what is in #items, it would be even better to do it in one query instead of two, using joins:
#items = Item.joins(:category)
.where('items.name LIKE ? AND categories.name = ? AND categories.client_id = 2', "%#{params[:filter_items]}%", "%#{params[:filter_categories]}%")
You can try smth like this:
item_ids = Client.find(2).categories.inject([]) { |ids, cat| ids |= cat.item_ids; ids }
items = Item.find(item_ids)
This how you can get a list of nested objects that associated through another table.
if I've two models.
Modela :has_many :modelbs
Modelb :belongs_to :modala
How can I use inner join to retrieve the data from the database, store them inside an array an return it back to the caller as a JSON string?
I understand there are find and related methods, but these methods seemed to be returning me array containing only objects of the class I did a find on (eg. Modela.find), and I will have to iterate through that array and and pull out Modelbs that belongs to Modela. Is there a easier way of doing it?
User has_many :posts
Post belongs_to :user
#user = User.find(params[:user_id])
#posts = #user.posts.to_json
update Multiple users and their posts to json:
#users = User.all
#posts = #users.to_json(:include => :posts)