22

Après avoir étudié DHH et d'autres articles de blog sur l'expiration du cache basée sur les clés et la mise en cache de poupées russes, je ne sais toujours pas comment gérer un type de relation. Pour être précis, une relation has_many. Je vais partager les résultats de mes recherches sur un exemple d'application. C'est un peu l'histoire, alors attendez. Disons que nous avons les modèles ActiveRecord suivants. Tout ce qui nous intéresse est un changement approprié de la cache_key du modèle, non?Structuration d'une application Rails pour Russian Doll Caching avec une relation has_many

class Article < ActiveRecord::Base 
    attr_accessible :author_id, :body, :title 
    has_many :comments 
    belongs_to :author 
end 

class Comment < ActiveRecord::Base 
    attr_accessible :article_id, :author_id, :body 
    belongs_to :author 
    belongs_to :article, touch: true 
end 

class Author < ActiveRecord::Base 
attr_accessible :name 
    has_many :articles 
    has_many :comments 
end 

Nous avons déjà un article, avec un commentaire. Tous deux par un auteur différent. L'objectif est d'avoir un changement dans le cache_key de l'article dans les cas suivants:

  1. corps de l'article ou titre change
  2. Son corps de commentaire change
  3. article nom de de l'auteur change
  4. article le nom de de commentaire auteur changements

donc, par défaut, nous sommes bons pour le cas 1 et 2.

1.9.3-p194 :034 > article.cache_key 
=> "articles/1-20130412185804" 
1.9.3-p194 :035 > article.comments.first.update_attribute('body', 'First Post!') 
1.9.3-p194 :038 > article.cache_key 
=> "articles/1-20130412185913" 

Mais pas pour le cas 3.

1.9.3-p194 :040 > article.author.update_attribute('name', 'Adam A.') 
1.9.3-p194 :041 > article.cache_key 
=> "articles/1-20130412185913" 

Définissons une méthode cache_key composite pour Article.

class Article < ActiveRecord::Base 
    attr_accessible :author_id, :body, :title 
    has_many :comments 
    belongs_to :author 

    def cache_key 
    [super, author.cache_key].join('/') 
    end 
end 

1.9.3-p194 :007 > article.cache_key 
=> "articles/1-20130412185913/authors/1-20130412190438" 
1.9.3-p194 :008 > article.author.update_attribute('name', 'Adam B.') 
1.9.3-p194 :009 > article.cache_key 
=> "articles/1-20130412185913/authors/1-20130412190849" 

Win! Mais bien sûr, cela ne fonctionne pas pour le cas 4.

1.9.3-p194 :012 > article.comments.first.author.update_attribute('name', 'Bernard A.') 
1.9.3-p194 :013 > article.cache_key 
=> "articles/1-20130412185913/authors/1-20130412190849" 

Alors quelles sont les options à gauche? Nous pourrions faire quelque chose avec l'association has_many sur Author, mais has_many ne prend pas l'option {touch: true}, et probablement pour une raison. Je suppose que cela pourrait être mis en œuvre de la façon suivante.

class Author < ActiveRecord::Base 
    attr_accessible :name 
    has_many :articles 
    has_many :comments 

    before_save do 
    articles.each { |record| record.touch } 
    comments.each { |record| record.touch } 
    end 
end 

article.comments.first.author.update_attribute('name', 'Bernard B.') 
article.cache_key 
    => "articles/1-20130412192036" 

Bien que cela ne fonctionne pas. Il a un énorme impact sur les performances, en chargeant, instanciant et mettant à jour chaque article et en faisant des commentaires par cet autre, un par un. Je ne crois pas que ce soit une bonne solution, mais qu'est-ce que c'est?

Bien sûr, le cas d'utilisation de 37 signaux/exemple peut être différent: project -> todolist -> todo. Mais j'imagine un seul objet todo appartenant également à un utilisateur.

Comment pourrait-on résoudre ce problème de mise en cache?

+0

