Непоследовательно обновляются ассоциации Rails ActiveRecord

Я сталкиваюсь с некоторым поведением ActiveRecord Rails 2.3.5, которое я не понимаю. Похоже, что идентификаторы ассоциации объекта могут обновляться непоследовательно.

Лучше всего это пояснить на примере:

Создайте модель Post со строковым атрибутом 'title' и модель Comment со строковым атрибутом 'content'.

Вот ассоциации:

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

Сценарий № 1. В следующем коде я создаю один Post со связанным Comment, создаю второй Post, find объединяя первый, добавляю второй Comment к первому Post и обнаруживаю, что второй Post имеет связанный с ним второй Comment без явное присвоение.

post1 = Post.new
post1 = Post.new(:title => 'Post 1')
comment1 = Comment.new(:content => 'content 1')
post1.comments << comment1
post1.save
# Create a second Post object by find'ing the first
post2 = Post.find_by_title('Post 1')
# Add a new Comment to the first Post object
comment2 = Comment.new(:content => 'content 2')
post1.comments << comment2 
# Note that both Comments are associated with both Post objects even
# though I never explicitly associated it with post2.
post1.comment_ids # => [12, 13]
post2.comment_ids # => [12, 13]

Сценарий № 2. Запустите приведенные выше команды еще раз, но на этот раз вставьте одну дополнительную команду, которая, на первый взгляд, не должна влиять на результаты. Дополнительная команда post2.comments выполняется после создания comment2 и перед добавления comment2 к post1.

post1 = Post.new
post1 = Post.new(:title => 'Post 1A')
comment1 = Comment.new(:content => 'content 1A')
post1.comments << comment1
post1.save
# Create a second Post object by find'ing the first
post2 = Post.find_by_title('Post 1A')
# Add a new Comment to the first Post object
comment2 = Comment.new(:content => 'content 2A')
post2.comments # !! THIS IS THE EXTRA COMMAND !!
post1.comments << comment2 
# Note that both Comments are associated with both Post objects even
# though I never explicitly associated it with post2.
post1.comment_ids # => [14, 15]
post2.comment_ids # => [14]

Обратите внимание, что в этом сценарии с post2 связан только один комментарий, тогда как в сценарии 1 их было два.

Большой вопрос: почему запуск post2.comments перед добавлением нового Comment в post1 имеет какое-то значение для того, какие комментарии были связаны с post2?


person rlandster    schedule 12.02.2010    source источник


Ответы (1)


Это связано с тем, как Active Record кэширует запросы и как обрабатываются ассоциации has_many.

Если ассоциация не загружена опцией :include во время поиска. Rails не будет заполнять ассоциацию для найденных записей до тех пор, пока это не потребуется. Когда ассоциация необходима, выполняется некоторая запоминание, чтобы сократить количество выполняемых SQL-запросов.

Прохождение кода в вопросе:

post1 = Post.new(:title => 'Post 1')
comment1 = Comment.new(:content => 'content 1')
post1.comments << comment1  # updates post1's internal comments cache
post1.save 

# Create a second Post object by find'ing the first
post2 = Post.find_by_title('Post 1') 

# Add a new Comment to the first Post object
comment2 = Comment.new(:content => 'content 2')
post1.comments << comment2   # updates post1's internal comments cache

# Note that both Comments are associated with both Post objects even
# though I never explicitly associated it with post2.
post1.comment_ids # => [12, 13]

# this is the first time post2.comments are loaded. 
# SELECT comments.* FROM comments JOIN comments.post_id = posts.id WHERE posts.id = #{post2.id}
post2.comment_ids # => [12, 13]

Сценарий 2:

post1 = Post.new(:title => 'Post 1A')
comment1 = Comment.new(:content => 'content 1A')
post1.comments << comment1
post1.save

# Create a second Post object by find'ing the first
post2 = Post.find_by_title('Post 1A')

# Add a new Comment to the first Post object
comment2 = Comment.new(:content => 'content 2A')

# first time post2.comments are loaded. 
# SELECT comments.* FROM comments JOIN comments.post_id = posts.id WHERE 
#   posts.id = post2.comments #=> Returns one comment (id = 14)
# cached internally.

post1.comments << comment2 
# Note that both Comments are associated with both Post objects even
# though I never explicitly associated it with post2.
post1.comment_ids # => [14, 15]

# post2.comment has already been cached, so the SQL query is not executed again.

post2.comment_ids # => [14]

Н.Б. post2.comment_ids внутренне определяется как post2.comments.map(&:id)

P.S. Мой ответ на этот вопрос может помочь вам понять, почему post2 обновляется, несмотря на то, что вы его не трогаете.

person EmFi    schedule 13.02.2010
comment
Спасибо за ответ. Но не кажется ли такое поведение неправильным? Кэширование должно повышать производительность, а не вызывать несогласованность результатов. - person rlandster; 14.02.2010
comment
Это больше проблема параллелизма. Rails не ожидает, что внешний источник изменит что-то, связанное с экземпляром модели. Внешний источник в этом случае означает любые действия, происходящие откуда-то, кроме конкретного экземпляра. Если вы твердо уверены, что это неправильно, отправьте отчет об ошибке. - person EmFi; 14.02.2010