Simple has_many :through association - ruby-on-rails-3

Pretty simple setup. I want to make sure my understanding of the ORM is correct.
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through => memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, through => memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
Now when a user creates a group I want the membership record in the link table to get populated. It should be an atomic(transaction).
class GroupsController < ApplicationController
def create
#group = current_user.groups.build(params[:group])
if #group.save
flash[:notice] = "Group has been created."
redirect_to #group
else
flash[:alert] = "Group has not been created."
render :action => "new"
end
end
end
This doesn't work. The group gets saved but no membership record created in the link table. However using a create vs build works. Is that how it's supposed to work?
What's the best approach here?

This behaviour is by design. As you mentioned, you can either do #group = current_user.groups.create(params[:group]).
Or you can add an additional statement to create a record in the join model's table as :
#group = current_user.groups.build(params[:group])
if #group.save
#group.memberships.create(:user_id => current_user)
# redirect and notify
Well, the reason being simply building #group and saving it does not add an additional record in the join table.
Infact, in this case, #group = current_user.groups.build(params[:group]) is somewhat similar to #group = Group.new(params[:group]). The difference being, in the former case, current_user.groups will contain #group (you can try that in Groups#create before redirect) but doing current_user.reload followed by current_user.groups will yield [].
The best way to do this is somewhat similar to your approach. Have a simple create action as :
def create
#group = Group.new(params[:group])
# if else for save and redirect
However, for this to work the params hash submitted to Groups#create should include user_ids as :
"group"=>{"name"=>"new group", "user_ids"=>["1", "2", "3"]}, "commit"=>"Create Group"
May be that was the reason why #bruno077 was asking you to paste your view's code, so as to get an idea on user_ids params being passed.
So, if the new group form contains fields to select multiple users, then its simple create action as shown right above (because of the user_ids params). But if have a new group form with no options to select users, then you are better off using the first option (one using create).

Related

How to reduce queries on associated object with Rails ActiveRecord

I have the following problem.
I have an Organization class that returns and array of "authorized user" emails and their associated User ID.
class Organization < ApplicationRecord
...
has_many :authorized_users
def authorized_user_opts
self.authorized_users.map do |authorized_user|
[authorized_user.email, authorized_user.user.id]
end
end
end
Then the AuthorizedUser class - note that we lookup the user via a find_by:
class AuthorizedUser < ApplicationRecord
...
def user
User.find_by(email: email)
end
end
And the User model:
class User < ApplicationRecord
validates :email, presence: true
end
This creates an extra query for each user to get their ID. Is there a way I can improve this query?
I thought about migrating the AuthorizedUser class to add a user_id field, but I'm wondering if there's a way to improve this just SQL instead of adding another field.
I think the addition of a user_id to AuthorizedUser is a decent choice, but if you must do it without, you should be able to use joins and includes:
authorized_users.joins("inner join users on users.email = authorized_users.email").includes(:users)
joins here is doing what an ActiveRecord association would do under the hood, and then includes eager loads the user objects in one query so that you don't have N queries for N users.
You might also be able to mess with the options on belongs_to which lets you specify the key that it uses under the hood. Something like:
# authorized_user.rb
belongs_to :user, foreign_key: 'email', primary_key: 'email'

Add Arbitrary Attribute to SQL Query from Joins Record without WHERE clause (Active record)

I'm trying to create an attribute in my select statement that depends on whether or not an association exists. I'm not sure if it's possible with a single query, and the goal is to not have to iterate a list afterward.
Here is the structure.
class Project < ApplicationRecord
has_many :subscriptions
has_many :users, through: :subscriptions
end
class User < ApplicationRecord
has_many :subscriptions
has_many :projects, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :project
belongs_to :user
end
Knowing a project, the goal of the query is to return ALL users and include on them a new attribute call subscribed - denoting whether or not they are subscribed.
non-working code (pseudo code):
project = Project.find_by(name: 'has_subscribers')
query = 'users.*, (subscriptions.project_id = ?) AS subscribed'
users = User.includes(:subscriptions).select(query, project.id)
user.first.subscribed
# => true or false
I'm open to whether or not there is a better way of going about this. However, the information is:
You know the project record.
You query a list of ALL users
Each user record has a subscribed attribute, denoting whether its
subscribed to the given project
Solution:
I was able to figure out a straight forward solution using the bool_or aggregate method. Coalesce ensures that the value returned is false instead of nil, should no subscriptions exists.
query = "users.*, COALESCE(bool_or(subscriptions.project_id = '#{project_id}'::uuid), false) as subscribed"
User.left_outer_joins(:subscriptions)
.select(query)
.group('users.id')
Yep, you can do this:
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id])
Which will result in a SQL query like this:
SELECT *, "subscriptions"."project_id" FROM "users" INNER JOIN "subscriptions" ON "subscriptions"."user_ud" = "users"."id";
If you want to specify a specific project (i.e. use an expression), you can do it with Arel like this:
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id].eq(42))
Unfortunately, you won't have a column name alias, and you can't call as on an Arel::Nodes::Equality instance. I don't know enough about the internals of Arel to have a way out of that box. But you can do this if you want the composability of Arel (e.g. if this is going to be something that needs to work with multiple models or columns):
User.joins(:projects).select(Arel.star, Subscription.arel_table[:project_id].eq(42).to_sql + " as has_project")
This is a bit clunky, but it works and provides a user.has_project method that returns a boolean. You can pretty it up like so:
class User
scope :with_project_status, lambda do |project_id|
has_project =
Subscription.arel_table[:project_id].
eq(project_id).to_sql + " as has_project"
joins(:projects).select(Arel.star, has_project)
end
end
User.with_project_status(42).where(active: true)

