Is it good practice to validate IDs in a join model? - ruby-on-rails

I have a HMT association setup between my Artist and Group models:
class Artist < ApplicationRecord
has_many :artist_groups, dependent: :destroy
has_many :artist_groups, through: :artist_groups
end
class ArtistGroup < ApplicationRecord
has_many :memberships, class_name: "ArtistGroupMembership", dependent: :destroy
belongs_to :artist
belongs_to :group
has_and_belongs_to_many :roles
accepts_nested_attributes_for :memberships, reject_if: :all_blank, allow_destroy: true
validates_presence_of :artist_id, :group_id
end
class Group < ApplicationRecord
has_many :artist_groups, dependent: :destroy
has_many :members, through: :artist_groups, source: :artist
end
As you'll notice in my ArtistGroup join model it validates to make sure the an artist and group are present.
When the association is saved, whether I do something like this:
artist.groups.push(Group.first)
or create a form in my view (sans ID inputs) ActiveRecord is smart enough to map the association. With this in my should I even be validating these IDs in my join models? I notice this becomes even more of a pain when dealing with polymorphic associations.

Rails 5 automatically requires that the belongs_to :artist refers to an existing artist so having extra validation is completely unnecessary. You can make that requirement optional by doing
belongs_to :artist, optional: true

Related

Multiple associations in the has_many "through" model

I have a Products & Parts model which would each have multiple uploads, which are also polymorphic. Is it possible for me to have a single ItemUpload model to handle the association between the Products/Parts and Uploads, or do they need to be separate? I'd try myself just to see, but don't want to cause any potential headaches down the line! Note that I'm aware I need to do the source: and source_type: stuff to clean up the polymorphic association with has_many, but would like to clarify this point first before proceeding. Current models:
class Product < ApplicationRecord
has_many :uploads, as: :uploadable, dependent: :destroy
end
class Part < ApplicationRecord
has_many :uploads, as: :uploadable, dependent: :destroy
end
class Upload < ApplicationRecord
belongs_to :uploadable, polymorphic: true
end
What I would ideally like:
Class ItemUpload < ApplicationRecord
belongs_to :product, optional: true
belongs_to :part, optional: true
belongs_to :upload
end
Is that ok or would I need a separate ProductUpload and PartUpload model?
I would have thought your associations would look more like:
class Product < ApplicationRecord
has_many :item_uploads, as: :itemable, dependent: :destroy
has_many :uploads, through: :item_uploads
end
class Part < ApplicationRecord
has_many :item_uploads, as: :itemable, dependent: :destroy
has_many :uploads, through: :item_uploads
end
class Upload < ApplicationRecord
has_many :item_uploads
has_many :products, through: :item_uploads, source: :itemable, source_type: 'Product'
has_many :parts, through: :item_uploads, source: :itemable, source_type: 'Part'
end
Class ItemUpload < ApplicationRecord
belongs_to :itemable, polymorphic: true
belongs_to :upload
end
That should allow you to do:
product.uploads
part.uploads
upload.products
upload.parts
BTW, in reference to the link you provided:
Upload ≈ User
ItemUpload ≈ Membership
Product, Part ≈ Project, Group
The above follows the pattern in the linked article.

How do I handle creating a join model when neither associated model has been created?

In a simplified set up I have 4 models.
App :has_many Environments,Variables
Environment :has_many VariableValues
Variable :has_many VariableValues
VariableValue :belongs_to Environment,Variable
In my react app, you create an App, then you can create Envrionments and Variables independently. Each variable can have a value for each environment, so I create a VariableValue that has the actual value, and then an environment_id and variable_id that associates the variable value to an environment. When saving this, and it all saves at once, I do not have a environment_id to give to the join model, since neither the variables or environments will be persisted before the VariableValues are created. I am doing this entire save of the App with accepts _nested_attributes. It may not be possible to do this, so I'm curious how someone else has handled this situation.
Thanks!
UPDATE:
Here are my models
class Workspace < ApplicationRecord
has_many :workspace_envs, inverse_of: :workspace
has_many :workspace_variables, inverse_of: :workspace
accepts_nested_attributes_for :workspace_envs, :workspace_variables, allow_destroy: true
end
class WorkspaceEnv < ApplicationRecord
acts_as_paranoid
belongs_to :workspace, inverse_of: :workspace_envs
has_many :workspace_env_variable_values, inverse_of: :workspace_env
has_many :workspace_variable_values, inverse_of: :workspace_env, through: :workspace_env_variable_values
has_many :workspace_variables, inverse_of: :workspace_envs, through: :workspace_env_variable_values
end
class WorkspaceVariable < ApplicationRecord
belongs_to :workspace, inverse_of: :workspace_variables
has_many :workspace_env_variable_values, inverse_of: :workspace_variable
has_many :workspace_variable_values, inverse_of: :workspace_variable, through: :workspace_env_variable_values, dependent: :destroy
has_many :workspace_envs, inverse_of: :workspace_variables, through: :workspace_env_variable_values
accepts_nested_attributes_for :workspace_variable_values, allow_destroy: true
end
class WorkspaceVariableValue < ApplicationRecord
include VariableValue
has_one :workspace_env_variable_value, inverse_of: :workspace_variable_value
has_one :workspace_variable, inverse_of: :workspace_variable_values, through: :workspace_env_variable_value
has_one :workspace_env, inverse_of: :workspace_variable_values, through: :workspace_env_variable_value
accepts_nested_attributes_for :workspace_env
end
class WorkspaceEnvVariableValue < ApplicationRecord
belongs_to :workspace_env, inverse_of: :workspace_env_variable_values
belongs_to :workspace_variable_value, inverse_of: :workspace_env_variable_value
belongs_to :workspace_variable, inverse_of: :workspace_env_variable_values
end
The important thing here is that all of these models are created at the same time, not even the Workspace exists, it is all on big save, in one big transaction, my current solution is to build the workspace_variables and workspace_envs and then to associated the workspace_envs with the workspace_env_variables_values based on their index.
:belongs_to can be optional, us just append ',optional: true' and you can save record into your db without any problems. but be careful about it, i.e. you leave your object in invalid state (from business logic point of view), don't forget to update saved records and add foreign id values.
class VariableValue < ApplicationRecord
belongs_to :environment, optional: true
belongs_to :variable, optional: true

Join Table Confusion Ruby on Rails

I have read a lot of the questions and answers here about join tables, STI tables, and polymorphic associations, in addition to many articles and documentation spread throughout the internet. While I've learned a lot I'm still confused about what I should do in my situation. I may have read the answer and not known I was reading the answer, but I wanted to see if someone could help me understand what it is I should do here.
I have a Gallery model, an Album model, an Image model and a Category model. These are all nested in a User model.
When you create an Album assign a Category to it and those are saved with an Album_Categories model. I want the Gallery model to be aware of what Categories exist and be able to choose which ones it would like to use.
Once it selects a Category, it should be able to access the Albums associated with the Category and the Album's Images, which are linked through and Album_Images join table. The Category should be able to continue to exist even if the Album or Gallery that it was originally created with is deleted so that another Album or Gallery can take advantage of it later.
My sense is that whenever a unique Category is created is should some how connect to Gallery through a Category_Galleries model, but in my use of Images which is connected to Gallery and Album with their own specific join tables, Gallery is unaware of an Album_images Connection, so I assume sharing the knowledge of a Category created by the other would be the same.
Any way to help me unerstand this would be appreciated.
Edit: model code
class User < ActiveRecord::Base
has_many :images, dependent: :destroy
has_many :galleries, dependent: :destroy
has_many :albums, dependent: :destroy
has_many :categories, dependent: :destroy
accepts_nested_attributes_for :images, :galleries, :albums, :categories, allow_destroy: true
accepts_attachments_for :images, attachment: :file, append: true
end
class Image < ActiveRecord::Base
belongs_to :user
has_many :gallery_images, dependent: :destroy
has_many :galleries, through: :gallery_images
has_many :album_images, dependent: :destroy
has_many :albums, through: :album_images
attachment :file, type: :image
validates :file, presence: true
end
class Album < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
has_many :album_galleries
has_many :galleries, through: :album_galleries # , dependent: :destroy
has_many :album_images, dependent: :destroy
has_many :images, through: :album_images
has_many :album_categories
has_many :categories, through: :album_categories
accepts_attachments_for :images, attachment: :file, append: true
accepts_nested_attributes_for :images
end
class Gallery < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
has_many :gallery_images, dependent: :destroy
has_many :images, through: :gallery_images
has_many :album_galleries, dependent: :destroy
has_many :albums, through: :album_galleries
accepts_attachments_for :images, attachment: :file, append: true
accepts_nested_attributes_for :images
end
class Category < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
has_many :albums, through: :album_categories
has_many :album_categories
end
class GalleryImage < ActiveRecord::Base
belongs_to :gallery
belongs_to :image
end
class AlbumCategory < ActiveRecord::Base
belongs_to :category
belongs_to :album
end
class AlbumGallery < ActiveRecord::Base
belongs_to :gallery
belongs_to :album
end
class AlbumImage < ActiveRecord::Base
belongs_to :album
belongs_to :image
end
It really depends on the requirements you're trying to model. Does this accurately reflect your requirements? (ignoring the user for the current moment and not necessarily detailing rails associations)
A gallery can consist of many categories
A category can contain many albums
An album can have many images
If so, you could simply have:
a has_many through association between galleries and categories
a has_many through association between albums and categories
a has_many through association between albums and images
The has_many through will allow your categories, galleries, albums and images to exist even after relations are destroyed.
At the moment I don't see any need for STI or polymorphism. Usually you use polymorphic associations when two models share (own) the same table. But since you would use has_many through associations, polymorphism wouldn't even be necessary. (It prevents clashes of the owning table ids when occuring as a foreign key in the owned table).
To get to images, from gallery, for example you would be essentially displaying all the images of all albums belonging to all categories that are assigned to a gallery. That can be done through associations and querying.
So basically, I don't think your scenario...based on my understanding...is too complex and has_many through associations should suffice.
An interesting question would by why a user is associated to all of your models. Are they responsible for creating/those model instances a user is associated to?

