rails7: category 无限分类实现

基于 ruby on rails 的无限分类实现
更新于: 2023-12-08 20:14:49

无限分类

一般分有多层分类。

常用站点的分类

技术点

所在分支: feature/feat-category

rails g model Category name:string parent:references --no-test-framework

修改 migration 文件

  • 注意,这里我去掉了 null: false,因为 category,主分类的 parent 就是 null
  • 添加了 foreign_key: { to_table: :categories },这里表明是自关联,所以需要添加这
category 自关联的实现
class CreateCategories < ActiveRecord::Migration[7.1]
  def change
    create_table :categories do |t|
      t.string :name
      t.references :parent, foreign_key: { to_table: :categories }

      t.timestamps
    end
  end
end

关于 migrations 这一块

网上看到的实现,这个应该更加的简洁,不用 reference,而直接用 parent_id 来。

  1. 可以处理 foreign_key 不能为nil 的尴尬
  2. 可以不用处理 to_table 这个约束
  3. 参考: https://stackoverflow.com/questions/30091996/using-a-parent-child-self-join-in-activerecord
class create_category_table < ActiveRecord::Migration
  def change
    create_table :categories do |t|
      t.integer :parent_id
    end
  end
end 

class Category < ActiveRecord::Base
  has_many :sub_categories, class: Category, foreign_key: :parent_id
  belongs_to :parent, class: Category
end

@parent = Category.new
@child  = @parent.sub_categories.build

调整 model

models/category.rb

解释一下内容

  • 说明关系: belongs_to :parent/has_many :children 是一个主分类,有多个子分类 → 一对多
  • class_name: 'Category' 指定了关联模型的类名是 Category。因为关联的模型是自身,所以需要显式指定类名。
  • optional: true 表示这个关联是可选的,即一个分类可以没有父分类;在我的这个设计中, parent: null 表示,顶层分类
  • dependent: :destroy 表示当父分类被销毁时,与之关联的子分类也会被销毁。
  • foreign_key: :parent_id:当前 category 取 children 的时候,知道用哪个 id(parent_id) 来查换
class Category < ApplicationRecord
  belongs_to :parent, class_name: 'Category', optional: true
  has_many :children, class_name: 'Category', foreign_key: :parent_id, dependent: :destroy
end

添加 seed 数据

root = Category.create(name: "Apparel Store")
shores = Category.create(name: "Shores", parent: root)
clothing = Category.create(name: "Clothing", parent: root)

限制层级

  • 限制网站分类为2级
class Category < ApplicationRecord
  # ... 省略10000行代码

  validate :validate_hierarchy_depth

  private

  def validate_hierarchy_depth
    max_depth = 2

    if depth_of_hierarchy > max_depth
      errors.add(:parent_id, "can't create more than #{max_depth} levels of hierarchy")
    end
  end

  def depth_of_hierarchy
    depth = 0
    node = self

    while node.parent.present?
      depth += 1
      node = node.parent
    end

    depth
  end
end
执行的时候,看一下效果
root = Category.create(name: "Apparel Store")
shores = Category.create(name: "Shores", parent: root)
clothing = Category.create(name: "Clothing", parent: root)

men_shores = Category.create(name: "Men's Shores", parent: shores)
women_shores = Category.create(name:"Women's Shores", parent: shores)

# 这一行已经开始报错了
boy_shores = Category.create(name: "Boy's Shores", parent: men_shores)

生成一个大树

class Category < ApplicationRecord
  # ... 省略10000行代码
  def self.tree
    roots = Category.where(parent_id: nil).includes(:children)
    roots.map { |root| build_category(root) }
  end

  def self.build_category(node)
    {
      id: node.id,
      name: node.name,
      children: node.children.map { |child| build_category(child) }
    }
  end
end
利用 Rails.cache 优化 tree 的查询
class Category < ApplicationRecord
  # ... 省略10000行代码
  def self.tree
    Rails.cache.fetch('category_tree', expires_in: 1.year) do
      roots = Category.where(parent_id: nil).includes(:children)
      roots.map { |root| build_category(root) }
    end
  end

  def self.build_category(node)
    {
      id: node.id,
      name: node.name,
      children: node.children.map { |child| build_category(child) }
    }
  end
end

优化 tree

这里不要这么多次 sql 查询,只有一次 all 的查询,将 build_tree 这个过程放在前端完成。

优化前,多次查询
与数据库保持一致
class Category < ApplicationRecord
  # ... 省略10000行代码
  def self.raw
    Category.all.map { |category| { id: category.id, name: category.name, parent_id: category.parent_id } }
  end
end

参考