Flask-SQLAlchemy 自引用一对多

无限级分类一直是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级分类来演示了自引用一对多的实现方式。如果你需要将该方法应到实际项目的无限级分类中,你需要做:

  • 使用递归来生成整个树。例中只生了2级树
  • 使用缓存来提升性能。递归是非常耗资源的,缓存是必不可少的。
  • 结合 path 等方法来提升性能
  • 学习其它无限级分类的原理,比如红黑树PATH

本文重点在于介绍如何在 Flask-SQLAlchemy 中定义自引用关系,难点是关系的定义:parent = db.relationship('Category', backref='child', remote_side=[id])。如果你想了解自引用的多对多关系,请阅读《第12章:关注-Flask Web 开发(第2版)

flask SQLAlchemy 2018-06-06 20:09 4556229