【问题标题】:rails validation error nested object undefined method... for nil:NilClassrails验证错误嵌套对象未定义方法...对于nil:NilClass
【发布时间】:2018-11-08 12:56:22
【问题描述】:

我制作了一个日程安排应用程序,每天将人员分配到房间。在星期四,必须为“寻呼机取件”指派某人,而我在验证时遇到了问题。

型号

class Schedule < ActiveRecord::Base 
  has_many :rooms
  ...
  validate :thursday_schedule_must_have_pager_pickup
  ...

  def add_rooms
    return unless self.rooms.count == 0                           
    n = 1
    tomorrow = DateTime.tomorrow                                  
    Schedule.site_list.each do |site|                             
      Schedule.const_get(site).each do |room|                     
        self.rooms.build(order: n,                                
                      site: site.to_s,                         
                      name: room,
                      start_hour: get_start_hour(tomorrow),    
                      start_minute: get_start_minute(tomorrow, site.to_s))                   
        n += 1                                                    
      end
    end
    self.add_pager_pickup(n, tomorrow) if true # self.for_thursday?
    self.add_today_call_data(n) if no_call_data                   
  end
...
def add_pager_pickup(order, tomorrow)
  self.rooms.build(order: order,
      site: "TSH",
      name: "Pager Pickup",
      start_hour: 7,
      start_minute: get_start_minute(tomorrow, "TSH"))
  end
end

class Room < ActiveRecord::Base
  belongs_to :schedule
  ...
end

我想写的代码是:

def thursday_schedule_needs_pager_pickup
  if self.for_thursday? && self.rooms.where(name: "Pager Pickup").first.initials.blank?
    errors.add(:rooms, "'Pager Pickup' can't be empty.  Select '-- late start' if no one should come in early to pick up pager.")
  end
end

这会产生以下错误:

