代码之家  ›  专栏  ›  技术社区  ›  Darkmouse

RubyonRails-NilClass的未定义方法

  •  0
  • Darkmouse  · 技术社区  · 10 年前

    我正在创建一个图片评级应用程序,用户可以点击图片并按1到5的比例对其进行评级。我正在计算一张照片的平均评分。在用户点击评级值之前,该值成为图片的评级。

    Rating: 5
    

    如果用户单击1,则评级将更改为1

    Rating: 1
    

    现实情况下,评级应该是3。

    (5 + 1) / 2
    => 3
    

    以下是我迄今为止在实现此功能方面所取得的成就。

    我添加了一个迁移,以便为图片表创建两个新列

    rails g migration AddRatingsToPictures ratings_count: integer, rating_total: integer
    

    新属性ratings_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
    

    我唯一的问题是NilClass错误。

    这是我的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
    

    以下是PicturesController中的picture_param方法

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

    下面是两个新列的作用

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

    在上面的代码中,如果图片没有评级,我首先将ratings_count设置为0。这意味着该图片尚未评级。

    然后,我需要首先将rating_total设置为图片中的星星数。如果用户更改了星级,我会将这些星级添加到rating_total中。如果总数增加了,那就是我提高收视率的提示。

    显然,为了计算平均值,我会这样做。

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

    现在,我想我有正确的想法,但我知道为什么这行不通。当使用整数值创建列时,默认情况下它们设置为零。当我加载网页时,这会导致NilClass错误。

    undefined method `/' for nil:NilClass
    

    这是我在视图中的代码

    <li><strong>Rating:</strong> <%= pluralize((@picture.rating_total / @picture.ratings_count), 'Star') %></li>
    
    4 回复  |  直到 10 年前
        1
  •  1
  •   nathanvda    10 年前

    好的,它不起作用的主要原因是

    • 你去拿照片
    • 你检查 stars 以及NOT传递的表单参数
    • 您可以执行update_attributes,如果我没有弄错的话,它用于设置属性,然后保存完整的对象,但是由于rails 4只更新传递的属性(这是您所期望的)

    有一点小意思:保持额定值正确是我将放在模型中的一个功能,而不是放在控制器中。

    此外,如何处理 如果为零,则初始化为零 我写了一篇短文 blogpost 关于简而言之:否决吸气者。

    因此,我提出以下解决方案。在你的模型中写下

    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
    

    然后控制器中的代码变得更加清晰易读:

    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
    

    我首先删除 :stars 因为我不想保存这些参数,所以我想将这些参数用于 add_rating 。然后我试着 update_attributes ,如果有任何失败的验证,这将失败,如果可以,我将 添加速率(_R) 其本身将正确地处理零或零。当然:我不知道你如何处理“非评级”(零?零?)。可能应该添加0的评级,因为它会添加评级,但我知道的大多数UI都不允许选择0作为评级,因此您可能需要更改零处理。

        2
  •  1
  •   SteveTurczyn    10 年前

    这将处理属性中未初始化(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
    

    您不需要将一系列评分或评分保存到数据库中,假设您只在评分发生变化的地方计算选票,您可以将计数和总数相加,然后将两者相加(事实上,这就是您正在做的,所以我向转换的人宣讲)。

    虽然在我看来,如果我将一张图片从5改为1,而它只改为3,我会继续单击1:)

        3
  •  1
  •   Lucas Nogueira    10 年前

    您可以在创建迁移时设置默认值。但不用担心,您可以创建一个新的迁移来更改它:

    # 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
    

    请记住,有些数据库不会自动将新更新的默认值分配给现有的列条目,因此您可能需要遍历已创建的每一个具有nil值并设置为0的图片。

        4
  •  1
  •   SteveTurczyn    10 年前

    好的,另一种选择。。。

    执行 after_initialize 因此,这些领域从来都不是零。即使您正在创建新的Picture对象,它们也将初始化为零。问题会消失的。

      class Picture << ActiveRecord::Base
    
        after_initialize do |picture|
          picture.ratings_count ||= 0
          picture.rating_total ||= 0
        end
    
        ...
      end