has_many :through and build

I have three models, Account, User and Contact:
class User < ActiveRecord::Base
has_one :account
has_many :contacts, :through => :account
end
class Account < ActiveRecord::Base
belongs_to :owner, :class_name => 'User'
has_many :contacts
end
class Contact < ActiveRecord::Base
belongs_to :account
end
I'm trying to scope build a new contact through the user record, like this in my contacts controller.
def create
#contact = current_user.contacts.build(params[:contact])
respond_to do |format|
if #contact.save
...
else
...
end
end
end
When I do this, I don't receive any errors, the contact record is saved to the database however the account_id column is not set on the contact, and it is not added to the collection so calling #current_user.contacts returns an empty collection.
Any suggestions?
Using build makes a new instance of Contact in memory, but you would need to manually set the account_id on the record (e.g. #contact.account_id = current_user.account.id), or perhaps set it in a hidden field in the new form used to display the contact for creation such that it is picked up in the params array passed to the build method.
You might also want to consider whether accepts_nested_attributes_for may be helpful in this case. Another option may be to use delegate, although in both cases, your use may be sort of the opposite of what these are intended for (typically defined on the "parent").
Update:
In your case, the build method is added to both the User instance and to the Account (maybe "Owner") instance, because you have both a many-to-many relationship between User and Contact, as well as a one-to-many relationship between Account and Contact. So to get the account_id I think you would need to call Account's build, like
#contact = current_user.accounts.contacts.build(params[:contact])
Does this work?

How could I update an intermediary table in rails using checkobxes?

I am not sure how to figure this one out, because I dont know how I would update my intermediary table, without having to do delete_all. Or where to do inserts and deletes, etc.
I have three classes:
class User < ActiveRecord::Base
has_many :players
has_many :teams, :through => :players
end
class Team < ActiveRecord::Base
has_many :players
end
class Player < ActiveRecord::Base
belongs_to :team
belongs_to :user
(contains other columns like, is_captain, has_paid, etc.)
end
I am displaying a page with checkboxes and you can select which 'users' you would like to be on your team (from your AddressBook). So imagine 25 names listed, and some of them are already checked and others are not. How could I save the data of checkboxes, cause they may have 3 different states: 1) insert brand new player (insert row into player) 2) remove player from team (delete existing player) or 3)do nothing (a player you dont want on your team)
The issue i am running into is that when you 'uncheck' a box, then the front-end does not send that to back-end? Any ideas how this can be done nicely?
Not sure if this is the best way to do this but it works, I am open to recommendations in improving this code
params[:team][:user_ids] ||= []
#team = Team.find(params[:team][:id])
current_players = #team.players
current_players_user_ids = current_players.collect { |player| player.user_id }
user_ids = params[:team][:user_ids]
puts "input ids #{user_ids}"
#add current_user because checkbox does not pass them in since its 'disabled'
user_ids << current_user.id
#delete the current_players that are not in the the received 'checkboxes'
current_players_user_ids.each do |current_player_user_id|
unless user_ids.include?(current_player_user_id)
Player.delete_all(:team_id => #team.id, :user_id => current_player_user_id)
end
end
#iterate through input ids and run 'inserts' on those who are not current-players
user_ids.each do |user_id|
unless current_players_user_ids.include?(user_id)
player = Player.new(:user_id=>user_id, :team_id=>#team.id)
#team.players << player
end
end

Querying for rows without matching ID in associated table

I have a very standard app backed by an SQL database with a User model, a Problem model, and a CompletedProblem model acting as a join table between the two.
I'm trying to create a method that returns all problems not solved by a particular user. I have run into a wall, however, and I would appreciate pointers on what my method should look like.
Below are the models as well as my latest (incorrect) pass at creating this method.
class User < ActiveRecord::Base
has_many :completed_problems
has_many :problems, :through => :completed_problems
def unsolved_problems
Problem.includes({:wall => :gym}, :completed_problems).
where('completed_problems.user_id != ? OR completed_problems.user_id IS NULL)', self.id)
end
end
class Problem < ActiveRecord::Base
has_many :completed_problems
has_many :users, :through => :completed_problems
end
class CompletedProblem < ActiveRecord::Base
belongs_to :user
belongs_to :problem
end
(For the curious: this method does work so long as there is only one user marking problems as solved. As soon as you add a second, each user starts to return only those problems that have been solved by other users, instead of those not solved by herself.)
Via a friend:
select * from problems where id not in (select problem_id from completed_problems where user_id = USER_ID))
Although I'd still be interested in hearing if there's a way in ActiveRecord to do this.
I think something like this will do it:
Problem.where(["id NOT IN (?)", self.problems.all.map(&:id)])