简体   繁体   English

Ruby on Rails-NilClass的未定义方法

[英]Ruby on Rails - Undefined methods for NilClass

I'm creating a picture-rating app where users can click on pictures and rate them on a scale from 1 to 5. I'm trying to calculate the average rating of a picture. 我正在创建一个图片分级应用,用户可以单击图片并以1到5的比例对其进行分级。我正在尝试计算图片的平均分级。 Before when users clicked on a rating value, that value became the picture's rating. 在用户单击评分值之前,该值成为图片的评分。

Rating: 5

If a user clicked on 1, the rating would change to 1 如果用户单击1,则评分将更改为1

Rating: 1

When reality, the rating should have been 3. 实际情况下,评分应为3。

(5 + 1) / 2
=> 3

Here's what I've accomplished so far in implementing this feature. 到目前为止,这是我在实现此功能时已经完成的工作。

I added a migration to create two new columns for my Pictures Table 我添加了一个迁移,以为图片表创建两个新列

rails g migration AddRatingsToPictures ratings_count: integer, rating_total: integer

Both the new attributes, ratings_count and rating_total are integer types, meaning they are assigned a nil value at default. 新属性rating_count和rating_total均为整数类型,这意味着它们在默认情况下被分配为nil值。

p = Picture.first
p.attribute_names
=> ['id', 'title', 'category', 'stars', 'updated_at', 'created_at', 
'ratings_count', 'rating_total']
p.ratings_count
=> nil
p.rating_total
=> nil

My only problem is the NilClass Error. 我唯一的问题是NilClass错误。

Here is my update method in my PicturesController. 这是我的PicturesController中的更新方法。

def update
  @picture = Picture.find(params[:id])
  @picture.ratings_count = 0 if @picture.stars.nil?
  @picture.rating_total = @picture.stars
  @picture.rating_total += @picture.stars if @picture.stars_changed?
  @picture.ratings_count += 1 if @picture.rating_total_changed?
  if @picture.update_attributes(picture_params)
    unless current_user.pictures.include?(@picture)
      @picture = Picture.find(params[:id])
      current_user.pictures << @picture
      redirect_to @picture, :flash => { :success => "Thank you! This picture has been added to your Favorites List" }
    else
      redirect_to :action => 'index'
      flash[:success] = 'Thank you! This picture has been updated' 
    end
  else
    render 'edit'
  end
end

Here is my picture_param method in my PicturesController 这是我的PicturesController中的我的picture_param方法

 def picture_params
  params.require(:picture).permit(:title, :category, :genre, :stars)
end

Here is what the two new columns do 这是两个新列的作用

ratings_count: Calculates the number of times a picture has been rated
rating_total: Calculates the sum of the stars a picture has received

In the above code, I first set the ratings_count to 0 if the picture doesn't have a rating. 在上面的代码中,如果图片没有评分,我首先将rating_count设置为0。 This means that the picture hasn't been rated yet. 这表示该图片尚未评级。

I then need to initially set the rating_total to the number of stars a picture has. 然后,我需要首先将rating_total设置为图片的星数。 If a user changed the star rating, I would add those stars to the rating_total. 如果用户更改了星级,则可以将这些星级添加到rating_total中。 And if the total increased, that's my cue to increase the number of ratings. 如果总数增加了,那是我增加评分数量的提示。

Obviously, to calculate the average, I'd do something like this. 显然,要计算平均值,我会做这样的事情。

(@picture.rating_total / @picture.ratings_count).to_f

Now, I think I have the right idea but I know why this doesn't work. 现在,我想我有一个正确的主意,但我知道为什么这行不通。 When columns are created with an integer value, by default they are set to nil. 使用整数值创建列时,默认情况下将它们设置为nil。 This leads to a NilClass Error when I load the web page. 加载网页时,这会导致NilClass错误。

