Ruby select method in Has Many Through Association - sql

Can you explain What the following method does and how it works? I have tried to understand it researching for several hours, but I couldn't figure it out yet.
def self.tag_counts
Tag.select("tags.name, count(taggings.tag_id) as count").joins(:taggings).group("taggings.tag_id")
end
What I understand is the ruby method 'select' usually takes a block, not like the one used in this method. The self.tag_counts method is in the event model of my application as follows.
models/event.rb
class Event < ActiveRecord::Base
extend FriendlyId
friendly_id :title, use: :slugged
belongs_to :organizers, class_name: "User"
has_many :taggings
has_many :tags, through: :taggings
def all_tags
tags.map(&:name).join(",")
end
def all_tags=(names)
self.tags = names.split(",").map do |n|
Tag.where(name: n.strip).first_or_create!
end
end
def self.tagged_with(name)
Tag.find_by_name!(name).events
end
def self.tag_counts
Tag.select("tags.name, count(taggings.tag_id) as count").joins(:taggings).group("taggings.tag_id")
end
end
models/tag.rb
class Tag < ActiveRecord::Base
has_many :taggings
has_many :events, through: :taggings
end
models/tagging.rb
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :event
end

This is an ActiveRecord query of the database. It's returning the list of tags with name and number of taggings. It looks like the input data for a tag cloud or a tag leaderboard.
This will produce something like the following SQL statement:
SELECT tag.name, count(taggings.tag_id) as count
FROM tags
LEFT JOIN taggings ON tags.id = taggings.tag_id
GROUP BY taggings.tag_id
When you think of the Ruby select method, you may be thinking of the Array#select method, which is essentially a filter for arrays. The ActiveRecord select method is a database query refinement tool. You can read more about it in the Active Record Query Interface guide in the Selecting Specific Fields section.

Related

Rails: Convert a complex SQL query to Arel or ActiveRecord

In a Rails app I'm working on, I've set up a scope on a Newsletter Author model which uses a complex query I've figured out in raw SQL like so:
scope :without_submissions_for, lambda { |newsletter_id|
query = <<~SQL.squish
SELECT * FROM "newsletter_authors"
WHERE "newsletter_authors"."discarded_at" IS NULL
AND "newsletter_authors"."newsletter_id" = :newsletter_id
AND "newsletter_authors"."group_member_id" IN (
SELECT DISTINCT "group_members"."id" FROM "group_members"
INNER JOIN "newsletter_stories" ON "newsletter_stories"."author_id" = "group_members"."id"
WHERE "newsletter_stories"."author_id" = "group_members"."id"
AND "newsletter_stories"."status" = 'draft'
AND NOT (
EXISTS (
SELECT 1 FROM "newsletter_stories"
WHERE "newsletter_stories"."author_id" = "group_members"."id"
AND "newsletter_stories"."status" = 'submitted'
)
)
);
SQL
find_by_sql([query, newsletter_id: newsletter_id])
}
This does exactly what I need it to, which, along with some context (models below) is this: A Group has Members and Newsletters. Some of those members can be authors for a given newsletter. Those authors can write stories for the newsletter, and each story can be in Draft, Submitted (for publication), Published or Retracted state. Draft stories may or may not be assigned to a specific newsletter, but stories in all other states are assigned to a single newsletter. This query identifies authors assigned to a specific newsletter who have drafts written but no submitted stories to that newsletter.
I'd love to figure out how to translate this to Arel or Active Record statements for better consistency with elsewhere in the codebase. I cannot quite wrap my head around all the details of making that happen, particularly around setting up the subquery correctly.
EDIT: Changed the query to sanitize the newsletter_id argument, as per #Eyeslandic's suggestion.
EDIT 2: Here are the models I'm working with here, condensed for clarity. Remember, the scope above is on the Newsletter::Author model:
group.rb
class Group < ApplicationRecord
has_many :members, class_name: 'GroupMember'
has_many :newsletters
end
group_member.rb
class GroupMember < ApplicationRecord
belongs_to :group
has_many :authorships, inverse_of: :group_member, class_name: "Newsletter::Author"
has_many :stories, inverse_of: :author, class_name: "Newsletter::Story"
end
newsletter.rb
class Newsletter < ApplicationRecord
has_many :authors, inverse_of: :newsletter
has_many :stories
end
newsletter/author.rb
class Newsletter::Author < ApplicationRecord
belongs_to :newsletter, inverse_of: :authors
belongs_to :group_member, class_name: "GroupMember", inverse_of: :authorships
end
newsletter/story.rb
class Newsletter::Story < ApplicationRecord
belongs_to :newsletter, inverse_of: :stories, optional: true
belongs_to :author, inverse_of: :stories, class_name: "GroupMember"
enum status: {draft: "draft", submitted: "submitted", published: "published"}, _default: "draft"
end
While we can certainly build your query in Arel, after reviewing your SQL a bit it looks like it would actually be much cleaner to simply build this using the AR API instead.
The following should produce the exact query you are looking for (sans "newsletter_stories"."author_id" = "group_members"."id" because this is already implied by the join)
class Newsletter::Author < Application Record
belongs_to :newsletter, inverse_of: :authors
belongs_to :group_member, class_name: "GroupMember", inverse_of: :authorships
scope :without_submissions_for, ->(newsletter_id) {
group_members = GroupMember
.select(:id)
.joins(:stories)
.where(newsletter_stories: {status: 'draft'})
.where.not(
Newsletter::Story
.select(1)
.where(status: 'submitted')
.where(Newsletter::Story.arel_table[:author_id].eq(GroupMember.arel_table[:id]))
.arel.exists
).distinct
where(discarded_at: nil, newsletter_id: newsletter_id, group_member_id: group_members)
}
end

