Flask奇妙探索之旅(四)---SQL

寫(xiě)在前面


本文學(xué)習(xí)來(lái)源The-Flask-Mega-Tutorial-zh,學(xué)習(xí)如何使用數(shù)據(jù)庫(kù),再次表達(dá)對(duì)作者和譯者的感謝,正是因?yàn)樗麄?,才能學(xué)習(xí)到這么好的免費(fèi)教程。本機(jī)環(huán)境:選用的Makedown編輯器為Atom,實(shí)驗(yàn)環(huán)境為Ubuntu18.04,Python版本為 3.7.1本篇僅作為自己Flask入門(mén)的記錄,想通過(guò)此來(lái)記錄代碼和自己不懂的概念。

Flask中數(shù)據(jù)庫(kù)插件


Flask本身不支持?jǐn)?shù)據(jù)庫(kù),我們可以通過(guò)使用數(shù)據(jù)庫(kù)插件來(lái)完成此項(xiàng)功能。數(shù)據(jù)庫(kù)大都提供了Python的客戶端包,它們被分為了兩大類:關(guān)系數(shù)據(jù)庫(kù)和非關(guān)系型數(shù)據(jù)庫(kù)(nosql),一般說(shuō)來(lái),nosql是為了處理雜亂的非結(jié)構(gòu)化數(shù)據(jù)來(lái)設(shè)計(jì)的,而SQL則更多的用于結(jié)構(gòu)化數(shù)據(jù)的應(yīng)用程序。
本章中,我們要用到兩個(gè)Flask擴(kuò)展,第一個(gè)插件是Flask-SQLAlchemy,第二個(gè)插件是Flask-Migrate,是SQLAlchemy的一個(gè)數(shù)據(jù)庫(kù)遷移框架。安裝命令如下(確認(rèn)自己激活了虛擬環(huán)境):

pip3 install flask-sqlalchemy
pip3 install flask-migrate

SQLAchemy配置


開(kāi)發(fā)階段,使用SQLite數(shù)據(jù)庫(kù),SQLite數(shù)據(jù)庫(kù)是開(kāi)發(fā)小型乃至中型應(yīng)用最方便的選擇。下面修改配置文件。

import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config(object):
  #...
  SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'my_app.db')
  SQLALCHEMY_TRACK_MODIFICATIONS = False

basedir = os.path.abspath(os.path.dirname(__file__))是返回腳本的路徑。Flask-SQLAlchemy插件從SQLALCHEMY_DATABASE_URI配置變量中獲取應(yīng)用的數(shù)據(jù)庫(kù)的位置。首先從環(huán)境變量獲取,沒(méi)有就使用默認(rèn)位置

SQLALCHEMY_TRACK_MODIFICATIONS用于設(shè)置數(shù)據(jù)發(fā)生變更之后是否發(fā)送信號(hào)給應(yīng)用,此處不需要。

數(shù)據(jù)庫(kù)在應(yīng)用中表現(xiàn)為數(shù)據(jù)庫(kù)實(shí)例,需修改__init__.py

from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

my_app = Flask(__name__)
my_app.config.from_object(Config)
my_db = SQLAlchemy(my_app)
migrate = Migrate(my_app,my_db)

from app import routes, models

在初始化腳本中,首先引入了數(shù)據(jù)庫(kù)支持模塊,并創(chuàng)建了實(shí)例化對(duì)象my_db來(lái)表示數(shù)據(jù)庫(kù),后添加了數(shù)據(jù)庫(kù)引擎,最后導(dǎo)入了models模塊來(lái)定義數(shù)據(jù)庫(kù)結(jié)構(gòu)。

數(shù)據(jù)庫(kù)模型


數(shù)據(jù)模型:定義一張表及其字段的類,先讓我們定義一個(gè)用戶模型。id存在于所有模型并用做主鍵,每個(gè)用戶都被分配一個(gè)id值;username,emailpassword_hash字段被定義為字符串,并指定最大長(zhǎng)度,模型存在于models.py中,代碼如下:

from app import my_db

