malomalo / activerecord-cached_at

Allows ActiveRecord and Rails to use a `cached_at` column for the `cache_key` if available

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

undefined local variable or method `name'

gemp opened this issue · comments

commented

After implementing activerecord-chached_at in the simplest manner possible (migration adding the column to all concerned tables and changing touch: true to cached_at: true everywhere) I have this error whenever I try to create or update anything:

undefined local variable or method `name' for #<ActiveRecord::Associations::BelongsToAssociation:0x00007fd8774a0b30>

What did I miss?

EDIT: The backtrace log: https://gist.github.com/gemp/b12499558353e5a18d82442dfc9c2956

commented

Ah, with the backtrace the error is more explicit I guess.
I rarely use inverse_of as it works without it. I'll try to add it if it's really necessary, tho it would be helpful to know where.

if !options[:inverse_of]
  puts "WARNING: cannot updated cached at for relationship: #{owner.class.name}.#{name}, inverse_of not set"
  return
end
commented

Well I added it for comments which have simple belongings:

class Comment < ApplicationRecord
  belongs_to :user, inverse_of: :comments, cached_at: true
  belongs_to :movie, inverse_of: :comments, cached_at: true
  ...

class User < ApplicationRecord
  has_many :comments, inverse_of: :user, dependent: :destroy
  ...

class Movie < ApplicationRecord
  has_many :comments, inverse_of: :movie, dependent: :destroy
  ...

Now I have:

   (1.4ms)  BEGIN
  Comment Create (6.6ms)  INSERT INTO "comments" ("user_id", "movie_id", "score", "created_at", "updated_at", "selections", "cached_at") VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING "id"  [["user_id", 1], ["movie_id", 5064], ["score", 2], ["created_at", "2020-07-22 11:48:57.277731"], ["updated_at", "2020-07-22 11:48:57.277731"], ["selections", "{\"selection\":[\"\"]}"], ["cached_at", "2020-07-22 11:48:57.277545"]]
  User Update All (1.4ms)  UPDATE "users" SET "comments_cached_at" = '2020-07-22 11:48:57.277545' WHERE "users"."id" = $1  [["id", 1]]
   (8.3ms)  ROLLBACK
Completed 500 Internal Server Error in 81ms (ActiveRecord: 44.2ms)

