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

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

目錄

前文列表

擴充套件閱讀

前言

models 中的關係能夠對映成為關係型資料庫表中的關係,models 中可以相互建立引用,使得相關聯的資料能夠很容易的一次性的從資料庫中取出。

一對多

  • 首先繼續在 models.py 中建立一個 Post models 來表示 Blog 中的文章。而且一個使用者 User 可以擁有多篇文章 Post,他們之間的關係是一對多。

表示一對多的關係時,在子表類 Post 中需要通過 foreign key (外來鍵)引用父表類 User。

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')) def
__init__(self, title):
self.title = title def __repr__(self): return "<Model Post `{}`>".format(self.title)

其中 user_id 欄位是 posts 表的外來鍵,代表了資料庫中的一種約束規則 —— 外來鍵約束。這種規則強制規定了欄位 user_id 的值必須同時存在於 User.id 列中。用來保證每一篇 post 都能對應找到一個 user,而且一個 user 能夠對應多篇 posts。

NOTE: 如果你沒有在父表類指定 __tablename__

屬性,那麼這一條語句我們應該這麼寫:

user_id = db.Column(db.String(45), db.ForeignKey('User.id'))

但是一般不建議寫成這樣,因為在 SQLAlchemy 初始化期間, User 物件可能還沒有被創建出來,所以同時也建議在定義 models class 的時候應該指定 __tablename__ 屬性。

  • 然後我們還需要在父表類 User 中定義出這種 one to many 的關係:
class User(db.Model):
    """Represents Proected users."""

    # Set the name for table
    __tablename__ = 'users'
    id = db.Column(db.String(45), primary_key=True)
    username = db.Column(db.String(255))
    password = db.Column(db.String(255))
    # Establish contact with Post's ForeignKey: user_id
    posts = db.relationship(
        'Post',
        backref='users',
        lazy='dynamic')

    def __init__(self, id, username, password):
        self.id = id
        self.username = username
        self.password = password

    def __repr__(self):
        """Define the string format for instance of User."""
        return "<Model User `{}`>".format(self.username)
  • db.relationsformat(self.username)hip: 會在 SQLAlchemy 中建立一個虛擬的列,該列會與 Post.user_id (db.ForeignKey) 建立聯絡。這一切都交由 SQLAlchemy 自身管理。

  • backref:用於指定表之間的雙向關係,如果在一對多的關係中建立雙向的關係,這樣的話在對方看來這就是一個多對一的關係。

  • lazy:指定 SQLAlchemy 載入關聯物件的方式。

    • lazy=subquery: 會在載入 Post 物件後,將與 Post 相關聯的物件全部載入,這樣就可以減少 Query 的動作,也就是減少了對 DB 的 I/O 操作。但可能會返回大量不被使用的資料,會影響效率。
    • lazy=dynamic: 只有被使用時,物件才會被載入,並且返回式會進行過濾,如果現在或將來需要返回的資料量很大,建議使用這種方式。Post 就屬於這種物件。

再一次 sync db

每一次新增了 models class,都需要匯入到 manage.py 中,在通過 manager shell 來同步資料庫。

# import Flask Script object
from flask.ext.script import Manager, Server
import main
import models

# Init manager object via app object
manager = Manager(main.app)

# Create some new commands
manager.add_command("server", Server())

@manager.shell
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)

if __name__ == '__main__':
    manager.run()

NOTE: 因為前面我們對原有的 users 表結構做了修改,所以我們需要將 users 表刪除,再重新同步資料庫。但需要注意,這是一種非常不推薦的方法,以後我們會介紹一種更加科學的增量同步資料庫的方法。

mysql> DROP TABLE users;
  • 重新同步資料庫
(env) [root@flask-dev JmilkFan-s-Blog]# python manage.py shell
>>> db.create_all()
>>> Post
<class 'models.Post'>
mysql> show tables;
+------------------+
| Tables_in_myblog |
+------------------+
| posts            |
| users            |
+------------------+
2 rows in set (0.00 sec)

mysql> desc posts
;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| id           | varchar(45)  | NO   | PRI | NULL    |       |
| title        | varchar(255) | YES  |     | NULL    |       |
| text         | text         | YES  |     | NULL    |       |
| publish_date | datetime     | YES  |     | NULL    |       |
| user_id      | varchar(45)  | YES  | MUL | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
5 rows in set (0.00 sec)

How to use

>>> from uuid import uuid4
# 例項化一個 User 的物件
>>> user = User(id=str(uuid4()), username='jmilkfan', password='fanguiju')
# 寫入一條 users 記錄     
>>> db.session.add(user)
>>> db.session.commit()

>>> user.posts
<sqlalchemy.orm.dynamic.AppenderBaseQuery object at 0x22bc410>

NOTE: 因為 user 的關聯物件的載入方式為動態方式,所以 user.posts 會返回一個 Query 物件,需要呼叫 filter()/all()/first() 來獲取實際需要被使用到的物件。
反之,如果是子查詢方式的話,就是直接將關聯物件全部返回

# 現在因為還沒有新增 posts 的記錄所以為空
>>> user.posts.all()
[]

# 例項化一個 Post 的物件
>>> post_one = Post('First Post')
# 主鍵值是非空的,必須指定一個,否則會報錯
>>> post_one.id = str(uuid4())
# 指定該 post 是屬於哪一個 user 的
>>> post_one.user_id = user.id
>>> db.session.add(post_one)
>>> db.session.commit()

>>> user.posts.all()
[<Model Post `First Post`>]

NOTE: 必須在 commit 了 post_one 物件之後,user 才能夠通過關係來獲取關聯物件 posts。

上面一個例子是為 user 新增一個 post,那麼反過來能不能為一個 post 指定一個 user 呢?
如果我們有使用到 backref 引數的話,那答案就是肯定的,這也是該引數所謂 雙向 的含義。

# 獲取一個已經存在資料庫中的記錄 user 
>>> user = db.session.query(User).first()
>>> user.id
u'ad7fd192-89d8-4b53-af96-fceb1f91070f'

# 例項化一個 Post 的物件 post_second
>>> post_second = Post('Second Post')
# 必須為其設定主鍵值
>>> post_second.id = str(uuid4())
# 現在該 post_second 物件是沒有關聯到任何 user 的
>>> post_second.users
# 為 post_second 指定一個 user 物件
>>> post_second.users = user

NOTE:# 另一種建立聯絡的寫法為 user.posts.append(post_second)
因為 user.posts 本質上是一個列表,只要該列表中存在 post 那麼 post 就是屬於這個 user 的

# 將 post_second 寫入資料庫
>>> db.session.add(post_second)
>>> db.session.commit()
# 寫入完成之後,user 才能夠通過關係來訪問到屬於其下的 posts
>>> user.posts.all()
[<Model Post `Second Post`>, <Model Post `First Post`>]

ERROR LOG

InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (pymysql.err.InternalError) (1364, u"Field 'id' doesn't have a default value") [SQL: u'INSERT INTO posts (title, text, publish_date, user_id) VALUES (%(title)s, %(text)s, %(publish_date)s, %(user_id)s)'] [parameters: {'text': None, 'title': 'First Post', 'publish_date': None, 'user_id': '9ecae9b3-f4d2-4c8e-b033-616bb1642842'}]

TS: 因為 commit 一個指定了 user_id 的 port 物件時,實際上資料庫中還不存在被關聯的 user 物件,導致報錯。