Efficient way to return select columns from Rails 4 ActiveRecord nested query

In my Rails 4 app, I have the following models:
class Person < ActiveRecord::Base
has_many :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
belongs_to :city
end
class City < ActiveRecord::Base
has_many :addresses
end
I'm using the :includes function to return query result into one variable:
Address.includes(:person, :city).where("person_id = 1")
It works as expected, except that I do not want the query to return every single column.
Here's what I've tried:
use select and specify table name and column names explicitly, e.g. "city.name", but Rails generates a big query with outer joins, that can be very costly, especially when there are lots of concurrent requests, so prefer a better solution.
don't want to hard code complete and raw SQL statements, because of maintenance issue later on
create a new "dummy" belongs_to relationship like in Address: belongs_to :city_select_columns, -> { select('name') }, :class => 'City', but that doesn't work (actually I'm not sure if that select is even supported, only came across documentation about where so far).
maybe define scope in City and Person? but I'm not sure how it should be defined or if it'd make sense to do it this way
Suggestions? Thanks
Have you tried this?
class Person < ActiveRecord::Base
has_many :addresses
has_many :cities, :through => :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
belongs_to :city
end
class City < ActiveRecord::Base
has_many :addresses
end
Then:
Person.find(1).cities.pluck(:name)
Looks like this generates an INNER JOIN but with indexes it shouldn't be too costly?
Did you try select?
Address.select(<output_columns>).includes(:person, :city).where("person_id = 1")
Could not find a good query method using Rails' API, I ended up writing a raw inner join SQL, then call ActiveRecord::Base.connection.execute to run it.

ActiveRecord query for multiple has_many associactions

Using Rails 3.2 I have the following models:
class Category < ActiveRecord::Base
has_many: posts
end
class Post < ActiveRecord::Base
belongs_to :category
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
I'm now looking for a query to find all comments belonging to a certain category.
I would do a join query like this:
Comment.joins(:post=>:category).where("categories.id = ?", category)
One thing to note is the memory usage. If you have lots of fields in comment, post and category and lots of records its not going to be pretty. So use select to specify the fields you need.

Rails 3 - associations "through" - how to get the data from DB?

I have a problem with fetching data from DB, where is between models association kind through.
On my site, I have a categories, like a sports, news, weather etc. When an user is logged in and has a selected the categories, from which want to see the articles, then I would like to display only these articles.
Here's how looks like my models:
class User < ActiveRecord::Base
has_many :user_categories
has_many :categories, :through => :user_categories
end
class Category < ActiveRecord::Base
has_many :articles
has_many :user_categories
has_many :users, :through => :user_categories
end
class UserCategory < ActiveRecord::Base
belongs_to :user
belongs_to :category
end
class Article < ActiveRecord::Base
belongs_to :category
end
But I still can't find the way, how to get all articles from user's selected categories... I tried something like
Article.joins("LEFT JOIN categories ON category.id = user_categories.category_id").where('user_categories.user_id = ?', current_user.id)
I would grateful for every advice!
Thank you
Here's one way to do it:
Article.where(:category_id => current_user.categories.map {|c| c.id})
That will create 2 queries. First one will return a list of the current user's categories. Then the ruby map function will create an array containing the ids of those categories. The second query will then return a list of articles whose category_id is in the array of ids. The second query will look something like:
select articles.* from articles where articles.category_id in(1,2,3);

Find all tags that are in use

I am little bit stuck with the following problem. I have two models:
class Book < ActiveRecord:Base
has_and_belongs_to_many :tags
end
class Tag < ActiveRecord:Base
has_and_belongs_to_many :books
end
I have a list of specific tags that can but must not be used in the tags table:
tag1, tag2, tag3, tag4, tag5, ...
Each new book can have several tags. As usual, relationships are stored in a join table "books_tags".
How can I get a list of all tags that are at least related to one book?
You can use :joins as an option in your find call. E.g.
Tag.find(:all, :select => 'distinct tags.*', :joins => :books)
This will only find tags that have a book associated and the :select => 'distinct tags.*' ensures you only retrieve each tag once even if they are associated with multiple books.
This would probably be easier using the has_many ..., :through method of joining than the old-fashioned has_and_belongs_to_many which is generally not as versatile.
A simple way of restructuring this is:
class Book < ActiveRecord:Base
has_many :book_tags
has_many :tags, :through => :book_tags
end
class Tag < ActiveRecord:Base
has_many :book_tags,
has_many :books, :through => :book_tags
end
class BookTag < ActiveRecord::Base
belongs_to :book
belongs_to :tag
end
If you're looking for a list of tags that have at least one book, you can retrieve this using the BookTag model. In this case you're looking for the distinct set of tags from the join list:
SELECT DISTINCT tag_id FROM book_tags
You can wrangle that into a find call easily enough.
You may find an acts_as_taggable type plugin that handles this for you, though.