Collection Select Multile tables Involved Rails 3.2.1, PostgreSQl - ruby-on-rails

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'%>

Related

has_one association not working with includes

I've been trying to figure out some odd behavior when combining a has_one association and includes.
class Post < ApplicationRecord
has_many :comments
has_one :latest_comment, -> { order('comments.id DESC').limit(1) }, class_name: 'Comment'
end
class Comment < ApplicationRecord
belongs_to :post
end
To test this I created two posts with two comments each. Here are some rails console commands that show the odd behavior. When we use includes then it ignores the order of the latest_comment association.
posts = Post.includes(:latest_comment).references(:latest_comment)
posts.map {|p| p.latest_comment.id}
=> [1, 3]
posts.map {|p| p.comments.last.id}
=> [2, 4]
I would expect these commands to have the same output. posts.map {|p| p.latest_comment.id} should return [2, 4]. I can't use the second command because of n+1 query problems.
If you call the latest comment individually (similar to comments.last above) then things work as expected.
[Post.first.latest_comment.id, Post.last.latest_comment.id]
=> [2, 4]
If you have another way of achieving this behavior I'd welcome the input. This one is baffling me.
I think the cleanest way to make this work with PostgreSQL is to use a database view to back your has_one :latest_comment association. A database view is, more or less, a named query that acts like a read-only table.
There are three broad choices here:
Use lots of queries: one to get the posts and then one for each post to get its latest comment.
Denormalize the latest comment into the post or its own table.
Use a window function to peel off the latest comments from the comments table.
(1) is what we're trying to avoid. (2) tends to lead to a cascade of over-complications and bugs. (3) is nice because it lets the database do what it does well (manage and query data) but ActiveRecord has a limited understanding of SQL so a little extra machinery is needed to make it behave.
We can use the row_number window function to find the latest comment per-post:
select *
from (
select comments.*,
row_number() over (partition by post_id order by created_at desc) as rn
from comments
) dt
where dt.rn = 1
Play with the inner query in psql and you should see what row_number() is doing.
If we wrap that query in a latest_comments view and stick a LatestComment model in front of it, you can has_one :latest_comment and things will work. Of course, it isn't quite that easy:
ActiveRecord doesn't understand views in migrations so you can try to use something like scenic or switch from schema.rb to structure.sql.
Create the view:
class CreateLatestComments < ActiveRecord::Migration[5.2]
def up
connection.execute(%q(
create view latest_comments (id, post_id, created_at, ...) as
select id, post_id, created_at, ...
from (
select id, post_id, created_at, ...,
row_number() over (partition by post_id order by created_at desc) as rn
from comments
) dt
where dt. rn = 1
))
end
def down
connection.execute('drop view latest_comments')
end
end
That will look more like a normal Rails migration if you're using scenic. I don't know the structure of your comments table, hence all the ...s in there; you can use select * if you prefer and don't mind the stray rn column in your LatestComment. You might want to review your indexes on comments to make this query more efficient but you'd be doing that sooner or later anyway.
Create the model and don't forget to manually set the primary key or includes and references won't preload anything (but preload will):
class LatestComment < ApplicationRecord
self.primary_key = :id
belongs_to :post
end
Simplify your existing has_one to just:
has_one :latest_comment
Maybe add a quick test to your test suite to make sure that Comment and LatestComment have the same columns. The view won't automatically update itself as the comments table changes but a simple test will serve as a reminder.
When someone complains about "logic in the database", tell them to take their dogma elsewhere as you have work to do.
Just so it doesn't get lost in the comments, your main problem is that you're abusing the scope argument in the has_one association. When you say something like this:
Post.includes(:latest_comment).references(:latest_comment)
the scope argument to has_one ends up in the join condition of the LEFT JOIN that includes and references add to the query. ORDER BY doesn't make sense in a join condition so ActiveRecord doesn't include it and your association falls apart. You can't make the scope instance-dependent (i.e. ->(post) { some_query_with_post_in_a_where... }) to get a WHERE clause into the join condition, then ActiveRecord will give you an ArgumentError because ActiveRecord doesn't know how to use an instance-dependent scope with includes and references.

Search for model by multiple join record ids associated to model by has_many in rails

