undefined local variable or method `name'
gemp opened this issue · 21 comments
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
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
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.
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.
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
.
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
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...
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.
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.
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 😅
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.
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.
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.
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