undefined method `/' for nil:NilClass

Here is my code in the View 这是我在视图中的代码

<li><strong>Rating:</strong> <%= pluralize((@picture.rating_total / @picture.ratings_count), 'Star') %></li>

This will handle the case of uninitialized (nil) values in your attributes... 这将处理属性中未初始化(nil)值的情况...

def update
  @picture = Picture.find(params[:id])
  if @picture.stars_changed?
    @picture.ratings_count = (@picture.ratings_count || 0) + 1
    @picture.rating_total = (@picture.rating_total || 0) + ( @picture.stars || 0)
  end

You don't need an array of ratings or ratings persisted to database, assuming you only count votes where the rating changes, you can accumulate the count and the total and divide the two (which is, in fact, what you're doing so I'm preaching to the converted). 您不需要一连串的评分或保留在数据库中的评分,假设您只对评分更改时的票数进行计数,则可以累加计数和总数,然后将两者相除(实际上,这就是在这样做)我正在向to依的人传道。

Although it seems to me that if I change a picture from 5 to 1 and it only changes to 3, I'm gonna keep clicking 1 :) 虽然在我看来,如果我将图片从5更改为1而仅更改为3,则我将继续单击1 :)

You could set the default value on the migration when you created it. 您可以在创建迁移时设置默认值。 But no worries, you can create a new migration to change it: 但是不用担心,您可以创建一个新的迁移来进行更改:

# Console
rails g migration change_default_for_ratings_count_and_rating_total

# Migration Code
class ChangeDefaultForRatingsCountAndRatingTotal < ActiveRecord::Migration

  def change
    change_column :pictures, :ratings_count, :integer, default: 0
    change_column :pictures, :rating_total,  :integer, default: 0
  end
end

Keep in mind that some databases don't automatically assign newly updated default values to existing column entries, so maybe you will have to iterate over every picture already created with nil values and set to 0. 请记住,某些数据库不会自动将新更新的默认值分配给现有的列条目,因此也许您必须遍历已经使用nil值创建并设置为0的每张图片。

Ok, the main reason it is not working is because 好的,它不起作用的主要原因是

  • you fetch the picture 你拿图片
  • you check the stars from the database, and the NOT the passed form-parameters 您从数据库中检查stars ,而不是通过传递的形参
  • you do update_attributes, which if I am not mistaken, used to set attributes and then save the complete object, but since rails 4 only updates the passed attributes (which is what you would expect) 您要做update_attributes,如果我没记错的话,它用于设置属性,然后保存完整的对象,但是由于rails 4仅更新传递的属性(这是您期望的)

One small remark: keeping the rating correct is a function I would place in the model, NOT in the controller. 一句话:保持正确的额定值是我要放在模型中的功能,而不是控制器中的功能。

Furthermore, how to handle the if nil, initialise to zero I wrote a short blogpost about. 此外,关于如何处理if nil,初始化为零的问题,我写了一篇简短的博客文章。 In short: overrule the getter. 简而言之:否决吸气剂。

So I would propose the following solution. 因此,我提出以下解决方案。 In your model write 在您的模型中写

class Picture < ActiveRecord::Base


  def ratings_count
    self[:ratings_count] || 0
  end

  def ratings_total
    self[:ratings_total] || 0
  end


  def add_rating(rating)
    return if rating.nil? || rating == 0

    self.ratings_count += 1
    self.ratings_total += rating
    self.stars = self.ratings_total.to_f / self.ratings_count
    self.save
  end

  def rating
    return 0 if self.ratings_count == 0
    self.ratings_total.to_f / self.ratings_count
  end

and then the code in your controller becomes much cleaner and readable: 然后控制器中的代码变得更加清晰易读:

def update
  @picture = Picture.find(params[:id])

  stars = picture_params.delete(:stars)

  if @picture.update_attributes(picture_params)
    @picture.add_rating stars
    unless current_user.pictures.include?(@picture)
      current_user.pictures << @picture
      redirect_to @picture, :flash => { :success => "Thank you! This picture has been added to your Favorites List" }
    else
      redirect_to :action => 'index'
      flash[:success] = 'Thank you! This picture has been updated' 
    end
  else
    render 'edit'
  end
end

I first delete the :stars from the parameters, because I do not want to save those, I want to use those for the add_rating . 我首先从参数中删除:stars ,因为我不想保存那些,我想将其用于add_rating I then try to update_attributes , which will fail if there are any failing validations, and if that is ok, I will add_rating which itself will handle nil or zero correctly. 然后,我尝试update_attributes ,如果验证失败,它将失败,如果可以,我将添加add_rating本身可以正确处理nil或零。 Well granted: I do not know how you handle a "non-rating" (nil? zero?). 理所当然:我不知道您如何处理“非评级”(零?零?)。 It is possible a rating of zero should be added, because it will add a rating, but most UI I know do not allow to select 0 as rating, so you might want to change the zero handling. 可能会添加零的评分,因为它将添加一个评分,但是我知道的大多数UI都不允许选择0作为评分,因此您可能需要更改零处理。

Ok, an alternative... 好,另一种...

Do an after_initialize so the fields are never, never, ever nil. 进行一次after_initialize以便字段永远不会,永远不会为零。 Even if you're creating a new Picture object, they'll be initialized as zero. 即使您正在创建新的Picture对象,它们也会被初始化为零。 Problem will go away. 问题将消失。

  class Picture << ActiveRecord::Base

    after_initialize do |picture|
      picture.ratings_count ||= 0
      picture.rating_total ||= 0
    end

    ...
  end

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM