Count on third level associated model - ruby-on-rails-4

I have the following association:
Reservation
- has_many reservation_occupations
ReservationOccupations
- has_many reservation_occupants
ReservationOccupants
I want to do the following queries:
1 - Get the number of occupants for one reservation
2 - Get the number of occupants for a group of reservations (Reservations.all for example)
Thanks in advance!

1 - Get the number of occupants for one reservation
First, add a has_many :through association from Reservation to ReservationOccupant:
class Reservation < ActiveRecord::Base
has_many :reservation_occupations
has_many :reservation_occupants, through: :reservation_occupations
end
Now you can simply do
reservation = Reservation.first
reservation.reservation_occupants.count
2 - Get the number of occupants for a group of reservations
First, add some more associations:
class ReservationOccupant < ActiveRecord::Base
belongs_to :reservation_occupation
has_one :reservation, through: :reservation_occupation
end
and
class ReservationOccupation < ActiveRecord::Base
belongs_to :reservation
# ...
end
Then, to count the number of occupants for a group of reservations, you can add to your Reservation class the following method:
class Reservation < ActiveRecord::Base
# ...
def self.num_occupants(reservations)
ReservationOccupant
.joins(:reservation_occupation)
.joins(:reservation)
.where("reservations.id": reservations)
.count
end
end
It's worth noting that this num_occupants method works regardless of whether reservations is a collection of reservations or a single reservation. In other words, this method could be used for both of your questions, #1 and #2. However, the first method generates a more efficient SQL query, and is arguably a little clearer, so I'd personally use that when finding the number of occupants for a single reservation.

Related

Active Record callbacks with has_many association

I want to run a before_save or after_add callback every time I add a child object to a parent object (has_many association). In the callback I want to set the end_date property on the parent (cohort) based on the end_date properties of all the children (courses).
class Cohort < ActiveRecord::Base
has_many :courses
before_save :update_end_date
def update_end_date
self.end_date = courses.order(:end_date).last.try(:end_date)
end
end
The problem I'm experiencing is that the courses are not yet persisted to the database in the before_save callback, so courses.order(:end_date) does not return the newly added course(s).
There are several workarounds I could use (e.g. using the Ruby courses.sort_by method or using after_save with update), but my impression is that using the Active Record order method, if possible, would be preferable in terms of efficiency and best practice. Is there a way to do this with Active Record in before_save, or what might be best practice for this? It seems like something that would come up a lot, but I'm having trouble finding solutions that work for me, so I feel like I must be thinking about it wrong. Thanks!
You could do an after save on the courses that may update the cohort if their end date is later than the cohorts end date. and an after destroy on the course, that tells the cohort to update its end date to correspond to the remaining courses.
class Course < ActiveRecord::Base
belongs_to :cohort
after_save :maybe_update_cohort_end_date
after_destroy :update_cohort_end_date
def maybe_update_cohort_end_date
if cohort && self.end_date > cohort.end_date
cohort.end_date = self.end_date
cohort.save
end
end
def update_cohort_end_date
cohort.update_end_date if cohort
end
end
class Cohort < ActiveRecord::Base
has_many :courses
def update_end_date
self.end_date = courses.order(:end_date).last.try(:end_date)
end
end
This way you only make an update if the new or updated course's end date would change the the cohorts end date. But also catches if a course is removed then check what the end date should be

How to restrict a has_many relationship to only 1 for a certain sub-type

I have a few models that have a has_many through relationship.
class Participant < ActiveRecord::Base
has_many :participant_scores
has_many :groups, through: :participant_scores
end
class Group < ActiveRecord::Base
has_many :participant_scores
has_many :participants, through: :participant_scores
end
class ParticipantScore < ActiveRecord::Base
belongs_to :group
belongs_to :participant
end
I have the need to have different types of groups, of which a participant can only be a member of 1 of that type.
For example, in a company there multiple departments and locations, but a person can only be associated with one department, and one location. On the other hand, there are generic groupings, that do not have a restriction, for example, there can be many social clubs, and a person could be a member of any number of them.
The groupings are all the same behaviorally, with the only difference being that some have the restriction around being only a member of 1.
I've contemplated using single table inheritance, but I've not figured out how to restrict it to only 1 association.
Parhaps add a type column, and then restrict with validations?
Use a scope?
None of those seem optimal, how can I best accomplish this?
You should use a validation with scope
http://guides.rubyonrails.org/active_record_validations.html#uniqueness

rails 4 eager loading - order nested associations

