has_many :through object inhertiance - ruby-on-rails

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.

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

How to have conditional views in Rails?

I saw a feature in an app that I'd like to be able to implement. The app has several resources - photos, articles etc.. In the nav bar next to the photos and articles tabs there were two buttons - organization and personal. When one clicks on the organization button if they then click on the photos or articles, they get a list of all photos and articles that belong to the members of their organization. If they clicked on the personal button and after that they click on photos or articles, they get lists of only their personal photos and articles, omitting the resources that belong to the other members of their organization. So I wonder how this state is kept between requests.
I imagine that one way would be to constantly pass a variable between the views and the controller and based on that variable to list a particular resource. Or maybe save the state in the session (though I suppose this should be done as a last resort). Another way would be to use a decorator like draper, but I am kind of confused about the specifics of implementing this. I would be very grateful if somebody points me to an article or to a tutorial that shows how to implement such a feature or just provides an overview of the steps.
To be clear, one again: there are links to index different resources, but based on a parameter the index action of the respective controller returns different results. Something like:
def index
if params[:type] == 'organization'
#photos = Organization.find_by(id: params[:organization][:id]).photos
else
#photos = User.find_by(id: params[:user][:id]).photos
end
end
The question is - how do I pass the type parameter - hard code it in the path helpers and have different views with different values for that parameter or is there a better way?
This is the relationship of my models:
class User < ActiveRecord::Base
belongs_to :organization,
inverse_of: :members
has_many :photos,
inverse_of: :owner,
foreign_key: :owner_id,
dependent: :destroy
...
end
class Photo < ActiveRecord::Base
belongs_to :owner,
class_name: User,
inverse_of: :photos
belongs_to :organization,
inverse_of: :photos
...
end
class Organization < ActiveRecord::Base
has_many :members,
class_name: User,
inverse_of: :organization,
dependent: :destroy
has_many :photos,
inverse_of: :organization,
dependent: :destroy
...
end
By Reloading the Page with URL Parameters
I had a similar issue and there's a couple different ways to pass parameters depending on what you want. I actually just went with passing them through the url. If you append ?type=organization to your url, you will get an additional key-value pair in your params, 'type' => 'organization' and you can just use that. If you want to go to this URL through a link, you can just do link_to photos_path + "?type=organization". I ended up using javascript since I wanted something other than a link. Whatever your html element is, give it the attribute onclick=changeParam() and then define that function in your javascript.
function changeParam() {
window.location.search = "?type=organization"
}
That will automatically reload the same page with that parameter. If you want more than one, append &param2=value2 to what you have so far.
Through AJAX
You can also use AJAX to pass parameters if you don't want a full refresh of the page. If you know how to use AJAX already, just use the data option:
$.ajax({
type: "GET",
url: "/photos",
data: { type : organization, param2 : value2 }
})
In this case, your params are sent though data. If you want to know more about AJAX and how to use that, let me know and I will update my answer with more details. It's pretty confusing in Rails and there isn't good documentation, so I'll add what I know if you want.
it can be solved on many different ways, but for example, if I understood you, one of them is that the resources photos, articles etc.. have one field which can be 'organization' or 'personal'. After that by clicking on this in application, you can filter resources (articles, photos,...) by that field.
Everything depends on situation, maybe another solution will be to create totally separated module where you gonna store things like organization, personal etc. This is better if you want to extend latter that and next to organization and personal add something else.
As I said, everything depends on situation and project.
additional:
ok, from your example I can see that by clicking on, for example, user link, you will have user id. Therefore, you can easily show all photos of that user:
# params[id] - user id which is got by clicking
user = User.find(params[id])
#photos = user.photos

Minimizing instance variables in views

