用 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 物件,導致報錯。