再次理解 Flask-SQLAlchemy 中数据模型之间的关联

Author Avatar
patrickcty 7月 29, 2017

再次理解 Flask-SQLAlchemy 中数据模型之间的关联

一对多

Post 表示博客的文章

class Post(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255))
    text = db.Column(db.Text())
    publish_date = db.Column(db.Datetime())
    # 外键的参数是'表名.主键'
    # 也可以是类名.主键(不是字符串)
    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))  

    def __init__(self, title):
        self.title = title

User 表示用户

class User(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    username = db.Column(db.String(255))
    password = db.Column(db.String(255))
    posts = db.relationship(  # 是一个 list,内容是该作者的所有 post 的对象
        'Post',  # Post 此时可能还没定义,所以用字符串作为参数传递
        backref='user',
        # 查询方式,dynamic 表示动态加载
        # 这样在加载完 Post 的时候并不立刻加载与其关联的 User
        # 而是在被使用时才加载
        lazy='dynamic' 
    )

在一个典型的博客系统中,一篇文章的作者只有一个,而一个作者可能有多篇文章,因此 Post 通过外键与 User 建立了联系,User.id 作为了 Post 表的一列存在了表中。而 User 则使用 db.relationship 来和 ForeignKey 来建立联系。注意 db.relationship 定义的内容并不真的存在于数据库表中,只是存在于 SQLAlchemy 中。

用法

通过在 Post 中指定 user_id 来影响 User.posts

>>> user = User.query.get(1)
>>> new_post = Post('Post Title')
>>> new_post.user_id = user.id
>>> db.session.add(new_post)
>>> db.session.commit()
>>> user.posts  # 此时新的 post 会被自动添加到 User 表中
[<Post 'Post Title'>]

通过反向引用来影响 User.posts

>>> second_post = Post('Second Title')
>>> second_post.user = user
>>> db.session.add(second_post)
>>> db.session.commit()
>>> user.posts  # 此时新的 post 也会被自动添加到 User 表中
[<Post 'Post Title'>, <Post 'Second Title'>]

在 relationship 中通过 backref 参数来指定了反向引用,因此在 Post 中只要通过 Post.user 就可以引用对应的 User 内容了,在这里就是通过反向引用来改变了 User.posts 的内容(新绑定了一个 post)

关于 User.posts

User 中的 posts 字段因为是 dynamic 方式,所以既可以看作列表,也可以看做查询对象(可以在后面使用 filter_by 等)

看作列表

>>> user.posts
[<Post 'Post Title'>, <Post 'Second Title'>]

看做查询对象

>>> user.posts.order_by(Post.publish_date.desc()).all()
[<Post 'Second Title'>, <Post 'Post Title'>]

一对多的最后

因为二者进行了绑定,因此对一个的内容(一般是‘多’的那边)进行更改,另一个就可以马上同步而不用自己手动修改

一对一

一对一和一对多类似,不过在声明 relationship 的时候要传入参数 uselist=False

Userinfo 表示没那么重要的数据,对 User 作为补充

class Userinfo(db.Model):
    id = db.Column(db.Integer(), db.ForeignKey('user.id'), primary_key=True)
    age = db.Column(db.String(255))
    introduction = db.Column(db.Text())
    update_date = db.Column(db.Datetime())

User 表示用户

class User(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    username = db.Column(db.String(255))
    password = db.Column(db.String(255))
    userinfo = db.relationship(  
        'Userinfo',
        backref='user',
        uselist=False
    )

一对一的作用主要是副表对主表进行扩展,主表中存放的是相对重要的内容,而副表中存放的是相对没那么重要的内容。副表访问主表数据的方法和一对多相同,但是主表访问附表的方式就多种多样了

通过外键来查询

>>> userinfo = Userinfo.query.get(user.id)

二者建立双向绑定,通过主表反向调用副表

class Userinfo(db.Model):
    id = db.Column(db.Integer(), db.ForeignKey('user.id'), primary_key=True)
    age = db.Column(db.String(255))
    introduction = db.Column(db.Text())
    update_date = db.Column(db.Datetime())
    user = db.relationship(  
        'Use',
        back_populates='userinfo',
        uselist=False
    )

class User(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    username = db.Column(db.String(255))
    password = db.Column(db.String(255))
    userinfo = db.relationship(  
        'Userinfo',
        back_populates='user',
        uselist=False
    )

这里 back_populates 和 backref 意义是相似的,但是 back_populates 必须要在两处都进行说明,这样同时也可以进行双向的反向引用了

因此主表访问附表就可以直接访问了

>>> userinfo = user.userinfo

多对多

数据库模型定义如下

# 直接用 db.Table 来建立表
tags = db.Table(  # 辅助表,用来建立多对多关系
    'post_tags',  # 表名
    db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
    db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'))
)


class Post(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255))
    text = db.Column(db.Text())
    publish_date = db.Column(db.Datetime())
    user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
    tags = db.relationship(  # 声明关系
        'Tag',  # 主要是给 Tag 引用
        secondary=tags,  # 通过 tags 作为中介
        backref=backref('posts', lazy='dynamic')
    )

    def __init__(self, title):
        self.title = title


class Tag(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    title = db.Column(db.String(255))

    def __init__(self, title):
        self.title = title

用法

>>> post_one = Post.query.all()[1]
>>> post_two = Post.query.all()[2]
>>> tag_one = Tag('Python')
>>> tag_two = Tag('SQLAlchemy')
>>> tag_three = Tag('Flask')
>>> post_one.tags = [tag_two]  # 手动创建对应关系
>>> post_two.tags = [tag_one, tag_two, tag_three]  # 里面是对象
>>> tag_two.posts  # 反向引用
[post_one, post_two]  # 里面是对象
>>> db.session.add(post_one)  # 似乎并没有 add tag 到 session
>>> db.session.add(post_two)  # 因为通过回话自动保存了标签
>>> db.session.commit()

在所对多关系中,backref 也变成了 list,可以通过 list 的 append 方法来添加文章到标签

>>> tag_one.posts.append(post_one)
[<Post 'Post Title'>, <Post 'Second Title'>] 
>> post_one.tags
[<Tag 'SQLAlchemy'>, <Tag 'Python'>]
>>> db.session.add(tag)
>>> db.session.commit()

最后

如果要重写 init 方法的话则要注意使用的变量是否已经初始化了,没有初始化的话可能变量值为 None,此时调用就会报错。