Reputation: 1039
In Rails 6.1, assuming two models:
PeriodicJob: has_many :executions
Execution
with a field state
that is either succeeded
or failed
I want to run the query:
Give me all
PeriodicJobs
which last (highest ID) execution has statesucceed
.
A potential solution would be raw SQL with a subquery, as pointed out in this other Stackoverflow question: Ransack searching for instances with specific value in last of has_many associations
However, this seems overly complicated code for such a simple question in English. Given the power of Rails, I'd have expected to see something like:
PeriodicJob.joins(:executions).where(cool_trick_im_yet_unaware_of_to_get_last_ordered_by_id_execution: { state: 'succeeded' }
Does such a thing exist in Rails and how would it be applied to this example?
Upvotes: 0
Views: 254
Reputation: 1722
If you are using regular numeric ids, you can add a condition to the relationship. This will set a custom relationship on the model that will return the latest execution that is succeeded. THe has one macro will limit the 'collection' to one.
has_one :current_succeeded_execution, -> { where(state: "succeeded").reorder(id: :desc) }, class_name: 'Execution'
If you're using uuid, I would have set a default scope on those mdoels to order by created at asc to ensure the first one is always the oldest, ie the first one created. So then you could reorder to desc to get latest one
has_one :current_succeeded_execution, -> { where(state: "succeeded").reorder(created_at: :desc) }, class_name: 'Execution'
Upvotes: 0
Reputation: 102001
One way of optimizing this for reading would be to setup a separate foreign key column and association as a "shortcut" to the latest execution:
class AddLatestExecutionToProducts < ActiveRecord::Migration[6.0]
def change
add_reference :latest_execution, :execution
end
end
class PeriodicJob < ApplicationRecord
has_many :executions,
after_add: :set_latest_execution
belongs_to :latest_execution,
optional: true,
class_name: 'Execution'
private
def set_latest_execution(execution)
update_attribute(:latest_execution_id, execution.id)
end
end
This lets you do PeriodicJob.eager_load(:latest_execution)
and avoid both a N+1 query and loading all the records off the executions table. This is especially important if you have a lot of executions per peroidic job.
The cost is that it requires an extra write query every time an execution is created.
If you want to limit this to just the latest success/failure you could add two columns:
class AddLatestExecutionToProducts < ActiveRecord::Migration[6.0]
def change
add_reference :latest_successful_execution, :execution
add_reference :latest_failed_execution, :execution
end
end
class Execution < ApplicationRecord
enum state: {
succeeded: 'succeeded',
failed: 'failed'
}
end
class PeriodicJob < ApplicationRecord
has_many :executions,
after_add: :set_latest_execution
belongs_to :latest_successful_execution,
optional: true,
class_name: 'Execution'
belongs_to :latest_failed_execution,
optional: true,
class_name: 'Execution'
private
def set_latest_execution(execution)
if execution.succeeded?
update_attribute(:latest_successful_execution_id, execution.id)
else
update_attribute(:latest_failed_execution_id, execution.id)
end
end
end
Upvotes: 1