scope for count children has_many relation - sql

I have two models
# == Schema Information
#
# Table name: answers
#
# id :integer not null, primary key
# content :text
# question_id :integer
# accept :boolean
# created_at :datetime not null
# updated_at :datetime not null
# user_id :integer
#
class Answer < ActiveRecord::Base
attr_accessible :accept, :content, :question_id, :user
belongs_to :question
belongs_to :user
delegate :username, to: :user, allow_nil: true, prefix: 'owner'
end
and Question
# == Schema Information
#
# Table name: questions
#
# id :integer not null, primary key
# title :string(255)
# content :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :integer
# viewed_count :integer default(0)
#
class Question < ActiveRecord::Base
validates_presence_of :title, :content, :user
attr_accessible :content, :title, :tag_list
acts_as_taggable
belongs_to :user, :counter_cache => true
has_many :answers
delegate :username, to: :user, allow_nil: true, prefix: 'owner'
scope :owner, joins(:user)
scope :without_answer, joins(:answers).
select('questions.id').
group('questions.id').
having('count(answers.id) = 0')
validate :validation_of_tag_list
def self.no_answer
Question.all.select{|question|question.answers.count == 0}
end
The scope without_answer and class method no_answer theoretically should be same. However, I run them in the console as below:
Loading development environment (Rails 3.2.13)
irb(main):001:0> Question.without_answer
Question Load (0.6ms) SELECT questions.id FROM `questions` INNER JOIN `answers` ON `answers`.`question_id` = `questions`.`id` GROUP BY questions.id HAVING count(answers.id) = 0
=> []
irb(main):002:0> Question.no_answer
Question Load (0.6ms) SELECT `questions`.* FROM `questions`
(0.5ms) SELECT COUNT(*) FROM `answers` WHERE `answers`.`question_id` = 1
(0.4ms) SELECT COUNT(*) FROM `answers` WHERE `answers`.`question_id` = 2
(0.4ms) SELECT COUNT(*) FROM `answers` WHERE `answers`.`question_id` = 16
(0.4ms) SELECT COUNT(*) FROM `answers` WHERE `answers`.`question_id` = 17
(0.3ms) SELECT COUNT(*) FROM `answers` WHERE `answers`.`question_id` = 34
=> [#<Question id: 2, title: "Here is the second", content: "here you go\r\n", created_at: "2013-04-20 00:34:15", updated_at: "2013-04-20 00:34:15", user_id: nil, viewed_count: 0>, #<Question id: 16, title: "my question", content: "Here is my question", created_at: "2013-04-21 02:02:47", updated_at: "2013-04-23 02:29:27", user_id: 1, viewed_count: 1>, #<Question id: 17, title: "Hello", content: "me", created_at: "2013-04-23 00:37:56", updated_at: "2013-04-23 00:37:56", user_id: nil, viewed_count: 0>, #<Question id: 34, title: "Question title", content: "question content", created_at: "2013-04-23 04:57:49", updated_at: "2013-04-23 04:57:49", user_id: 42, viewed_count: 0>]
why the scope dose not work as expect?
which way will be better to due with such situation or even better solution?

Your without_answer scope is very close, but needs an outer join like this:
scope :without_answer,
joins('LEFT OUTER JOIN answers ON answers.question_id = questions.id').
select('questions.id').
group('questions.id').
having('count(answers.id) = 0')
Then, you can get the count with length:
Question.without_answer.length
Note: if you want without_answer to be the same as no_answer (i.e. return actual Question objects), you would need to remove the select.
A simpler and faster way to count the unanswered questions is like this:
Question.joins('LEFT OUTER JOIN answers ON answers.question_id = questions.id').
where('answers.id' => nil).count
Also, this will return the same as no_answer as-is, simply use all instead of count.

Related

How to list all posts with favorites status that is implemented by many to many relations

I have three models User, Post and Favorite.
class User < ApplicationRecord
has_many :posts
end
class Post < ApplicationRecord
belongs_to :user
end
class Favorite < ApplicationRecord
belongs_to :post
belongs_to :user
end
> user = User.create(name: 'user1')
> user.posts.create(title: 'Title 1')
> user.posts.create(title: 'Title 2')
> Favorite.create(user_id: 1, post_id: 2)
I want to retrieve all posts belongs to user1 with fav status by the user.
> User.first.posts_with_fav_status
=> [#<Post id: 1, title: "Title 1", user_id: 1, faved: false> ],
[#<Post id: 2, title: "Title 2", user_id: 1, faved: true> ]
How can I write the query method like this?
Edit
I could get fav status with the following query. But this query calls subquery every time. It will be too slow when DB become bigger. How can I rewrite this query?
def posts_with_fav_status
posts.select(<<-SQL)
*,
EXISTS(SELECT * FROM favorites
WHERE favorites.user_id = posts.user_id
AND favorites.post_id = posts.id) as faved
SQL
end
I came to this solution (tested on Rails 6.1.4.4)
class User < ApplicationRecord
has_many :posts
# User.first.posts_with_faved
def posts_with_faved
posts.select("posts.*, favorites.user_id = #{id} as faved")
.left_joins(:favorites)
end
end
# due to how the inspect works, you won't see the faved attribute in the output, but it is there
irb(main):053:0> first, second = User.first.posts_with_faved
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
Post Load (0.2ms) SELECT posts.*, favorites.user_id = 1 as faved FROM "posts" LEFT OUTER JOIN "favorites" ON "favorites"."post_id" = "posts"."id" WHERE "posts"."user_id" = ? [["user_id", 1]]
=> #<ActiveRecord::AssociationRelation [#<Post id: 1, title: "Title 1", user_id: 1, created_at: "2022-01-28 09:34:19.048598000 +0000", updated_at: "2022-01-28 09:34:19.048598000 +0000">, #<Post id: 2, title: "Title 2", user_id: 1, created_at: "2022-01-28 09:34:22.172245000 +0000", ...
irb(main):054:0> first.faved
=> nil
irb(main):055:0> second.faved
=> 1
Things to take into account:
be careful and not override with select if you chain the query. Otherwise everything will break
The query does not return a boolean, but nearly: nil instead of false and 1 instead of true. This will still work if you use it with an if
You miss relationships in User and Post model:
User:
class User < ApplicationRecord
has_many :posts
has_many :favorites
has_many :favorite_posts, through: :favorites, source: :post
end
Post:
class Post < ApplicationRecord
belongs_to :user
has_many :favorites
has_many :favorited_by, through: :favorites, source: :user
end
With that you can write
u = User.first
u.favorite_posts.where(user_id: u.id)
The way you have your tables and models I don't see a way to avoid that subquery to get the result you want.
Not sure what your use case is, but I'm assuming these are posts users are saving, and some will be favorites, because if the user is the author of the post and they are favoriting their own posts, then why not just add a column to the posts table designating it as a favorite?
If my assumption is correct, it might work better to ditch the Favorite model and use a UserPosts loookup model instead that includes the user_id, post_id, favorite.
Class User < ApplicationRecord
has_many :user_posts
has_many :posts, through: :user_posts
end
Class UserPost < ApplicationRecord
belongs_to :user
belongs_to :post
end
Class Post < ApplicationRecord
has_many :user_posts
has_many :users, through: :user_posts
end
Then you can use
user = User.first
user_posts = user.user_posts.eager_load(:posts)
The fave is on the user_posts table/model so it could be accessed by user_posts.first.fave? and the actual post content user_posts.first.post.
You can add accepts_nested_attributes_for :post to the UserPost model and to create a new post association for a user:
User.first.user_posts.create(fave: true, post: {title: 'title text', ...})
Other than something like that, you're going to have to use a subquery or second query and remap into a hash/array of your own design afterwards.

Association for the child of the child in ruby on rails

Example of data in User table
Expected result in rails console, grandfather.grandchildren and grandmother.grandchildren should return the same group of objects:
grandfather = User.first
grandfather.grandchildren
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 5, name: "father's son", father_id: 3, mother_id: nil>, #<User id: 6, name: "uncle's son", father_id: 4, mother_id: nil>]>
grandmother = User.find(2)
grandmother.grandchildren
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 5, name: "father's son", father_id: 3, mother_id: nil>, #<User id: 6, name: "uncle's son", father_id: 4, mother_id: nil>]>
This is my association now in User.rb model.
has_many :children, ->(user) { unscope(:where).where("father_id = :id OR mother_id = :id", id: user.id) }, class_name: "User"
has_many :grandchildren, through: :children, source: :children
belongs_to :mother, class_name: "User", optional: true
belongs_to :father, class_name: "User", optional: true
Output in rails console now:
irb(main):001:0> grandfather = User.first
(0.3ms) SELECT sqlite_version(*)
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "grandfather", mother_id: nil, father_id: nil>
irb(main):002:0> grandfather.grandchildren
User Load (0.3ms) SELECT "users".* FROM "users" INNER JOIN "users" "children_grandchildren" ON "users"."user_id" = "children_grandchildren"."id" WHERE (father_id = 1 OR mother_id = 1) /* loading for inspect */ LIMIT ? [["LIMIT", 11]]
Traceback (most recent call last):
ActiveRecord::StatementInvalid (SQLite3::SQLException: ambiguous column name: father_id)
You can't get the grandchildren from a grandparent by going through its children because it implies that their father/mother ids are equal to the grandparent, it doesn't travel through the grandchildren parents:
SELECT "users".*
FROM "users"
INNER JOIN "users" "children_grandchildren"
ON "users"."user_id" = "children_grandchildren"."id"
WHERE (father_id = 1 OR mother_id = 1) -- this is the grandparent id, when it should be the child parent's id
You can add a callable to the grandchildren relationship, similar to the one for children, but this time extracting the grandparent children ids, and using the IN clause to filter those user rows matching those ids, with their father/mother ids:
has_many :grandchildren,
->(user) { unscope(:where).where('father_id IN (:ids) OR mother_id IN (:ids)', ids: user.children.ids) },
class_name: "User"

How to form a rails query to grab all the discussion posts a particular user has made?

I am using rails 5.0.0
I want to make a query that will let me display all the posts the current user has made. The two relevant tables I have are:
create_table "discussions", force: :cascade do |t|
t.string "title"
t.text "content"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id"
t.integer "channel_id"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
....
t.string "unconfirmed_email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "username"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["username"], name: "index_users_on_username", unique: true
end
create_table "replies", force: :cascade do |t|
t.text "reply"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "discussion_id"
t.integer "user_id"
end
and the relationships are as follows:
class Discussion < ApplicationRecord
belongs_to :channel
belongs_to :user
has_many :replies, dependent: :destroy
has_many :users, through: :replies
class Reply < ApplicationRecord
belongs_to :discussion
belongs_to :user
class User < ApplicationRecord
rolify
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable,
:confirmable
has_many :notifications, foreign_key: :recipient_id
has_many :discussions, dependent: :destroy
has_many :channels, through: :discussions
in my discussions_controller.rb file i have the following line
#discussions = Discussion.includes(:users).where('users.id' => current_user).order('discussions.created_at desc')
and in my view file I have
<% #discussions.each do |discussion| %>
...
<% end %>
I expect there to be a few entries since I have created them, however no entries are displayed at all. This is what is printed in the terminal window
Processing by DiscussionsController#index as HTML
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ? [["id", 1], ["LIMIT", 1]]
Rendering discussions/index.html.erb within layouts/application
SQL (0.5ms) SELECT "discussions"."id" AS t0_r0, "discussions"."title" AS t0_r1, "discussions"."content" AS t0_r2, "discussions"."created_at" AS t0_r3, "discussions"."updated_at" AS t0_r4, "discussions"."user_id" AS t0_r5, "discussions"."channel_id" AS t0_r6, "users"."id" AS t1_r0, "users"."email" AS t1_r1, "users"."encrypted_password" AS t1_r2, "users"."reset_password_token" AS t1_r3, "users"."reset_password_sent_at" AS t1_r4, "users"."remember_created_at" AS t1_r5, "users"."sign_in_count" AS t1_r6, "users"."current_sign_in_at" AS t1_r7, "users"."last_sign_in_at" AS t1_r8, "users"."current_sign_in_ip" AS t1_r9, "users"."last_sign_in_ip" AS t1_r10, "users"."confirmation_token" AS t1_r11, "users"."confirmed_at" AS t1_r12, "users"."confirmation_sent_at" AS t1_r13, "users"."unconfirmed_email" AS t1_r14, "users"."created_at" AS t1_r15, "users"."updated_at" AS t1_r16, "users"."username" AS t1_r17 FROM "discussions" LEFT OUTER JOIN "replies" ON "replies"."discussion_id" = "discussions"."id" LEFT OUTER JOIN "users" ON "users"."id" = "replies"."user_id" WHERE "users"."id" = 1 ORDER BY discussions.created_at desc
Rendered shared/_discussions.html.erb (8.5ms)
Channel Load (0.3ms) SELECT "channels".* FROM "channels" ORDER BY created_at desc
Role Load (0.3ms) SELECT "roles".* FROM "roles" INNER JOIN "users_roles" ON "roles"."id" = "users_roles"."role_id" WHERE "users_roles"."user_id" = ? AND (((roles.name = 'admin') AND (roles.resource_type IS NULL) AND (roles.resource_id IS NULL))) [["user_id", 1]]
Rendered discussions/_sidebar.html.erb (34.1ms)
Rendered discussions/index.html.erb within layouts/application (46.1ms)
Completed 200 OK in 245ms (Views: 165.7ms | ActiveRecord: 6.7ms)
If I use
#discussions = Discussion.includes(:users).order('discussions.created_at desc')
Then all the discussion posts display as they normally would as if the .includes statement was not there.
So, how can I change my query to list out the discussions made by the current user?
Update As per Shiko's comment, here is the output given with his input in the rails console
2.3.0 :001 > User.find(1).discussions
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Discussion Load (0.5ms) SELECT "discussions".* FROM "discussions" WHERE "discussions"."user_id" = ? [["user_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Discussion id: 1, title: "Test", content: "alkjdflk slkfj ", created_at: "2018-04-08 22:40:06", updated_at: "2018-04-08 22:40:06", user_id: 1, channel_id: nil>, #<Discussion id: 2, title: "Fake Bakesale", content: "Come buy cookies", created_at: "2018-04-08 23:29:17", updated_at: "2018-04-08 23:29:17", user_id: 1, channel_id: 1>, #<Discussion id: 3, title: "Fake Bakesale", content: "Come buy cookies", created_at: "2018-04-08 23:30:18", updated_at: "2018-04-08 23:30:18", user_id: 1, channel_id: 1>, #<Discussion id: 4, title: "Meeting today", content: "Come to the meeting", created_at: "2018-04-08 23:35:59", updated_at: "2018-04-08 23:35:59", user_id: 1, channel_id: 1>, #<Discussion id: 5, title: "New post", content: "asdf ", created_at: "2018-04-15 21:50:20", updated_at: "2018-04-15 21:50:20", user_id: 1, channel_id: 2>]>
2.3.0 :002 >
First thing, if you run below command in rails console, you should get an below expected error :
#discussions = Discussion.includes(:users).where('users.id' => current_user).order('discussions.created_at desc')
Expected error:
ActiveRecord::ConfigurationError: Can't join 'Discussion' to
association named 'users'; perhaps you misspelled it?
To fix this issue, you have to use :user instead of :users as below, simply because each discussion belongs to one user and not many :
#discussions = Discussion.includes(:user).where('users.id' => current_user).order('discussions.created_at desc')
There is a more a clean one way to get the current user discussions, using below:
Controller:
#user = User.find(curren_user_id)
ERB file:
<% #user.discussions.each do |discussion| %>
.....
<% end %>
You can do this:
#discussions = Discussion.includes(:user).
where(users: { id: current_user.id }).
order("discussions.created_at desc")
If you don't need to reference the user attributes in your view, you can also do this, which avoids the join altogether:
#discussions = Discussion.
where(user_id: current_user.id).order(created_at: :desc)

How to update nested data in Rails

I have some users who take quizzes. I track the result they chose. I need to figure out how to allow them to change their quiz submission. If I just associate the answer, they'll have answered the question twice. Building up the data is complex, does ActiveRecord provide a way to deal with this?
This whole example will run in a standalone file.
Here is my schema:
require 'active_record'
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
ActiveRecord::Schema.define do
self.verbose = false
create_table :users do |t|
t.string :name
end
create_table :questions do |t|
t.string :name
end
create_table :question_results do |t|
t.string :name
t.integer :question_id
end
create_table :question_results_users do |t|
t.integer :user_id
t.integer :question_result_id
end
end
here are my models:
class User < ActiveRecord::Base
has_and_belongs_to_many :question_results
has_many :questions, through: :question_results
end
class Question < ActiveRecord::Base
has_many :question_results
end
class QuestionResult < ActiveRecord::Base
belongs_to :question
has_and_belongs_to_many :users
end
Lets make three questions and some answers:
Question.create! name: "What's your favourite movie?" do |question|
question.question_results.build name: 'Gattaca'
question.question_results.build name: 'Super Troopers'
end
Question.create! name: 'Who do you want to be president?' do |question|
question.question_results.build name: 'Barack Obama'
question.question_results.build name: 'Mitt Romney'
question.question_results.build name: 'Mickey Mouse'
end
Question.create! name: "What's your favourite colour?" do |question|
question.question_results.build name: 'black'
question.question_results.build name: 'green'
end
Lets make a user:
jim = User.create! name: 'Jim'
jim.question_results << QuestionResult.find_by_name('Gattaca')
jim.question_results << QuestionResult.find_by_name('Barack Obama')
jim.question_results << QuestionResult.find_by_name('black')
jim.question_results # => [#<QuestionResult id: 1, name: "Gattaca", question_id: 1>, #<QuestionResult id: 3, name: "Barack Obama", question_id: 2>, #<QuestionResult id: 6, name: "black", question_id: 3>]
jim.question_results.map(&:question) # => [#<Question id: 1, name: "What's your favourite movie?">, #<Question id: 2, name: "Who do you want to be president?">, #<Question id: 3, name: "What's your favourite colour?">]
Now Jim changes his mind, he decides he likes Super Troopers and doesn't want to vote, but he still likes black. How do I update this without having him answer the question multiple times?
# pretend we're in a controller here (also note that I can change the format of the data, if there is something more convenient)
posted_from_form = {
questions: {
Question.all[0].id => 'Super Troopers',
Question.all[1].id => '',
Question.all[2].id => 'black',
}
}
First lets change the format of our data so that the results are in id form instead of name form:
posted_from_form = {
questions: {
Question.all[0].id => QuestionResult.find_by_name('Super Troopers').id.to_s,
Question.all[1].id => '',
Question.all[2].id => QuestionResult.find_by_name('black').id,
}
}
Then you can set the ids directly, and ActiveRecord will handle all of the complexity:
jim.question_result_ids = posted_from_form[:questions].values # => ["2", "", 6]
jim.question_results # => [#<QuestionResult id: 6, name: "black", question_id: 3>, #<QuestionResult id: 2, name: "Super Troopers", question_id: 1>]

Rails :include doesn't include

My models:
class Contact < ActiveRecord::Base
has_many :addresses
has_many :emails
has_many :websites
accepts_nested_attributes_for :addresses, :emails, :websites
attr_accessible :prefix, :first_name, :middle_name, :last_name, :suffix,
:nickname, :organization, :job_title, :department, :birthday,
:emails_attributes
end
class Email < ActiveRecord::Base
belongs_to :contact
validates_presence_of :account
validates_format_of :account, :with => /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
attr_accessible :contact_id, :account, :label
end
If I run the following query, the emails are returned as expected:
c = Contact.find(3)
Contact Load (3.2ms) SELECT `contacts`.* FROM `contacts` LIMIT 1
=> #<Contact id: 3, prefix: nil, first_name: "Micah", middle_name: nil, last_name: "Alcorn", suffix: nil, nickname: nil, organization: nil, job_title: nil, department: nil, birthday: nil, created_at: "2011-07-04 23:50:04", updated_at: "2011-07-04 23:50:04">
c.emails
Email Load (4.4ms) SELECT `emails`.* FROM `emails` WHERE `emails`.`contact_id` = 3
=> [#<Email id: 3, contact_id: 3, account: "not#real.address", label: "work", created_at: "2011-07-04 23:50:04", updated_at: "2011-07-04 23:50:04">]
However, attempting to :include the relationship does not:
c = Contact.find(3, :include => :emails)
Contact Load (0.5ms) SELECT `contacts`.* FROM `contacts` WHERE `contacts`.`id` = 3 LIMIT 1
Email Load (0.8ms) SELECT `emails`.* FROM `emails` WHERE `emails`.`contact_id` IN (3)
=> #<Contact id: 3, prefix: nil, first_name: "Micah", middle_name: nil, last_name: "Alcorn", suffix: nil, nickname: nil, organization: nil, job_title: nil, department: nil, birthday: nil, created_at: "2011-07-04 23:50:04", updated_at: "2011-07-04 23:50:04">
As you can see, the SQL is being executed, but the emails are not being returned. I intend to return all contacts with each containing email(s), so :joins won't do any good. What am I missing?
The emails are there. Did you try c.emails? You will find that the emails will be there without Rails doing an additional DB query.
The thing that :include does is called eager loading, which basically means Rails will try a best effort method of prepopulating your objects with their relations, so that when you actually ask for the relation no additional DB queries are needed.
See the section "Eager loading of associations" here:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
You might also want to check out this RailsCast:
http://railscasts.com/episodes/181-include-vs-joins