Rails has_one :through with different model name - ruby-on-rails

A transaction_record has many workflows, and each workflow has many milestones. One of the milestones is marked current: true, and I want to go from the transaction_record to the current_milestone:
class TransactionRecord < ApplicationRecord
has_many :workflows
has_many :milestones, through: :workflows
# DOES NOT WORK, but what I want to do...
has_one :current_milestone, through: :workflows, class: Milestone, source: :milestones
# Works, but want to make an association for including
def current_milestone
milestones.where(current: true).first
end
end
class Workflow < ApplicationRecord
belongs_to :transaction_record
has_many :milestones
end
class Milestone < ApplicationRecord
belongs_to :workflow
end
I can create a method that returns the desired milestone, but I want to make it an actual association so I can include it for DB performance.
I have a transaction_records#index page where I list the transaction_records and the current_milestone for each one. That's an n+1 unless I can figure this out.
I really want to be able to do something like:
#transaction_records = TransactionRecord.includes(:current_milestone)
<% #transaction_records.each do |transaction_record| %>
<%= transaction_record.name %> - <%= transaction_record.current_milestone.name %>
<% end %>
update
I can specify a direction relationship between transaction_record and milestone, and then do transaction_record has_one :current_milestone, -> { where(current: true) }, class_name: Milestone. But now I'm changing my DB schema for a more efficient load query. Not the end of the world, but not my preference if I already have an association.

To be honest I don't like the concept, that transaction_record has some kind of active_milestone without any mentioning about joining current_workflow.
Good solution is to think about both workflow and milestone to have an ability to be current and then:
class TransactionRecord < ApplicationRecord
has_one :current_workflow, ....
has_one :current_milestone, through: current_workflow, ....
end
class Workflow < ApplicationRecord
has_one :current_milestone, condition: ....
end
This is far better for me, but you still need to add additional current flag attribute in workflow.
That's why better solution is rework your concept at all.
Remove current from milestone and add current_milestone_id to workflow. If it's nil, then this workflow has no current_milestone. If it contains some id, then this is your current_workflow and current_milestone_id.
Code will look pretty the same, but it will don't have ugly condition in Workflow

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.

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

Create Rails scope comparing fields on two tables

I have a number of associated tables in an application
class Listing < ActiveRecord::Base
belongs_to :house
belongs_to :multiple_listing_service
end
class House < ActiveRecord::Base
has_one :zip_code
has_one :primary_mls, through: :zip_code
end
I wanted to create a scope that produces all the Listings that are related to the Primary MLS for the associated House. Put another way, the scope should produce all the Listings where the multiple_listing_service_id = primary_mls.id for the associated house.
I've tried dozens of nested joins scopes, and none seem to work. At best they just return all the Listings, and normally they fail out.
Any ideas?
If I understand correctly, I'm not sure a pure scope would be the way to go. Assuming you have:
class MultipleListingService < ActiveRecord::Base
has_many :listings
has_many :zip_codes
end
I would go for something like:
class House < ActiveRecord::Base
...
def associated_listings
primary_mls.listings
end
end
Update 1
If your goal is to just get the primary listing then I would add an is_primary field to the Listing. This would be the most efficient. The alternative is a 3 table join which can work but is hard to optimize well:
class Listing < ActiveRecord::Base
...
scope :primary, -> { joins(:houses => [:zip_codes])
.where('zip_codes.multiple_listing_service_id = listings.multiple_listing_service_id') }

How to build a has_many-alike association containing a list of various Models?

