【问题标题】:Rails: Why “collection=” doesn't update records with existing id?Rails:为什么“collection=”不更新现有 id 的记录?
【发布时间】:2013-06-17 07:22:04
【问题描述】:

用户可以有很多帖子:

class User < ActiveRecord::Base
  has_many :posts
  accepts_nested_attributes_for :posts
end

class Post < ActiveRecord::Base
  belongs_to :user
end

为什么下面的顺序没有更新第一个帖子?

$ rails c

> user = User.create(name: 'Misha')
 => #<User id: 7, name: "Misha", ... >
> user.posts << Post.create(description: 'hello')
 => #<ActiveRecord::Associations::CollectionProxy [#<Post id: 9, description: "hello", user_id: 7, ... >]> 
> post1 = Post.find(9)
> post1.assign_attributes(description: 'world')
> post1
 => #<Post id: 9, description: "world", user_id: 7, ... >
> post2 = Post.new(description: 'new post')
> user.posts = [post1, post2]
> user.posts.second.description
 => "new post"   # As expected
> user.posts.first.description
 => "hello"      # Why not "world"?

【问题讨论】:

    标签: ruby-on-rails ruby-on-rails-3.2 rails-activerecord


    【解决方案1】:

    您将保存帖子对象与保存从帖子到用户的关联混为一谈。

    就像@zeantsoi 所说,assign_attributes 永远不会保存它——查看执行的 SQL,collection= 也不会保存任何东西。

    > user.posts = [post1, post2]
       (0.1ms)  begin transaction
      SQL (0.7ms)  INSERT INTO "posts" ("created_at", "description", "updated_at", "user_id") VALUES (?, ?, ?, ?)  [["created_at", Mon, 17 Jun 2013 10:48:13 UTC +00:00], ["des
    cription", "p2"], ["updated_at", Mon, 17 Jun 2013 10:48:13 UTC +00:00], ["user_id", 2]]
       (22.8ms)  commit transaction
    => [#<Post id: 3, description: "p1 modified", user_id: 2, created_at: "2013-06-17 10:46:43", updated_at: "2013-06-17 10:46:43">, #<Post id: 4, description: "p2", user_id: 
    2, created_at: "2013-06-17 10:48:13", updated_at: "2013-06-17 10:48:13">]
    >
    

    post2 被插入只是因为它必须是为了设置关系;如果无法唯一识别 Post,则 User 对象无法知道特定的 Post 属于它。

    查看CollectionAssociation 的源代码,has_many 是在此基础上构建的,observe how wholesale replacement is implemented

    # Replace this collection with +other_array+. This will perform a diff
    # and delete/add only records that have changed.
    def replace(other_array)
      other_array.each { |val| raise_on_type_mismatch!(val) }
      original_target = load_target.dup
    
      if owner.new_record?
        replace_records(other_array, original_target)
      else
        transaction { replace_records(other_array, original_target) }
      end
    end
    

    作品核心在replace_records

    def replace_records(new_target, original_target)
      delete(target - new_target)
    
      unless concat(new_target - target)
        @target = original_target
        raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
                              "new records could not be saved."
      end
    
      target
    end
    

    换句话说,它会删除不在目标列表中的项目,然后添加不在新列表中的项目;结果是目标列表和新列表 (post1) 中的任何项目在集合分配期间都不会被触及。

    根据上述代码,传入参数的target 是返回的内容,似乎反映了更改:

    => [#<Post id: 3, description: "p1 modified", user_id: 2, created_at: "2013-06-17 10:46:43", updated_at: "2013-06-17 10:46:43">, #<Post id: 4, description: "p2", user_id: 
    2, created_at: "2013-06-17 10:48:13", updated_at: "2013-06-17 10:48:13">]
    

    但是在重新访问集合时,更改不会反映:

    > post1
    => #<Post id: 3, description: "p1 modified", user_id: 2, created_at: "2013-06-17 10:46:43", updated_at: "2013-06-17 10:46:43">
    > user.posts
    => #<ActiveRecord::Associations::CollectionProxy [#<Post id: 3, description: "p1", user_id: 2, created_at: "2013-06-17 10:46:43", updated_at: "2013-06-17 10:46:43">, #<Pos
    t id: 4, description: "p2", user_id: 2, created_at: "2013-06-17 10:48:13", updated_at: "2013-06-17 10:48:13">]>
    >
    

    注意这里的返回略有不同;赋值的返回值是你传入的 array 对象;这是ActiveRecord::Associations::CollectionProxyreader function is called here

    # Implements the reader method, e.g. foo.items for Foo.has_many :items
    def reader(force_reload = false)
      if force_reload
        klass.uncached { reload }
      elsif stale_target?
        reload
      end
    
      @proxy ||= CollectionProxy.new(klass, self)
    end
    

    然后,这会根据 has_many 关系创建集合代理,其值是根据我们在分配选项时所知道的内容填充的。这个答案中唯一未被发现的部分是为什么结果对象被清除了脏值——我已经阅读了一些代码,并认为使用调试器最容易回答,但我没有心情为了。 :) 但很明显,它要么从缓存加载,要么传入的对象的更改被丢弃。

    无论哪种方式,如果您希望更改出现在目标对象中,您应该先保存它——仅仅分配集合是不够的,就好像它已经是一个成员一样,它不会被触及。


    更新:有趣的是,这只是因为我们使用Post.find 来获取post1;如果我们改为使用post1 = (user.posts &lt;&lt; Post.create(description: 'p1')),那么最后在user.posts 中观察到的集合实际上有脏对象。

    这首先揭示了它是如何诞生的。观看object_ids:

    >
    u = User.create; p1 = (u.posts << Post.create(description: 'p1'))[0]; p1.assign_attributes(description: 'p1 mod'); p2 = Post.new(description: 'p2'); u.posts = [p1, p2]; u.posts
    ...
    => #<ActiveRecord::Associations::CollectionProxy [#<Post id: 21, description: "p1 mod", user_id: 10, created_at: "2013-06-17 11:43:30", updated_at: "2013-06-17 11:43:30">, #<Post id: 22, description: "p2", user_id: 10, created_at: "2013-06-17 11:43:30", updated_at: "2013-06-17 11:43:30">]>
    > _[0].object_id
    => 70160940234280
    > p1.object_id
    => 70160940234280
    >
    

    请注意,集合代理中返回的对象与我们创建的对象相同。如果我们重新find它:

    > u = User.create; u.posts << Post.create(description: 'p1'); p1 = Post.find(u.posts.first.id); p1.assign_attributes(description: 'p1 mod'); p2 = Post.new(description: 'p2'); u.posts = [p1, p2]; u.posts
    ...=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 23, description: "p1", user_id: 11, created_at: "2013-06-17 11:43:47", updated_at: "2013-06-17 11:43:47">, #<Post id: 24, description: "p2", user_id: 11, created_at: "2013-06-17 11:43:47", updated_at: "2013-06-17 11:43:47">]>
    > _[0].object_id
    => 70264436302820
    > p1.object_id
    => 70264441827000
    >
    

    最初让我感到困惑的部分是没有脏数据的对象来自哪里;没有发生 SQL,甚至没有缓存命中,所以它必须来自某个地方。我以为它要么是其他缓存源,要么是明确地获取给定的对象并清理它们。

    上面说的很清楚,缓存其实就是我们在插入的时候创建的Post。 100% 确定,让我们看看返回的Post 是否与创建的相同:

    > u = User.create; p0 = (u.posts << Post.create(description: 'p1'))[0]; p1 = Post.find(u.posts.first.id); p1.assign_attributes(description: 'p1 mod'); p2 = Post.new(description: 'p2'); u.posts = [p1, p2]; u.posts
    ...
    => #<ActiveRecord::Associations::CollectionProxy [#<Post id: 27, description: "p1", user_id: 13, created_at: "2013-06-17 12:01:05", updated_at: "2013-06-17 12:01:05">, #<Post id: 28, description: "p2", user_id: 13, created_at: "2013-06-17 12:01:07", updated_at: "2013-06-17 12:01:07">]>
    > _[0].object_id
    => 70306779571100
    > p0.object_id
    => 70306779571100
    > p1.object_id
    => 70306779727620
    >
    

    所以CollectionProxy 中没有反映更改的对象实际上与我们最初添加到集合时创建的对象相同;这解释了缓存数据的来源。然后我们置换一个副本,它不会反映在收集后分配中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-05-07
      • 1970-01-01
      • 1970-01-01
      • 2021-10-18
      • 1970-01-01
      • 2019-07-06
      • 1970-01-01
      相关资源
      最近更新 更多