has_and_belongs_to_many and accepts_nested_attributes_for - ruby-on-rails

class Trip
has_many :trip_places
has_many :places, through: :trip_places
accepts_nested_attributes_for :places
end
class Place
has_many :trip_places
has_many :trips, through: :trip_places
validates :name, uniqueness: true
end
class TripPlace
belongs_to :trip
belongs_to :place
end
So we got a trip which has many places through trip places, and accepts nested attributes for places. Also places must be unique by name.
I'd like to have the following functionality though, and can't find an elegant solution to it:
Let's say we create a trip T, with two places P1 = 'hawaii' and P2 = 'costa rica'
If I edit the trip, and change hawaii to bora bora, it will modify the Place.
The problem is that I'd like to create a new place called bora bora and modify the TripPlace model to update the place_id with the new one.
Same thing goes to destroy, if I destroy a place in the form, I'd like to remove only the reference from the TripPlace, and not the actual Place
And of course, the create functionality should be alike, if the place exists, just create the TripPlace reference.
Right now, I don't think that accepts_nested_attributes_for really helps, but can't think of a good solution for this

Related

Rails model associations - has_one or single table inheritance?

I'm having trouble deciding between Single Table Inheritance and a simple has_one relationship for my two models.
Background: I'm creating a betting website with a "Wager" model. Users may create a wager, at which point it is displayed to all users who may accept the wager if they choose. The wager model has an enum with three statuses: created, accepted, and finished.
Now, I want to add the feature of a "Favorite Wager". The point of this is to make it more convenient for users to create a wager, if they have ones they commonly create. One click instead of ten.
FavoriteWagers exist only as a saved blueprint. They are simply the details of a wager -- when the User wants to create a Wager, they may view FavoriteWagers and click "create", which will take all the fields of the FavoriteWager and create a Wager with them. So the difference is that FavoriteWagers acts as only as a storage for Wager, and also includes a name specified by the user.
I read up on STI, and it seems that a lot of examples have multiple subclassing - eg. Car, Motorcycle, Boat for a "Vehicle" class. Whereas I won't have multiple subclasses, just one (FavoriteWager < Wager). People have also said to defer STI until I can have more classes. I can't see myself subclassing the Wagers class again anytime soon, so that's why I'm hesitant to do STI.
On the other hand, has_one doesn't seem to capture the relationship correctly. Here is an example:
Class User < ApplicationRecord
has_many :favorite_wagers, dependent: :destroy
has_many :wagers, dependent: destroy
end
Class FavoriteWager < ApplicationRecord
has_one :wager
belongs_to: user, index: true, foreign_key: true
end
Class Wager < ApplicationRecord
belongs_to :favorite_wager, optional: true
belongs_to :user
end
I've also thought about just copying the fields directly, but that's not very DRY. Adding an enum with a "draft" option seems too little, because I might need to add more fields in the future (eg. time to auto-create), at which point it starts to evolve into something different. Thoughts on how to approach this?
Why not just do a join table like:
Class User < ApplicationRecord
has_many :favorite_wagers, dependent: :destroy
has_many :wagers, through: :favorite_wagers
end
Class FavoriteWager < ApplicationRecord
belongs_to :wager, index: true, foreign_key: true
belongs_to :user, index: true, foreign_key: true
end
Class Wager < ApplicationRecord
has_one :favorite_wager, dependent: destroy
has_one :user, through: :favorite_wager
end
Your FavoriteWager would have the following fields:
|user_id|wager_id|name|
That way you can access it like:
some_user.favorite_wagers
=> [#<FavoriteWager:0x00007f9adb0fa2f8...
some_user.favorite_wagers.first.name
=> 'some name'
some_user.wagers.first.amount
=> '$10'
some_user.wagers.first.favorite_wager.name
=> 'some name'
which returns an array of favorite wagers. If you only want to have ONE favorite wager per user you can tweak it to limit that. But this gives you the ability to have wagers and users tied together as favorites with a name attribute. I don't quite understand your use case of 'a live wager never has a favorite' but that doesn't matter, you can tweak this to suit your needs.

convert "has many, through" association to simply a "belongs to" association

I wrote a rails program for a non-profit to help track encounters. Originally the thought was that a single encounter might deliver multiple services, hence the setup:
class Encounter < ApplicationRecord
has_many :encounters_services, dependent: :destroy, inverse_of: :encounter
has_many :services, through: :encounters_services
accepts_nested_attributes_for :encounters_services
class Service < ApplicationRecord
has_many :encounters, :through => :encounters_services
has_many :encounters_services, dependent: :destroy, inverse_of: :service
The end user has now figured out that they only want to associate a single service with an encounter. But there's already a lot of data in the database under the original structure. Is there a clean way to convert it to a scenario where a Service "has many" Encounters, and an Encounter "belongs to" a Service, without messing up the data that's already stored in the database in the "EncounterServices" table?
Thanks! I'm still a newbie so I appreciate the help!
I guess you could add a new migration to add a new column to the services and add a little script to set the value from EncountersServices.
Something like
def change
add_column :services, :encounter_id, :integer, index: true
Service.each do |s|
s.update_column :encounter_id, s.encounters.first.id
end
end
You can leave the previous data untouched. Since the old association's data has it's own table, your models' tables won't have garbage.
EDIT: I understood the relationship the wrong way, if an encounter should belong yo a service, the migration would look like this:
def change
add_column :encounters, :service_id, :integer, index: true
Encounter.each do |e|
e.update_column :service_id, e.services.first.id
end
end

Ruby on Rails: Is it possible to link a column with a column on another table?

I have models with deep associations in my Ruby on Rails API, sometimes 4 associations deep. For example:
Group has_many Subgroups has_many: Posts has_many: Comments
If I want to return Group.title with my comments, I need to say:
#comment.post.subgroup.group.title
Since this is way too many queries per Comment, I have added a column to the Comment table called group_title. This property is assigned when the Comment is created. Then every time the associated Group.title is updated, I have an after_update method on the Group model to update all associated Comment group_titles.
This seems like a lot of code to me and I find myself doing this often in this large scale app. Is there a way to link these 2 properties together to automatically update Comment.group_title every time its associated Group.title is updated?
I also had a similar relation hierarchy, and solved it (maybe there are better solutions) with joins.
Quarter belongs_to Detour belongs_to Forestry belongs_to Region
For a given detour, I find region name with one query.
Quarter.select("regions.name AS region_name, forestries.name as forestry_name, \
detours.name AS detour_name, quarters.*")
.joins(detour: [forestry: :region])
Sure, you can encapsulate it in a scope.
class Quarter
...
scope :with_all_parents, -> {
select("regions.name AS region_name, forestries.name as forestry_name, \
detours.name AS detour_name, quarters.*")
.joins(detour: [forestry: :region])
}
end
You can also use same approach.
class Comment
...
scope :with_group_titles, -> {
select("groups.title AS group_title, comments.*").joins(post: [subgroup: :group])
}
end
You can build hierarchies by using indirect associations:
class Group
has_many :subgroups
has_many :posts, through: :subgroups
has_many :comments, through: :posts
end
class Subgroup
belongs_to :group
has_many :posts
has_many :comments, through: :posts
end
class Post
belongs_to :subgroup
has_one :group, through: :subgroup
has_many :comments
end
class Comment
belongs_to :post
has_one :subgroup, through: :post
has_one :group, through: :post
end
The has_many :through Association
The has_one :through Association
This allows you to go from any end and rails will handle joining for you.
For example you can do:
#comment.group.title
Or do eager loading without passing a nested hash:
#comment = Comment.eager_load(:group).find(params[:id])
This however does not completely solve the performance issues related to joining deep nested hierarchies. This will still produce a monster of a join across four tables.
If you want to cache the title on the comments table you can use an ActiveRecord callback or you can define a database trigger procedure.
class Group
after_save :update_comments!
def update_comments!
self.comments.update_all(group_title: self.title)
end
end
You can do this by updating all the Comments from one side.
class Group
after_update do
Comment.joins(post: [subgroup: :group]).where("groups.title=?", self.title).update_all(group_title: self.title)
end
end

How to make a has_many_through association mandatory for one member?

I have the following models :
class City < ActiveRecord::Base
has_many :cities_regions_relationships
has_many :regions, through: :cities_regions_relationships
end
class Region < ActiveRecord::Base
has_many :cities_regions_relationships
has_many :cities, through: :cities_regions_relationships
end
class CitiesRegionsRelationship < ActiveRecord::Base
belongs_to :city
belongs_to :region
validates :city_id, presence: true
validates :region_id, presence: true
end
What I want is to make it impossible to create a city without it linked to a region. However, before attempting that, I have to be able to create a relationship on a city that's not yet saved.
I tried this in the console (with a region already created)
c = City.new(name: "Test")
c.cities_region_relationship.build(region_id: Region.first.id)
c.save
However, this fails because the relationship doesn't have a city_id (which is normal because I didn't save the city yet, it doesn't have an ID).
I could try other ways, but I will always have the same problem : How do I create a relationship on a new object, not yet saved in database ?
If you have other completely different solutions to my initial problem (Forcing cities to have at least one region), don't hesitate to suggest something entirely different.
You do not need to build a cities_region_relationship. By passing region_ids to a new City instance, this will create the cities_region_relationship for you.
You can try in the console:
c = City.new(name: "Test", region_ids: [an array of existing region ids])
c.save
for the validation, you can define a new validate method that checks if self.regions.blank? like mentioned in the SO post in my comment above.

In has_many :through, what is the correct way to create object inheritance

I've hit something that I don't understand how to model with Rails associations and neither STI nor polymorphism seem to address it.
I want to be able to access attributes from a join table via the collection that's created by has_many :through.
In the code below, this means that I want to be able to access the name and description of a committee position via the objects in the .members collection but as far as I can see I can't do that. I have to go through the original join table.
e.g. modelling a club and it's committee members
class User < ActiveRecord::Base
attr_accessible :full_name,
:email
has_many: committee_positions
has_many: committees, :through => committee_positions
end
class Committee < ActiveRecord::Base
attr_accessible :name
has_many :committee_positions
has_many :members, :through => :committee_positions
end
class CommitteePosition < ActiveRecord::Base
attr_accessible :user_id,
:committee_id,
:member_description,
:role_title
belongs_to :committee
belongs_to :user
end
Assume that each committee position instance has a unique description
i.e. the description is particular to both the member and the committee and so has to be stored on the join table and not with either the user or the club.
e.g.
Committee member: Matt Wilkins
Role: "Rowing club president"
Description: "Beats the heart of the rowing club to his own particular drum"
Is there a way to access the data in the join table via the committee.members collection?
While active record gives us this great alias for going directly to the members, there doesn't seem to be any way to access the data on the join table that created the collection:
I cannot do the following:
rowing_committee.members.find_by_role_title('president').name
Each item in the .members collection is a user object and doesn't seem to have access to either the role or description that's stored in the CommitteePositions join table.
The only way to do this would be:
rowing_committee.committee_positions.find_by_role_title('president').user.name
This is perfectly do-able but is clunky and unhelpful. I feel like the use-case is sufficiently generic that I may well be missing something.
What I would like to access via objects in the committee.members collection
member
- full_name
- email
- role_title (referenced from join table attributes)
- member_description (referenced from join table attributes)
This is only a small thing but it feels ugly. Is there a clean way to instruct the "member" objects to inherit the information contained within the join table?
-------------- addendum
On working through this I realise that I can get half way to solving the problem by simply defining a new class for committee member and referencing that instead of user in the has_many :through relationship. It works a little bit better but is still pretty clunky
class Committee < ActiveRecord::Base
...
has_many :committee_positions
has_many :members,
:through => :committee_positions,
:class_name => 'CommitteeMember'
...
end
class CommitteeMember < User
def description( committee )
self.committees.find_by_committee_id( committee.id ).description
end
def role( committee )
self.committees.find_by_committee_id( committee.id ).description
end
end
Now this is getting closer but it still feels clunky in that the code to use it would be:
committee = Committee.first
president_description = committee.members.find_by_title('president').description( committee )
Is there any way to initialize these objects with the committee they are referencing?
I think you could use some delegation here. In your Committee_Position class:
class Committee_Position < ActiveRecord::Base
attr_accessible :user_id,
:committee_id,
:member_description,
:role_title
belongs_to :committee
belongs_to :user
delegate :name, :email, :to => :user
end
so you could do what you say you want:
rowing_club.committee_members.find_by_role_title('president').name

Resources