class User(my_db.Model):

  id = my_db.Column(my_db.Integer, primary_key=True)
  username = my_db.Column(my_db.String(64), index=True, unique=True)
  email = my_db.Column(my_db.String(120), index=True, unique=True)
  password_hash = my_db.Column(my_db.String(128))

  def __repr__(self):
      return('<User {}>'.format(self.username))

User類繼承自my_db.Model,它是Flask-SQLAlchemy中所有模型的基類。字段被創(chuàng)建為my_db.Column類的實(shí)例,它傳入字段類型以及其他可選參數(shù)。

__repr__這個(gè)特殊方法可以在調(diào)試時(shí)打印用戶實(shí)例。測(cè)試情況如下:

>>> from app.models import User
>>> u = User(username='devil', email='devil@gmail.com')
>>> u
<User devil>
>>> u.username
'devil'
>>> u.email
'devil@gmail.com'

如果可以達(dá)到這一步測(cè)試,那我們可以開(kāi)始來(lái)創(chuàng)建數(shù)據(jù)庫(kù)遷移儲(chǔ)存庫(kù)了。

創(chuàng)建數(shù)據(jù)庫(kù)遷移存儲(chǔ)庫(kù)


在開(kāi)發(fā)過(guò)程中,我們需要修改數(shù)據(jù)庫(kù)模型,而且還要在修改之后更新數(shù)據(jù)庫(kù)。最直接的方式就是刪除舊表,但這樣會(huì)丟失數(shù)據(jù)。更好的解決辦法是使用數(shù)據(jù)庫(kù)遷移框架。在Flask中可以使用Flask-Migrate擴(kuò)展,來(lái)實(shí)現(xiàn)數(shù)據(jù)遷移。Flask-Migrate添加了flask db子命令來(lái)管理與數(shù)據(jù)庫(kù)遷移相關(guān)的所有事情。 那么讓我們通過(guò)運(yùn)行flask db init來(lái)創(chuàng)建microblog的遷移存儲(chǔ)庫(kù),代碼如下:

$ flask db init
  Creating directory /home/evil/microblog/migrations ... done
  Creating directory /home/evil/microblog/migrations/versions ... done
  Generating /home/evil/microblog/migrations/script.py.mako ... done
  Generating /home/evil/microblog/migrations/alembic.ini ... done
  Generating /home/evil/microblog/migrations/env.py ... done
  Generating /home/evil/microblog/migrations/README ... done
  Please edit configuration/connection/logging settings in
  '/home/evil/microblog/migrations/alembic.ini' before proceeding.

第一次數(shù)據(jù)庫(kù)遷移


包含映射到User數(shù)據(jù)庫(kù)模型的用戶表的遷移存儲(chǔ)庫(kù)生成后,可以創(chuàng)建第一次數(shù)據(jù)庫(kù)遷移了??梢酝ㄟ^(guò)手動(dòng)或自動(dòng)。 要自動(dòng)生成遷移,Alembic會(huì)將數(shù)據(jù)庫(kù)模型定義的數(shù)據(jù)庫(kù)模式與數(shù)據(jù)庫(kù)中當(dāng)前使用的實(shí)際數(shù)據(jù)庫(kù)模式進(jìn)行比較。 然后,使用必要的更改來(lái)填充遷移腳本,以使數(shù)據(jù)庫(kù)模式與應(yīng)用程序模型匹配。 當(dāng)前情況是,由于之前沒(méi)有數(shù)據(jù)庫(kù),自動(dòng)遷移將把整個(gè)User模型添加到遷移腳本中。 flask db migrate子命令生成這些自動(dòng)遷移:

$ flask db migrate -m "users table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'user'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']'
  Generating /home/evil/microblog/migrations/versions/2e70cde282af_users_table.py
  ... done

從輸出信息可以看到輸出了一個(gè)User表和倆個(gè)索引,給出了遷移腳本的輸出路徑,2e70cde282af則是自動(dòng)生成的遷移標(biāo)識(shí),之后我們使用flask db upgrade更新數(shù)據(jù)庫(kù)。

$ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> 2e70cde282af, users table
(venv)

