HABTM Lifecycle hook - ruby-on-rails

I have two models: team and season associated so that a team can belong to many seasons and each season can also have many teams. So far I have used an simple HABTM relationship between the models using a join table seasons_teams without an ID attribute.
Now I would like to add a hook for when an association is deleted, to be executed when a team drops out from a season. Is it correct that the best way to do this is to transform the HABTM association into a has_many / :trough, adding an ID attribute to what was the join table and creating the corresponding model file that will contain the new before_destroy hook? If so, how do I write the migration to add an auto-incremented index to my join table? (Or would it be better to create a new join table/model with index and to copy all the entries in the existing table)

Following the Rails Style Guide:
Prefer has_many :through to has_and_belongs_to_many. Using has_many :through allows additional attributes and validations on the join model
In your case:
class SeasonTeam < ActiveRecord::Base # couldn't find a better name...
belongs_to :team
belongs_to :season
# the validates are not mandatory but with it you make sure this model is always a link between a Season and a Team
validates :team_id, :presence => true
validates :season_id, :presence => true
before_destroy :do_some_magic
#...
end
class Season < ActiveRecord::Base
has_many :teams, :through => :season_teams
end
class Team < ActiveRecord::Base
has_many seasons, :through => :season_teams
end

You could also look at Rails' Association Callbacks. It provides before_remove and after_remove callback methods which you can use to customize behaviour.

Related

Rails ActiveRecord equivalent of Laravel ORM `attach()` method for polymorphic has_many :through

I'm transitioning from "Laravel ORM" to "Rails Active Record" and I couldn't find how do you do something like this:
$this->people()->attach($person['id'], ['role' => $role]);
Explanation for Laravel code snippet
People is a polymorphic association to the class that is being accessed via $this via the Role class. The function above, creates a record in the middle table (roles/peopleables) like this:
id: {{generically defined}}
people_id: $person['id']
role: $role
peopleable_type: $this->type
peopleable_id: $this->id
How the association is defined on the Laravel end:
class XYZ {
...
public function people()
{
return $this->morphToMany(People::class, 'peopleable')->withPivot('role','id');
}
...
}
My efforts in Ruby
Here is how I made the association in Ruby:
class Peopleable < ApplicationRecord
belongs_to :people
belongs_to :peopleable, polymorphic: true
end
class People < ApplicationRecord
has_many :peopleables
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
I have seen the operation << but I don't know if there is any way to set an additional value on the pivot table while triggering this operation. [in this case the roles or peopleables tables; I use these two terms interchangeably in this app.]
PS. So, basically the question is how to define additional values on the pivot table in a polymorphic-many association in ActiveRecord and dynamically set those values while initiating an attachment relationship
Description of Functionality
Our application has a limitless [generally speaking, not that there is no computational limits!] content type: post, novel, poem, etc.
Each of these content types can be associated to individuals who play certain roles: editor, author, translator, etc.
So, for example:
X is the translator of Post#1. X, Y and Z are authors of Post#1.
There is a distinct People model and each content type has its own unique model [for example: Post, Poem, etc].
The idea of :through is referring to the 'Role class' or 'the pivot table' [whichever way you want to understand it] that the polymorphic association is recorded on it.
In addition to the information regarding a simple polymorphic relationship, there is also the kind of role that is recorded on the pivot table.
For example, X is both the author and the translator for Post#1, so there are two rows with the same people_id, peopleable_type and peopleable_id, however they have different values for role.
From what I understand given your description, I think you have this models (I'll change the names to what I understand they are, hope it's clear enough):
class Person < ApplicationRecord # using singular for models
has_many :person_roles
end
class Poem < ApplicationRecord
has_many :person_roles, as: :content
end
class Novel < ApplicationRecord
has_many :person_roles, as: :content
end
etc...
class PersonRole < ApplicationRecord
belongs_to :person
belongs_to :content, polymorphic: true
# you should have a "role" column on your table
end
So a Person is associated to a "content" (Novel, Poem, etc) via the join model PersonRole with a specific role. A Person that is the author of some novel and the editor of some peom would have two PersonRole records.
So, if you have a person and you want to assign a new role on some content, you can just do:
person.person_roles.create(role: :author, content: some_poem)
or
PersonRole.create(person: person, role: :author, content: some_poem)
or
some_poem.person_roles.create(person: person, role: :author)
You have two things in play here: "belongs_to :content, polymorphic: true" is covers the part of this being a polymorphic association. Then you have the "PersonRole" table that covers the part you know as "pivot table" (join table/model on rails).
Note that :through in rails has other meaning, you may want to get all the poems that a user is an author of, you could then have a "has_many :poems, through: :person_roles" association (that won't actually work, it's more complex than that in this case because you have a polymorphic association, you'll need to configure the association with some extra options like source and scope for this to work, I'm just using it as an example of what we understand as a has many :through association).
Rails is 'convention over configuration'. Models' must be in singular 'Person'.
ActiveRecord has has_many ... through and polymorphic association
"Assignable" and "Assignments" are more natural to read than "peoplable"
class Person < ApplicationRecord
has_many :assignments, as: :assignable
has_many :roles, through: :assignments
end
class Role < ApplicationRecord
has_many :assignments
has_many :people, through: :assignments
end
class Assignment
belongs_to :role
belongs_to :assignable, polymorphic: true
end
You can read more Rails has_many :through Polymorphic Association by Sean C Davis

Create Join Table for a belongs_to association in Rails 5

