Web API中,使用Jbuilder来给你的controller瘦个身

瘦身无处不在。

写在前面

MVC 框架中的一个设计原则:fat model, skinny controller。且不说这原则是不是绝对的正确,不过面对臃肿的controller,IDE也会发出提醒表示抗议。

比如在Web API中,一般用get获取资料的时候,返回的JSON中,字段会比较多,相应地,你在controller里面写的index或者show 也相对有些臃肿。那么如何简化?

试试Jbuilder

下面用个简单的例子来说明下怎么给controller瘦身。

正文

假定你完成了一个products的API接口,对应的app/controllers/api/v1/products_controller.rb文件内容如下:

class Api::V1::ProductsController < ApiController
  before_action :authenticate_user!, only: %i[index show create update destroy]

  def index
    @products = Product.all
    render json: {
        data: @products.map do |product|
          {
              id: product.id,
              creator: product.user.user_name,
              name: product.name,
              description: product.description,
              price: product.price
          }
        end
    }
  end
  def create
    @product = Product.new(
        name: params[:name],
        description: params[:description],
        price: params[:price]
    )
    @product.user = current_user
    if @product.save
      render json: @product
    else
      render json: { message: 'failed', errors: @product.errors }, status: 400
    end
  end

  def show
    @product = Product.find(params[:id])
    render json: {
        id: product.id,
        creator: product.user.user_name,
        name: product.name,
        description: product.description,
        price: product.price
    }
  end

  def update
    @product = Product.find(params[:id])
    @product.update(name: params[:name], description: params[:description],price: params[:price])
    render json: {
        message: 'update product successfully'
    }
  end

  def destroy
    @product = Product.find(params[:id])
    @product.destroy
    render json: {
        message: 'product deleted'
    }
  end
end  

看着很冗长啊,怎么瘦?

分三步走:

  • 对index, show部分,用Jbuilder,
  • 对create,update部分,用 strong params
  • 对show, update, destory部分共有的@product,抽出来,用before_action

第一步:简化index,show

这里可以看到show和index中重复代码较多,用上partial

  • 新建文件:
    app/views/api/v1/products/_item.json.jbuilder, app/views/api/v1/products/show.json.jbuilder,
    app/views/api/v1/products/index.json.jbuilder,

  • app/views/api/v1/products/_item.json.jbuilder,添加如下内容:

    json.id  product.id
    json.creator product.user.user_name
    json.name product.name
    json.description product.description
    json.price product.price
    
  • app/views/api/v1/products/show.json.jbuilder,添加如下内容:

    json.partial! 'item', product: @product
    
  • app/views/api/v1/products/index.json.jbuilder, 添加如下内容:

    json.array! @products, partial: 'item', as: :product
    
  • 修改app/controllers/api/v1/products_controller.rb的index和show部分, 删除render:

    class Api::V1::ProductsController < ApiController
      before_action :authenticate_user!, only: %i[index show create update destroy]
    
      def index
        @products = Product.all
      end
     .......
    
      def show
        @product = Product.find(params[:id])
      end
    
    .......
    end  
    

    可以用postman测试一下,确保接口仍正常。

第二步:简化create,update, 用params.require

  • 修改app/controllers/api/v1/products_controller.rb的create和update部分,如下:

    class Api::V1::ProductsController < ApiController
      before_action :authenticate_user!, only: %i[index show create update destroy]
      .....
      def create
        @product = Product.new(product_params)
        @product.user = current_user
        if @product.save
          render json: @product
        else
          render json: { message: 'failed', errors: @product.errors }, status: 400
        end
      end
     .......
    
      def update
        @product = Product.find(params[:id])
        @product.update(product_params)
        render json: {
            message: 'update product successfully'
        }
      end
    
    .......
      private
    
      def product_params
        params.permit(:name, :description, :price)
      end         
    end  
    

    注意,这里**不能使用params.requrie(:product).permit(:name, :description, :price)**,因为你是调用接口来新建或者修改的。

    可参见这个:When JSON data posted in rails throws param is missing or the value is empty: stall

第三步:简化show,update, destroy

  • 把show,update, destroy相同的部分: @product = Product.find(params[:id])抽出来:

    class Api::V1::ProductsController < ApiController
      before_action :authenticate_user!, only: %i[index show create update destroy]
      before_action :find_product, only: %i[show update destroy]
    .....
      def show; end
    
      def update
        @product.update(product_params)
        render json: {
            message: 'update product successfully'
        }
      end
    
      def destroy
        @product.destroy
        render json: {
            message: 'product deleted'
        }
      end
    
      private
    .......
    
      def find_product
        @product = Product.find(params[:id])
      end
    end
    

最后瘦身的controller长这样:

class Api::V1::ProductsController < ApiController
  before_action :authenticate_user!, only: %i[index show create update destroy]
  before_action :find_product, only: %i[show update destroy]

  def index
    @products = Product.all
  end
  def create
    @product = Product.new(product_params)
    @product.user = current_user
    if @product.save
      render json: @product
    else
      render json: { message: 'failed', errors: @product.errors }, status: 400
    end
  end

  def show; end

  def update
    @product.update(product_params)
    render json: { message: 'update product successfully'}
  end

  def destroy
    @product.destroy
    render json: { message: 'product deleted'}
  end

  private

  def product_params
    param.permit(:name, :description, :price)
  end

  def find_product
    @product = Product.find(params[:id])
  end
end

是不是清爽了很多?看着舒服多了,大功告成!

这里多一句,如果你的某个method部分代码较多,也可以使用拆分的方式, 比如下面这个show:

def show
  @category = Category.find(params[:id])
  @categories = Category.all
  @search = @category.products.approved.order(updated_at: :desc).ransack(params[:q])
  @products = @search.result.page(params[:page]).per(50)
  rate
end

可以换成:

before_action :fetch_current_category, only: [:show]
before_action :fetch_categories, only: [:show]
before_action :fetch_search_results, only: [:show]

def show
  rate
end

private

def fetch_current_category
  @category = Category.find(params[:id])
end

def fetch_categories
  @categories = Category.all
end

def fetch_search_results
  @search = @category.products.approved.order(updated_at: :desc).ransack(params[:q])
  @products = @search.result.page(params[:page]).per(50)
end

参考

What is meant by ‘Assignment Branch Condition Size too high’ and how to fix it?

对于”fat model, skinny controller“这个原则,也有人反对,提出**”skinny everything”**, 戳链接了解:

“Fat model, skinny controller” is a load of rubbish