论坛首页 编程语言技术论坛

rails陷阱:has_many的build和create

浏览 3325 次
精华帖 (0) :: 良好帖 (1) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2010-02-06   最后修改:2010-02-06
这是我在实际中遇到的问题.
废话不多说,直接上代码。
在out里
 has_many :out_items
 def pick(os)
    update_attributes(os)
    out_items.each(&:change_placement)
 end
 def out_item_attributes=(out_item_attributes)
    out_item_attributes.each do |key,value|
      OutItem.find_by_id(key).update_attributes(value)
    end
  end

在out_item里
  has_many :out_placements
  def new_out_placement_attributes=(out_placement_attributes)
    out_placement_attributes.each do |out_placement_attribute|
      #out_placements.create(out_placement_attribute)
      op = out_placements.build(out_placement_attribute)
      op.save
    end
  end
  def change_placement
    self.out_placements.each(&:change_placement)
  end

在执行完out中update_attributes后,out_items.each(&:change_placement),而这时debug代码到change_placement方法中,发现self.out_placements这里是empty array。但是此时,打开console,通过命令,你发现,out_placements不是空的,而且通过数据库也可以看到已经成功的将out_placement更新并且和out_item关联。out_placements明明有,为什么在这里却是empty array呢?

是哪里出问题?

经过一番折腾,猜想是不是在has_manyut_placements中,默认的out_placements使用了缓存?这里的out_placements用的缓存,而没有从数据库里重新查询。如果是这样,应该将缓存更新即可。通过分析代码。发现可能是这里出问题里。
      op = out_placements.build(out_placement_attribute)
      op.save

但是如果我将out_item里的代码改成(注释部分即改变部分)out_placements.create(out_placement_attribute),发现debug到self.out_placements后,out_placements就不是空数组,运行正常了。

成功了。原来使用out_placements.create(out_placement_attribute)这样的操作,不但更新了数据库,而且更新了out_placements缓存,而直接的用op = out_placements.build(out_placement_attribute);op.save,虽然将out_placement保存到了数据库,但是此时再调用out_placements得到的还是之前缓存。
   发表时间:2010-02-22   最后修改:2010-02-22
我觉得,这确实是association的缓存原因,但你的写法也有点问题。
你把out文件的out_item_attributes=方法如下改一下试试:
# RAILS_ROOT/app/models/out.rb
def out_item_attributes=(out_item_attributes)  
  out_item_attributes.each do |key,value|  
    # OutItem.find_by_id(key).update_attributes(value)
    # 在association查出来的array中去找,而不是直接find_by_id
    # 看你这种写法,可能是从前端用params批量传值过来,进行批量提交的,所以key可能是字符串,采用id.to_s比较
    oi = out_items.detect {|o| o.id.to_s == key }
    oi.update_attributes value
  end
end

下面的代码还是用build,应该没问题的。
暂时想到这些,还没测试过,LZ自己试试吧
PS: 你这样直接上代码,而且只是一部分代码,很容易让人迷惑的。我是猜测你用的params批量传值,也不知道对不对……有空写写文字吧。个人觉得代码从来不是程序员的问题。
0 请登录后投票
   发表时间:2010-02-22  
引用

# 看你这种写法,可能是从前端用params批量传值过来,进行批量提交的,所以key可能是字符串,采用id.to_s比较

我很是不同意这点。就算key是字符串怎样?本来他也就是字符串。
你可以在console下试下,A.find('1')。
引用

     oi = out_items.detect {|o| o.id.to_s == key } 
     oi.update_attributes value 

至于这段代码,我觉得也没有必要这样写。
引用

我是猜测你用的params批量传值,也不知道对不对……有空写写文字吧。个人觉得代码从来不是程序员的问题。

代码也就是实现这个功能。我在前面没有表述,让大家看着受累了。
多写写文字确实是不错,代码只是实现手段。
这点偶吸取教训,多写些文字。

0 请登录后投票
   发表时间:2010-02-22  

刚才仿造你的代码测试了一下,结果和我原先想的有些出入。

我的测试结果是,不管是create还是build,最后out_placements中都是空数组。

我下面会说明我的测试方法,并说明原因。

现在先说说LZ的结论,其实对Rails的association而言,不管是使用create还是build都会相应的改变association中的缓存。要是这点都没想到DHH也可以退休了。

LZ的错误还是在Out#out_item_attributes方法上

def out_item_attributes=(out_item_attributes)  
  out_item_attributes.each do |key,value|  
    # 这里不该用OutItem直接查。
    # 因为对这个OutItem查出来的对象(暂称oi)调用update_attributes的话,
    # 会间接调用oi的new_out_placement_attributes方法,虽然生成了OutPlacements,但更新的是oi.out_placements的缓存。
    # 而oi会在这个each循环结束后销毁,自然什么都记不住。实际上这个oi和最外层Out对象的out_items是没有任何关系的。
    OutItem.find_by_id(key).update_attributes(value)  
  end  
end

所以对out对象调用如下方法:

out.pick ...

 时,out.out_items的缓存根本就没动过,实际上在update_attributes的整个阶段,它都应该没有查询过数据库,缓存自然无从谈起,直到下面一句each。