I have an Order which has many line_items. Only this is not a LineItem module, but a list of "things that act Orderable". E.g. Addon or Site.
class Order
attr_accessor :line_items
before_save :persist_line_items
private
def persist_line_items
#line_items.each {|li| li.save }
end
end
class Addon
belongs_to: order
end
class Site
belongs_to: order
end
Which can be used as:
order = Order.new
order.line_items << Addon.new(order: order)
order.line_items << Site.new(order: order)
But, now I want to load an Order and join the "associated" line_items. I
could load them in an after_initialize hook, and do an
Addon.find_by(order_id: self.id) but that quickly leads to a lot of
queries; where a JOIN would be more appropriate. In addition, I
currently miss the validations trickling up: when a normal has_many
related item is invalid the containing model will not be valid either:
order = Order.new(line_items: [an_invalid_line_item])
order.valid? #=> false
I am wondering if there is a way
to leverage ActiveRecords' has_many-relation to be used with a list of
different models.
I think that a polymorphic association should do the trick.
Would look like this:
class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :orderable, polymorphic: true
end
class Site < ActiveRecord::Base
has_many :line_items, as: :orderable
end
class Addon < ActiveRecord::Base
has_many :line_items, as: :orderable
end
It would use a join table, but i think this is actually a good thing. Otherwise you could use STI for your Addon and Site models, but that would not make a lot of sense in my regard.

How to create an association that sets join table attributes automatically?

I am totally confused about how I should go about "the rails way" of effectively using my associations.
Here is an example model configuration from a Rails 4 app:
class Film < ActiveRecord::Base
# A movie, documentary, animated short, etc
has_many :roleships
has_many :participants, :through => :roleships
has_many :roles, :through => :roleships
# has_many :writers........ ?
end
class Participant < ActiveRecord::Base
# A human involved in making a movie
has_many :roleships
end
class Role < ActiveRecord::Base
# A person's role in a film. i.e. "Writer", "Actor", "Extra" etc
has_many :roleships
end
class Roleship < ActiveRecord::Base
# The join for connecting different people
# to the different roles they have had in
# different films
belongs_to :participant
belongs_to :film
belongs_to :role
end
Given the above model configuration, the code I wish I had would allow me to add writers directly to a film and in the end have the join setup correctly.
So for example, I'd love to be able to do something like this:
## The Code I WISH I Had
Film.create!(name: "Some film", writers: [Participant.first])
I'm not sure if I'm going about thinking about this totally wrong but it seems impossible. What is the right way to accomplish this? Nested resources? A custom setter + scope? Something else? Virtual attributes? thank you!
I created a sample app based on your question.
https://github.com/szines/hodor_filmdb
I think useful to setup in Participant and in Role model a through association as well, but without this will work. It depends how would you like to use later this database. Without through this query wouldn't work: Participant.find(1).films
class Participant < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
class Role < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
Don't forget to give permit for extra fields (strong_parameters) in your films_controller.rb
def film_params
params.require(:film).permit(:title, :participant_ids, :role_ids)
end
What is strange, that if you create a new film with a participant and a role, two records will be created in the join table.
Update:
You can create a kind of virtual attribute in your model. For example:
def writers=(participant)
#writer_role = Role.find(1)
self.roles << #writer_role
self.participants << participant
end
and you can use: Film.create(title: 'The Movie', writers: [Participant.first])
If you had a normal has_and_belongs_to_many relationship i.e. beween a film and a participant, then you can create a film together with your examples.
As your joining model is more complex, you have to build the roleships separately:
writer= Roleship.create(
participant: Participant.find_by_name('Spielberg'),
role: Role.find_by_name('Director')
)
main_actor= Roleship.create(
participant: Participant.find_by_name('Willis'),
role: Role.find_by_name('Actor')
)
Film.create!(name: "Some film", roleships: [writer, main_actor])
for that, all attributes you use to build roleships and films must be mass assignable, so in a Rails 3.2 you would have to write:
class Roleship < ActiveRecord::Base
attr_accessible :participant, :role
...
end
class Film < ActiveRecord::Base
attr_accessible :name, :roleships
...
end
If you want to user roleship_ids, you have to write
class Film < ActiveRecord::Base
attr_accessible :name, :roleship_ids
...
end
Addendum:
Of cause you could write a setter method
class Film ...
def writers=(part_ids)
writer_role=Role.find_by_name('Writer')
# skiped code to delete existing writers
part_ids.each do |part_id|
self.roleships << Roleship.new(role: writer_role, participant_id: part_id)
end
end
end
but that makes your code depending on the data in your DB (contents of table roles) which is a bad idea.

Resources