Невозможно получить доступ к измененному экземпляру объекта ActiveRecord при вызове метода во время транзакции - PullRequest
2 голосов
/ 16 февраля 2010

Когда я изменяю объект ActiveRecord во время сеанса, я не могу получить этот измененный экземпляр в вызове метода. Ниже приведен упрощенный пример:

Предположим, у нас есть модель только с двумя объектами: Project и Task, связанными отношением 1-n. Оба объекта могут быть активными, но задачи требуют, чтобы их родительский проект был активным перед активацией. Существует два способа активации: глобально через Project (который активирует все задачи) или индивидуально через Task.

При следующей простой реализации возникает ошибка:

class Project < ActiveRecord::Base

  # Relations
  has_many :tasks

  def activate
    self.transaction do
      self.active = true
      tasks.each {|task| task.activate}
    end
  end

end

class Task < ActiveRecord::Base

  # Relations
  belongs_to :project

  def activate
    raise ArgumentError, "Cannot activate a task of an inactive project" unless project.active?
    self.active = true
  end

end

Действительно, консоль выдаст сообщение

>> project = Project.first
=> #<Project id: 1, name: "Test project", active: false>
>> project.activate
ArgumentError: Cannot activate a task of an inactive project
    from /Rails/cache_issue/app/models/task.rb:7:in `activate'
    from /Rails/cache_issue/app/models/project.rb:9:in `activate'

Проблема заключается в том, что экземпляр объекта Project, измененный в методе Project#activate, не совпадает с тем, что ActiveRecord загружает при доступе к отношению Task#project в методе Task#activate. При отладке оба объекта представляют собой «одну и ту же» запись ActiveRecord, но не один и тот же экземпляр объекта Ruby.

>> project = Project.first
=> #<Project id: 1, name: "Test project", active: false>
>> project.activate
"Project#activate:    self.id = 1,    self.object_id = 2176477060"
"   Task#activate: project.id = 1, project.object_id = 2176246440"
ArgumentError: Cannot activate a task of an inactive project
    from /Rails/cache_issue/app/models/task.rb:8:in `activate'
    from /Rails/cache_issue/app/models/project.rb:10:in `activate'

В других системах ORM выборка экземпляра модели по идентификатору базы данных всегда выглядит в «кэше», по крайней мере, во время транзакции и даже во время сеанса. Я пытался загрузить отношения, но это не меняет проблему, поскольку я все еще мог использовать другой экземпляр Project, чем тот, который ActiveRecord решил связать с объектом Task.

Есть ли какая-либо техника (или гем, или сторонний разработчик), чтобы заставить этот простой процесс работать? То есть каждая ссылка на одну и ту же запись ActiveRecord во время сеанса / потока всегда ссылается на один и тот же экземпляр объекта Ruby?

Спасибо

1028 * Джейсон *

Ответы [ 2 ]

1 голос
/ 16 февраля 2010

Вот несколько вещей, которые вы можете попробовать.

  1. Переопределение Project # активировать, чтобы сохранить проект до того, как какая-либо из задач будет активирована.

    class Project < ActiveRecord::Base    
      # Relations
      has_many :tasks
    
      def activate
        self.transaction do
          save!
          self.active = true
          tasks.each {|task| task.activate}
        end
      end    
    end
    

    По существу, проект загружается из этой базы данных каждой из них, поскольку он проверяет активированный статус связанного проекта. Сохранение проекта сначала должно исправить это.

  2. Используйте автосохранение и установите статус активности задач напрямую.

    class Project < ActiveRecord::Base    
      # Relations
      has_many :tasks, :autosave => true
    
      def activate
        self.transaction do           
          self.active = true
          tasks.each {|task| task.active = true}
        end
      end    
    end
    

    Н.Б. Требуется Рельсы 2.3. Кроме того, задачи проекта не будут активированы до тех пор, пока проект не будет сохранен.

  3. Пусть Task # Activate принимает логический аргумент, указывающий, активировать ли связанный проект или нет. По сути зеркальное отображение ActiveRecord :: Base # save.

    class Task < ActiveRecord::Base    
      # Relations
      belongs_to :project
    
      def activate(validate_active_project = true)
        if validate_active_project && ! project.active?
          raise ArgumentError, "Cannot activate a task of an inactive project" 
        end
        self.active = true
      end
    
    end
    
    class Project < ActiveRecord::Base    
      # Relations
      has_many :tasks
    
      def activate
        self.transaction do           
          self.active = true
          tasks.each {|task| task.activate(false)}
        end
      end    
    end
    
0 голосов
/ 17 февраля 2010

Обратная ссылка на отношение "own_to" фактически создает новый проект, как вы там видите. DataMapper строго придерживается философии «Одна запись, один экземпляр», но ActiveRecord действительно близко к этому.

Когда дело доходит до выполнения чего-то такого простого, как переворот, вы можете обнаружить, что это более эффективно, так как оно не вводит зависимую загрузку модели:

class Project < ActiveRecord::Base
  has_many :tasks

  def activate!
    self.transaction do
      self.active = true
      tasks.update_all(:active => true)
      self.save
    end
  end
end

Вы можете реализовать битовое переключение как метод after_save, чтобы несколько упростить это и автоматически отключить задачи, которые находятся в неактивных проектах:

after_save :deactivate_tasks

def deactivate_tasks
  if (!self.active)
    tasks.update_all(:active => false)
  end
end
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...