首页 > 解决方案 > Ruby Rails 在 ERB 搜索最佳实践中创建变量

问题描述

我有一个带有搜索功能的项目。我有三个类,产品、评论和用户。我在产品模型中有一个 self.search 方法对所有三个类表进行查询。

当我将搜索视图放在一起时,它列出了产品名称、评论作者以及评论中与搜索字符串匹配的任何文本。

然后我想,让漂亮一点。

所以我交叉引用 id 来列出用户头像、时间戳和星级。但要做到这一点,我要在所有类中搜索局部变量。

所以我认为现在这里有一股代码味道。没有控制器可以使用@users = User.all 加载。我坚持使用 _user = User.find_by 类型的局部变量赋值。搜索手指现在在所有类别中。

所以我的问题是,我的臃肿搜索现在是否应该被正确地搭建到一个实际的班级中?或者有没有像助手这样的另一种方法?这有可能不是那么糟糕的方法吗?

红宝石 2.6.5 轨道 5.2.4

这是我的看法:

<div class="home-container">
  <div class="welcome-sub-header">
    <h1>Search Results</h1>

    <div class='search-flex-container'>
      <h2>Products that Match</h2>
        <% if @results_products.empty?%>
          <div class='search-noresult-card'>
            <p>No products matched the search.</p>
          </div>
        <% end %>
        <% @results_products.each do |results_product| %>
          <div class='search-product-card'>

            <div class='search-product-flex'>

              <%= render partial: "shared/product_image", locals: { product: results_product } %>

              <%= link_to results_product.name, product_path(results_product.id) %>
              <p>Imported From <%= results_product.country  %></p>

              <div class="search-adjust-stars">
                <%= render partial: "shared/review_stars", locals: { review: results_product.average_review } %>
              </div>

            </div>
          </div>
        <% end %>
        <%= will_paginate @results_products %>

      <h2>Product Reviews that Match</h2>
        <% if @results_reviewers.empty?%>
          <div class='search-noresult-card'>
            <p>No reviewers matched the search.</p>
          </div>
        <% end %>


        <% @results_reviewers.each do |results_reviewer| %>
          <div class='search-review-card'>
            <div class='search-review-flex'>

              <% _reviewed_product = Product.find_by(id: results_reviewer.product_id) %>
              <% _user = User.find_by(id: results_reviewer.user_id) %>

              <div class="search-review-box1">

                <%= render partial: "shared/avatar", locals: { user: _user } %>

                <%= link_to (results_reviewer.author + " review of " + _reviewed_product.name), review_path(id: results_reviewer, product_id: results_reviewer.product_id) %><br>
              </div>

              <div class="search-review-box2">
                <%= render partial: "shared/review_stars", locals: { review: results_reviewer.rating } %>
              </div>

              <div class="search-review-box3">
                <p>On <%= results_reviewer.created_at.strftime('%m-%d-%Y') %></p>
              </div>

              <div class="search-review-box5">

                <%= render partial: "shared/product_image", locals: { product: _reviewed_product } %>

              </div>

            </div>
          </div>  
        <% end %>
        <%= will_paginate @results_reviewers %>

      <h2>Review Text that Matches</h2>
        <% if @results_reviews.empty?%>
          <div class='search-noresult-card'>
            <p>No review text matched the search.</p>
          </div>
        <% end %>
        <% @results_reviews.each do |results_review| %>
          <% _user = User.find_by(id: results_review.user_id) %>
          <div class='search-review-card'>

            <%= render partial: "shared/avatar", locals: { user: _user } %>

            <% _reviewed_product = Product.find_by(id: results_review.product_id) %>
            <%= link_to (results_review.author + " review of " + _reviewed_product.name), review_path(id: results_review, product_id: results_review.product_id) %>
            <p>Reviewed on <%= results_review.created_at.strftime('%m-%d-%Y') %></p>
            <div class="search-review-text">
              <%= results_review.content_body %>
            </div>
          </div>
        <% end %>
        <%= will_paginate @results_reviews %>
    </div>

  </div>  

  <div>Font made from <a href="http://www.onlinewebfonts.com">oNline Web Fonts</a>is licensed by CC BY 3.0</div>

</div>