觀察自己的文件目錄發(fā)現(xiàn)多出了一個(gè)my_app.db的文件,這是因?yàn)槲覀兪褂昧薙QLite,所以u(píng)pgrade命令檢測(cè)到數(shù)據(jù)庫(kù)不存在時(shí),會(huì)創(chuàng)建它。在使用類似MySQL和PostgreSQL的數(shù)據(jù)庫(kù)服務(wù)時(shí),必須在運(yùn)行upgrade之前在數(shù)據(jù)庫(kù)服務(wù)器上創(chuàng)建數(shù)據(jù)庫(kù)。

數(shù)據(jù)庫(kù)關(guān)系


關(guān)系數(shù)據(jù)庫(kù)擅長(zhǎng)存儲(chǔ)數(shù)據(jù)項(xiàng)之間的關(guān)系。 考慮用戶發(fā)表動(dòng)態(tài)的情況, 用戶將在user表中有一個(gè)記錄,并且這條用戶動(dòng)態(tài)將在post表中有一個(gè)記錄。 標(biāo)記誰(shuí)寫(xiě)了一個(gè)給定的動(dòng)態(tài)的最有效的方法是鏈接兩個(gè)相關(guān)的記錄。

也就是說(shuō)你看到一個(gè)動(dòng)態(tài),想知道這個(gè)動(dòng)態(tài)是誰(shuí)發(fā)布的,或者知道一個(gè)用戶,想看到他的所有動(dòng)態(tài),就可以在數(shù)據(jù)庫(kù)中查詢

下面對(duì)對(duì)數(shù)據(jù)庫(kù)擴(kuò)展以支持用戶動(dòng)態(tài),設(shè)計(jì)一個(gè)新表post,post將具有必須的id、用戶動(dòng)態(tài)的bodytimestamp字段。 并在此基礎(chǔ)上添加了一個(gè)user_id字段,將該用戶動(dòng)態(tài)鏈接到其作者。 由于用戶有唯一的id主鍵, 將用戶動(dòng)態(tài)鏈接到其作者的方法是添加對(duì)用戶id的引用,這正是user_id字段所在的位置。 這個(gè)user_id字段被稱為外鍵。

下面對(duì)app/models.py進(jìn)行修改:

# -*- coding: utf-8 -*-
from datetime import datetime
from app import my_db

class User(my_db.Model):
    id = my_db.Column(my_db.Integer, primary_key=True)
    username = my_db.Column(my_db.String(64), index=True, unique=True)
    email = my_db.Column(my_db.String(120), index=True, unique=True)
    password_hash = my_db.Column(my_db.String(128))
    posts = my_db.relationship('Post', backref='author', lazy='dynamic')

    def __repr__(self):
        return '<User {}>'.format(self.username)

class Post(my_db.Model):
    id = my_db.Column(my_db.Integer, primary_key=True)
    body = my_db.Column(my_db.String(140))
    timestamp = my_db.Column(my_db.DateTime, index=True, default=datetime.utcnow)
    user_id = my_db.Column(my_db.Integer, my_db.ForeignKey('user.id'))

    def __repr__(self):
        return '<Post {}>'.format(self.body)

Post類表示用戶的發(fā)表動(dòng)態(tài),中間有timestamp字段被編入索引,可以按時(shí)間順序檢索用戶動(dòng)態(tài)。通常,在服務(wù)應(yīng)用中使用UTC日期和時(shí)間是推薦做法。 這可以確保你使用統(tǒng)一的時(shí)間戳,無(wú)論用戶位于何處,這些時(shí)間戳?xí)陲@示時(shí)轉(zhuǎn)換為用戶的當(dāng)?shù)貢r(shí)間。