Cela ne répond pas à la question de la mise en cache spécifique, mais la mise en œuvre tactile ne doit pas être si intense. Vous pourriez simplement faire quelque chose comme 'articles.update_all (updated_at: Time.now)', ce qui donnerait un op pour les articles (et un pour les commentaires). – numbers1311407

+0

@ numbers1311407 Le problème avec cette méthode est que 'update_all' n'exécute que SQL, aucun callback n'est effectué, donc les' touch'es suivantes ne se produiront pas, et 'cache_key's dans les objets mémoire ne sont pas régénérés. –

+0

Bien sûr, mais la mise à jour automatique des objets en mémoire est-elle vraiment votre préoccupation? C'est un problème dans la console rails, mais généralement pas dans le monde réel où l'auteur change son nom en une seule action, alors lui et d'autres sont en train de rendre des vues de commentaires dans un autre. Si vous vous trouvez dans une situation où vous avez besoin d'actualiser la clé de cache sur un objet en mémoire, rechargez-la simplement. – numbers1311407

Répondre

8

Une méthode que j'ai trébuché sur serait de gérer cela via les clés de cache.Ajouter une has_many_through relation commenters à l'article:

class Article < ActiveRecord::Base 
    attr_accessible :author_id, :body, :title 
    has_many :comments 
    has_many :commenters, through: :comments, source: :author 
    belongs_to :author 
end 

Puis à l'article/spectacle que nous construisions la clé de cache comme ceci:

<% cache [@article, @article.commenters, @article.author] do %> 
    <h2><%= @article.title %></h2> 
    <p>Posted By: <%= @article.author.name %></p> 
    <p><%= @article.body %></p> 
    <ul><%= render @article.comments %></ul> 
<% end %> 

L'astuce est que la clé de cache générée à partir de l'association commenters changera chaque fois qu'un commentaire est ajouté, supprimé ou mis à jour. Bien que cela nécessite des requêtes SQL supplémentaires pour générer la clé de cache, il fonctionne bien avec le low level caching de Rails et l'ajout de quelque chose comme le identity_cache gem peut facilement aider avec cela.

Je voudrais voir si d'autres personnes ont des solutions plus propres à cela.

+0

Cette technique fonctionne bien pour l'action show d'un seul enregistrement, mais elle n'est pas viable pour le cache de poupée russe (externe) de haut niveau pour une action index de plusieurs enregistrements - où le cache de chaque enregistrement a le problème décrit ici –

+0

Vous pouvez utiliser une aide de clé de cache personnalisée dans ce cas, détaillée ici dans les guides Rails: http://edgeguides.rubyonrails.org/caching_with_rails.html#highlighter_32543 –

+0

Roger que - comme le message original fait avec 'def cache_key [super , author.cache_key] .join ('/') end' –

0

Comme indiqué ici https://rails.lighthouseapp.com/projects/8994/tickets/4392-add-touch-option-to-has_many-associations, dans mon cas je viens de créer un rappel after_save pour mettre à jour les horodatages des objets connexes.

def touch_line_items_and_tactics 
    self.line_item_advertisements.all.map(&:touch) 
    end 

Un côté, nous avons construit notre application de rails sur une base de données existante qui a last_modified_time comme nom de colonne et il était la sémantique « lorsque l'utilisateur dernier a modifié ». Donc, à cause de la sémantique différente, nous ne pouvions pas utiliser l'option :touch sortie de la boîte. J'ai dû sélectionner la clé cache_key et toucher les méthodes comme ceci https://gist.github.com/tispratik/9276110 afin de stocker l'horodatage mis à jour dans memcached au lieu de la colonne updated_at de la base de données.

Notez également que je ne pouvais pas utiliser le cache_timestamp_format par défaut de Rails car il fournissait des horodatages seulement jusqu'à quelques secondes. J'ai ressenti le besoin d'avoir un timestamp plus granulaire donc j'ai choisi: nsec (nanosecondes).

Horodatage avec cache_timestamp_format: 20140227181414
Horodatage avec nanosecondes: 20140227181414671756000

Questions connexes