这是具有 self.search 方法的 Product 模型。

class Product < ApplicationRecord
  has_many :reviews, dependent: :destroy
  validates :name, presence: true
  validates_length_of :name, maximum: 30
  validates :price, presence: true
  validates_length_of :price, maximum: 8
  validates :country, presence: true
  validates_length_of :country, maximum: 50

  has_one_attached :product_photo

  before_save(:titleize_product)

  scope :most_reviewed, -> {(
    select("products.id, products.name, products.average_review,products.price, products.country, count(reviews.id) as reviews_count")
    .joins(:reviews)
    .group("products.id")
    .order("reviews_count DESC")
    .limit(6)
    )}

  scope :newest_product, -> {  order(created_at: :desc).limit(6) }

  scope :highest_reviewed, -> {(
    select("products.id, products.name, products.price, products.country, products.average_review as average_review")
    .joins(:reviews)
    .group("products.id")
    .order("average_review DESC")
    .limit(6)
    )}

  def self.search(search)
    where("lower(reviews.author) LIKE :search OR lower(products.name) LIKE :search OR lower(reviews.content_body) LIKE :search", search: "%#{search.downcase}%").uniq
  end

  def next
    Product.where("id > ?", id).order("id ASC").first || Product.first
  end

  def previous
    Product.where("id < ?", id).order("id DESC").first || Product.last
  end

  private
    def titleize_product
      self.name = self.name.titleize
      self.country = self.country.titleize
    end
end

标签: ruby-on-rails

解决方案


我认为您应该将搜索方法分离到可搜索模块中,并允许可搜索模型声明搜索查询中涉及的属性。

module Searchable
  extend ActiveSupport::Concern

  included do
    @search_clauses ||= Hash.new
  end

  module ClassMethods
    def search(key_search, scope: 'default')
      search_clause ||= search_string_for(scope)
      relation = joins(*search_clause[:joins].map(&:to_sym)) unless search_clause[:joins].blank?
      (relation || self).where(search_clause[:where], key: "%#{key_search.downcase}%")
    end

    def search_clause(clause_str, scope: 'default')
      @search_clauses[scope] = parse(clause_str)
      # we can add dynamic function like seach_`scope_name` 
    end

    def search_attributes(attributes, scope: 'default')
      search_clause(attributes.map { |attribute| "%#{attribute}%"}.join(" OR "), scope: scope)
    end

    private

    def search_string_for(scope)
      @search_clauses&.dig(scope)
    end

    def parse(clause)
      join_tables = Set.new
      where_clause = clause.dup
      parse_tables(clause) do |table|
        join_tables << table unless table == self.class_name.tableize || table.blank?
        where_clause.sub!("#{table}.", "#{table.tableize}.")
      end
      parse_logics!(where_clause)
      parse_matches!(where_clause)
      # other cases ...
      {
        joins: join_tables,
        where: where_clause
      }
    end

    def parse_logics!(clause)
      clause.gsub!(/\|/, "OR")
      clause.gsub!(/\&/, "AND")
      # other ...
    end

    def parse_matches!(clause)
      clause.scan(/(?<=%)[^\s\|\&\z\Z]*(?=%)/).each do |attribute|
        clause.sub!(/%[^\s\|\&\z\Z]*%/, "lower(#{attribute}) LIKE :key")
      end
    end

    def parse_tables(clause, &block)
      clause.scan(/(?<=[%\s])[^%\s\|\&\z]*(?=\.)/).each(&block)
    end
  end
end

我的实践项目中的示例:

class Task < ApplicationRecord
  include Searchable
  belongs_to :requirement
  has_many   :skills

  # declare search clause, we can set the whole where-clause here
  # those syntax %,|,& ... just for fun (simple way)
  search_clause "%content% | %requirement.description%"
  # with scope
  search_clause "(%content% | %requirement.description%) & status = 0", scope: :open

  # declare attributes
  search_attributes %w[content skills.name], scope: :skill
end

# Demo
Task.search("setup") # default scope
Task.search("setup", scope: :open)
Task.search("ruby", scope: :skill)

注意:我建议您使用 search-gems,例如 ransack、pg_search(在全文搜索的情况下)。


推荐阅读