I am new in RoR and I am trying to write a query on a join table that retrieve all the data I need
class User < ActiveRecord::Base
has_many :forms, :through => :user_forms
end
class Form < ActiveRecord::Base
has_many :users, :through => :user_forms
end
In my controller I can successfully retrieve all the forms of a user like this :
User.find(params[:u]).forms
Which gives me all the Form objects
But, I would like to add a new column in my join table (user_forms) that tells the status of the form (close, already filled, etc).
Is it possible to modify my query so that it can also retrieve columns from the user_forms table ?
it is possible. Once you've added the status column to user_forms, try the following
>> user = User.first
>> closed_forms = user.forms.where(user_forms: { status: 'closed' })
Take note that you don't need to add a joins because that's taken care of when you called user.forms.
UPDATE: to add an attribute from the user_forms table to the forms, try the following
>> closed_forms = user.forms.select('forms.*, user_forms.status as status')
>> closed_forms.first.status # should return the status of the form that is in the user_forms table
It is possible to do this using find_by_sql and literal sql. I do not know of a way to properly chain together rails query methods to create the same query, however.
But here's a modified example that I put together for a friend previously:
#user = User.find(params[:u])
#forms = #user.forms.find_by_sql("SELECT forms.*, user_forms.status as status FROM forms INNER JOIN user_forms ON forms.id = user_forms.form_id WHERE (user_forms.user_id = #{#user.id});")
And then you'll be able to do
#forms.first.status
and it'll act like status is just an attribute of the Form model.
First, I think you made a mistake.
When you have 2 models having has_many relations, you should set an has_and_belongs_to_many relation.
In most cases, 2 models are joined by
has_many - belongs_to
has_one - belongs_to
has_and_belongs_to_many - has_and_belongs_to_many
has_and_belongs_to_many is one of the solutions. But, if you choose it, you must create a join table named forms_users. Choose an has_and_belongs_to_many implies you can not set a status on the join table.
For it, you have to add a join table, with a form_id, a user_id and a status.
class User < ActiveRecord::Base
has_many :user_forms
has_many :forms, :through => :user_forms
end
class UserForm < ActiveRecord::Base
belongs_to :user
belongs_to :form
end
class Form < ActiveRecord::Base
has_many :user_forms
has_many :users, :through => :user_forms
end
Then, you can get
User.find(params[:u]).user_forms
Or
UserForm.find(:all,
:conditions => ["user_forms.user_id = ? AND user_forms.status = ?",
params[:u],
'close'
)
)
Given that status is really a property of Form, you probably want to add the status to the Forms table rather than the join table.
Then when you retrieve forms using your query, they will already have the status information retrieved with them i.e.
User.find(params[:u]).forms.each{ |form| puts form.status }
Additionally, if you wanted to find all the forms for a given user with a particular status, you can use queries like:
User.find(params[:u]).forms.where(status: 'closed')
Related
Ok so have created 2 models User and Following. Where User has a username attribute and Following has 2 attributes which are User associations: user_id, following_user_id. I have set up these associations in the respective models and all works good.
class User < ActiveRecord::Base
has_many :followings, dependent: :destroy
has_many :followers, :class_name => 'Following', :foreign_key => 'following_user_id', dependent: :destroy
end
class Following < ActiveRecord::Base
belongs_to :user
belongs_to :following_user, :class_name => 'User', :foreign_key => 'following_user_id'
end
Now I need to order the results when doing an ActiveRecord query by the username. I can achieve this easily for the straight-up User association (user_id) with the following code which will return to me a list of Followings ordered by the username of the association belonging to user_id:
Following.where(:user_id => 47).includes(:user).order("users.username ASC")
The problem is I cannot achieve the same result for ordering by the other association (following_user_id). I have added the association to the .includes call but i get an error because active record is looking for the association on a table titled following_users
Following.where(:user_id => 47).includes(:user => :followers).order("following_users.username ASC")
I have tried changing the association name in the .order call to names I set up in the user model as followers, followings but none work, it still is looking for a table with those titles. I have also tried user.username, but this will order based off the other association such as in the first example.
How can I order ActiveRecord results by following_user.username?
That is because there is no following_users table in your SQL query.
You will need to manually join it like so:
Following.
joins("
INNER JOIN users AS following_users ON
following_users.id = followings.following_user_id
").
where(user_id: 47). # use "followings.user_id" if necessary
includes(user: :followers).
order("following_users.username ASC")
To fetch Following rows that don't have a following_user_id, simply use an OUTER JOIN.
Alternatively, you can do this in Ruby rather than SQL, if you can afford the speed and memory cost:
Following.
where(user_id: 47). # use "followings.user_id" if necessary
includes(:following_user, {user: :followers}).
sort_by{ |f| f.following_user.try(:username).to_s }
Just FYI: That try is in case of a missing following_user and the to_s is to ensure that strings are compared for sorting. Otherwise, nil when compared with a String will crash.
I have a many2many relationship with a has_many through association:
class User < ActiveRecord::Base
has_many :trips_users
has_many :trips, through: :trips_users
end
class Trip < ActiveRecord::Base
has_many :trips_users
has_many :users, through: :trips_users
end
class TripsUser < ActiveRecord::Base
belongs_to :user
belongs_to :trip
end
The joining table trips_user contains a column named 'pending' which id like get when I ask for a list of trips of a user.
So in my controller I need to get all trips a user has, but also adding the 'pending' column.
I was trying
current_user.trips.includes(:trips_users)
that will be done by this select statement:
SELECT trips.* FROM trips INNER JOIN trips_users ON trips.id
= trips_users.trip_id WHERE trips_users.user_id = 3
which is missing the information in the trips_users table that I want.
The desired sql would be:
SELECT trips.*, trips_users.* FROM trips INNER JOIN trips_usersON trips.id =
trips_users.trip_id WHERE trips_users.user_id = 3
This finally worked:
current_user.trips.select('trips_users.*, trips.*')
Overriding the select part of the SQL.
Not very pretty in my opinion thou, I shouldn't be messing with tables and queries but models, specially in such a common case of a m2m association with extra data in the middle.
You'll want to use joins rather than includes for this... See the following Rails Guide:
http://guides.rubyonrails.org/active_record_querying.html#joining-tables
Essentially you'd do something like this:
current_user.trips.joins(:trips_users)
The includes method is used for eager loading, while joins actually performs the table join.
You could also try:
trips_users = current_user.trips_users.includes(:trip)
trips_users.first.pending?
trips_users.first.trip
Which should give you the trips_users records for that user but also eager loading the trips so that accessing them wouldn't hit the database again.
i have 3 models like :
location, user, discovered_location(location_id,user_id)
I think i need an outer join in order to get all locations, as well as include the discovered_location model, if that location has been discovered by the user.
I would need something like {location1, location2, location3:includes discovered_location, location4 ..}
Is there a Rails 3 way to do that ? If not, what is the best way ?
EDIT
I want to get the locations specified above, for a certain user. To better illustrate, it should be :
user {location1, location2, location3:includes discovered_location, location4 ..}
(A user has many discovered locations)
You can do an outer join in Rails only by using an SQL literal in the joins method:
joins("OUTER JOIN table2 on table2.column = table1.column")
joins makes an inner join, includes makes an outer join.
http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-eager-loaded-associations
You need to check that the user id in your discovered_locations table is either equal to the id of the user in question, or is null. This is easily accomplished with the meta_where gem. Given the following models:
class User < ActiveRecord::Base
has_many :discovered_locations
has_many :locations, :through => :discovered_locations
end
class Location < ActiveRecord::Base
has_many :discovered_locations
has_many :users, :through => :discovered_locations
end
class DiscoveredLocation < ActiveRecord::Base
belongs_to :user
belongs_to :location
end
Insert some dummy data, then execute a statement such as this:
Location.includes(:discovered_locations).where(
{:discovered_locations => {:user_id => User.first.id}} |
{:discovered_locations => {:user_id => nil}}
).each do |loc|
puts "#{loc.name} #{loc.discovered_locations.empty? ? 'not visited' : 'visited'}"
end
I have 2 models:
class Video < ActiveRecord::Base
belongs_to :categories, :foreign_key => "category", :class_name => "Category"
end
class Category < ActiveRecord::Base
has_many :videos
end
This is fine so far, in my videos controller for the index page I have:
def index
#videos = Video.all(:joins => :categories)
etc, etc
end
The above produces the following SQL Query: SELECT videos.* FROM videos INNER JOIN categories ON categories.id = videos.category
Which is fine up to a certain point, basically I need to get the category name (a field in that table) that way I don't have to do another call in the view to get category name based on the category id. Any ideas?
Thank you, and yes I am new to ruby, I tried reading the API but couldn't find much help there.
If think the association in your Video class is set up incorrectly. It should be:
class Video < ActiveRecord::Base
belongs_to :category
end
You can do #videos = Video.all(:include => :category). This will retrieve the video and associated category records using a single SQL statement.
Incidentally, your :class_name option on the belongs_to association is redundant because ActiveRecord will already have automatically inferred the class name from the association name. You should only use this option if you want to have an association name that is different from the underlying class e.g. authors/Person.
join_condition = " ,categories where INNER JOIN categories ON categories.id=videos.category"
#videos = Video.all(:joins => join_condition)
try this
I'm working on this record insert/delete I'm not sure what the syntax is to perform the query.
I have a user model and an event model. I have created a joining table called Personal that stores the user_id, and event_id of any events that the users like.
I created an "Add" method in my events controller so whenever someone clicks it run to that and perform the create logic I'm trying to develop now.The action is tied to a extra column I added to the grid displaying all the events.
The user model =>
has_many :personals
The event model =>
has_many :personals
The personal model =>
belongs_to :user
belongs_to :events
I thought it would be something like =>
#user = User.find(session[:user_id])
#event = Event.find(params[:id])
# Personal.new = User.Event?
can anyone help?
If you're using a has_and_belongs_to_many association, which would be unfortunate, removing the associated links can be tricky as there's no identifier for each link.
Using a has_many :through relationship is much easier to maintain and will allow you to do simple things like:
class User < ActiveRecord::Base
has_many :user_events
has_many :events,
:through => :user_events
end
#user.events.delete(#event)
This doesn't remove the Event itself, that'd require an Event#destroy call, but the join record that links the two.