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
Related
I currently have the following models: MinorCategory > Product > Review
On a view, I show the 12 MinorCategories that have the most reviews. This view is very slow to respond, and I think it is a problem with how I do the query.
Here is my current code:
class MinorCategory < ActiveRecord::Base
has_many :products
has_many :reviews, through: :products
...
def count_reviews
self.reviews.count
end
...
end
class Review < ActiveRecord::Base
belongs_to :product, touch: true
...
end
class HomeController < ApplicationController
#categories = MinorCategory.all.sort_by(&:count_reviews).reverse.take(12)
end
So that is basically it. In the view itself I go through each #categories and display a few things, but the query in the controller is what seems to be slow. From SkyLight:
SELECT COUNT(*) FROM "reviews" INNER JOIN "products" ON "reviews"."product_id" = "products"."id" WHERE "products"."minor_category_id" = ? ... avg 472ms
I am not good with sql or active record, and still pretty new to Ruby on Rails. I've spent a couple hours trying other methods, but I can not get them to work so I thought I would check here.
Thank you in advance to anybody that has a moment.
You need some basic SQL knowledge to better understand how database queries work, and how to take advantage of a DBMS. Using ActiveRecord is not an excuse to not learn some SQL.
That said, your query is very inefficient because you don't use the power of the database at all. It's a waste of resources both on the Ruby environment and on the database environment.
The only database query is
MinorCategory.all
which extracts all the records. This is insanely expensive, especially if you have a large number of categories.
Moreover, self.reviews.count is largely inefficient because it is affected by the N+1 query issue.
Last but not least, the sorting and limiting is made in the Ruby environment, whereas you should really do it in the database.
You can easily obtain a more efficient query by taking advantage of the database computation capabilities. You will need to join the two tables together. The query should look like:
SELECT
minor_categories.*, COUNT(reviews.id) AS reviews_count
FROM
"minor_categories" INNER JOIN "reviews" ON "reviews"."minor_category_id" = "minor_categories"."id"
GROUP BY
minor_categories.id
ORDER BY
reviews_count DESC
LIMIT 10
which in ActiveRecord translates as
categories = MinorCategory.select('minor_categories.*, COUNT(reviews.id) AS reviews_count').joins(:reviews).order('reviews_count DESC').group('minor_categories.id').limit(10)
You can access a single category count by using reviews_count
# take a category
category = categories[0]
category.reviews_count
Another approach that doesn't require a JOIN would be to cache the counter in the category table.
I have an invoicing rails app and I am trying to add a column that is called invoice_number and the table has a primary key of invoice_id I want invoice_number to be set on save and increment by 1
So every user should have invoices where the invoice_number starts from 1
The number I want to add is only for auditing purposes and should be private.
I wanted to know what the best way to do something like this would be
after_save :increment_invoice_number
private
def increment_invoice_number
self.invoice_number = current_user.invoice.count + 1
end
I know this block wont work but its kind of what im trying to do.
Does anyone know of a way I can achieve this?
The following should work... it assumes that you never destroy invoices (otherwise you may end up with duplicate invoice numbers)
before_save :increment_invoice_number
private
def increment_invoice_number
return if persisted?
self.invoice_number = user.invoices.count + 1
end
Note the return if persisted? which means you only set the invoice number for a new record, not for an existing one.
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.
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
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.