Categories

Побочные эффекты в рельсах. ActiveRecord::Base#update_attribute

Метод ActiveRecord::Base#update_attribute согласно документации предназначен для обновления отдельного атрибута и … дальше мало кто читает внимательно — сохранения без прохождения процесса валидации, что приводит к некоторым побочным эффектам (side effects), когда метод может обновлять любое кол-во атрибутов и даже сохранять ассоциации! Смотрите.

Создаем тестовый проект

$ rails side_effects_in_rails_1_active_record_update_attribute
$ cd side_effects_in_rails_1_active_record_update_attribute
$ script/generate rspec_model Post title:string body:string
$ rake db:migrate
$ rake db:test:clone
$ script/generate rspec

Напишем тест

# spec/models/post_spec.rb
describe Post, "#update_attribute" do
  before(:each) do
    @post = Post.create :title => "Old Title", :body => "Old Body"
    @post.should_not be_new_record
  end

  # строим наше предположение

  it "should update title and should NOT update body" do
    @post.body = "New Body"
    @post.update_attribute(:title, "New Title")

    @post.reload         # делаем reload, чтобы "сбросить" присвоение body

    @post.title.should == "New Title"
    @post.body.should == "Old Body"
  end
end

Запускаем

$ spec -c -fs spec/models/post_spec.rb


Post#update_attribute
- should update title and should NOT update body (FAILED - 1)

1)
'Post#update_attribute should update title and should NOT update body' FAILED
expected: "Old Body",
     got: "New Body" (using ==)
./spec/models/post_spec.rb:18:

Finished in 0.12975 seconds

Атрибут body сохранился, хотя мы этого совсем не хотели. Интересно? Идем дальше.

Сохраняем ассоциации через #update_attribute

Создадим модель Comment

$ script/generate rspec_model Comment body:string post:belongs_to
$ rake db:migrate
$ rake db:test:clone    

добавим минимальную валидацию

# app/model/comment.rb
class Comment < ActiveRecord::Base
  validates_presence_of :body
end

и в Post добавим ассоциацию comments

# app/model/post.rb
class Post < ActiveRecord::Base
  has_many :comments
end

Добавим несколько тестов

  • добавление валидного коментария
  • добавление невалидного коментария
  • добавление сразу двух валидного и невалидного комментариев

 

# spec/models/post_spec.rb

it "should update title and should NOT save a valid associated comment" do
  @post.comments.should be_blank
  # build добвляет объект в ассоциацию, но не сохраняет его
  @post.comments.build :body => "Comment"

  @post.update_attribute(:title, "New Title")

  @post.reload

  @post.title.should == "New Title"

  # вместо ...should be_blank, напишем should == [], чтобы получить @post.comments.inspect
  @post.comments.should == []
end

it "should update title and should NOT save an invalid comment" do
  @post.comments.should be_blank
  @post.comments.build :body => ""

  @post.update_attribute(:title, "New Title")

  @post.reload

  @post.title.should == "New Title"
  @post.comments.should == []
end

it "should update title and should NOT save any comment" do
  @post.comments.should be_blank
  @post.comments.build :body => ""        # invalid comment
  @post.comments.build :body => "Comment" # valid comment

  @post.update_attribute(:title, "New Title")

  @post.reload

  @post.title.should == "New Title"
  @post.comments.should == []
end

Снова запускаем

$ spec -c -fs spec/models/post_spec.rb

Post#update_attribute
- should update title and should NOT update body (FAILED - 1)
- should update title and should NOT save a valid associated comment (FAILED - 2)
- should update title and should NOT save an invalid comment
- should update title and should NOT save any comment (FAILED - 3)

1)
'Post#update_attribute should update title and should NOT update body' FAILED
expected: "Old Body",
     got: "New Body" (using ==)
./spec/models/post_spec.rb:19:

2)
'Post#update_attribute should update title and should NOT save a valid associated comment' FAILED
expected: [],
     got: [#<Comment id: 1, post_id: 1, body: "Comment", created_at: "2009-07-22 09:11:35", updated_at: "2009-07-22 09:11:35">] (using ==)
./spec/models/post_spec.rb:50:

3)
'Post#update_attribute should update title and should NOT save any comment' FAILED
expected: [],
     got: [#<Comment id: 1, post_id: 1, body: "Comment", created_at: "2009-07-22 09:11:35", updated_at: "2009-07-22 09:11:35">] (using ==)
./spec/models/post_spec.rb:75:

Finished in 0.174812 seconds

4 examples, 3 failures

Как видим — валидный коментарий сохранился, невалидный — нет, при добалении обоих в ассоциацию сохранился только 1 валидный и никакого сообщения об ошибке мы не получили. А ведь мы всего лишь хотели обновить один аттрибут!

Какой выход?

Использовать ActiveRecord::Base#update_attribute только в тех случаях, когда известно на 100%, что объект не изменяется, в других же ситуациях использовать ActiveRecord::Base::update

Напишем тест

it "should update title and should NOT update body (v2)" do
  @post.body = "New Body"

  Post.update(@post, :title => "New Title")

  @post.reload

  @post.title.should == "New Title"
  @post.body.should == "Old Body"
end

Запускаем тест

$ spec -c -fs spec/models/post_spec.rb -l 70

Post#update_attribute
- should update title and should NOT update body (v2)

Finished in 0.224997 seconds

1 example, 0 failures

Вуаля! работает (-l 70 означает запустить отдельный тест, обрамляющий 70-ю строку)

Я создал на гитхабе репозиторий, чтобы можно было пощупать пример целиком.

P.S. Проверено на rails 2.3.2 и 2.3.3

UPD. Этот вопрос уже поднимали в англоязычной среде

2 comments to Побочные эффекты в рельсах. ActiveRecord::Base#update_attribute

  • Я в свое время тоже долго ломал голову, почему же когда я делаю update_attribute для одного атрибута, у меня в БД записывались и другие атрибуты измененные через сеттеры.

    Отличная статья! Спасибо!

  • Мы в нашем проекте буквально сегодня столкнулись с подобной проблемой в update_attribute, но вроде бы даже не припомнили, что save(false) внутри него может сохранить все dirty аттрибуты в объеке. Думали, как предотвратить проблемы с этим связанные в рамках проекта на будущее. Думали или пропатчить метод и задокументировать у себя, или райзить внутри, чтобы его не использовали и написать дополнительный rake task вроде rake convention:find_update_attribute.

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>