最近在项目中出现了一个比较奇怪的问题,部分记录出现了重复操作,开始自己以为是数据操作的代码上出现了问题,但是经过定位发现是一个自己之前没有注意到的语法使用导致的问题。

项目代码基于Ruby 2.4.4以及Rails 5.1.6,其他版本无法保证完全一致。

在项目中有如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# snack_foods为ActiveRecord的查询结果
tmp_snack_foods = snack_foods.to_a.dup

snack_foods.each do |snack_food|
  if MEAL_TIME_TYPE.include?(snack_food.time_type_value)
    tmp_snack_foods.delete(snack_food) if delete_snack_food(snack_food)
  end
end

check_snack_calory_rule(tmp_snack_foods)

整体代码逻辑就是拷贝snack_foods至一个临时变量tmp_snack_foods,然后遍历snack_foods,符合条件时从tmp_snack_foods中删除对应的项。但是在实际运行时发现数据没有正确的删除,导致后续又进行了一遍处理。

首先来看下Ruby语法中在Objectdupclone的区别:

  • clone会拷贝单例方法,而dup不会
1
2
3
4
5
6
7
o = Object.new
def o.hello
  'hello'
end

o.dup.hello    # raises NoMethodError
o.clone.hello  # return 'hello'
  • clone会保留frozen的状态,而dup不会
1
2
3
4
5
6
7
8
9
class Hello
  attr_accessor :hello
end

o = Hello.new
o.freeze

o.dup.hello = 'world' # success
o.clone.hello = 'world' # untimeError: can't modify frozen Hello

上面是两者的区别,而当处理ActiveRecord数据时,两者最大的区别是:

  • dup创建了一个无ID的新对象,我们可以使用save去保存数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
irb(main)> food = Wisdom::Food.last
#<Wisdom::Food:0x00007ff5c7036ff0> {
               :id => 395,
             :name => "胡萝卜黑木耳包子",
             :code => "huluoboheimuerbaozi",
        :time_type => "lunch"
}

irb(main)> tmp_food = food.dup
#<Wisdom::Food:0x00007ff5c59f40a8> {
               :id => nil,
             :name => "胡萝卜黑木耳包子",
             :code => "huluoboheimuerbaozi",
        :time_type => "lunch"
}

irb(main)> tmp_food.save
   (0.4ms)  BEGIN
  SQL (3.8ms)  INSERT INTO `wisdom_foods` ...
   (1.4ms)  COMMIT
true
  • clone创建了一个拥有相同的ID的对象,因此所有对新对象的修改都会覆盖原来的对象数据。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
irb(main):001:0> food = Wisdom::Food.last
#<Wisdom::Food:0x00007ff5c5939528> {
               :id => 396,
             :name => "胡萝卜黑木耳包子",
             :code => "huluoboheimuerbaozi",
        :time_type => "lunch"
}

irb(main):002:0> tmp_food = food.clone
#<Wisdom::Food:0x00007ff5c591e868> {
               :id => 396,
             :name => "胡萝卜黑木耳包子",
             :code => "huluoboheimuerbaozi",
        :time_type => "lunch"
}

irb(main):003:0> tmp_food.update(name: '胡萝卜木耳包子')
   (0.3ms)  BEGIN
  SQL (2.8ms)  UPDATE `wisdom_foods` SET `name` = '胡萝卜木耳包子' WHERE `wisdom_foods`.`id` = 396
   (1.5ms)  COMMIT

irb(main):004:0> food
#<Wisdom::Food:0x00007ff5c5939528> {
               :id => 396,
             :name => "胡萝卜木耳包子",
             :code => "huluoboheimuerbaozi",
        :time_type => "lunch"
}

明白了上述两个方法的区别后就会发现问题所在:

1
2
3
4
5
# 这行代码导致的是tmp_snack_foods里面的值都是不包含ID的新对象
tmp_snack_foods = snack_foods.to_a.dup

# 因此导致这里的删除无法生效,数组保留传到了后续的操作上导致报错
tmp_snack_foods.delete(snack_food)

针对上述问题,修改成clone应该就可以达到效果了。这一个问题自己以前确实不太了解,这一次正好是碰到了知识盲区,学到了两者的差别,下次使用就会注意这个问题了。