欢迎来到cool的博客
7

Music box

Click to Start

点击头像播放音乐
新博客链接

关于Rails model中使用scope容易出现的bug

转自: https://gist.github.com/seairy/ac5a529a00c27706bfbcc1cd5701b026

 

我刚刚用了2.5个小时解决了一个简单model scope的bug,但过程曲折,而且rails也没有相关的best practice或warning,现分 享给大家以便应对未来可能出现的问题。

在model中我们经常会使用chainable的scope,但有时scope中会使用一些无法通过sql获取到的数据,比如在Area中我增加了一个scope,用于过滤掉所有无权限预约的area,代码如下:

  scope :without_denied, ->(user) { where.not(id: ReservationCondition.denied_reservables_for(user).map(&:id)) }

使用该scope的场景如下:

  Area.some_scope.another_scope.without_denied(current_user)

在test case中,一切正常,ReservationCondition.denied_reservables_for方法也完全按照预期返回了数据,但是在实际App中却获取到了非预期的结果,具体表现为:

  ReservationCondition.denied_reservables_for(user)
  without_denied(user)

以上两段代码返回的结果不一样,经过一系列的检查后,确认直接执行ReservationCondition.denied_reservables_for(user)和在scope中执行的结果不同,于是定位到denied_reservables_for这个方法,发现了如下代码片段存在的问题:

1  belonged_building_infos = Renting.where(user: user).map do |renting|
2    ...
3    puts "****** renting.area_id: #{renting.area_id}"
4    puts "****** renting.area: #{renting.area}"
5    ...
6  end.compact

我们把直接执行ReservationCondition.denied_reservables_for(user)的context取名为C1,把在scope中执行上述方法的context取名为C2,那么大家注意第4行,C1返回正常的area object,而C2却返回了nil,而第3行却返回的结果都正常。经过反复检查,代码和变量上都没有任何区别,为什么两个context会有不同的表现呢?此时发现了一个现象,在C2中,实际执行的SQL会存在一个我并没有显式声明的where条件(where area_type = xxx),恰恰因为这个where条件,导致了无法找到相应的area,于是再往上层去寻找,发现了C2中的调用包含了该条件,即:

  Area.meeting_room.xxx.without_denied(current_user)

此时发现了真正的问题,由于是chainable,所以.meeting_room之后的scope都会包含了where area_type = meeting_room的条件,甚至包括在scope中执行的完全不相关的代码,如第4行的renting.area,于是我尝试了如下改动:

1  belonged_building_infos = Renting.where(user: user).map do |renting|
2    ...
3    puts "****** renting.area_id: #{renting.area_id}"
4    puts "****** renting.area: #{Area.where(id: renting.area_id).unscoped.first}"
5    ...
6  end.compact

此时无论C1还是C2,第4行都返回了正常的area object!问题解决!

以上即是整个寻找问题的过程,由于chainable带来的极大的便捷性,因此有时确实需要使用相对复杂一些的scope,那么在使用中一定要注意之前的chain中是否包含了会影响到该scope的条件。

返回列表