Memoize Planting-related methods for performance optimization

This commit introduces memoization to various methods in the Planting model,
PredictPlanting and PredictHarvest concerns, PlantingsHelper, and
PlantingsController.

Specifically:
- Memoized database-intensive lookups like `nearby_same_crop`, `first_harvest_date`,
  and `last_harvest_date`.
- Memoized calculated fields like `finish_predicted_at`, `expected_lifespan`,
  and `age_in_days`.
- Optimized `PlantingsHelper#transplantable_gardens_by_owner` using a hash
  to cache results per planting instance within a request.
- Applied the `defined?(@variable)` pattern where appropriate to ensure
  efficient handling of `nil` results.

These changes reduce redundant database queries and expensive calculations,
particularly during view rendering where these methods are frequently accessed.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
This commit is contained in:
google-labs-jules[bot]
2026-04-28 03:07:18 +00:00
parent 8cdfda3660
commit 2e0c8a910d
5 changed files with 73 additions and 46 deletions

View File

@@ -160,7 +160,7 @@ class PlantingsController < DataController
end
def matching_seeds
Seed.where(crop: @planting.crop, owner: @planting.owner)
@matching_seeds ||= Seed.where(crop: @planting.crop, owner: @planting.owner)
.where('(finished_at IS NULL OR finished_at >= ?)', @planting.planted_at)
.where('(saved_at IS NULL OR saved_at <= ?)', @planting.planted_at)
end

View File

@@ -46,9 +46,13 @@ module PlantingsHelper
# Returns a list of gardens the planting can be transplanted to
# based on the planting's owner.
def transplantable_gardens_by_owner(planting)
garden_ids = planting.owner.gardens.select(:id).to_a + GardenCollaborator.where(member_id: planting.owner.id).select(:garden_id).to_a
@transplantable_gardens ||= {}
cache_key = planting.id || planting.object_id
@transplantable_gardens[cache_key] ||= begin
garden_ids = planting.owner.gardens.select(:id).to_a + GardenCollaborator.where(member_id: planting.owner.id).select(:garden_id).to_a
Garden.active.where.not(id: planting.garden_id).where(id: garden_ids)
Garden.active.where.not(id: planting.garden_id).where(id: garden_ids)
end
end
def days_from_now_to_last_harvest(planting)

View File

@@ -6,23 +6,31 @@ module PredictHarvest
included do
# dates
def first_harvest_date
harvests_with_dates.minimum(:harvested_at)
return @first_harvest_date if defined?(@first_harvest_date)
@first_harvest_date = harvests_with_dates.minimum(:harvested_at)
end
def last_harvest_date
harvests_with_dates.maximum(:harvested_at)
return @last_harvest_date if defined?(@last_harvest_date)
@last_harvest_date = harvests_with_dates.maximum(:harvested_at)
end
def first_harvest_predicted_at
return unless crop.median_days_to_first_harvest.present? && planted_at.present?
return @first_harvest_predicted_at if defined?(@first_harvest_predicted_at)
planted_at + crop.median_days_to_first_harvest.days
@first_harvest_predicted_at = if crop.median_days_to_first_harvest.present? && planted_at.present?
planted_at + crop.median_days_to_first_harvest.days
end
end
def last_harvest_predicted_at
return unless crop.median_days_to_last_harvest.present? && planted_at.present?
return @last_harvest_predicted_at if defined?(@last_harvest_predicted_at)
planted_at + crop.median_days_to_last_harvest.days
@last_harvest_predicted_at = if crop.median_days_to_last_harvest.present? && planted_at.present?
planted_at + crop.median_days_to_last_harvest.days
end
end
# actions
@@ -65,16 +73,18 @@ module PredictHarvest
end
def neighbours_for_harvest_predictions
# use this planting's harvest if any
return harvests if harvests.size.positive?
# otherwise use nearby plantings
if location
return Harvest.where(planting: nearby_same_crop.has_harvests)
.where.not(planting_id: nil)
@neighbours_for_harvest_predictions ||= begin
# use this planting's harvest if any
if harvests.size.positive?
harvests
# otherwise use nearby plantings
elsif location
Harvest.where(planting: nearby_same_crop.has_harvests)
.where.not(planting_id: nil)
else
Harvest.none
end
end
Harvest.none
end
private

View File

@@ -13,40 +13,49 @@ module PredictPlanting
# dates
def finish_predicted_at
if planted_at.blank? || failed?
nil
elsif crop.median_lifespan.present?
planted_at + crop.median_lifespan.days
elsif crop.parent.present? && crop.parent.median_lifespan.present?
planted_at + crop.parent.median_lifespan.days
end
return @finish_predicted_at if defined?(@finish_predicted_at)
@finish_predicted_at = if planted_at.blank? || failed?
nil
elsif crop.median_lifespan.present?
planted_at + crop.median_lifespan.days
elsif crop.parent.present? && crop.parent.median_lifespan.present?
planted_at + crop.parent.median_lifespan.days
end
end
# days
def expected_lifespan
if actual_lifespan.present?
actual_lifespan
elsif crop.median_lifespan.present?
crop.median_lifespan
elsif crop.parent.present? && crop.parent.median_lifespan.present?
crop.parent.median_lifespan
end
return @expected_lifespan if defined?(@expected_lifespan)
@expected_lifespan = if actual_lifespan.present?
actual_lifespan
elsif crop.median_lifespan.present?
crop.median_lifespan
elsif crop.parent.present? && crop.parent.median_lifespan.present?
crop.parent.median_lifespan
end
end
def actual_lifespan
return unless planted_at.present? && finished_at.present? && !failed?
return @actual_lifespan if defined?(@actual_lifespan)
(finished_at - planted_at).to_i
@actual_lifespan = if planted_at.present? && finished_at.present? && !failed?
(finished_at - planted_at).to_i
end
end
def age_in_days
return if planted_at.blank?
return if failed?
return @age_in_days if defined?(@age_in_days)
known_last_day ||= finished_at || Time.zone.today
known_last_day = Time.zone.today if known_last_day > Time.zone.today
@age_in_days = if planted_at.blank? || failed?
nil
else
known_last_day = finished_at || Time.zone.today
known_last_day = Time.zone.today if known_last_day > Time.zone.today
(known_last_day - planted_at).to_i
(known_last_day - planted_at).to_i
end
end
def percentage_grown

View File

@@ -119,14 +119,18 @@ class Planting < ApplicationRecord
end
def nearby_same_crop
return Planting.none if location.blank? || latitude.blank? || longitude.blank?
return @nearby_same_crop if defined?(@nearby_same_crop)
# latitude, longitude = Geocoder.coordinates(location, params: { limit: 1 })
Planting.joins(:garden)
.where(crop:)
.located
.where('gardens.latitude < ? AND gardens.latitude > ?',
latitude + 10, latitude - 10)
@nearby_same_crop = if location.blank? || latitude.blank? || longitude.blank?
Planting.none
else
# latitude, longitude = Geocoder.coordinates(location, params: { limit: 1 })
Planting.joins(:garden)
.where(crop:)
.located
.where('gardens.latitude < ? AND gardens.latitude > ?',
latitude + 10, latitude - 10)
end
end
private