ActiveRecord::StatementInvalid (PG::UndefinedColumn: ERROR:  column "comments_cached_at" of relation "users" does not exist
LINE 1: UPDATE "users" SET "comments_cached_at" = '2020-07-22 11:48:...
...

Apart from this it's pretty redundant to add inverse_of in my case. As the Bi-directional associations chapter mentions:

By default, Active Record can guess the inverse of the association based on the name of the class.

In your example did you add a comments_cached_at in your migration? For every relation where you do cached_at: true you need the column on the opposing model (similar to counter caches).

After you add the column in your example you should be able to do:

cache user.cache_key_with_version(:comments) do
...
end

That tells Rails to use the max timestamp from users.cached_at and users.comments_cached_at, so when a comment is added for that user it will blow that cache.

By default it just uses users.cached_at, since it's difficult if not impossible to tell what relations are used in the template.

If I remember corrctly you should only need to add the inverse_of on one of the relations not both sides. Rails added the guessing of the inverse after I built this, but it doesn't seem to be exposed in the same location, I will investigate.

commented

My migration looks like this:

class AddColumnCachedAtToManyTables < ActiveRecord::Migration[5.2]
  def change
    add_column :comments, :cached_at, :datetime, default: Time.current
    add_column :movies,   :cached_at, :datetime, default: Time.current
    add_column :users,    :cached_at, :datetime, default: Time.current
    ...
  end
end

It didn't work at first with only one inverse_of in the User and Movie models, with the same error described in the first post. I had to add it to Comment also and it provoked the PG::UndefinedColumn: ERROR: column "comments_cached_at" error.

As for the use of cache it wasn't yet implemented anywhere in Comments views...

To be more specific:

class Comment < ApplicationRecord
  belongs_to :user, inverse_of: :comments, cached_at: true # needs a `comments_cached_at` column on `users`
  belongs_to :movie, inverse_of: :comments, cached_at: true # needs a `comments_cached_at` column on `movies`
  ...

This is a good use of cached_at.

Note that sometimes cached_at can become expensive.

  • Example 1: if a user has a million comments when the user is updated it will trigger a million comment updates if caching the comment -> user relation (comments.user_cached_at)

  • Example 2: If the caches are short lived; ie Loading 1000 users and rendering, if a 300 are misses this will cause an additional 300 queries, in which case using preload would be better for performance. There might be a way to tell automatically if a certain % are misses to do the preload, I may look into that down the road.

So your migration is just adding the cached_at for your tables, if you are just wanting to use the cached_at to not use updated_at you do not need to configure your relations.

commented

I see. And that's only what I want, not using updated_at.

But without relations configuration, I had the initial error about not finding a variable or method name and consequently the if !options[:inverse_of] error therefore I tried with inverse_of.

commented

And again there is not yet any cache implemented in Comments views. I was just trying to make the cached_at column work first before doing some actual caching.

Ah okay you just installed it and got the relation error, let me look that code should not be trigged

I think you put a cached_at: true, because that method returns if cached_at is not set. To just use cached_at all you should need to do is make the migration and include the gem in your gemfile

commented

I followed your manual ^^

class Photo
  belongs_to :user, cached_at: true
end

I'll try without the option, but I'd like the relations taken into account...

commented

Ok, that works without the cached_at: true option, but, evidently, it doesn't trigger the related User and Movie cached_at update... and when I add the option I get the initial error.

commented

The thing I do is pretty similar to your users' photos example, therefore it should be working:

class User < ApplicationRecord
  has_many :comments, dependent: :destroy
  ...

class Comment < ApplicationRecord
  belongs_to :user, cached_at: true
  ...

The only difference is that you use ActiveRecord::Base when I use ApplicationRecord (that's what Rails gives me when I generate models). I'm not good enough to know the difference between the two.

Ah so thats under Relationship Cache Keys, I should specify it's not necessary.

CachedAt won't automatically update the cached_at on relations, that why you need to specify the cached_at: true. But it needs columns added on the relationship model. So with your example:

class Comment < ApplicationRecord
  belongs_to :user
end

class User < ApplicationRecord
  has_many :comments
end

Lets say you have a views like this:

View A:

<% cache @user do %>
  <%= @user.name %>
<% end %>

View B:

<% cache @user do %>
  <%= @user.name %>
  <%= render partial: 'comments/comment', collection: @user.comments, cached: true %>
<% end %>

This would require everytime a user or that users comment is created/updated users.cached_at to be updated. Normally I think in Rails you make your own callback to touch that column and it'll work.

With CachedAt instead of writing the callback yourself you can add a users.comments_updated_at and update the comment:

class Comment < ApplicationRecord
  belongs_to :user, inverse_of: :comments, cached_at: true
end

Now when a user is updated users.comments_cached_at is touched. This will allow View A's cache to still be valid since it's still valid as only the relation is updated. We would then update view b to the following:

<% cache @user.cache_key_with_version(:comments) do %>
  <%= @user.name %>
  <%= render partial: 'comments/comment', collection: @user.comments %>
<% end %>

Now that views cache will be invalidated when users.cached_at or users.comments_cached_at is updated.

Oh and there is no difference between ActiveRecord::Base and ApplicationRecord, most of my stuff was written before ApplicationRecord came into existence.

commented

Normally I think in Rails you make your own callback to touch that column and it'll work.

Well it worked with adding the option touch: true to Comment, but on the udpated_at column of course. I thought cached_at would behave the same... I'll add a manual callback then.

I cannot have comments_cached_at like columns, especially on the User which has 12 has_many... that would mean 12 more columns 😅

commented

From the manual:

:touch
If true, the associated object will be touched (the updated_at/on attributes set to current time) when this record is either saved or destroyed. If you specify a symbol, that attribute will be updated with the current time in addition to the updated_at/on attribute.

commented

Oh, you linked the option touch with cached_at... If I use touch: true it produce the same error.

It could have behaved like touch but on the cached_at column, that would have been convenient.

I'll do some callbacks then. Thanks for your explanations and your help.

Oh touch didn't even enter my mine, I think it should work as you are expecting, I will make some test cases and update.

commented

In fact, I thought your gem was more like this one which would be exactly what I want: https://github.com/delwyn/cached_at which uses touch but update a cached_at column instead of updated_at. Unfortunately, it doesn't seem to be maintained any more.

EDIT: Seems to be working, still ^^
EDIT: Nope, finally. Sad.

But yours is waaay more complex and does a lot more.

commented

Callbacks rule. Don't bother the stuff with touch it's probably useless.

Thanks again for your time! I'll try not to bother you anytime soon ☺️