Elegantly selecting attributes from has_many :through join models in Rails - ruby-on-rails

I'm wondering what the easiest/most elegant way of selecting attributes from join models in has_many :through associations is.
Lets say we have Items, Catalogs, and CatalogItems with the following Item class:
class Item < ActiveRecord::Base
has_many :catalog_items
has_many :catalogs, :through => :catalog_items
end
Additionally, lets say that CatalogueItems has a position attribute and that there is only one CatalogueItem between any catalog and any item.
The most obvious but slightly frustrating way to retrieve the position attribute is:
#item = Item.find(4)
#catalog = #item.catalogs.first
#cat_item = #item.catalog_items.first(:conditions => {:catalog_id => #catalog.id})
position = #cat_item.position
This is annoying because it seems that we should be able to do #item.catalogs.first.position since we have completely specified which position we want: the one that corresponds to the first of #item's catalogs.
The only way I've found to get this is:
class Item < ActiveRecord::Base
has_many :catalog_items
has_many :catalogs, :through => :catalog_items, :select => "catalogue_items.position, catalogs.*"
end
Now I can do Item.catalogs.first.position. However, this seems like a bit of a hack - I'm adding an extra attribute onto a Catalog instance. It also opens up the possibility of trying to use a view in two different situations where I populate #catalogs with a Catalog.find or with a #item.catalogs. In one case, the position will be there, and in the other, it won't.
Does anyone have a good solution to this?
Thanks.

You can do something like this:
# which is basically same as your "frustrating way" of doing it
#item.catalog_items.find_by_catalogue_id(#item.catalogs.first.id).position
Or you can wrap it into in an instance method of the Item model:
def position_in_first_catalogue
self.catalog_items.find_by_catalogue_id(self.catalogs.first.id).position
end
and then just call it like this:
#item.position_in_first_catalogue

Just adding answer so that it might help others
CatalogItem.joins(:item, :catalog).
where(items: { id: 4 }).pluck(:position).first

You should be able to do #catalog.catalog_item.position if you provide the other end of the association.
class Catalog < ActiveRecord::Base
belongs_to :catalog_item
end
Now you can do Catalog.first.catalog_item.position.

Why don't You just
#item = Item.find(4)
position = #item.catalog_items.first.position
why do you go through catalogs? It doesn't make any sense to me since you are looking for first ANY catalog!?

Related

Best way to update (create & delete) multiple associations (has_many :through)

I have what i feel could be a simple question, and i have this working, but my solution doesn't feel like the "Rails" way of doing this. I'm hoping for some insight on if there is a more acceptable way to achieve these results, rather than the way i would currently approach this, which feels kind of ugly.
So, lets say i have a simple has_many :through setup.
# Catalog.rb
class Catalog < ActiveRecord::Base
has_many :catalog_products
has_many :products, through: :catalog_products
end
# Products.rb
class Product < ActiveRecord::Base
has_many :catalog_products
has_many :catalogs, through: :catalog_products
end
# CatalogProduct.rb
class CatalogProduct < ActiveRecord::Base
belongs_to :catalog
belongs_to :product
end
The data of Catalog and the data of Product should be considered independent of each other except for the fact that they are being associated to each other.
Now, let's say that for Catalog, i have a form with a list of all Products, in say a multi-check form on the front end, and i need to be able to check/uncheck which products are associated with a particular catalog. On the form field end, i would return a param that is an array of all of the checked products.
The question is: what is the most accepted way to now create/delete the catalog_product records so that unchecked products get deleted, newly checked products get created, and unchanged products get left alone?
My current solution would be something like this:
#Catalog.rb
...
def update_linked_products(updated_product_ids)
current_product_ids = catalog_products.collect{|p| p.product_id}
removed_products = (current_product_ids - updated_product_ids)
added_products = (updated_product_ids - current_product_ids)
catalog_products.where(catalog_id: self.id, product_id: removed_products).destroy_all
added_products.each do |prod|
catalog_products.create(product_id: prod)
end
end
...
This, of course, does a comparison between the current associations, figures out which records need to be deleted, and which need to be created, and then performs the deletions and creations.
It works fine, but if i need to do something similar for a different set of models/associations, i feel like this gets even uglier and less DRY every time it's implemented.
Now, i hope this is not the best way to do this (ignoring the quality of the code in my example, but simply what it is trying to achieve), and i feel that there must be a better "Rails" way of achieving this same result.
Take a look at this https://guides.rubyonrails.org/association_basics.html#methods-added-by-has-many-collection-objects
You don't have to remove and create manually each object.
If you have already the product_ids array, I think this should work:
#Catalog.rb
...
def update_linked_products(updated_product_ids)
selected_products = Product.where(id: updated_product_ids)
products = selected_products
end
...
First,
has_many :products, through: :catalog_products
generate some methods for you like product_ids, check this under auto-generated methods to know more about the other generated methods.
so we don't need this line:
current_product_ids = catalog_products.collect{|p| p.product_id}
# exist in both arrays are going to be removed
will_be_removed_ids = updated_product_ids & product_ids
# what's in updated an not in original, will be appended
will_be_added_ids = updated_product_ids - product_ids
Then, using <<, and destroy methods which are also generated from the association (it gives you the ability to deal with Relations as if they are arrays), we are going to destroy the will_be_removed_ids, and append the will_be_added_ids, and the unchanged will not be affected.
Final version:
def update_linked_products(updated_product_ids)
products.destroy(updated_product_ids & product_ids)
products << updated_product_ids - product_ids
end

Rails - Manipulating the returned objects in an ActiveRecord query

So I have the following simplified models and associations:
class Barber < User
has_many :barber_styles, inverse_of: :barber
end
class Style < ActiveRecord::Base
end
class BarberStyle < ActiveRecord::Base
belongs_to :barber, inverse_of: :barber_styles
belongs_to :style
delegate :name, to: :style
end
If I wanted to make a query based on all BarberStyle's that belong to a specific Barber, is there a way I can include the name of the associated Style?
I want to use a line something like:
BarberStyle.joins(:barber).where(barber: 109).include(:style)
So I'd be able to access an associated style.name. But I don't think a line like this exists.
I know I could just use map to build an array of my own objects, but I have to use a similar query with many different models, and don't want to do a bunch of extra work if it's not necessary.
In one of my previous projects, I was able to render json with the following lines:
#colleges = College.includes(:sports).where(sports: { gender: "male" })
render json: #colleges, include: :sports
But I don't want to render json in this case.
Edit:
I don't really have any unincluded associations to show, but I'll try to elaborate further.
All I'm trying to do is have extra associated models/fields aggregated to each BarberStyle result within my ActiveRecord Relation query.
In an attempt to make this less confusing, here is an example of the type of data I want to pass into my JS view:
[
#<BarberStyle
id: 1,
barber_id: 116,
style_id: 91,
style: { name: "Cool Hairstyle" }
>,
#<BarberStyle
id: 2,
barber_id: 97,
style_id: 92,
style: { name: "Cooler Hairstyle" }
>,
etc...
]
The reason I want the data formatted this way is because I can't make any queries on associated models in my JS view (without an AJAX call to the server).
I had very similarly formatted data when I did render json: #colleges, include: :sports in the past. This gave me a collection of Colleges with associated Sport models aggregated to each College result. I don't want to build json in this fashion for my current situation, as it will complicate a few things. But I suppose that's a last resort.
I'm not sure if there's a way to structure an ActiveRecord query where it adds additional fields to the results, but I feel like it should, so here I am looking for it. Haven't found anything in the docs, but then again there is sooooo much left out of those.
Doug as far as I understand your code, BarberStyle must be a joining model between Barber and Style. You mentioned in your comment that you removed the has_many :styles, through: :barber_styles from your Barber model because you thought that it wasn't relevant. That's not true, it's exactly the point that would help you to achieve your goal. Add this relation again then you can do something like this:
barber = Barber.includes(:styles).where(barber: {id: 109})
barber.styles.each { |style| puts style.name }
Since barber.styles is a collection, I added a loop between all the possible styles you can have. But, from that, you can use your data as you feel like, looping through it or any other way you want.
Hope to have helped!
First off, in your case inverse_of does not accomplish anything, since you are setting the default values. Remove that.
Secondly, it seems you need to better understand the concept of HABTM relationships.
Using has many through is generally a good idea since you can add data and logic to the model in the middle.
It ideally suits your case, so you should set it up like this:
class Barber < User
has_many :barber_styles
has_many :styles, through: :barber_styles
end
class Style < ActiveRecord::Base
has_many :barber_styles
has_many :barbers, through: :barber_styles
end
class BarberStyle < ActiveRecord::Base
belongs_to :barber
belongs_to :style
end
Now it is an easy task to get the styles of a given barber. Just do this:
barber = Barber.find(1)
barber.styles
# => AR::Collection[<#Style name:'Cool Hairstyle'>,<#Style name:'Cooler Hairstyle'>]
Rails automatically uses the BarberStyle model in between to find the styles of a certain barber.
I assume this covers your need, if you have extra information stored only in BarberStyle, let me know.

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 object inhertiance

I am trying to make an application wherein Users have many Items, and each Item they have through Possession is an entity in its own right. The idea behind this is if I have a MacBook item, eg, and a user adds it to their inventory, they may apply attributes (photos, comments, tags, etc) to it without directly affecting them Item itself, only their Possession.
The Item will in turn aggregate attributes from its corresponding Possessions (if you were to go to /item/MacBook, rather than /user/101/possession/5). I have the following models setup (ignoring attributes like photos for now).
class User
has_many :possessions
has_many :items, :through => :possessions
end
class Item
has_many :possessions
has_many :users, :through => possessions
end
class Possession
belongs_to :user
belongs_to :item
end
My first question is, am I doing this right at all. Is has_many :through the right tool here?
If so, how would I deal with class inheritance here? I might not be stating this right, but what I mean is, if I were to do something like
#possession = Possession.find(params[:id])
#photos = #possession.photos.all
and there were no photos available, how could it fall back to the corresponding Item and search for photos belonging to it?
Your initial data structure seems appropriate.
As for the second part, with the "fall back" to a corresponding item, I don't think there would be a direct Active Record way of doing this. This behavior seems pretty specific, and may be confusing to future developers working on your app unless you have a clear method for this.
You could create a method inside Possession like:
def photos_with_fallback
return self.photos if self.photos.size > 0
self.item.photos
end
There is a huge consequence to doing this. If you have a method like this, you won't be able to do any write activities down the wrode like #photos.build or #photos.create because you won't know where you're putting them. They could be linked to the Item or the Posession.
I think you're better of pushing the conditional logic out to your controller and checking for photos on the Posession first and then on the Item.
#In the controller
#photos = #posession.photos
#photos = #posession.item.photos if #photos.size == 0
This will be more clear when you go to maintain your code later, and it will allow you to make other decisions down the road.

Resources