无限级分类一直是Web项目的一个重点,尤其是电商项目。无限级分类有很多实现方法,其中一个就是通过父子关系进行递归的实现,此方法的核心在于,通过一个 parent_id
的字段,来识别某一分类的父分类。如果该字段为 NULL
(或其它特殊值,比如0
),表示该分类是一级分类;反之,如果该字段不是NULL
,表示该分类隶属于某个分类。
如果用 SQL 表示,它的结构如下:
字段 | 数据类型 | 说明 |
---|---|---|
id |
CHAR(36) |
唯一标识,主键。我们使用 UUID |
name |
VARCHAR(30) |
分类名称 |
parent_id |
CHAR(36) |
父分类id,外键,指向本表的其它记录,如果是一级分类,值为 NULL ,否则,值为具体的父分类的id |
我们可以看到,在这个结构中,parent_id
实际上是指向本表的一个引用,这称为自引用
。
SQLAlchemy
是一个 Python 里非常强大的 ORM 库。现在的问题是,如何使用它来描述?我们的项目使用的是 Flask
,所以,我们使用封装好的 Flask-SQLAlchemy
来作演示。
首先,定义 Model:
# -*- coding: utf-8 -*- # wrdll <[email protected]> # _ _____ ___ __ __ # | | /| / / _ \/ _ \/ / / / # | |/ |/ / , _/ // / /__/ /__ # |__/|__/_/|_/____/____/____/ # from app import db from uuid import uuid4 def gen_pk(): """生成主键""" return str(uuid4()) class Category(db.Model): """分类""" __tablename__ = 'category' id = db.Column(db.CHAR(36), primary_key=True, default=gen_pk) name = db.Column(db.String(30), comment='分类名称') # 外键 parent_id = db.Column(db.CHAR(36), db.ForeignKey('category.id', ondelete='CASCADE'), comment='上级分类id') # 关系 parent = db.relationship('Category', backref='child', remote_side=[id])
定义好 Model 之后,生成数据库迁移脚本,并更新数据库(需要安装 Flask-Migrate
):
$ export FLASK_APP=runner.py $ # 如果你是第一次执行数据库迁移 $ # 请先执行: $ # flask db init $ flask db migrate $ flask db upgrade
下面是各步骤的结果:
flask db init
:flask db init Creating directory /Users/wrdll/PycharmProjects/flask-layground/migrations ... done Creating directory /Users/wrdll/PycharmProjects/flask-layground/migrations/versions ... done Generating /Users/wrdll/PycharmProjects/flask-layground/migrations/script.py.mako ... done Generating /Users/wrdll/PycharmProjects/flask-layground/migrations/env.py ... done Generating /Users/wrdll/PycharmProjects/flask-layground/migrations/README ... done Generating /Users/wrdll/PycharmProjects/flask-layground/migrations/alembic.ini ... done Please edit configuration/connection/logging settings in '/Users/wrdll/PycharmProjects/flask- layground/migrations/alembic.ini' before proceeding.
flask db migrate
:INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'category' Generating /Users/wrdll/PycharmProjects/flask-layground/migrations/versions/465ac59a70db_.py ... done
flask db upgrade
:INFO [alembic.runtime.migration] Context impl PostgresqlImpl. INFO [alembic.runtime.migration] Will assume transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> 465ac59a70db, empty message
下面,我在入口文件中(本例为 runner.py
,也就是 FLASK_APP
环境变量指向的模块)添加一个 函数 demo_create_category
用于添加分类
# -*- coding: utf-8 -*- # wrdll <[email protected]> # _ _____ ___ __ __ # | | /| / / _ \/ _ \/ / / / # | |/ |/ / , _/ // / /__/ /__ # |__/|__/_/|_/____/____/____/ # from app import create_app, db from flask_migrate import Migrate from app.models import Category app = create_app('default') migrate = Migrate(app=app, db=db) @app.shell_context_processor def shell_context_processor(): return dict(app=app, db=db, Category=Category) @app.cli.command() def demo_create_category(): """演示分类的自引用(一对多)之添加""" # 一级分类 top_cats = ['编程开发', '小姐姐'] # 二级分类 sub_cats = [['Python', 'Go', 'Java', 'C'], ['寿险你昳姐', '温婉', '于小仙', '空姐芊芊']] for i, v in enumerate(top_cats): top = Category() top.name = v for sub in sub_cats[i]: sub_cat = Category() sub_cat.name = sub sub_cat.parent = top # 站在子分类的角度 # 上面的语句效果和:top.child.append(sub_cat) 一样,两条语句二选一,站在的角度不同而已,本条语句站在父分类的角度。 db.session.add(sub_cat) db.session.add(top) db.session.commit()
再添加一个 demo_tree
用于生成树:
@app.cli.command() def demo_tree(): """通过一级分类生成树(只生成二级)""" cat_lst = Category.query.filter(Category.parent == None).all() for cat in cat_lst: print('+', cat.name) for sub_cat in cat.child: print('\t-', sub_cat.name)
结果如下:
+ 编程开发 - Python - Go - Java - C + 小姐姐 - 寿险你昳姐 - 温婉 - 于小仙 - 空姐芊芊
上面是由父分类找到所有子分类,如何通过子分类找到父分类?方法如下:
@app.cli.command() def demo_get_parent(): """通过子分类找到父分类""" sub_cat = 'Python' # 子分类的名称 sub = Category.query.filter_by(name=sub_cat).first() parent = sub.parent print(parent.name)
结果:
编程开发
本例通过一个2级分类来演示了自引用一对多的实现方式。如果你需要将该方法应到实际项目的无限级分类中,你需要做:
path
等方法来提升性能红黑树
、PATH
等本文重点在于介绍如何在 Flask-SQLAlchemy 中定义自引用关系,难点是关系的定义:parent = db.relationship('Category', backref='child', remote_side=[id])
。如果你想了解自引用的多对多关系,请阅读《第12章:关注-Flask Web 开发(第2版)》