So let's say I have the following models:
class Building < ActiveRecord::Base
has_many :rooms
has_many :training_rooms, class_name: 'TrainginRoom', source: rooms
has_many :computers, through: :training_rooms
end
class Computer < ActiveRecord::Base
belongs_to :room
end
class Room < ActiveRecord::Base
belongs_to :building
end
class Office < Room
end
class TrainingRoom < Room
has_many :computers
end
And let's also say I am following the jsonapi spec and using the included top-level member to render each related object in a single http call.
So buildings/show looks sort of like this:
json.data do
json.id building.id
json.type building.type
json.attributes do
# render the attributes
end
json.relationships do
# render the relationships
end
end
json.included.do
json.array! building.rooms.each do |room|
json.type room.type
json.id room.id
json.attributes do
# render the attribtues
end
json.relationships do |room|
# render included member's relationships
# N+1 Be here
end
end
end
I have not been able to eagerly load the relationships from the included member, since it is not present on all members of the array.
For instance, in the controller:
#building = Building.eager_load(
{ rooms: :computers }
).find(params[:id])
Will not work if there is an office in the rooms relationship, as it does not have a computers relationship.
#building = Building.eager_load(
:rooms,
traning_rooms: :computers
).find(params[:id])
Also does not work, as the training rooms relationship provides access to the computers to be sideloaded, but is not accessed directly in the rendering code and thus is a useless cache.
Additionally I tried applying a default scope to training room to eager load the computers association, but that also didn't have the desired affect.
The only thing I can think of at this point is to apply the computers relationship to the Room class, but I don't really want to do it, because only training rooms should have computers.
I'd love to hear any thoughts.
Since there is no training_room_id in the Computer model, you will have to explicitly mention the foreign_key while defining the relationship.
class TrainingRoom < Room
has_many :computers, foreign_key: :room_id
end
Now you will be able to eager load the records:
#building = Building.eager_load({ training_rooms: :computers }).where(id: params[:id])
Hope it helps !
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
Given the following model structures;
class Project < ApplicationRecord
has_many :leads
has_and_belonds_to_many :clients
end
class Lead < ApplicationRecord
belongs_to :project
end
class Client < ApplicationRecord
has_and_belongs_to_many :projects
end
How you would suggest reporting on duplicate leads across a Client?
Right now I am doing something very gnarly with flattens and counts, it feels like there should be a 'Ruby way'.
Ideally I would like the interface to be able to say either Client.first.duplicate_leads or Lead.first.duplicate?.
Current (terrible) solution
#duplicate_leads = Client.all.map(&:duplicate_leads).flatten
Class Client
def duplicate_leads
leads = projects.includes(:leads).map(&:leads).flatten
grouped_leads = leads.group_by(&:email)
grouped_leads.select { |_, v| v.size > 1 }.map { |a| a[1][0] }.flatten
end
end
Environment
Rails 5
Ruby 2.3.1
You could try this.
class Client < ApplicationRecord
has_and_belongs_to_many :projects
has_many :leads, through: :projects
def duplicate_leads
duplicate_ids = leads.group(:email).having("count(email) > 1").count.keys
Lead.where(id: duplicate_ids)
end
end
You could try creating a has_many association from Lead through Project back to Lead, in which you use a lambda to dynamically join based on a match on email between the two records, and on the id not matching. This would mark both records as a duplicate -- if you wanted to mark only one then you can require that the id of one is less than the id of the other.
Some hints: Rails has_many with dynamic conditions
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.
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 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.