I have a products model and a variations model as a belongs_to association. There are some variations that absolutely belong to a single product, but there are others that can belong to many products. Can I create a join table on a belongs_to association like in a has_and_blongs_to_many association?
My Models Currently
product.rb
class Product < ApplicationRecord
has_many :variations, dependent: :destroy
has_and_belongs_to_many :categories
has_and_belongs_to_many :subcategories
include FriendlyId
friendly_id :name, use: :slugged
def should_generate_new_friendly_id?
name_changed?
end
end
variation.rb
class Variation < ApplicationRecord
has_and_belongs_to_many :categories
has_and_belongs_to_many :subcategories
belongs_to :product
include FriendlyId
friendly_id :name, use: :slugged
def should_generate_new_friendly_id?
name_changed?
end
end
From Rails guides association basics - the belongs_to association:
A belongs_to association sets up a one-to-one connection with another model, such that each instance of the declaring model "belongs to" one instance of the other model.
When you do the belong_to :product association on the Variation model, it expect to have a field named product_id which will point to the associated product.
use example:
variation = Variation.first
product = variation.product # this line will get the product which is associated to the variation by the product_id column.
Since it can hold only one integer (one product id) the best option is to restructure your code. It makes no sense to use a "belong_to" as "has_many" association.
You need to change the association to some king of many to many association.
To chose the best option for you, read and learn the differences in the Rails guides - Association Basics
*** Make sure you won't lose your data when changing the association:
Idea of doing that:
Create the join table
Copy the info from your variations table (the variation.id and the associated product_id)
Start using the new association
(You can probably copy the data in the migration file, just search how to do it)

Rails join table record creation

If I have a HABTM join table called, :inventory_items_shopping_lists, how do I create and call records in this table? I do not have a join model. The two joined models are :inventory_items and :shopping_lists. I'd like to be able to have a user add :inventory_items to their :shopping_lists. Thanks in advance!
EDIT
Here are the relevant models and my goal:
class InventoryItem < ActiveRecord::Base
belongs_to :item, :foreign_key => :item_id
belongs_to :vendor
has_many :list_items
end
class ListItem < ActiveRecord::Base
belongs_to :inventory_item, :foreign_key => :item_id
belongs_to :shopping_list
end
class ShoppingList < ActiveRecord::Base
has_many :list_items
belongs_to :user, :foreign_key => :user_id
end
I'm attempting to have a :user add :inventory_items to a :shopping_list where they will become :list_items. list_items have almost identical attributes of :inventory_items, so I will likely just reference those attributes through association than by duplicating the attributes in the :list_items table. Thoughts or suggestions on this? I'm newish to RoR so I appreciate any feedback on any part of this plan. Thanks!
how do I create and call records in this table?
You basically don't with a HABTM association type. If you want to store more state on the join table than you will need to use a standard has_many (and maybe a has_many :through) so you can manipulate the underlying join model.
For this reason I have never been a fan of HABTM and in fact I have a handful of large Rails apps in production and I have never used HABTM myself.

Enforcing uniqueness of a model in has_many though

I have a User model, which has_many Dish through Recommendation. I would like to enforce uniqueness of Dish, as well as uniqueness of Recommendation.
How should I go about this in ActiveRecord?
In my dish.rb:
validate_uniqueness_of :dish_name
What I would like to have is: when an user recommends a dish, create a new dish if it does not exist, then create recommendation. If the dish already exists, then just create recommendation and point to existing dish.
Do I need to handle these situations manually (i.e., checking existence of dish in controller), or ActiveRecord has a way to handle it internally?
Update:
validate_uniqueness_of :dish_name only checks and return error message if the dish was created there. It probably won't create new recommendation that points to existing dish.
You could always .find_or_create_by_<attribute> to find the dish to begin with
As I see, more than one user can recommend the same dish.
Your models should look like:
Class User < ActiveRecord::Base
has_many :recommendations
has_many :dishes, :through => :recommendations
end
Class Dish < ActiveRecord::Base
has_many :recommendations
has_many :users, :through => :recommendations
end
So your Recommendations table in database should have two columns (beside it's id and timestamps) called user_id and dish_id . To validate that a user doesn't recommend
the same dish twice, do:
Class Recommendations < ActiveRecord::Base
belongs_to :dish
belongs_to :user
validates_uniqueness_of :dish_id, :scope => :user_id
end
And i didn't know about the .find_or_create_by method that Dan recommended, so definetly try to use something like that.
Hope i helped :)

rails, model naming question

I'm creating a model called Chats. And I want to assign users to a discussion. They are either a part of the Chats or they aren't...
So I create one model Chats.
What's the standard Rails naming convention for the other table?
ChatUsers?
While has_and_belongs_to_many is an ok option here, I recommend going with has_many :through instead.
In essence you will have an explicit join model, which you can call something like ChatSession.
class Chat < ActiveRecord::Base
has_many :chat_sessions
has_many :users, :through => :chat_sessions
end
class User < ActiveRecord::Base
has_many :chat_sessions
has_many :chats, :through => :chat_sessions
end
class ChatSession < ActiveRecord::Base
belongs_to :user
belongs_to :chat
end
Now you will need a table called chat_sessions with columns :user_id, and :chat_id in it. This is your join table.
Advantage
You get a model which is fully under your control, and isn't just a dumb join table managed by rails. So for example, if you want to track number of messages particular user left in particular chat, it could be a column in chat_sessions table. Presence of :through renders habtm unneeded in most cases. There is no complexity overhead either.
If it is a join table, it would be both table names joined by '_' and in alphabetical order of table names:
chats_users
This is called a has_and_belongs_to_many association in rails. You basically have two models that call has_and_belongs_to_many and create a linking table that uses the two models in the name (alphabetical and plural).
models:
class Chat < ActiveRecord::Base
has_and_belongs_to_many :users
end
class user < ActiveRecord::Base
has_and_belongs_to_many :chats
end
Then your tables would be
chats
users
chats_users

Resources