user_id字段被初始化為user.id的外鍵,這意味著它引用了來(lái)自用戶表的id值。本處的user是數(shù)據(jù)庫(kù)表的名稱,F(xiàn)lask-SQLAlchemy自動(dòng)設(shè)置類名為小寫(xiě)來(lái)作為對(duì)應(yīng)表的名稱。 User類有一個(gè)新的posts字段,用my_db.relationship初始化。這不是實(shí)際的數(shù)據(jù)庫(kù)字段,而是用戶和其動(dòng)態(tài)之間關(guān)系的高級(jí)視圖,因此它不在數(shù)據(jù)庫(kù)圖表中。對(duì)于一對(duì)多關(guān)系,my_db.relationship字段通常在“一”的這邊定義,并用作訪問(wèn)“多”的便捷方式。因此,如果我有一個(gè)用戶實(shí)例u,表達(dá)式u.posts將運(yùn)行一個(gè)數(shù)據(jù)庫(kù)查詢,返回該用戶發(fā)表過(guò)的所有動(dòng)態(tài)。 db.relationship的第一個(gè)參數(shù)表示代表關(guān)系“多”的類。 backref參數(shù)定義了代表“多”的類的實(shí)例反向調(diào)用“一”的時(shí)候的屬性名稱。這將會(huì)為用戶動(dòng)態(tài)添加一個(gè)屬性post.author,調(diào)用它將返回給該用戶動(dòng)態(tài)的用戶實(shí)例。 lazy參數(shù)定義了這種關(guān)系調(diào)用的數(shù)據(jù)庫(kù)查詢是如何執(zhí)行的

一旦變更了應(yīng)用模型,就需要生成一個(gè)新的數(shù)據(jù)庫(kù)遷移:

$ flask db migrate -m "posts table"
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'post'
INFO  [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']'
  Generating /home/evil/microblog/migrations/versions/3db91c590389_posts_table.py
  ... done

使用flask db upgrade遷移到數(shù)據(jù)庫(kù)

$ flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade 2e70cde282af -> 3db91c590389, posts table
(venv)

Demo


現(xiàn)在讓我們?cè)趐ython交互環(huán)境中進(jìn)行測(cè)試,導(dǎo)入數(shù)據(jù)庫(kù)實(shí)例和模型:

>>> from app import my_db
>>> from app.models import User, Post

創(chuàng)建一個(gè)新用戶:

>>> u = User(username='evil',email='evil@gmail.com')
>>> my_db.session.add(u)
>>> my_db.session.commit()

db.session進(jìn)行訪問(wèn)驗(yàn)證。,一旦所有更改都被注冊(cè),你可以發(fā)出一個(gè)指令db.session.commit()來(lái)以原子方式寫(xiě)入所有更改。 如果在會(huì)話執(zhí)行的任何時(shí)候出現(xiàn)錯(cuò)誤,調(diào)用db.session.rollback()會(huì)中止會(huì)話并刪除存儲(chǔ)在其中的所有更改。 要記住的重要一點(diǎn)是,只有在調(diào)用db.session.commit()時(shí)才會(huì)將更改寫(xiě)入數(shù)據(jù)庫(kù)。
下面添加一個(gè)新用戶:

>>> u = User(username='Devil',email='Devil@gmail.com')
>>> my_db.session.add(u)
>>> my_db.session.commit()

進(jìn)行查詢操作:

>>> users = User.query.all()
>>> users
[<User evil>, <User Devil>]
>>> [print(u.id,u.username,u.email) for u in users]
1 evil evil@gmail.com
2 Devil Devil@gmail.com
[None, None]

query屬性是數(shù)據(jù)庫(kù)查詢的入口,最基本的查詢就是返回該類的所有元素,在添加市,用戶的id字段被設(shè)置為1和2。得到了用戶id,我們現(xiàn)在可以直接獲取用戶實(shí)例:

>>> u=User.query.get(1)
>>> u
<User evil>
# 添加用戶動(dòng)態(tài)
>>> p = Post(body='My first post!',author = u)
>>> my_db.session.add(p)
>>> my_db.session.commit()

User類中創(chuàng)建的my_db.relationship為用戶添加了posts屬性,并為用戶動(dòng)態(tài)添加了author屬性。 我使用author虛擬字段來(lái)調(diào)用其作者,而不必通過(guò)用戶ID來(lái)處理。

現(xiàn)在看一下另外的數(shù)據(jù)庫(kù)查詢栗子:

# get all posts written by a user
>>> u=User.query.get(1)
>>> u
<User evil>
>>> posts = u.posts.all()
>>> posts
[<Post My first post!>]
# same, but with a user that has no posts
>>> u=User.query.get(2)
>>> u
<User Devil>
>>> posts = u.posts.all()
>>> posts
[]
# print post author and body for all posts
>>> posts = Post.query.all()
>>> [print(p.id, p.author.username, p.body) for p in posts]
1 evil My first post!
[None]
# get all users in reverse alphabetical order
>>> User.query.order_by(User.username.desc()).all()
[<User evil>, <User Devil>]
>>> User.query.order_by(User.username).all()
[<User Devil>, <User evil>]