Using inverse_of on a has_many :through?

I'll start off with my test, to show the desired functionality:
item = LineItem.new
order = Order.new
OrderLineItem.new(order: order, line_item: item)
related_order = item.orders.first
expect(related_order).to eq order
which evaluates to:
expected: #<Order ....>
got: nil
The 3 models above are related as follows:
class LineItem < ActiveRecord::Base
has_many :order_line_items, as: :line_item, inverse_of: :line_item
has_many :orders,
through: :order_line_items,
source: :order,
inverse_of: :line_items
end
class OrderLineItem < ActiveRecord::Base
belongs_to :order, inverse_of: :order_line_items
belongs_to :line_item,
polymorphic: true, inverse_of: :order_line_items
end
class Order < ActiveRecord::Base
has_many :line_items,
through: :order_line_items,
source: :line_item,
inverse_of: :orders
has_many :order_line_items, dependent: :destroy, inverse_of: :order
end
I can make this all work by saving everything and calling item.reload before my expect line. But isn't inverse_of supposed to hook all this up?
Maybe I'm expecting too much of inverse_of with pre-saved objects?
No, inverse_of doesn't work with has_many :through
See the "Bidirectional associations" section of the ActiveRecords associations docs: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods.
There are limitations to :inverse_of support:
does not work with :through associations.
does not work with :polymorphic associations.
for belongs_to associations has_many inverse associations are ignored.
See also: ActiveRecord :inverse_of does not work on has_many :through on the join model on create

Get attribute value from the join in a many-to-many relationship

I have a many-to-many relation between User and "Link".
The join model is called LinkAddress and besides for saving the IDs of the other two models, it has an attribute called address - information it collects at creation.
How can I access the address attribute for a certain link in a request scenario like the following: User.first.links.first.address ?
Models:
class User < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :links, through: :link_addresses
accepts_nested_attributes_for :link_addresses, allow_destroy: true
end
class LinkAddress < ActiveRecord::Base
belongs_to :user
belongs_to :link
end
class Link < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :users, through: :link_addresses
end
You could access it through User since it's a has_many ... :through relation:
User.first.link_addresses.first.address
Or, if you'd like to go through links then:
User.first.links.first.link_addresses.first.address
SQL Aliases
I had this exact question: Rails Scoping For has_many :through To Access Extra Data
Here's the answer I got:
#Images
has_many :image_messages, :class_name => 'ImageMessage'
has_many :images, -> { select("#{Image.table_name}.*, #{ImageMessage.table_name}.caption AS caption") }, :class_name => 'Image', :through => :image_messages, dependent: :destroy
This uses SQL Aliases which I found at this RailsCast (at around 6:40 in). It allows us to call #user.image.caption (even though .caption is in the join model)
Your Code
For your query, I'd use this:
class User < ActiveRecord::Base
has_many :link_addresses, dependent: :destroy
has_many :links, -> { select("#{Link.table_name}.*, #{LinkAddress.table_name}.address AS address") }, through: :link_addresses
accepts_nested_attributes_for :link_addresses, allow_destroy: true
end
This will allow you to write #user.links.first.address, and gracefully handles an absence of the address record

Resources