I have 4 models
class A < ActiveRecord::Base
has_many :Bs
end
class B < ActiveRecord::Base
belongs_to :A
has_many :Cs
has_many :Ds
end
class C < ActiveRecord::Base
belongs_to :B
end
class D < ActiveRecord::Base
belongs_to :B
end
I need to eager load A, and get all nested associations. So from A:
eager_load(Bs: [:Cs, :Ds]).where('id=?',id).to_a
Now I want to order B records through the eager loading. I tried to add the order through the association like this:
class A < ActiveRecord::Base
has_many :Bs, -> { order("id desc") }
end
But it doesn't work. When I do something like
A.first.bs
B records are not ordered by id desc.
Could you please advice on this one?
Thanks.
I haven't tested this, but you could do this to order the Bs by id or any other field you like:
eager_load(Bs: [:Cs, :Ds]).where('id=?',id).order('Bs.id DESC')
The other option would be to setup a default scope on the model, like so:
def self.default_scope
order(id: :desc)
end

Retargeting the foreign key in Rails

tldr;
In Rails, how do you set the target column of a foreign key to be a column other than its parent’s id?
Efforts so far
This feels like it should be a simple operation, but I’m having little success. I have a parent model, Order, which has many OrderItems, but I want the foreign key of OrderItems to reference a composite of Order’s reference1 and reference2 fields.
I’ve looked at a few paths:
First try
class Order < ActiveRecord::Base
has_many :order_items, foreign_key: :order_reference,
primary_key: :unique_reference
inverse_of: :order
validates :reference1, uniqueness: { scope: :reference2 }
end
class OrderItem < ActiveRecord::Base
belongs_to :order, foreign_key: unique_reference,
primary_key: order_reference
inverse_of: :order_item
end
(Where I created a redundant-feeling unique_reference column, that we populated before creation with reference1+reference2, and gave OrderItem a corresponding order_reference column)
I tried a few variants of the above, but couldn’t persuade the OrderItem to accept unique_reference as the key. It managed to link records, but then when I called OrderItem#order_reference, instead of contents matching the corresponding Order#unique_reference field it would return a stringified version of its parent’s id.
Second try
I removed the unique_reference column from the Order class, replacing it with a method of the same name and a has_many block:
class Order
has_many :order_items, -> (order) { where("order_items.order_reference = :unique_reference", unique_reference: order.unique_reference) }
def unique_reference
"#{reference1}#{reference2}"
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order, ->(item){ where("CONCAT(surfdome_archived_order.source, surfdome_archived_order.order_number) = surfdome_archived_order_items.archived_order_reference") }
end
This time, calling Order#order_items raises a SQL error:
Unknown column 'order_items.order_id' in 'where clause': SELECT order_items.* FROM order_items WHERE order_items.order_id = 1 AND (order_items.order_reference = 'ref1ref2')
Every SQL query I’ve thought to try has the same underlying problem - somewhere, Rails decides we’re still implicitly trying to key by order_id, and I can’t find a way to persuade it otherwise.
Other options
At the moment my options seem to be to use the Composite Primary Keys gem or just ignoring Rails' built-in associations and hacking our own with db queries, but neither seems ideal - and it seems like Rails would surely have an option to switch foreign keys. But if so, where is it?
Thanks,
Sasha

Get attributes of children in a recursive tree structure unless it's a leaf node

I have a recursive tree structure for handling my categories. Each leaf category can have zero or more deals. The categories is defined by
class Category < ActiveRecord::Base
has_many :sub_categories, class_name: "Category", foreign_key: "parent_category_id"
belongs_to :parent_category, class_name: "Category"
has_many :deal_categories
has_many :deals, through: :deal_categories
def leaf?
!has_sub_categories?
end
def has_sub_categories?
!sub_categories.empty?
end
end
Deals and DealCategories looks like follows:
class Deal < ActiveRecord::Base
has_many :deal_categories
has_many :categories, through: :deal_categories
end
class DealCategory < ActiveRecord::Base
belongs_to :deal
belongs_to :category
end
There are also some validations making sure that Deals only can exist as leaf categories. Thus, if I call category.deals on a leaf node I get some deals, and if I call it on a root node I get an empty result. All good.
But now I want category.deals to return the deals of it's children if it's not a root node. My approach has been to override the following method in my Category class as follows:
alias_method :original_deals, :deals
def deals
if leaf?
self.original_deals
else
self.sub_categories.deals
end
end
This however does not work as I can't call deals directly on sub_categories, the error being
undefined method `deals' for #<Category::ActiveRecord_Associations_CollectionProxy:0x00000009243d40>
How do I solve this?
You can't call deals on sub_categories because it is not a category...it is a collection of categories. Instead you could do something like
sub_categories.reduce([]) { |union, sub_category| union + sub_category.deals }
Using reduce creates a memo object (represented by the union variable) and the evaluation of the block becomes the new memo object. I am starting with an empty array and add in the result of calling deals on each sub_category in your sub_categories collection.