NoMethodError in SchedulesController#create
undefined method `initials' for nil:NilClass

通过最后将“寻呼机取货”房间添加到日程表中,我可以使用以下代码破解验证:

... self.rooms.last.initials.blank?

但这很脆弱,并且使我无法在第一个之后添加第二个可选的寻呼机拾音器“第二个寻呼机拾音器”。

根据朱利安的观点:

调度控制器

class SchedulesController < ApplicationController 
...
  def new
    s = current_user.schedules.new
    s.add_rooms
    @schedule = s
  end

  def create 
    @schedule = current_user.schedules.build(schedule_params)
    if @schedule.save 
      flash.now[:success] = "Draft Schedule Saved! Now Confirm or Edit."
      render :show
    else
    render :new
  end
...
end
    

有人有什么想法吗?

提前致谢!

【问题讨论】:

  • 您有名为Pager Pickup的房间记录吗?
  • 问得好,@Pavan。是的,我非常有信心,我已经小心确保拼写或大小写错误等拼写错误不会阻止名为“Pager Pickup”的房间存在。

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


【解决方案1】:

如果我正确阅读了您的代码,则您有一个未保存的对象,并且您正在尝试对其运行此验证:

self.rooms.where(name: "Pager Pickup").first.initials.blank?

这种方法的问题是.where on association 将运行数据库查询(或者更具体地说,如果您的对象已保存,它将运行查询,但对于未保存的关系不会执行任何操作)。这对你不起作用,你还没有保存任何东西,你必须对内存中的对象进行操作。如果您将该行更改为:

self.rooms.detect {|r| r.name == "Pager Pickup" }.initials.blank?

应该可以工作,但是如果您只是将它留在模型中,它仍然容易出错,因为在另一种情况下,可能没有具有该名称的房间并且仍然会调用 .initialsnil。我建议您将此类逻辑移至工厂对象,在那里您可以将您的验证严格绑定到上下文。

为了完全理解这个概念,你可以在rails console运行这个:

s = Schedule.new
# => #<Schedule id: nil>
s.rooms << Room.new(foo: "bar")
# => #<ActiveRecord::Associations::CollectionProxy [#<Room id: nil, schedule_id: nil, foo: "bar">]>
s.rooms.where(foo: "bar")
# => #<ActiveRecord::AssociationRelation []>
s.rooms.detect { |r| r.foo == "bar" }  
# => #<Room id: nil, schedule_id: nil, foo: "bar">

注意:您对 .last 的“破解”有效,因为它在数组上操作,而不是 ActiveRecord::Relation。

【讨论】:

  • 太棒了!谢谢!问题解决了。认为问题在于where 是否能够对尚未保存到数据库但不知道检测和选择的对象采取行动。非常感谢。试图奖励 +50 分,但 SO 说必须等待 22 小时......不过,如果可以的话,会这样做。
【解决方案2】:

您面临的问题是您试图对尚未保存到数据库中的模型进行查询。它没有id,关联的模型rooms本身也没有id

您的验证会引发错误,因为 .where 调用了数据库并且它找不到模型,因为它们目前只存在于内存中。

self.rooms.where(name: "Pager Pickup").first.initials.blank?

但是,您拥有两个模型的信息来正确地进行验证,您只是在错误的地方寻找它。

如果您调试您的应用程序以构建您的 Schedule 实例(尚未保存),并向其中添加一些 rooms(也未保存),您将看到以下行为:

@schedule.rooms.length # It will be some value bigger than 0

@schedule.rooms.count # It will be zero

为什么?因为.length 作为数组处理对象,.count 在数据库中搜索它。正如我之前提到的,您的模型不存在于数据库中,因此它不会找到它们,但它们在内存中,因此您可以测量它的 length

在这个问题中你需要做的改变是模仿.where在内存中所做的,用一个简单的.select

self.rooms.select { |r| r.name == 'Pager Pickup' }.first.initials.blank?

这是你唯一需要做的改变,但你应该明白为什么。

【讨论】:

  • .detect { }.select { }.first 好一点,但仍然是一个有效的答案。
  • 这行得通,感谢您的回答和解释。选择@MarcinKołodziej,因为比您早几分钟提交......不过非常感谢。
  • 是的,我注意到了时间上的差异。我们很高兴它成功了!
【解决方案3】:

好的,首先

self.rooms.where(name: "Pager Pickup") 

可能会返回多个对象,因此它不会为您提供Room 对象,但可能是ActiveRecord::Relation 对象,因此您需要在.initials 之前添加.first 之类的内容才能获得Room像这样:

self.rooms.where(name: "Pager Pickup").first.initials.blank?

但无论如何,错误表明它没有找到任何东西,因此在验证时,此计划的 rooms 关系似乎不存在或为“空”,因为它声称 nil 是从该查询而不是空数组,所以我猜测您的验证发生在实际创建/保存房间之前。

也许向我们展示SchedulesControllercreate 操作,看看那里是否有问题。

更新

看到您的额外代码后,问题是您将 add_rooms 调用到与您正在创建的计划对象不同的计划对象,我猜您的印象是实例变量(以 @ 开头的变量)是在请求之间保留但它们不是,因此您的 create 操作中的 @schedule 对象与您的 new 操作中的对象不同,因此它还没有任何房间,请更新您的 create 操作以填充该对象的房间做这样的事情:

  def create 
    @schedule = current_user.schedules.build(schedule_params)
    @schedule.add_rooms    # <-- Add this line
    if @schedule.save 
      flash.now[:success] = "Draft Schedule Saved! Now Confirm or Edit."
      render :show
    else
    render :new
  end

【讨论】:

  • 谢谢,@朱利安。添加了 .first ,正如您预测的那样,它仍然不起作用。我编辑了我的答案,以在计划模型中包含 SchedulesController#Create 和其他可能包含我的错误的方法。非常感谢您的洞察力。
  • 何时调用add_rooms?我在create 操作中没有看到它,我猜它不在schedule_params 中,所以你在哪里称呼它?
  • add_rooms 在 SchedulesController#New 操作中被调用。我加了。
  • 那是您的问题,您要在与您创建的计划对象不同的计划对象上添加房间,您需要在调用之前将房间添加到 create 操作中的 @schedule 对象save 就可以了。
  • 我已经编辑了我的答案,以包含有关我之前评论的一些详细信息。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-09-25
  • 1970-01-01
  • 1970-01-01
  • 2013-07-16
  • 2017-01-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多