I have a product model setup like the following:
class Product < ActiveRecord::Base
has_many :product_atts, :dependent => :destroy
has_many :atts, :through => :product_atts
has_many :variants, :class_name => "Product", :foreign_key => "parent_id", :dependent => :destroy
end
And I want to search for products that have associations with multiple attributes.
I thought maybe this would work:
Product.joins(:product_atts).where(parent_id: params[:product_id]).where(product_atts: {att_id: [5,7]})
But this does not seem to do what I am looking for. This does where ID or ID.
So I tried the following:
Product.joins(:product_atts).where(parent_id: 3).where(product_atts: {att_id: 5}).where(product_atts: {att_id: 7})
But this doesn't work either, it returns 0 results.
So my question is how do I look for a model by passing in attributes of multiple join models of the same model type?
SOLUTION:
att_ids = params[:att_ids] #This is an array of attribute ids
product = Product.find(params[:product_id]) #This is the parent product
scope = att_ids.reduce(product.variants) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE product_id=products.id AND att_id=?)', att_id)
end
product_variant = scope.first
This is a seemingly-simple request made actually pretty tricky by how SQL works. Joins are always just joining rows together, and your WHERE clauses are only going to be looking at one row at a time (hence why your expectations are not working like you expect -- it's not possible for one row to have two values for the same column.
There are a bunch of ways to solve this when dealing with raw SQL, but in Rails, I've found the simplest (not most efficient) way is to embed subqueries using the EXISTS keyword. Wrapping that up in a solution which handles arbitrary number of desired att_ids, you get:
scope = att_ids_to_find.reduce(Product) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE parent_id=products.id AND att_id=?)', att_id)
end
products = scope.all
If you're not familiar with reduce, what's going on is it's taking Product, then adding one additional where clause for each att_id. The end result is something like Product.where(...).where(...).where(...), but you don't need to worry about that too much. This solution also works well when mixed with scopes and other joins.

Squeel to execute outer join on Rails

I'm trying to find a way to create a simple outer join without too much hassle. I know I can do this manually by specifying an outer join, but I'm looking for a simple way.
Therefore, I was taking a look at Squeel, which seems to be the new alternative for Metawhere. It seems to be able to handle outer joins, but I can't get what I want.
In particular, I have three models :
City
Building
CityBuilding
I would simply like a list of all the buildings whether they exist in a city or not. CityBuilding is, of course, the model that connects a city to a building. I would like to get something like :
city 1{
TownCenter => city_building
Sawmill => city_building
Quarry => nil
}
The query is null since there is no city_building entry for this one, you get the idea.
Is there a way that Squeel does that? Or maybe another gem, without having to manually do an outer join?
I think you can try something like the below using Squeel. I am not sure about the where part. You will have to give one of the two join conditions.
Building.joins{city}.joins(city_buildings.outer).where{(buidlings.id == city_buildings.building_id) & (cities.id == city_buildings.city_id)}
or
Building.joins{city}.joins(city_buildings.outer).where{buidlings.id == city_buildings.building_id}
or
Building.joins{city}.joins(city_buildings.outer).where{cities.id == city_buildings.city_id}
The AR association includes uses LEFT OUTER JOIN. If you have a model relationship as follows, then:
class City
has_many :city_buildings
has_many :buildings, :through => :city_buildings
end
class Building
has_one :city_building
has_one :city, :through => :city_building
end
class CityBuilding
belongs_to :city
belongs_to :building
end
To get a list of buildings regardless of city link
Building.includes(:city).where(...)
To get a list of buildings with a of city link
Building.includes(:city).where("cities.id IS NOT NULL")
Note
I am assuming you want to access the city object after the query(if present).
This is not a good solution if you DO NOT want to eager load the city object associated with a building (as AR eager loads include associations after executing the OUTER JOIN).

Rails way to COUNT the nr of records in my case

I am using Rails v2.3.2.
I have a model called UsersCar:
class UsersCar < ActiveRecord::Base
belongs_to :car
belongs_to :user
end
This model mapped to a database table users_cars, which only contains two columns : user_id, car_id.
I would like to use Rails way to count the number of car_id where user_id=3. I konw in plain SQL query I can achieve this by:
SELECT COUNT(*) FROM users_cars WHERE user_id=3;
Now, I would like to get it by Rails way, I know I can do:
UsersCar.count()
but how can I put the ...where user_id=3 clause in Rails way?
According to the Ruby on Rails Guides, you can pass conditions to the count() method. For example:
UsersCar.count(:conditions => ["user_id = ?", 3])
will generates:
SELECT count(*) AS count_all FROM users_cars WHERE (user_id = 3)
If you have the User object, you could do
user.cars.size
or
user.cars.count
Another way would be to do:
UserCar.find(:user_id => 3).size
And the last way that I can think of is the one mentioned above, i.e. 'UserCar.count(conditions)'.
With the belogngs to association, you get several "magic" methods on the parent item to reference its children.
In your case:
users_car = UsersCar.find(1) #=>one record of users_car with id = 1.
users_car.users #=>a list of associated users.
users_car.users.count #=>the amount of associated users.
However, I think you are understanding the associations wrong, based on the fact that your UsersCar is named awkwardly.
It seems you want
User has_and_belongs_to_many :cars
Car has_and_belongs_to_manu :users
Please read abovementioned guide on associations if you want to know more about many-to-many associations in Rails.
I managed to find the way to count with condition:
UsersCar.count(:condition=>"user_id=3")

Write join table data - has_many :through

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')

Resources