这时的问题才刚出现,刚才pick方法的第一句update_attributes已经将out_placements写进数据库了(不管是create还是build),这时执行第二句out_items.each时,才进行数据库查询(association只在“需要”的时候才会执行数据库查询,这就不多说了),那查询出来的结果应该是正确的,绝不可能是空数组。

而LZ碰到的是空数组,我想是因为在调用out.pick之前,在其他地方遍历过out.out_items,从而提前查出了数据,放进了out_items的缓存,之后在update_attributes时,又没有更新缓存,才出现的问题。所以不管create还是build,应该都会报空数组的错误的。

要改过来很简单,就照我上一个帖子的代码改。

 

下面附上本人的测试代码,和一些逻辑。我根据方法命名,猜测LZ做的这个功能是利用update_attributes来做批量数据提交。所以测试逻辑都是围绕这个来写的,如果错了,还请LZ指正。

# RAILS_ROOT/test/unit/out_test.rb
require 'test_helper'

class OutTest < ActiveSupport::TestCase
  
  fixtures :outs, :out_items

  def setup
    # 初始数据库环境如下:
    # outs表中有一条记录
    # out_items表中也有一条记录,从属于outs表中的那一条记录
    # out_placements表没有记录
    @out = outs(:o1)
    @out_attrs = {
      :out_item_attrs => {
        # 更新一个id为1的OutItem对象
        "1" => {
          :title => "out item 1",
          # 为out_placements写入两条记录
          :new_out_placement_attributes => [
            {:title => "placement 1"},
            {:title => "placement 2"}
          ]
        }
      }
    }
  end

  # 测试1,调用@out.pick,再看之后的数据库记录是不是增加了
  test "1" do
    # 直接执行pick,方法调用顺序如下:
    # Out#pick -> Out#update_attributes -> Out#out_item_attr= -> OutItem#Update_attributes -> OutItem#new_out_placement_attrs=
    @out.pick @out_attrs
    oi1 = @out.out_items[0]
    # 可以看到,写入了两条记录
    assert_equal oi1.out_placements.size, 2
  end

  # 测试2,和测试1唯一不同的就是在调用pick之前,先把@out.out_items的数据读出来,生成缓存了
  test "2" do
    # 这段代码没其他目的,就是提前把out_placements的数据读到内存中来
    # 此时out_placements是空数组
    @out.out_items.each { |oi| oi.out_placements.each {|op|} }
    
    @out.pick @out_attrs
    oi1 = @out.out_items[0]
    # 这里因为缓存原因,还是0条记录,其实数据已经写进去了
    assert_equal oi1.out_placements.size, 0
    # reload之后就显示真实的数字了
    assert_equal oi1.reload.out_placements.size, 2
  end
end

然后是fixtures,很简单,就不解释了

# RAILS_ROOT/test/fixtures/outs.yml
# outs表,title属性是随意加的,要不要无所谓
o1:
  id: 1
  title: out

# RAILS_ROOT/test/fixtures/out_items.yml
# out_items表
oi1:
  id: 1
  title: out item 1
  out_id: 1

 运行

ruby -I test ./test/unit/out_test.rb
 
0 请登录后投票
   发表时间:2010-02-22  
orcl_zhang 写道
引用

# 看你这种写法,可能是从前端用params批量传值过来,进行批量提交的,所以key可能是字符串,采用id.to_s比较

我很是不同意这点。就算key是字符串怎样?本来他也就是字符串。
你可以在console下试下,A.find('1')。
引用

     oi = out_items.detect {|o| o.id.to_s == key } 
     oi.update_attributes value 

至于这段代码,我觉得也没有必要这样写。

这点你误会了,使用find时,当然不管是字符串还是数字都可以。
我注明这点只是因为
o.id.to_s == key

这句可能看起来有点奇怪,所以对为什么要把o.id变成字符串来再比较,做的解释。

至于那样改写的原因,只是要利用out_items这个association找出来的对象,而不是OutItem.find,这也是我和你代码的不同,原因我在上面一个帖子里都写明了。

再说一句,console中看到的东西不见得是真相哦。我刚开始学习Rails被它欺骗了好一阵子。所以在console中测试不是最好的办法。
不信你可以分别试试下面两句,其中第一句是不会查询数据库的,但第二句会(其实真实运行环境中都是不会查数据库的,这是console的一点小把戏):
out.out_items; nil
out.out_items

你可以在console中敲一句,然后在日志中看看生成了SQL语句没有。
最后,如果不明白,你可以找一下irb和inspect方法的文章。
0 请登录后投票
   发表时间:2010-02-23   最后修改:2010-02-23
out.out_items这个,第一句执行应该是要查询数据库的吧?第二次就不需要了。应该是rails的缓存。应该是类似
def current_user
  @current_user ||= User.find(session[:user_id])
end

这样的方式实现的。
引用
console中看到的东西不见得是真相哦。

这个还是真不知道。。
out.out_items; nil 这句确实没有查询。暂时还不知道原因。
有空了看下irb和inspect方法的文章。
挺晚了,睡觉。明天继续搞。
0 请登录后投票
   发表时间:2010-03-12  
发现其实在rails里面,在表间关联中很多这样的例子。
self.my_steps.build(:name => name)
self.my_steps.each do |mstep|
   mstep.save(false)
end
ms = self.my_steps

如果这样写
self.my_steps.create(:name => name)
ms = self.my_steps

则返回的ms是不同的。
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics