1. 程式人生 > >用 Flask 來寫個輕部落格 (7) — (M)VC_models 的關係(many to many)

用 Flask 來寫個輕部落格 (7) — (M)VC_models 的關係(many to many)

目錄

前文列表

擴充套件閱讀

前期準備

在實現多對多之前,我們還需要先增加一個評論(Comment) models class,而且 Comment 是 Post 的關係是 one to many。

  • 首先依舊是在 models.py 中定義模型類:
class Post(db.Model):
    """Represents Proected posts."""

    __tablename__ = 'posts'
    id = db.Column(db.String(45
), primary_key=True) title = db.Column(db.String(255)) text = db.Column(db.Text()) publish_date = db.Column(db.DateTime) # Set the foreign key for Post user_id = db.Column(db.String(45), db.ForeignKey('users.id')) # Establish contact with Comment's ForeignKey: post_id comments = db.relationship( 'Comment'
, backref='posts', lazy='dynamic') def __init__(self, title): self.title = title def __repr__(self): return "<Model Post `{}`>".format(self.title) class Comment(db.Model): """Represents Proected comments.""" __tablename__ = 'comments' id = db.Column(db.String(45
), primary_key=True) name = db.Column(db.String(255)) text = db.Column(db.Text()) date = db.Column(db.DateTime()) post_id = db.Column(db.String(45), db.ForeignKey('posts.id')) def __init__(self, name): self.name = name def __repr__(self): return '<Model Comment `{}`>'.format(self.name)
  • 然後要記住在 manage.py 中返回 Comment
def make_shell_context():
    """Create a python CLI.

    return: Default import object
    type: `Dict`
    """
    return dict(app=main.app,
                db=models.db,
                User=models.User,
                Post=models.Post,
                Comment=models.Comment)
  • 最後在 manager shell 驗證是否成功匯入了 Comment
(env) [root@flask-dev JmilkFan-s-Blog]# python manage.py shell
>>> Comment
<class 'models.Comment'>

多對多

如果我們有兩個 models,它們之間是相互引用的,而且彼此都可以相互引用對方的多個物件。這就是所謂 many to many 的關係型別。

多對多關係會在兩個類之間增加一個關聯表。 這個關聯的表在 relationship() 方法中通過 secondary 引數來表示。通常的,這個表會通過 MetaData 物件來與宣告基類關聯, 所以這個 ForeignKey 指令會使用連結來定位到遠端的表:

posts_tags = db.Table('posts_tags',
    db.Column('post_id', db.String(45), db.ForeignKey('posts.id')),
    db.Column('tag_id', db.String(45), db.ForeignKey('tags.id')))


class Post(db.Model):
    """Represents Proected posts."""

    __tablename__ = 'posts'
    id = db.Column(db.String(45), primary_key=True)
    title = db.Column(db.String(255))
    text = db.Column(db.Text())
    publish_date = db.Column(db.DateTime)
    # Set the foreign key for Post
    user_id = db.Column(db.String(45), db.ForeignKey('users.id'))
    # Establish contact with Comment's ForeignKey: post_id
    comments = db.relationship(
        'Comment',
        backref='posts',
        lazy='dynamic')
    # many to many: posts <==> tags
    tags = db.relationship(
        'Tag',
        secondary=posts_tags,
        backref=db.backref('posts', lazy='dynamic'))

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

    def __repr__(self):
        return "<Model Post `{}`>".format(self.title)


class Tag(db.Model):
    """Represents Proected tags."""

    __tablename__ = 'tags'
    id = db.Column(db.String(45), primary_key=True)
    name = db.Column(db.String(255))

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

    def __repr__(self):
        return "<Model Tag `{}`>".format(self.name)
  • many to many 的關係仍然是由 db.relationship() 來定義
  • seconddary(次級):會告知 SQLAlchemy 該 many to many 的關聯儲存在 posts_tags 表中
  • backref:宣告表之間的關係是雙向,幫助手冊 help(db.backref)。需要注意的是:在 one to many 中的 backref 是一個普通的物件,而在 many to many 中的 backref 是一個 List 物件。

NOTE 1:實際上 db.Table 物件對資料庫的操作比 db.Model 更底層一些。後者是基於前者來提供的一種物件化包裝,表示資料庫中的一條記錄。 posts_tags 表物件之所以使用 db.Table 不使用 db.Model 來定義,是因為我們不需要對 posts_tags (self.name)進行直接的操作(不需要物件化),posts_tags 代表了兩張表之間的關聯,會由資料庫自身來進行處理。

NOTE 2posts_tags 的宣告定義最好在 Post 和 Tag 之前。

NOTE 3: 沒新增一個 models class 都要記得在 manage.py 中匯入並返回,方便之後的除錯,這裡就不作重複了。

  • 同步資料庫
(env) [root@flask-dev JmilkFan-s-Blog]# python manage.py shell
>>> db.create_all()

使用樣例

>>> posts = db.session.query(Post).all()
>>> posts
[<Model Post `Second Post`>, <Model Post `First Post`>]
>>> post_one = posts[1]
>>> post_two = posts[0]

# 例項化 3 個 Tag 的物件
>>> from uuid import uuid4
>>> tag_one = Tag('JmilkFan')
>>> tag_one.id = str(uuid4())
>>> tag_two = Tag('FanGuiju')
>>> tag_two.id = str(uuid4())
>>> tag_three = Tag('Flask')
>>> tag_three.id = str(uuid4())

# 將 Tag 的例項化物件賦值給 Post 例項化物件的 tags 屬性
# 即指定 Tag 和 Post 之間的關聯狀態
# post_one 對應一個 tag
# post_two 對應三個 tags
# tag_one/tag_three 對應一個 post
# tag_two 物件兩個 posts
>>> post_one.tags
[]
>>> post_one.tags = [tag_two]
>>> post_two.tags = [tag_one, tag_two, tag_three]

NOTE: 此時的資料庫中還是隻有原來就已經存在的兩條 posts 記錄,但是還沒有 tags 記錄。這是因為在剛剛例項化的 Tag 物件還沒有被提交,所以不會被寫入到資料庫中。

mysql> select * from posts;
+--------------------------------------+-------------+------+--------------+--------------------------------------+
| id                                   | title       | text | publish_date | user_id                              |
+--------------------------------------+-------------+------+--------------+--------------------------------------+
| 0722c107-296f-4ac5-8133-48f3470d85ca | Second Post | NULL | NULL         | ad7fd192-89d8-4b53-af96-fceb1f91070f |
| 140078fe-c53b-4226-ad47-33734793e47e | First Post  | NULL | NULL         | ad7fd192-89d8-4b53-af96-fceb1f91070f |
+--------------------------------------+-------------+------+--------------+--------------------------------------+
2 rows in set (0.00 sec)

mysql> select * from tags;
Empty set (0.00 sec)

NOTE:再次提交 post_one/post_two 物件。post_one/post_two 物件相應的記錄本就已經存在於資料庫中了為什麼要重新提交呢?
這是因為 post_one/post_two 都被指定了新的關聯屬性 tags,所以提交 post_one/post_two 不僅僅是更新 posts 的引用,更重要的是將新建立的 3 個 tags 物件寫入到資料庫中,同時也是將 posts 和 tags 的對映關係寫入到 posts_tags 表中。

>>> db.session.add(post_one)
>>> db.session.add(post_two)
>>> db.session.commit()

再次檢視資料庫:

mysql> select * from tags;
+--------------------------------------+----------+
| id                                   | name     |
+--------------------------------------+----------+
| 22c46fa9-6899-4851-b95b-cc6267b68c7c | FanGuiju |
| 2a3f5c3f-1e1b-4d4a-89c6-62ea74fed75e | JmilkFan |
| 76b38f31-6a23-4acd-b252-fc580e46d186 | Flask    |
+--------------------------------------+----------+
3 rows in set (0.00 sec)

mysql> select * from posts_tags;
+--------------------------------------+--------------------------------------+
| post_id                              | tag_id                               |
+--------------------------------------+--------------------------------------+
| 140078fe-c53b-4226-ad47-33734793e47e | 22c46fa9-6899-4851-b95b-cc6267b68c7c |
| 0722c107-296f-4ac5-8133-48f3470d85ca | 76b38f31-6a23-4acd-b252-fc580e46d186 |
| 0722c107-296f-4ac5-8133-48f3470d85ca | 2a3f5c3f-1e1b-4d4a-89c6-62ea74fed75e |
| 0722c107-296f-4ac5-8133-48f3470d85ca | 22c46fa9-6899-4851-b95b-cc6267b68c7c |
+--------------------------------------+--------------------------------------+
4 rows in set (0.00 sec)

NOTE:在上面說過了 many to many 的 backref 是一個 List 物件,所以我們還可以反過來為 tags 新增一個 posts 物件(引用)。

>>> tag_one.posts.all()
[<Model Post `Second Post`>]
>>> tag_one.posts.append(post_one)
>>> tag_one.posts.all()
[<Model Post `Second Post`>, <Model Post `First Post`>]
>>> post_one.tags
[<Model Tag `FanGuiju`>, <Model Tag `JmilkFan`>]
# 因為修改了 tag_one 的 posts 屬性(添加了 post_one 的引用),所以需要重新提交 tag_one 才會被寫入到資料庫。
>>> db.session.add(tag_one)
>>> db.session.commit()

再次檢視資料庫的記錄:

mysql> select * from tags;
+--------------------------------------+----------+
| id                                   | name     |
+--------------------------------------+----------+
| 22c46fa9-6899-4851-b95b-cc6267b68c7c | FanGuiju |
| 2a3f5c3f-1e1b-4d4a-89c6-62ea74fed75e | JmilkFan |
| 76b38f31-6a23-4acd-b252-fc580e46d186 | Flask    |
+--------------------------------------+----------+
3 rows in set (0.00 sec)

mysql> select * from posts;
+--------------------------------------+-------------+------+--------------+--------------------------------------+
| id                                   | title       | text | publish_date | user_id                              |
+--------------------------------------+-------------+------+--------------+--------------------------------------+
| 0722c107-296f-4ac5-8133-48f3470d85ca | Second Post | NULL | NULL         | ad7fd192-89d8-4b53-af96-fceb1f91070f |
| 140078fe-c53b-4226-ad47-33734793e47e | First Post  | NULL | NULL         | ad7fd192-89d8-4b53-af96-fceb1f91070f |
+--------------------------------------+-------------+------+--------------+--------------------------------------+
2 rows in set (0.00 sec)

mysql> select * from posts_tags;
+--------------------------------------+--------------------------------------+
| post_id                              | tag_id                               |
+--------------------------------------+--------------------------------------+
| 140078fe-c53b-4226-ad47-33734793e47e | 22c46fa9-6899-4851-b95b-cc6267b68c7c |
| 0722c107-296f-4ac5-8133-48f3470d85ca | 76b38f31-6a23-4acd-b252-fc580e46d186 |
| 0722c107-296f-4ac5-8133-48f3470d85ca | 2a3f5c3f-1e1b-4d4a-89c6-62ea74fed75e |
| 0722c107-296f-4ac5-8133-48f3470d85ca | 22c46fa9-6899-4851-b95b-cc6267b68c7c |
| 140078fe-c53b-4226-ad47-33734793e47e | 2a3f5c3f-1e1b-4d4a-89c6-62ea74fed75e |
+--------------------------------------+--------------------------------------+
5 rows in set (0.00 sec)

NOTE: 從 posts_tags 的記錄中可以看出 posts 和 tags 之間的多對多關係。

獲取文章 First Post 下有哪些標籤:

>>> db.session.query(Post).filter_by(title='First Post').first().tags
[<Model Tag `JmilkFan`>, <Model Tag `FanGuiju`>]

獲取標籤 JmilkFan 下有哪些文章

>>> db.session.query(Tag).filter_by(name='JmilkFan').first().posts.all()
[<Model Post `First Post`>, <Model Post `Second Post`>]

NOTE:再次說明一下,在定義 models 間關係時使用的 backref 引數,指定了載入關聯物件的方式(這裡使用了動態方式),所以載入 Tag 的關聯物件 Post 時,返回的是 sqlalchemy.orm.dynamic.AppenderBaseQuery object 而不是全部的關聯物件。

那麼為什麼反之卻是直接返回全部的關聯物件呢?
這是因為我們是在 Post 中使用了 backref 物件,所以對於兩者的關係而言,backref 指的是 Tag。

>>> db.session.query(Post).filter_by(title='First Post').first().tags
[<Model Tag `JmilkFan`>, <Model Tag `FanGuiju`>]
>>> db.session.query(Tag).filter_by(name='JmilkFan').first().posts
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x38ff850>

一直在使用的 session

session 是連線到資料庫的一個橋樑,實際上 session 具有更多的功能,例如:事務
事務:是對資料庫進行操作即集合,在我們 commit 的時候,實際事務幫我們實現了一系列有效的資料庫操作。
例如:剛剛我們在 commit 一個 post_one/post_two 前,明明沒有 commit tag_one/tag_two/tag_three,為什麼資料庫中還會寫入這三條記錄呢?這些都是由事務去幫我們進行的隱式的資料庫操作。如果沒有事務我們就需要按部就班一步步的完成對資料庫的寫入,這樣的效率是非常低的。除此之外 SQLAlchemy 的 session 還會提供很多有用的功能,感興趣的話可以繼續挖掘,這裡就不多做介紹了。