How do I get my model to make use of my cache when saving?
Asked Answered
D

2

6

I'm using Rails 5. I have the following model

class MyObject < ActiveRecord::Base
    ...
  belongs_to :distance_unit

    ...
  def save_with_location
    transaction do
      address = LocationHelper.get_address(location) 
      if !self.address.nil? && !address.nil?
        self.address.update_attributes(address.attributes.except("id", "created_at", "updated_at")) 
      elsif !address.nil?
        address.race = self
        address.save
      end

      # Save the object
      save
    end 
  end

Through some crafty debugging, I figured out that the "save" method causes this query to be executed ...

  DistanceUnit Load (0.3ms)  SELECT  "distance_units".* FROM "distance_units" WHERE "distance_units"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
  ↳ app/models/my_object.rb:54:in `block in save_with_location'

This heppens each time the above method gets called. this is not optimal becaues I have set up my DistanceUnit model to have a cache. Below is its code. How do I get my "save" method to automatically make use of the cache instead of executing this query every time?

class DistanceUnit < ActiveRecord::Base

  def self.cached_find_by_id(id)
    Rails.cache.fetch("distanceunit-#{id}") do
      puts "looking for id: #{id}" 
      find_by_id(id)
    end
  end

  def self.cached_find_by_abbrev(abbrev)
    Rails.cache.fetch("distanceunit-#{abbrev}") do
      find_by_abbrev(abbrev)
    end
  end

  def self.cached_all()
    Rails.cache.fetch("distanceunit-all") do
      all
    end
  end

end
Doody answered 16/2, 2017 at 16:3 Comment(4)
Are you running this in development or production?Cumbersome
I ran the test above in development but it is happening in production as well.Doody
can you include the relationships for distance_unit and my_object?Cysticercus
one bug i notice is your cache-key for all will ignore all newly saved items.Cysticercus
C
3

Rails 5 makes belongs_to association required by default after this change. It means that associated record must be present in the database on save or validation will fail. There are a few possible ways to resolve your problem

1) Set distance_unit manually from cache before saving MyObject instance to prevent fetching it from the database:

  def save_with_location
      # ...

      # Save the object
      self.distance_unit = DistanceUnit.cached_find_by_id(self.distance_unit_id)
      save
    end 
  end

2) Or opt out this behaviour:

You can pass optional: true to the belongs_to association which would remove this validation check:

class MyObject < ApplicationRecord
  # ...
  belongs_to :distance_unit, optional: true
  # ...
end
Construct answered 22/2, 2017 at 7:29 Comment(0)
H
2

To be honest I think there was a bad design

For distance calculation a suggest to use another approach

Props:

  • no cache needed
  • SOLID
  • no self written logic in Models

The code for distance calculation logic is following:

require 'active_record'

ActiveRecord::Base.logger = Logger.new STDOUT

class Address < ActiveRecord::Base
  establish_connection adapter: 'sqlite3', database: 'foobar.db'
  connection.create_table table_name, force: true do |t|
    t.string  :name
    t.decimal :latitude,  precision: 15, scale: 13
    t.decimal :longitude, precision: 15, scale: 13
    t.references :addressable, polymorphic: true, index: true
  end

  belongs_to :addressable, polymorphic: true
end

class Shop < ActiveRecord::Base
  establish_connection adapter: 'sqlite3', database: 'foobar.db'
  connection.create_table table_name, force: true do |t|
    t.string  :name
  end

  has_one :address, as: :addressable
end

class House < ActiveRecord::Base
  establish_connection adapter: 'sqlite3', database: 'foobar.db'
  connection.create_table table_name, force: true do |t|
    t.string  :name
  end

  has_one :address, as: :addressable
end

class Measure
  def initialize address_from, address_to
    @address_from = address_from
    @address_to   = address_to
  end

  def distance
    # some calculation logic
    lat_delta = @address_from.latitude  - @address_to.latitude
    lon_delta = @address_from.longitude - @address_to.longitude
    lat_delta.abs + lon_delta.abs
  end

  def units
    'meters'
  end
end

shop_address  = Address.new name: 'Middleberge FL 32068', latitude: 30.11, longitude: 32.11
house_address = Address.new name: 'Tucson AZ 85705-7598', latitude: 40.12, longitude: 42.12
shop  = Shop.create!  name: 'Food cort'
house = House.create! name: 'My home'
shop.update!  address: shop_address
house.update! address: house_address
p 'Shop:',  shop
p 'House:', house
measure = Measure.new shop_address, house_address
p "Distance between #{shop.name} (#{shop.address.name}) and #{house.name} (#{house.address.name}): #{measure.distance.to_s} #{measure.units}"

You can run it with: $ ruby path_to_file.rb

And result should be following:

"Shop:"
#<Shop id: 1, name: "Food cort">
"House:"
#<House id: 1, name: "My home">
"Distance between Food cort (Middleberge FL 32068) and My home (Tucson AZ 85705-7598): 20.02 meters"
Hallel answered 26/2, 2017 at 8:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.