I am developing a portal via RubyOnRails where pupils, teachers and parents can participate in different contests with their art works.
There are 3 entities: Contests, Categories (competitor categories / age groups) and Nominations (kinds of activity). Contest can have many Categories. Each ContestCategory can have many Nominations. Each Category can belong to several Contests. Every Nomination can belong to many ContestCategories. So I presume there is many-to-many relationship between Contests and Categories and many-to-many relationship between ContestCategories and Nominations. I've created following models: Contest (contest.rb), Category (category.rb), Nomination (nomination.rb), ContestCategory (contest_category.rb) and ContestCategoryNomination (contest_category_nomination.rb).
My models:
class Category < ActiveRecord::Base
has_many :contest_categories
has_many :contests, through: :contest_categories
end
class Contest < ActiveRecord::Base
has_many :contest_categories
has_many :categories, through: :contest_categories
has_many :nominations, through: :contest_categories
has_one :provision
end
class ContestCategory < ActiveRecord::Base
belongs_to :contest
belongs_to :category
has_many :contest_category_nominations
has_many :nominations, through: :contest_category_nominations
end
class ContestCategoryNomination < ActiveRecord::Base
belongs_to :contest_category
belongs_to :nomination
end
class Nomination < ActiveRecord::Base
has_many :contest_category_nominations
has_many :contest_categories, through: :contest_category_nominations
has_many :contests, through: :contest_categories
end
I want to create an ajax-based modal window during creation of new Contest to link it with Category and select multiple Nominations that belong to this Category.
What controllers should I create to satisfy has_many relationships
between my models?
What are the naming conventions (singular and plural) in rails to satisfy my
relationships? For example, ContestsCategoriesController or
ContestCategoryNominationsController or may be
ContestCategoryNominationsController?
What action method should I create in this controllers to invoke to
render this modal window? Should it be new_category action in
CategoriesController or new action in ContestsCategoriesController
or new action in ContestsCategoriesNominationsController?
That completely depends on how you want to manipulate your objects. If you only want to modify all attributes and relations through Contests, you'd just need the ContestsController. Using a nifty little method like accept_nested_attributes_for http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html you can provide all relevant values, even to the associated records.
Referring on your requirement you noted in the comments, I'd create a form in the modal dialogue, which represent a form object, like:
class Nomination::Category
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
# validates :your_attributes
def initialize(category, attributes = {})
#category = category
#attributes = attributes
end
def persisted?
false
end
def save
return false unless valid?
if create_objects
# after saving logic
else
false
end
end
# more business logic according to your requirements
private
def create_objects
ActiveRecord::Base.transaction do
# #category.contests = ... saving logic
#category.save!
end
rescue
false
end
end
and a representing controller:
class NominationCategoriesController < ApplicationController
def new
category = Category.find params[:category_id]
#nomination_category = Nomination::Category.new category
end
def create
category = Category.find params[:category_id]
#nomination_category = Nomination::Category.new category, params[:nomination_category]
#nomination_category.save
end
end
Please beware this is just an example/idea. The concrete implementation depends on your specific business logic requirements.
It worth reading more about the form objects approach.
Related
Lets say I have two models: Performance and Band, and to connect the two I have a join table called performers. My ActiveRecord models are setup as follows:
class Band < ApplicationRecord
has_many :performers
has_many :performances, through: :performers, dependent: :destroy
end
class Performance < ApplicationRecord
has_many :performers
has_many :bands, through: :performers, dependent: :destroy
end
class Performer < ApplicationRecord
belongs_to :band
belongs_to :performance
end
Now the tricky part. I have a custom attribute called permissions in the performers table that captures the permission levels (owner, editor, viewer) for a performer, which defines who can make changes to a performance. Which brings me to my question: when a band creates a new performance, how can I set a value on the join table during creation e.g.
def create
performance = Performance.new(performance_params)
# here I add a performance to a band's performances, which creates a new performer record
band.performances << performance
# what I would also like to do (at the same time if possible) is also define the permission level
# during creation something like but:
performer = band.performers.last
performer.permissions = 'owner'
performer.save
render json: serialize(performance), status: 200
end
Is there something in Rails that can let me modify a join tables attribute during creation of an association?
EDIT
For reference, right now I do:
def create
performance = Performance.new(performance_params)
performer = Performer.new
performer.band = Band.find(params[:band_id])
performer.permissions = 'owner'
performance.performers << performer
performance.save!
render json: serialize(performance), status: 200
end
But was wondering if there was something simpler.
You can use either Association Callbacks on has_many or add appropriate callback to Performer model as it's still created as a model even it's joining one.
Something like:
class Performer < ApplicationRecord
belongs_to :band
belongs_to :performance
before_create :set_permissions
private
def set_permissions
self.permissions = 'owner'
end
end
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.
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.
I am having models like
// Contains the details of Parties (Users)
class Party < ActiveRecord::Base
has_many :party_races
has_many :races, :through=>:party_races
end
// Contains the party_id and race_id mappings
class PartyRace < ActiveRecord::Base
belongs_to :party
belongs_to :race
end
// Contains list of races like Asian,American,etc..
class Race < ActiveRecord::Base
has_many :party_races
has_many :parties, :through => :party_races
end
Now, lets say I'm creating an instance of Party
party_instance = Party.new
How am I supposed to add multiple Races to party_instance and save to database ?
You could use nested attributes to make one form that allows children. There are many examples on this site. Read up on the following first:
Accepts nested attributes
Fields for
A railscast about your use case
You also can create new PartyRace for each Races that you can add:
def addRace( party_instance, new_race )
party_race = PartyRace.new( party: party_instance, race: new_race )
party_race.save
end
I wonder if we could eager load in model level:
# country.rb
class Country < ActiveRecord::Base
has_many :country_days
def country_highlights
country_days.map { |country_day| country_day.shops }.flatten.uniq.map { |shop| shop.name }.join(", ")
end
end
# country_day.rb
class CountryDay < ActiveRecord::Base
belongs_to :country
has_many :country_day_shops
has_many :shops, :through => :country_day_shops
end
# shop.rb
class Shop < ActiveRecord::Base
end
Most of the times it's difficult to use .includes in controller because of some polymorphic association. Is there anyway for me to eager load the method country_highlights at the model level, so that I don't have to add .includes in the controller?
You can't "eager load" country_days from a model instance, but you can certainly skip loading them all together by using a has_many through:. You can also skip the extra map, too.
# country.rb
class Country < ActiveRecord::Base
has_many :country_days
has_many :country_day_shops, through: :country_days #EDIT: You may have to add this relationship
has_many :shops, through: :country_day_shops #And change this one to use the new relationship above.
def country_highlights
shops.distinct_names.join(", ")
end
end
# country_day.rb
class CountryDay < ActiveRecord::Base
belongs_to :country
has_many :country_day_shops
has_many :shops, :through => :country_day_shops
end
# shop.rb
class Shop < ActiveRecord::Base
def self.distinct_names
pluck("DISTINCT shops.name") #Edit 2: You may need this instead of 'DISTINCT name' if you get an ambiguous column name error.
end
end
The has_many through: will use a JOIN to load the associate shop records, in effect eager loading them, rather than loading all country_day records and then for each country_day record, loading the associated shop.
pluck("DISTINCT name") will return an array of all of the unique names of shops in the DB, using the DB to perform a SELECT DISTINCT, so it will not return duplicate records, and the pluck will avoid loading ActiveRecord instances when all you need is the string name.
Edit: Read the comments first
You could cache the end result (the joined string or text record in your case), so you'll not have to load several levels of records just to build this result.
1) Add a country_highlights text column (result might be beyond string column limits)
2) Cache the country_highlights in your model with a callback, e.g. before every save.
class Country < ActiveRecord::Base
has_many :country_days
before_save :cache_country_highlights
private
def cache_country_highlights
self.country_highlights = country_days.flat_map(&:shops).uniq.map(&:name).join(", ")
end
end
Caching you calculation will invoke a little overhead when saving a record, but having to load only one instead of three model records for displaying should speed up your controller actions so much that it's worth it.