Write join table data - has_many :through - ruby-on-rails

That should be a simple question but i can't find a good solution online.
I have three tables/models. User, Alliance and Alliance_Membership. The latter is a join table describing the :Alliance has_many :Users through :Alliance_Membership relationship.
Everything works ok, but Alliance_Membership now has an extra field called 'rank'. The question is, how do i set that when creating my new object ? Currently, i do something like :
#alliance.users << current_user
This is really convenient since it populates my Alliance_Membership table automatically. But, how can i set the Alliance_Membership.rank field as well ?

You'll need to create the membership yourself to set the 'rank' attribute. Something like this:
#alliance.alliance_memberships.create!(
:user => current_user,
:rank => 'whatever')

Related

Accessing the associated join model when iterating through a has_many :through association

I have a feeling this is a pretty basic question, but for some reason I'm stumped by it (Rails newbie) and can't seem to find the answer (which may be I'm not searching properly).
So I have a basic has_many :through relationship like this:
class User < ApplicationRecord
has_many :contacts, through :user_contacts
class Contact < ApplicationRecord
has_many :users, through :user_contacts
In users/show.html.erb I'm iterating through a single user's contacts, like:
<% #user.contacts.each do |c| %>
<%= c.name %>
<% end %>
Now inside of that each loop, I want to access the user_contact join model that's associated with the given user and contact in order to display the created_at timestamp that indicates when the user <--> contact relationship was made.
I know I could just do a UserContact.find call to look up the model in the database by the user_id and contact_id but somehow this feels superfluous. If I understand correctly how this works (it's entirely possible I don't) the user_contact model should have already been loaded when I loaded the given user and its contacts from the database already. I just don't know how to properly access the correct model. Can someone help with the correct syntax?
Actually the join model will not have been loaded yet: ActiveRecord takes the through specification to build its SQL JOIN statements for querying the correct Contact records but effectively will only instantiate those.
Assuming you have a UserContact model, you could do sth like this:
#user.user_contacts.includes(:contact).find_each do |uc|
# now you can access both join model and contact without additional queries to the DB
end
If you want to keep things readable without cluttering your code with uc.contact.something, you can set up delegations inside the UserContact model that delegate some properties to contact or user respectively. For example this
class UserContact < ActiveRecord::Base
belongs_to :user
belongs_to :contact
delegate :name, to: :contact, prefix: true
end
would allow you to write
uc.contact_name
First of all, the has_many :things, through: :other_things clause is going to look for the other_things relationship to find :things.
Think of it as a method call of sorts with magic built in to make it performant in SQL queries. So by using a through clause you're more or less doing something like:
def contacts
user_contacts.map { |user_contact| user_contact.contacts }.flatten
end
The context of the user_contacts is completely lost.
Since it looks like user_contacts is a one-to-one join. It would be easier to do something like this:
<% #user.user_contacts.each do |user_contact| %>
<%= user_contact.contact.name %>
<% end %>
Also since you're new to Rails it's worth mentioning that to load those records without an N+1 query you can do something like this in your controller:
#user = User.includes(user_contacts: [:contacts]).find(params[:id])
Use .joins and .select in this way:
#contacts = current_user.contacts.joins(user_contacts: :users).select('contacts.*, user_contacts.user_contact_attribute_name as user_contact_attribute_name')
Now, inside #contacts.each do |contact| loop, you can call contact.user_contact_attribute_name.
It looks weird because contact doesn't have that user_contact_attribute_name, only UserContact does, but the .select portion of the query will make that magically available to you on each contact instance.
The contacts.* portion is what tells the query to make all contact's attributes available as well.

Collection Select Multile tables Involved Rails 3.2.1, PostgreSQl

I am trying to get data for my collection_select list.
Details
There are 4 tables involved
volunteers
has_many :signed_posts
has_many :posts, :through=>:signed_posts
signed_posts
belongs_to :volunteer
belongs_to :post
posts
belongs_to :organization
has_many :signed_posts
has_many :volunteers, :through=>:signed_posts
organizations
has_many :post
If I would have to write in plain SQL, it would be like below.This sql is for postgreSQL. I would like to write in rails 3.2.1 syntax.
Select sp.id,p.title ||'-'|| o.name AS project from signed_posts sp
inner join volunteers v on v.id=sp.volunteer_id
inner join posts p on p.id=sp.post_id
inner join organizations o on o.id=p.organization_id
where v.id=1
As a result, I want to get signed_posts.id and posts.title - organizations.name for a volunteer id 1
which I would like to use on dropdown list.
Thank you very much for your help
My Solution
I solved the problem using hash table like this one
#signed_projects=Volunteer.find(1).signed_posts.joins(:post)
#ddl_test=Hash.new
#signed_projects.each do |signed_project|
ddl_test[signed_project.post.title+"-"+signed_project.post.organization.name]=signed_project.id
end
On View
<%=select_tag 'ddl_test',options_for_select(#ddl_test.to_a),:prompt => 'Please select the project'%>
I am not completely satisfied. I think there must be some elegant solution. If any body has better idea, please let me know
There are a couple ways to do this. In your controller you would want a statement such as:
#signed_posts = Volunteer.find(1).signed_posts.includes({:post => :organization})
After that, one way would be to create a method in your SignedPost model to return the data you need, something like the following
def post_name_with_organization
"#{id} #{post.title} - #{post.organization.name}"
end
Then, in your collection_select, you can use (this is partially guessing, since I don't know the specifics of your form):
form.collection_select(:signed_post_id, #signed_posts, :id, :post_name_with_organization)
Edit based on question updates
If you want to use select_tag and options_for_select as you did in your solution, a more elegant way would be to create this variable in your controller:
#signed_posts_options = #signed_posts.map {|sp| [sp.post_name_with_organization, sp.id]}
Then in your view:
<%=select_tag 'ddl_test',options_for_select(#signed_posts_options),:prompt => 'Please select the project'%>

How can I stop collection<<(object, …) from adding duplicates?

Given a Dinner model that has many Vegetable models, I would prefer that
dinner.vegetables << carrot
not add the carrot if
dinner.vegetables.exists? carrot
Yet it does. It will add a duplicate record every time << is called.
There is a :uniq option you can set on the association, but it only FETCHES AND RETURNS one result if there are multiples, it doesn't ENFORCE unique values.
I could check for exists? every time I add an obj to a collection, but that is tedious and error-prone.
How can I use << freely and not worry about errors and not check for already existing collection members every time?
The best way is to use Set instead of Array:
set = Set.new
set << "a"
set << "a"
set.count -> returns 1
You can add an ActiveRecord unique constraint if you have a join model representing a many-to-many relationship between dinners and vegetables. That's one reason I use join models and has_many :through as opposed to has_and_belongs_to_many. It's important to add a uniqueness constraint at the database level if possible.
UPDATE:
To use a join model to enforce constraint you would need an additional table in your database.
class Dinner
has_many :dinner_vegetables
has_many :vegetables, :through => :dinner_vegetables
end
class Vegetable
has_many :dinner_vegetables
has_many :dinners, :through => :dinner_vegetables
end
class DinnerVegetable
belongs_to :dinner
belongs_to :vegetable
validates :dinner_id, :uniqueness => {:scope => :vegetable_id} # You should also set up a matching DB constraint
end
The other posters' ideas are fine, but as another option you can also enforce this on the database level using e.g. the UNIQUE constraint in MySQL.
After a lot of digging, I've discovered something cool: before_add, which is an association callback, which I never knew even existed. So I could do something like this:
has_many :vegetables, :before_add => :enforce_unique
def enforce_unique(assoc)
if exists? assoc
...
end
Doing this at the DB level is a great idea if you REALLY NEED this to be unique, but in the case that it's not mission critical the solution above is enough for me.
It's mostly to avoid the icky feeling of having extra records lying around in the db...

Access join table data in rails :through associations

I have three tables/models. User, Alliance and Alliance_Membership. The latter is a join table describing the :Alliance has_many :Users through :Alliance_Membership relationship. (:user has one :alliance)
Everything works ok, but Alliance_Membership now has an extra field called 'rank'. I was thinking of the best way to access this little piece of information (the rank).
It seems that when i do "alliance.users", where alliance is the user's current alliance object, i get all the users information, but i do not get the rank as well. I only get the attributes of the user model. Now, i can create a helper or function like getUserRole to do this for me based on the user, but i feel that there is a better way that better works with the Active Record associations. Is there really a better way ?
Thanx for reading :)
Your associations are all wrong - they shouldn't have capital letters. These are the rules, as seen in my other answer where i told you how to set this up yesterday :)
Class names: Always camelcase like AllianceMembership (NOT Alliance_Membership!)
table names, variable names, methods and associations: always underscored and lower case:
has_many :users, :through => :alliance_memberships
To find the rank for a given user of a given alliance (held in #alliance and #user), do
#membership = #alliance.alliance_memberships.find_by_user_id(#user.id)
You could indeed wrap this in a method of alliance:
def rank_for_user(user)
self.alliance_memberships.find_by_user_id(user.id).rank
end

Ruby on Rails Associations

I am starting to create my sites in Ruby on Rails these days instead of PHP.
I have picked up the language easily but still not 100% confident with associations :)
I have this situation:
User Model
has_and_belongs_to_many :roles
Roles Model
has_and_belongs_to_many :users
Journal Model
has_and_belongs_to_many :roles
So I have a roles_users table and a journals_roles table
I can access the user roles like so:
user = User.find(1)
User.roles
This gives me the roles assigned to the user, I can then access the journal model like so:
journals = user.roles.first.journals
This gets me the journals associated with the user based on the roles. I want to be able to access the journals like so user.journals
In my user model I have tried this:
def journals
self.roles.collect { |role| role.journals }.flatten
end
This gets me the journals in a flatten array but unfortunately I am unable to access anything associated with journals in this case, e.g in the journals model it has:
has_many :items
When I try to access user.journals.items it does not work as it is a flatten array which I am trying to access the has_many association.
Is it possible to get the user.journals another way other than the way I have shown above with the collect method?
Hope you guys understand what I mean, if not let me know and ill try to explain it better.
Cheers
Eef
If you want to have user.journals you should write query by hand. As far as I know Rails does has_many :through associations (habtm is a kind of has_many :through) one level deep. You can use has_many with finder_sql.
user.journals.items in your example doesn't work, becouse journals is an array and it doesn't have items method associated. So, you need to select one journal and then call items:
user.journals.first.items
I would also modify your journals method:
def journals
self.roles(:include => :journals).collect { |role| role.journals }.flatten.uniq
end
uniq removes duplicates and :inlcude => :journals should improve sql queries.
Similar question https://stackoverflow.com/questions/2802539/ruby-on-rails-join-table-associations
You can use Journal.scoped to create scope with conditions you need. As you have many-to-many association for journals-roles, you need to access joining table either with separate query or with inner select:
def journals
Journal.scoped(:conditions => ["journals.id in (Select journal_id from journals_roles where role_id in (?))", role_ids])
end
Then you can use user.journals.all(:include => :items) etc

Resources