I'm seeking brainstorming input for a Rails design issue I've run across.
I have simple Book reviews feature. There's a Book class, a User class, and a UserBook class (a.k.a., reviews and ratings).
class User < ActiveRecord::Base
has_many :user_books
end
# (book_id, user_id, review data...)
class UserBook < ActiveRecord::Base
belongs_to :user
belongs_to :book
end
In the corresponding book controller for the "show" book action, I need to load the book data along with the set of book reviews. I also need to find out whether the current user (if there is one) has contributed to those reviews.
I'm currently running two queries, Book.where(...) and UserBook.where(...), and placing the results into two separate objects passed on to the view. Now, while I could run a third query to find whether the user is among those reviews (on UserBook), I'd prefer to pull that from the #reviews result set. But do I do that in the controller, or in the view?
Also worth noting is that in the view I have to draw Add vs Update review buttons accordingly, with their corresponding ajax URLs. So I'd prefer to know it before I start looping through a result set.
If I detect this in the controller though, I'll need three instance variables passed in, which I understand is considered distasteful in Rails land. Not sure how to avoid this.
Suggestions appreciated.
This smells like a case for has_many through, which is designed for cases where you want to access the data of a third table through an intermediate table (in this case, UserBook)
Great explanation of has_many :through here
Might look something like this:
class User < ActiveRecord::Base
has_many :user_books
has_many :users, through: :books
end
Then you can simply call
#user = User.find(x)
#user.user_books` # (perhaps aliased as `User.find(x).reviews`)
and
#user.books
to get a list of all books associated with the User.
This way, you can gain access to all of the information you need for a particular user with a single #user instance variable.
PS - You'll want to take a look at the concept of Eager loading, which will prevent you from making extraneous database calls while fetching all of this information.

How to decide which action to use

I'm very new to web-development (I feel like all my posts lately have started that way) and becoming, with time, less new to rails. I'm at a point where I can do a sizeable amount of the things required for my job but there's one nagging problem I keep running into:
How do I decide if which action I should use for a given task? index, show, new, edit, create, update or destroy?
destroy is pretty obvious and I can loosely divide the rest into two buckets with index/show in one and new/edit/create in the other. But how do I decide which one to use or if I should build one of my own?
Some general guidelines or links to further reading would be very beneficial for me.
Here is how I think of these 7 RESTful Controller actions. Take, for example, a Person resource. The corresponding PeopleController would contain the following actions:
index: List a set of people (maybe with some optional conditions).
show: Load a single, previously created Person with the intention of viewing. The corresponding View is usually "read-only."
new: Setup or build an new instance of a Person. It hasn't been saved yet, just setup. The corresponding View is usually some type of form where the user can enter attribute values for this new Person. When this form is submitted, Rails sends it to the "create" action.
create: Save the Person that was setup using the "new" action.
edit: Retrieve a previously created Person with the intention of changing its attributes. The changes have not been made or submitted yet. The corresponding View is usually a form that Rails will submit to the "update" action.
update: Save the changes made when editing a previously created Person.
destroy: Well, as you guessed, destroy or delete a previously created Person.
Of course there is some debate as to whether these 7 actions are sufficient for all controllers, but in my experience they tend to do the job with few exceptions. Adding other actions is usually a sign of needing an additional type of resource.
For example, say you have an HR application full of Person resources you are just dying to hire. In order to accomplish this, you may be tempted to create a "hire" action (i.e., /people/456/hire). However, a more RESTful approach would instead consider this the "creation" of an Employment resource. Something like the following:
class Person < ActiveRecord::Base
has_many :employments
has_many :employers, :class_name => 'Company', :through => :employments, :source => :company
end
class Employement < ActiveRecord::Base
belongs_to :person
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :employments
has_many :employees, :class_name => 'Person', :through => :employments, :source => :person
end
The EmploymentsController's create action would then be used.
Okay, this is getting long. Don't be afraid to setup a lot of different resources (and you probably won't use all 7 Controller actions for each of these). It pays off in the long run and helps you stick to these 7 basic RESTful actions.
You can name your actions whatever you want. Generally, by Rails convention, index is the default one, show shows one item, list shows many, new and edit start editing a new or old item, and create and update will save them, respectively. destroy will kill an item, as you guessed. But all these are just conventions: you can name your action yellowtail if that's what you want to do.

Elegantly selecting attributes from has_many :through join models in 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!?

Resources