Includes with a related element - ruby-on-rails

I would like to use the includes method with the related element of my Post
My Post can be associated with different type of element. And I use a value :cat to knows witch kind of element is associated.
The value work as this (cat: (1 => Message, 2=>Question, 3=>Task, 4=>Event) with the association has_one
Example : If post.cat == 3, I can call the task related with a method post.task
Now, I would like to optimize the SQL requests of my Post/Index with the method includes. But is not working for the moment. Can you help me to find the error of my code ?
Post_controller :
def index
#posts = current_user.posts
#posts.each do |post|
if post.cat == 3
#task = post.task.includes(:users)
elsif post.cat == 4
#event = post.event.includes(:reminds)
end
end
end
Error: undefined method `includes'
Edit :
Post_model:
class Post < ApplicationRecord
has_one :post_message, dependent: :destroy
has_one :question, dependent: :destroy
has_one :task, dependent: :destroy
has_one :event, dependent: :destroy
end
Task_model :
class Task < ApplicationRecord
belongs_to :post
has_many :users_task, dependent: :destroy
has_many :users, through: :users_task
end

Why are you using #posts.each ?
For me, the best solution for that is to find all the posts whith the defined cat to run the includes method. In your case, it would be like that :
#posts.where(cat: 1).includes(:message)
#posts.where(cat: 2).includes(:question)
#posts.where(cat: 3).includes(task: :users)
#posts.where(cat: 4).includes(event: :reminds)

Well, after many tries, I opted for a scope method to run the includes method. It's not a really elegant solution, but I think it's the best in my case.
So I'm preparing the scopes in my Post_Model:
scope :with_tasks, -> { where(cat: 3).includes(:user).includes(task: :users) }
scope :with_events, -> { where(cat: 4).includes(:user).includes(event: :reminds) }
And after, I render them in my index action like this :
#posts = current_user.posts.with_tasks + current_user.posts.with_events
So the code is generating 2 SQL Requests to find the posts (one for each category).
I think there is a way to join all that directly into a new global scope, but I don't know how. So if there is anyone knows that, he can edit the answer
Enjoy !

If you're getting an undefined method: 'includes' error, it means that either post.task or post.event are not returning ActiveRecord objects like your code is expecting. Are you sure there will always be values set for .task or .event at that point in execution? Are there any cases where that value might be nil or blank?
By the way, have you heard about 'polymorphic associations'? Defining an association as polymorphic allows you to associate records of arbitrary types with a specific column (by storing both object ID and class name on each record behind the scenes). It seems like this exactly matches your use case. It would be much easier to use the built-in mechanism than trying to do all the if-then switching based on category in your code.

Related

Scope associated element with join table

I'm trying to scope the main group of my user. This group is noted with a cat: which is 2.
So I thought of doing this with a scope like
class User < ApplicationRecord
has_many :users_group, dependent: :destroy
has_many :groups, through: :users_group
scope :my_group, -> { self.joins(:groups).where('groups.cat = 2').limit(1) }
end
But the command below is not working :
current_user.my_group
Can you lead me on the good way to achieve it ?
As Mario says, a scope works on a collection, not an instance.
If you want to keep the method in the User model you can use the following:
user.rb
def my_group
groups.find_by_cat(2)
end
Using find_by will return a single group, rather than using where / limit. If the group isn't found, it will return nil.
I'd suggest using a scope to return a single instance is a bit of an anti-pattern, and it would be better achieved using this method, or dropping the following method into Group and calling current_user.groups.my_group - although the name my_group sounds a bit out of place like that. For completeness, here it is regardless:
group.rb
def my_group
find_by_cat(2)
end
current_user doesn't return an ActiveRecord relation, it just returns the user so you can't chain it together with a scope (I'm assuming the error message you're getting is undefined method 'my_group' for #<User>?). Add the scope to your Group class and use it through your groups has_many relationship e.g.
current_user.groups.my_group

Ruby on Rails Association build and assign 2 related associations

So I've got a User model, a Building model, and a MaintenanceRequest model.
A user has_many :maintenance_requests, but belongs_to :building.
A maintenance requests belongs_to :building, and belongs_to: user
I'm trying to figure out how to send a new, then create a maintenance request.
What I'd like to do is:
#maintenance_request = current_user.building.maintenance_requests.build(permitted_mr_params)
=> #<MaintenanceRequest id: nil, user_id: 1, building_id: 1>
And have a new maintenance request with the user and building set to it's parent associations.
What I have to do:
#maintenance_request = current_user.maintenance_requests.build(permitted_mr_params)
#maintenance_request.building = current_user.building
It would be nice if I could get the maintenance request to set its building based of the user's building.
Obviously, I can work around this, but I'd really appreciate the syntactic sugar.
From the has_many doc
You can pass a second argument scope as a callable (i.e. proc or lambda) to retrieve a specific set of records or customize the generated query when you access the associated collection.
I.e
class User < ActiveRecord::Base
has_many :maintenance_requests, ->(user){building: user.building}, through: :users
end
Then your desired one line should "just work" current_user.building.maintenance_requests.build(permitted_mr_params)
Alternatively, if you are using cancancan you can add hash conditions in your ability file
can :create, MaintenanceRequest, user: #user.id, building: #user.building_id
In my opinion, I think the approach you propose is fine. It's one extra line of code, but doesn't really increase the complexity of your controller.
Another option is to merge the user_id and building_id, in your request params:
permitted_mr_params.merge(user_id: current_user.id, building_id: current_user.building_id)
#maintenance_request = MaintenanceRequest.create(permitted_mr_params)
Or, if you're not concerned about mass-assignment, set user_id and building_id as a hidden field in your form. I don't see a tremendous benefit, however, as you'll have to whitelist the params.
My approach would be to skip
maintenance_request belongs_to :building
since it already belongs to it through the user. Instead, you can define a method
class MaintenanceRequest
belongs_to :user
def building
user.building
end
#more class stuff
end
Also, in building class
class Building
has_many :users
has_many :maintenance_requests, through: :users
#more stuff
end
So you can completely omit explicit building association with maintenance_request
UPDATE
Since users can move across buildings, you can set automatic behavior with a callback. The job will be done like you do it, but in a more Railsey way
class MaintenanceRequest
#stuff
before_create {
building=user.building
}
end
So, when you create the maintenance_request for the user, the building will be set accordingly

Condition for association Rails 4

There's a way to condition something to an associative table of ActiveRecord?
I retrieve segments this way:
#segments = Segment.all
But, a Segment has_many products. See:
models/product.rb:
class Product < ActiveRecord::Base
belongs_to :segment, dependent: :destroy
end
models/segment.rb:
class Segment < ActiveRecord::Base
has_many :products
end
The problem is: I just want to retrieve products whose its status is equals to 1. I can condition something like this using where on Segment model, but how can I achieve this for products?
What I already tried
I found a solution. Take a look:
#segments = Segment.find(:all, include: :products, conditions: {products: {status: 1}})
It worked, but I think the code can be better.
Why I think the code can be better
Well, why should I use include: :products if the association is already live within the models? We're associating things through the model and I'm sure that is something near to enough.
Ideas?
Segment.joins(:products).where("products.status = 1")
You can also use includes instead of joins. But rails will convert it into a join internally since you are using the products table attribute in the query
A few tips, that might help you.
For easy naming purposes, I am considering the status==1 as being active. Of course I have no idea what it means in your specific case.
class Product
ACTIVE=1
def self.active
where(status: ACTIVE)
end
end
Now you write something like:
segment.products.active
and this will return only the active products for the given segment.
The solution you found, which will retrieve all segments with (active) products, could be written differently as follows:
Segment.includes(:products).where(products: {status: 1})
Now, why so elaborate: this actually translates to a sql query, so you have to be a little more explicit about it.
If you only ever want those with a status of 1
class Segment < ActiveRecord::Base
has_many :products, :conditions => { :status => 1 }
end
In rails 3 or
class Segment < ActiveRecord::Base
has_many :products, -> { where status: 1 }
end
In rails 4
Obviously can use status: true if it's a boolean
Then
#segments = Segment.includes(:products)
The association has_many :products makes it possible to use include: :products in your scope. Therefore you shouldn't doubt in your solution. It is right, and it is just the same as solutions presented in the other answers but by other syntacsis.
This should do the job - and it's compatibile with AREL syntax:
#segments = Segment.joins(:products).where(products: {status: 1})
It's quite different that solution with include (or includes, as it would be Rails 3/4), because it generates query with INNER JOIN, while includes generates LEFT OUTER JOIN. Also, includes is usually used for eager loading associated records, not for queries with JOIN.

Any better way to execute something like this?

I'm trying to list all the user's products with a probable association where a flag 'notification' is set to zero.
user.probable_associations.where(:notified => 0).collect{|a| Product.where(:id => a.product_id).collect{|p| p.name}}.to_sentence
It seems like using a where and collect method twice within the statement isn't very good. Is there a better way to go about this?
Also, the result is something like
"[\"Product A\"] and [\"Product B\"]"
which is pretty ugly...and I still need to remove the extra punctuation "[\" \"]
instead of something clean like
"Product A and Product B"
EDIT based on Rich's Answer, still have issues because notified is a field in associations NOT product:
has_many :probable_associations, -> { where "associations.category = 3"}, class_name: 'Association', before_add: :set_probable_category
has_many :probable_products, class_name: 'Product', through: :probable_associations, source: :product do
def not_notified
select(:name).where(notified: 0)
end
end
I'd use an ActiveRecord Association extension:
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :products do
def not_notified
select(:name).where(notified: 0)
end
end
end
#-> #user.products.not_notified
That's my contribution, but you could then use #spickermann & #tompave's controbutions and use .flatten.to_sentence
Without knowing what probable_associations does would I rewrite the code to something like this:
product_ids = user.probable_associations.where(:notified => 0).map(&:product_id)
Product.where(:id => product_ids).map(&:name).to_sentence
Assuming that probable_associations is just an ActiveRecord has_many association, and that you want to end up with a list of titles for Product records, you can use this:
ids = user.probable_associations
.where(notified: 0)
.pluck(:product_id)
result = Product.where(id: ids).pluck(:name).to_sentence
It's similar to #spikermann's answer, but pluck(:column_name) is faster than using a block and only extracts the required column from the DB.
Also, the reason your code produces that string is that, by the time you call to_sentence, you have an Array of sub-arrays. Each sub-array contains a single element: a product name.
That's because the second collect is sent to an ActiveRecord::Relation containing just one record.
You could have solved that problem with flatten, but the whole operation could just be refactored.

has_many :through breaks upon find_or_initialize_by

I have a model, Feed, that has and belongs to many FilteredUsers. In this case I have implemented it through a has_many :through relationship.
class Feed < ActiveRecord::Base
has_many :denials, :dependent => :destroy
has_many :filtered_users, :through => :denials
I would like to create a record if it doesn't exist or find the object if it does exist. When I try and use the find_or_initialize_by (or find_or_create_by) an exception is thrown saying undefined method 'feed_id=' for <FilteredUser..
Here is the code:
feed = Feed.find(params[:id])
user = feed.filtered_users.find_or_initialize_by_user_url(params[:user_url])
if params[:status] == "block"
feed.filtered_users << user
else
feed.filtered_users.delete(user)
end
feed.save
Any suggestions on how to fix this or how to work around it DRYly?
Thanks!
First, because it's a has_many :through relationship, the initialization has no way of knowing which denial the new filtered_user should be associated with. If you want to use find_or_initialize_by, you need to run it on a specific denial that is associated with the feed.
Build a new filtered_user and associate it with a specific denial.
Second, agreeing with ErsatzRyan, the general logic seems a bit off.
Wouldn't it be easier to check the params[:status] first and then do what you need to do?
feed = Feed.find(params[:id])
if params[:status] == 'block'
feed.filtered_users.build(:user_url => params[:user_url])
else
feed.filtered_users.find_by_user_url(params[:user_url]).delete
end
feed.save
warning this is air coded not tested

Resources