具體操作可以參考Flask-SQLAlchemy
下面清除這些測(cè)試用戶和用戶動(dòng)態(tài):

>>> users = User.query.all()
>>> [my_db.session.delete(u) for u in users]
[None, None]

>>> posts = Post.query.all()
>>> [my_db.session.delete(p) for p in posts]
[None]

>>> my_db.session.commit()
>>>

補(bǔ)充姿勢(shì)


在每次啟動(dòng)Python解釋器之后,第一件事是運(yùn)行兩條導(dǎo)入語(yǔ)句:

>>> from app import my_db
>>> from app.models import User, Post

很煩有木有,這種反人類的事情總會(huì)有解決方案。flask shell就是這樣的讓我們看個(gè)栗子:

>>> app
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'app' is not defined

$ flask shell
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
App: app [production]
Instance: /home/evil/microblog/instance
>>> app
<Flask 'app'>

當(dāng)使用flask shell時(shí),該命令預(yù)先導(dǎo)入應(yīng)用實(shí)例。 flask shell的絕妙之處不在于它預(yù)先導(dǎo)入了app,而是你可以配置一個(gè)“shell上下文”,也就是可以預(yù)先導(dǎo)入一份對(duì)象列表。
下面在microblog.py中實(shí)現(xiàn)一個(gè)函數(shù),它通過(guò)添加數(shù)據(jù)庫(kù)實(shí)例和模型來(lái)創(chuàng)建了一個(gè)shell上下文環(huán)境:

from app import my_app, my_db
from app.models import User, Post

@my_app.shell_context_processor
def make_shell_context():
    return {'db': my_db, 'User': User, 'Post': Post}

則現(xiàn)在運(yùn)行flask shell:

$ flask shell
>>> my_db
<SQLAlchemy engine=sqlite:////home/evil/microblog/my_app.db>
>>> User
<class 'app.models.User'>
>>> Post
<class 'app.models.Post'>
>>>

如果產(chǎn)生NameError,說(shuō)明 make_shell_context() 沒(méi)有被Flask注冊(cè)。最有可能的原因是你的環(huán)境變量中沒(méi)有設(shè)定 FLASK_APP=microblog.py,可以查看之前寫(xiě)的第一篇文章,配置,或者使用export FLASK_APP=microblog.py,本篇到此結(jié)束,謝謝您的觀看。(可以給我點(diǎn)個(gè)贊嗎,就一個(gè),謝謝了嘞)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載,覺(jué)得這篇寫(xiě) SQLAlchemy Core,寫(xiě)得非常不錯(cuò)。不過(guò)后續(xù)他沒(méi)寫(xiě)SQLAlchemy ORM... ...
    非夢(mèng)nj閱讀 5,591評(píng)論 1 14
  • Flask-SQLAlchemy的使用: ORM的好處:可以讓我們操作數(shù)據(jù)庫(kù)跟操作對(duì)象是一樣的,非常方便,因?yàn)橐粋€(gè)...
    Dozing閱讀 23,485評(píng)論 3 22
  • 首發(fā)公眾號(hào):寵愛(ài)有減 外公,抱歉,不能去送你最后一程。隔著一千多公里,我能感受到我媽媽那情不自禁無(wú)法控制的悲傷。沙...
    我的支離不破碎閱讀 238評(píng)論 0 0
  • 拍攝夜景的注意事項(xiàng)及一點(diǎn)個(gè)人感悟: 1、拍攝夜景主要需要三腳架,或者自己找個(gè)穩(wěn)定的地方拍攝也行,比如路邊的垃圾桶、...
    一把狂刀閱讀 510評(píng)論 2 8
  • 我喜歡看武志紅老師的書(shū),他幾乎所有公開(kāi)出版的書(shū)我都仔細(xì)看了N遍,尤其是《夢(mèng)知道答案》。兩三年前,我也開(kāi)始關(guān)注和分析...
    呱呱呱愛(ài)啊閱讀 627評(píng)論 1 4

友情鏈接更多精彩內(nèi)容