【譯】可編程的web項目(一)web開發導論

寫在開頭: 本譯文源自芬蘭奧盧大學Ivan Sanchez的課程Programmable web project,由三位在奧盧大學交換生分享譯製,若有措辭不當或任何不妥,請前輩們多多在評論中指點。原課程programmable-web-project。課程中的練習本來有上傳自動檢驗,但須要學校帳號選課登錄,在此直接分享答案。
任何轉載、再翻譯等共享方式需遵循 CC-BY-SA 4.0 協議,詳見底部許可。html

網絡開發導論

這個練習中涵蓋了比較基礎的簡單網頁應用開發,目的是開發一個能遠程訪問、管理服務器數據庫的簡單應用。這個項目涉及到的知識較爲寬泛——會有比較多不一樣方面的知識都被稍稍說起。而且咱們會用到一些現代python web 開發工具,這些工具封裝了許多開發上的細節之處並對開發者隱藏。瞭解這些有利於咱們理解高級開發工具的工做原理,但咱們不要求掌握如何使用。python

兩個在此課程中主要要使用到的工具就是FlaskSQLAlchemy。Flask是一個用於Python web開發的微框架(microframework)。十分適合用於不須要像Django那樣大型框架的小應用。SQLAlchemy是一個數據庫工具包,能夠繞過使用它的須要結構化查詢語言,容許徹底經過Python對象進行數據庫管理。linux

部署靜態內容

在python虛擬環境中安裝flask,而且經過瀏覽器訪問服務令它輸出 'hello'git

python 虛擬環境 (Virtual environments)

儘管這一步操做不是必要的,但虛擬環境在各類python開發中都是強烈建議使用的。虛擬環境多用於建立純淨的Python環境並與系統的Python環境相隔離。每個虛擬環境獨立於主環境而管理着自有的一系列模塊。虛擬環境也能在用戶的主目錄下管理,這將容許安裝沒有管理員權限的新模塊。github

若是你須要安裝具備特定的第三方模塊但但願爲其餘全部模塊建立另外一個版本,則虛擬環境特別有用。這在具備多個第三方組件的項目中很容易發生:一個組件(模塊A)經歷向後不兼容的更改,而另外一個組件(模塊B)將模塊A做爲依賴關係不會當即跟進更新,從而破壞模塊B - 除非安裝保留模塊A的最後一個兼容版本。此外,在另外一個系統上設置應用程序時,虛擬環境可用於確保Python環境與開發系統中的相同。web

固然,若是你以某種方式弄亂你的Python環境,你能夠刪除這個環境並從新開始。此外,在具備包管理的Linux系統上,使用Pip安裝Python模塊可能會破壞包管理器安裝的Python模塊。可是,在虛擬環境中使用Pip安裝模塊是徹底安全的。spring

建立並啓動虛擬環境

咱們以python 3.3爲例子,必備的工具都會自動安裝。以防萬一,你須要安裝Pip並用它安裝virtualenv模塊。建立虛擬環境你只須要輸入:sql

python -m venv myvenv /path/to/the/virtualenv
複製代碼

其中"myvenv"爲自定義虛擬環境名,可省略數據庫

在OS X和許多linux發行版上你須要使用python3(在Python 2上運行上面的命令會產生「沒有名爲venv的模塊」的錯誤消息)編程

啓動虛擬環境
windows:

[虛擬環境路徑]\Scripts\activate.bat
複製代碼

在OS X或Linux中:

source /path/to/the/virtualenv/bin/activate
複製代碼

而後你就能看到提示符

(myvenv) C:\>  
複製代碼

當在虛擬環境中進行cd時 - 請注意,你實際上不須要在虛擬環境文件夾中工做,它只是包含你的Python環境。只要你在括號中看到名稱,就會知道這個虛擬環境是激活的。當你運行Python時,它將經過虛擬環境啓動。一樣,當您使用Pip安裝模塊時,它們將僅安裝到這個虛擬環境文件夾中。

對於剩下的步驟,請確保您始終在virtualenv中。

web框架的簡單介紹

若是咱們儘量地簡化來看,web應用就是一個讀取HTTP請求(request)相應產生一個HTTP響應(response)的‘軟件’。這種狀況也發生在一些與HTTP服務器的神祕通訊中。request和response中包含各類各樣的請求頭部(headers)和一個request body(並不是必定有)。參數能夠經過URL或者消息的body傳遞。在這個項目中既然咱們已經將結構至關簡化了,咱們能夠通俗地說這個web框架(framework)是用來將HTTP請求轉化爲Python函數調用並將Python函數返回的值放到HTTP response中去。

當你在使用web框架的時候,URL會經過路由(routing)映射到函數。路由支持URL模板,那麼部分URL會被定義爲變量

http://api.hundredacrewood.org/profile/winnie  
http://api.hundredacrewood.org/profile/tigger
複製代碼

以上兩個URL都匹配同一URL模板

/profile/{用戶名}
複製代碼

用戶名就會做爲參數(或以其它方式)傳遞給被分配到指定URL的函數中。這些意味着你的web應用實際上包含了許多處理response和路由註冊的函數上。這些函數被稱爲view函數。

簡單的flask應用

如今咱們簡單地瞭解了一下給予開發者便利的web框架,如今咱們來嘗試一下。在全部的例題和練習中咱們都使用Flask,並須要使用到十分基礎簡單的python web框架功能,那麼Flask就十分合適。
安裝Flask(確保在你的虛擬環境中)

pip install Flask
複製代碼

以及咱們立刻要用到的模塊:

pip install pysqlite3
pip install flask-sqlalchemy
複製代碼

這個Flask應用十分簡單。你能夠查閱Flask官方文檔瞭解更多。建立一個文件夾而後把如下文件下載到這。

app.py

from flask import Flask
app = Flask("hello")

@app.route("/")
def index():
    return "You expected this to say hello, but it says \"donkey swings\" instead. Who would have guessed?"
複製代碼

若是你的這段代碼文件名也是app.py,你就能夠經過輸入如下命令啓動:

flask run
複製代碼

在終端上,假設你正在你的app文件所在文件夾。(若是你用了其餘文件名,你須要設置環境變量FLASK_APP來指向你的代碼文件)那如今你能夠在瀏覽器中經過訪問http://localhost:5000http://127.0.0.1:5000預覽你的應用。Flask開發服務器會默認將你的應用放到本地5000端口上運行。

若是咱們正在編寫一個能讓人經過瀏覽器使用的web應用,顯然咱們更須要返回一個HTML文件而不是純字符串。在這裏咱們不會把重心放在HTML上。

驅動簡單的動態內容

靜態網頁是十分90年代的東西了,幾乎不必在那上面使用web框架。然而如今若是你但願一些東西變成動態的,web框架的好處便顯而易見了。向HTTP請求傳參有許多方法,每一個方法都略微有其獨特用途,但咱們就簡單地介紹兩種並展現Flask如何將他們傳遞給你的Python函數。第三個也會在稍後介紹。

嘗試對某人說Hello

第一種傳參方法就是把他們放在URL中做爲變量,正如上面講到的。一般這種機制用於辨別你想要訪問的是哪一個資源。好比你想在網站上訪問你本身的資料,你就能夠用這樣的URL /profile/jack

讓咱們按照以上例子,在app.py中添加一個函數以響應全部匹配如下模板的URL:

/hello/
複製代碼

咱們如今來演示如何向路由設置變量而且變量在對應函數中如何得以使用。這個匹配路徑的新函數以下:

@app.route(("/hello/<name>")
def hello(name):
    return "Hello {}".format(name)
複製代碼

那麼如今全部以/hello/開頭且帶有任何字符串的url都會使該函數被調用並將字符串放到name參數中

練習1:數學計算

爲了稍微練習如下基本的路由,咱們引入簡單的編程,讓這個web程序經過URL中提供的變量進行一些基本數學運算。

學習目標:瞭解如何使用Flask的路由系統建立簡單Web應用程序。

如何開始
在你的虛擬環境中建立一個項目文件夾,在文件夾中建立app.py並將全部代碼放在此文件中。代碼應包含5個函數,以下:

函數1index

  • 路徑:"/"
  • 返回:
    • 任意字符串

函數2plus

  • 路徑:"/add/<float:number_1>/<float:number_2>"
  • 參數:
    • 第一個數(浮點)
    • 第二個數(浮點)
  • 返回:
    • 格式化的加法計算結果字符串

函數3minus

  • 路徑:"/sub/<float:number_1>/<float:number_2>"
  • 參數:
    • 第一個數(浮點)
    • 第二個數(浮點)
  • 返回:
    • 格式化的減法結果字符串

函數4mult

  • 路徑:"/mul/<float:number_1>/<float:number_2>"
  • 參數:
    • 第一個數(浮點)
    • 第二個數(浮點)
  • 返回:
    • 格式化的乘法結果字符串

函數5div

  • 路徑:"/div/<float:number_1>/<float:number_2>"
  • 參數:
    • 被除數(浮點)
    • 除數(浮點)
  • 返回:
    • 格式化的除法結果字符串或NaN(當除數爲0.0時返回NaN)

答案_t1

查詢

第二種將參數做爲URL的一部分傳遞的另外一種機制是使用查詢參數。若是你在進行Google搜索後查看了地址欄,你會注意到這些內容,例如搜索「eeyore」(已略去許多其餘無關查詢參數):

https://www.google.fi/search?q=eeyore&oq=eeyore
複製代碼

在URL中全部?以後的都是查詢參數。每一個都屬於鍵值對,其中=左邊爲鍵,=右邊是值,他們被&彼此分開。查詢參數是傳遞具備人一直的參數的正確方法。例如,上面計算器則更適合用這種方法。一樣適用「Hello」的例子。讓咱們將hello函數轉換爲使用查詢參數:

@app.route("/hello/")
def hello():
    try:
        name = request.args["name"]
    except KeyError:
        return "Missing query parameter: name", 400
    return "Hello {}".format(name)
複製代碼

第一個重要的區別在於route的聲明上:徹底找不到name的出處。這並非一個錯誤:查詢參數並非路徑的一部分 —— 若是你嘗試把他們放在那,你的URL將不會生效。一樣name參數也不會出如今hello函數的聲明中,由於它不是route的一個變量。相反,咱們必須從請求對象(request object)中「挖掘」出來。這個對象是完整web框架功能的一部分而且包含了全部可能從客戶端發出的HTTP請求中獲取到的信息。從Flask中引入後便能全局地使用它。

查詢參數能夠在一個類字典對象request.args中找到(具體請看這裏)。另外一個在這個例子中的不一樣點是咱們在最後報告錯誤的匹配。以前當name變量仍是URL的一部分時,空值會形成404 Not Found。然而,這裏404不是一個合適的錯誤提示,由於就算沒有查詢參數這個目標URL還是存在的。而400 (Bad Request) 不是很準確,但在這種狀況下是合適的。意思是當客戶端確實將請求發送到正確位置是,請求由於缺乏所需信息而沒法被處理。

實際上有幾種不一樣的方法能夠向客戶端返回錯誤響應。在這種狀況下,咱們只是添加了狀態代碼的return語句。其餘方法將會在以後的課程中介紹。或者你能夠閱讀Flask中文文檔以瞭解更多。使用這樣的return語句並不理想,由於這不容易將此結果歸爲錯誤。在此咱們只是爲了簡化示例。

練習2:三角運算

學習目標:學會如何在Flask應用中使用查詢參數,如何在view函數中返回錯誤碼,如何在view函數中的校驗請求裏控制相關的數據。

如何開始: 你能夠在以前的計算練習文件中直接添加函數,也能夠建立一個新的app文件。此次咱們只須要編寫一個函數:功能由路徑變量選擇的三角運算。同時你須要從Flask中引入request對象的組件,將引入的代碼改爲以下:

from flask import Flask, request
複製代碼

函數:trig

  • 路徑:"/trig/<func>"
  • 參數:
    • 須要執行的三角運算名(cos,sin 或 tan)
  • 查詢參數
    • angle 用於三角運算的角度(必填)
    • unit 單位,"radian"(弧度)或 "degree"(角度),默認爲弧度(選填)
  • 返回值
    • 200:三角運算的結果字符串(精確到三位小數)
      • 例:/trig/sin?angle=3.14
      • 例:/trig/sin?angle=90&unit=degree
    • 404:錯誤信息Missing query parameter: angleInvalid query parameter value(s)
      • 例:/trig/cos/
      • 例:/trig/tan?angle=pi
      • 例:/trig/sin?angle=4&unit=donkey

這個view函數計算角的sine,cosine或tangent值。若是給出的角的單位是度,那首先須要將其轉換爲弧度。若是func路徑變量與任何一個三角運算都不匹配就給出404狀態碼。若是角度沒給出或者查詢參數值不正確,則返回400狀態碼以及相對應的錯誤信息。

答案_t2

數據庫和ORM

目前,咱們還只能處理不須要存儲數據的請求。數據庫是Web開發不可或缺的一部分,由於這是存儲全部長期性信息的地方。全部數據都不是靜態的,存儲在數據庫中能夠進行有效地查詢。至少相對有效 - 最終數據庫交互最可能成爲Web服務性能瓶頸。因爲本課程的重點是開發利用數據庫的Web API,所以省略了有關數據庫的大部分細節。這些能夠從其餘課程得到或在線學習。遵循現代網絡開發的實踐,咱們甚至會跳過SQL(結構化查詢語言),鼓勵使用對象關係映射器 (object relation mapper ORM)。

這篇文章涉及數據庫的幾個主題(稍稍說起):數據庫是什麼,設計考慮因素,如何在Python應用程序中使用它們以及如何在開發過程當中維護它們。

數據庫的結構

在本課程中,咱們使用的是「傳統」數據庫,這些數據庫具備預約義的結構,可存儲全部信息。這包括衆所周知的在Web開發中經常使用的MySQL和PostgreSQL,以及常常用於原型設計的SQLite。這些類型的數據庫包含數據表。每一個表都有一組預約義的列定義該表中的項必須或能夠具備的屬性。這些項目存儲在行。此外,數據庫中的不一樣表能夠具備它們之間的關係,以便一個表中的列引用另外一個表中的行。全部這一切都在數據庫架構(database schema)中定義。

在數據表中一個比較重要的保證行獨特性的概念就是主鍵(Primary key)。每一個表至少指定一列爲主鍵(簡稱PK)。對於每一行,PK值必須是惟一的。

另外一種鍵叫外鍵(簡稱FK),一般在建立表之間的關係時要用到。表A中的外鍵字段是一個列,它只能經過使用表B中的惟一列來識別關係,從而獲取引用表B中行的值。

對象關係映射

在本課程中,經過對象關係映射(簡稱ORM)管理數據庫。在此方法中,數據庫表及其關係在代碼中做爲對象進行管理。就像Flask如何隱藏不少有關如何使用的細節同樣使HTTP請求成爲Python函數調用,對象關係映射器隱藏了不少關於數據庫表如何成爲Python類(class)的細節,數據庫操做變成了方法調用。雖然理解ORM自動生成的基礎SQL查詢對性能考慮頗有用,但在正常的小規模使用中,咱們所關注的一切都是Python代碼。咱們的示例使用的SQLAlchemy經過Flask SQLAlchemy的。它實際上不只僅是ORM(實際上ORM只是它的一個可選組件),可是與本課程的整體主題一致,咱們只是在表面上涉獵。

不管使用哪一種ORM,定義數據庫結構的基本方法是建立具備表示所調用列的各類屬性的Python類,叫模型類(model class)。每一個類對應於數據庫中的一個表,而且初始化爲列的類的每一個屬性表示表中的列。類實例(即對象)表示已從數據庫檢索到程序工做空間的行。請注意,一般是數據庫架構(schema)是從類定義生成的。這就是爲何不事先建立模式的緣由。事實上,由於咱們如今正在使用SQLite,因此咱們也不須要費心去建立數據庫。使用Flask SQLAlchemy,建立簡單表的類將以下所示:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///test.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)

class Measurement(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    sensor = db.Column(db.String(20), nullable=False)
    value = db.Column(db.Float, nullable=False)
    time = db.Column(db.DateTime, nullable=False)
複製代碼

開頭介紹了爲了將SQLAlchemy與Flask一塊兒使用所須要作的事情。咱們只須要知道咱們想要數據庫的位置(同一文件夾中的文件)。第二個配置行是爲了禁用警告,由於咱們如今不須要跟蹤修改。經過從flask_sqlalchemy初始化SQLAlchemy來獲取數據庫接口,而後使用此對象來定義咱們的單個數據庫類。在SQLAlchemy中,經過使用db.Column構造函數將屬性定義爲列,該構造函數將列類型做爲第一個參數,將其餘列屬性做爲關鍵字參數 - 請參閱列屬性 術語表示經常使用關鍵字列表。

若是您願意,還能夠覆蓋模型的__repr__方法。這樣作會更改模型實例在控制檯中的顯示方式。咱們在本頁的示例中沒有這樣作,可是將在後面的示例中執行此操做。不然它將顯示爲例如(型號名稱和主鍵值)。例如,您能夠編寫一個顯示測量值的方法,例:

<Measurement 1>uo-donkeysensor-1: 44.51 @ 2019-01-01 00:00:00
複製代碼

用ORM作些實事

此時咱們尚未數據庫。它能夠從python控制檯建立。轉到flask應用的文件夾,激活Python控制檯。這裏使用iPython,而後執行:

In [1]: from app import db
In [2]: db.create_all()
複製代碼

練習3:模型中的基礎類

學習目的:學會如何定義一個模型類 模型:StorageItem

  • 列:
    • id (integer, primary key)
    • handle (string, max length 64, unique, not null)
    • qty (integer, not null)
    • price (float, not null)

用Model做爲基礎類定義這個類,而後像例子中同樣調用列構造器。

答案_t3

添加對象

在此以後,您的應用程序文件夾會包含test.db文件,該文件如今是一個帶有一個空表(名爲measurement)的SQLite數據庫。下一步是在表中添加一些行。在ORM模式中,這是經過建立模型類的實例來完成的。默認狀況下,模型類有一個構造函數,它接受數據庫列名做爲關鍵字參數。請注意,單首創建實例不會對數據庫執行任何操做。它須要標記爲添加而後提交。所以,咱們能夠經過如下方式建立measurement實例:

In [1]: from app import db, Measurement
In [2]: from datetime import datetime
In [3]: meas = Measurement(
   ...:     sensor="donkeysensor2000",
   ...:     value=44.51,
   ...:     time=datetime(2018, 11, 21, 11, 20, 30)
   ...: )
In [4]: db.session.add(meas)
In [5]: db.session.commit()
複製代碼

檢驗是否與數據庫結構一致是在最後完成的,若是因爲違反約束而沒法完成一個或多個插入則會引起異常。一般爲IntegrityError,並附有致使錯誤的緣由解釋。

檢索對象

咱們如今已將measurement值保存到數據庫中。爲了應用程序中使用這些數據,須要檢索它。使用類的查詢屬性完成表中的搜索。檢索起來十分很簡單:

In [1]: from app import db, Measurement
In [2]: Measurement.query.all()
Out[2]: [<Measurement 1>]
複製代碼

查詢對象的all方法將表中的全部記錄做爲Python列表返回。這在咱們的小測試中運行得很好,但在現實生活中咱們通常不用將全部內容加載到內存中。這時應該使用filter_by方法執行過濾。

In [3]: Measurement.query.filter_by(sensor="donkeysensor2000").all()
Out[3]: [<Measurement 1>]
複製代碼

最後的all方法再次返回過濾結果,filter_by只返回一個查詢對象。值得注意的是,filter_by是一種簡單的過濾方法,僅適用於精確匹配。經過某個sensor查找值頗有用,但若是咱們想要找到高於某個閾值的值,則須要使用filter方法。語法不一樣:

In [4]: Measurement.query.filter(Measurement.value > 100).all()
Out[4]: []
複製代碼

總而言之,咱們可使用SQLAlchemy的查詢對象作各類查詢(SQLAlchemy文檔-query - 注意在咱們使用Flask SQLAlchemy的例子中,是一個簡寫,完整寫做Measurement.querydb.session.query(Measurement))。
不管是哪一種查詢,一旦咱們取出查詢結果,他們就是Measurement實例:

In [5]: meas = Measurement.query.first()
In [6]: type(meas)
Out[6]: app.Measurement
In [7]: meas.sensor
Out[7]: 'donkeysensor2000'
In [8]: meas.value
Out[8]: 44.51
In [9]: meas.time
Out[9]: datetime.datetime(2018, 11, 21, 11, 20, 30)
複製代碼

刪除和修改對象

修改十分簡單,每當一個對象被修改了,它就會被標記(marked as dirty),以後的任何提交都會將這個修改應用到數據庫中。

In [10]: meas.sensor = "donkeysensor2001"
In [11]: db.session.commit()
In [12]: Measurement.query.first().sensor
Out[12]: "donkeysensor2001"
複製代碼

從數據庫中刪除行也簡單,和添加很類似:

In [13]: db.session.delete(meas)
In [14]: db.session.commit()
複製代碼

若是這項操做失敗了不會報錯,只會給出警告。

新建關係

在ORM中管理由外鍵鏈接的表已經很方便了。一般有一種容許模型類相互引用的機制以便一個實例裏的屬性能與另外一個模型類的產生直接聯繫。在咱們的例子中,咱們能夠建立另外一個包含sensors表,而不經過name來識別sensor。因此咱們定義第二個模型類:

class Sensor(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32), nullable=False, unique=True)
    model = db.Column(db.String(128), nullable=False)
    location = db.Column(db.String(128), nullable=False)
    latitude = db.Column(db.Float, nullable=True)
    longitude = db.Column(db.Float, nullable=True)

    measurements = db.relationship("Measurement", back_populates="sensor")
複製代碼

咱們能夠看到,此表包含有關咱們以前可以呈現的sensor的更多信息。咱們還定義了一個關係屬性。這建立了咱們的外鍵鏈接的反面,能夠方便地訪問爲此傳感器記錄的全部測量值。爲了建立鏈接,咱們須要修改Measurement類:

class Measurement(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    sensor_id = db.Column(db.Integer, db.ForeignKey("sensor.id"))
    value = db.Column(db.Float, nullable=False)
    time = db.Column(db.DateTime, nullable=False)

    sensor = db.relationship("Sensor", back_populates="measurements")
複製代碼

關係構造函數中的back_populates關鍵字參數表示它將在關係的另外一側(Sensor類)引用回該模型的屬性。另請注意,使用db.ForeignKey定義外鍵時,必須使用表名(默認爲小寫的模型名),在建立與db.relationship的關係時,必須使用模型名稱。

此時咱們的數據庫結構就改變了。這意味着咱們必須刪除數據庫並從頭開始從新建立它。用於生產的數據庫,有一個名爲的流程叫作數據庫遷移(migration),不刪除而是保留數據並修改它以適應新樣式。
咱們刪除數據庫文件後,再次啓動控制檯並作一些操做:

In [1]: from app import db
In [2]: db.create_all()
In [3]: from app import Sensor, Measurement
In [4]: from datetime import datetime
In [5]: sensor = Sensor( 
   ...:     name="uo-donkeysensor-1", 
   ...:     model="donkeysensor2000", 
   ...:     location="kylymä", 
   ...: )
In [6]: meas = Measurement( 
   ...:      value=55.41, 
   ...:      sensor=sensor, 
   ...:      time=datetime.now() 
   ...: )
複製代碼

較爲奇妙的是:咱們能夠直接定義sensor屬性而不是在Measurement對象中定義sensor_id,它會自動填充外鍵字段。這是比較方便的,由於這時咱們的sensor實際上沒有id,由於咱們還沒提交修改。提交後能夠嘗試查詢並查看實際關係:

In [7]: db.session.add(sensor)
In [8]: db.session.add(meas)
In [9]: db.session.commit()
In [10]: Sensor.query.first().measurements
Out[10]: [<Measurement 1>]
In [11]: Measurement.query.first().sensor_id
Out[11]: 1
In [12]: Measurement.query.first().sensor
Out[12]: <Sensor 1>
複製代碼

注意:在meas中添加sensor也能夠省略,轉而經過append方法把sensor實例添加到其屬性上。(注意db.session.add也被省略了)

In [13]: meas2 = Measurement( 
    ...:     value=12.35, 
    ...:     time=datetime.now() 
    ...:     )
In [14]: sensor.measurements.append(meas2)
In [15]: db.session.commit()
In [16]: Sensor.query.first().measurements
Out[16]: [<Measurement 1>, <Measurement 2>]
複製代碼

練習4:構造關係

學習目的:學會如何用SQLAlchemy ORM定義一對多/多對一關係。

您將經過直接添加一個新的模型來拓展上一個練習。與示例相似,咱們但願你建立的表能包含products的更多信息。另外一個須要添加的是位置字段,以便數據庫能夠跟蹤存儲在不一樣位置的數據。

修改模型StorageItem

  • 須要新建的列:
    • product_id (integer, foreign key product.id)
    • location: (string, max length 64, not null)
  • 須要刪除的列:
    • handle
    • price

這個模型類還須要定義一個叫product的關係,並帶有Product表in_storage屬性的反向引用(back reference)。

答案_t4

注:SQLite中的外鍵

默認狀況下SQLite不強制執行外鍵約束。換句話說,若是你使用上面的方法來建立相關的對象,你就不被容許違反它們。惟一的方法是手動將外鍵字段(例如sensor_id)值設置爲無效的值。可是,你仍然應該知道將此代碼段放在應用程序中會爲SQLite啓用外鍵(來源:SQLite文檔):

from sqlalchemy.engine import Engine
from sqlalchemy import event

@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
    cursor = dbapi_connection.cursor()
    cursor.execute("PRAGMA foreign_keys=ON")
    cursor.close()
複製代碼

「同生共死」

因爲外鍵被約束到另外一個表中存在的值,當刪除另外一個表中的目標行時會發生什麼?答案是:取決於給它分配的操做。最經常使用的操做是將指向被刪除行的那一行也刪除(CASCADE)。或者將它的值設爲空(SET NULL)。同理,更新項目也適用:若是設置cascade,外鍵的值也會隨着它指向的值改變。當同時把主鍵設爲外鍵時做用不大,由於此時通常主鍵不會改變。設置這些屬性只須要在外鍵構造器中添加ondelete和onupdate屬性並賦以相應字符串值,例如:"CASCADE" 和 "SET NULL"。

操做的選擇取決於它將如何被應用。例如,刪除sensor數據時,咱們例子中的sensor若是使用cascade將會丟失measurement數據,因此咱們設置set null,measurement記錄得以保存,但它將不屬於任何sensor。一般這兩種作法都不是很好,最好不要刪除sensor,而是給他們作一個標記「inactive」。
這裏咱們暫且只選擇set null。

class Measurement(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    sensor_id = db.Column(db.Integer, db.ForeignKey("sensor.id", ondelete="SET NULL"))
    value = db.Column(db.Float, nullable=False)
    time = db.Column(db.DateTime, nullable=False)

    sensor = db.relationship("Sensor", back_populates="measurements")
複製代碼

經過關係查詢

有時候把數據放到關聯起來的表裏就好了,但當咱們想要,好比,查找全部被同一個sensor測量出的measurement記錄時該怎麼辦?簡單的查詢作不到這一點,由於咱們要的信息不在一張表上。這就須要用到SQLAlchemy的join方法。它是一個SQL鏈接語句的簡化版本,可用於涉及多個表的查詢。那麼剛剛問題的答案就是:

In [1]: from app import db, Measurement, Sensor
In [2]: Measurement.query.join(Sensor).filter(Sensor.model == "donkeysensor2000").all()
Out[2]: [<Measurement 1>]
複製代碼

咱們須要的結果來自左表,同時也有咱們要用做join方法搜索的參數列。咱們固然能夠從measurement中向過濾方法中填加列以篩選到超過某閾值的記錄:

In [3]: Measurement.query.join(Sensor).filter(Sensor.model == "donkeysensor2000",  Measurement.value > 1.0).all()
Out[3]: [<Measurement 1>]
複製代碼

現代關係模型

若是咱們的數據庫中只可能有一種關係,那就有些落後了。儘管多對一在這門課程中最經常使用的用例關係,但wine本週股市該應知道如何使用SQLAlchemy定義其餘類型關係。一對一的關係很容易定義,由於它只是對上述多對一關係的修改。這是經過將關係構造函數的關鍵字參數uselist設置爲False來實現的。例如,讓咱們將傳感器的位置參數轉換爲位置表,每一個位置只能容納一個傳感器:

class Location(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    latitude = db.Column(db.Float, nullable=True)
    longitude = db.Column(db.Float, nullable=True)
    altitude = db.Column(db.Float, nullable=True)
    description=db.Column(db.String(256), nullable=True)

    sensor = db.relationship("Sensor", back_populates="location", uselist=False)

class Sensor(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(32), nullable=False, unique=True)
    model = db.Column(db.String(128), nullable=False)
    location_id = db.Column(db.Integer, db.ForeignKey("location.id"))

    location = db.relationship("Location", back_populates="sensor")
    measurements = db.relationship("Measurement", back_populates="sensor")
複製代碼

多對多關係就沒有那麼簡單明瞭。它須要建立一個由引用對組成的「關係表」,每一個關係的兩邊都各有一個外鍵。這些是必須的,由於數據庫的列中不能存在可變的序列類型變量(好比你的數據庫表中不能有python list - 儘管理論上你能夠將list序列化但這會很不明智,由於數據庫將不能理解表之間的關係了,從而使你的查詢效率低下)

舉個例子,若是咱們建立一個deployments新表 - 描述在一段時間內sensor所部署的位置。因爲deployment包含許多sensor,sensor也能夠表示在許多deployment中,這就是一個多對多關係。正如以前所說,須要新建一個輔助表來實現這個關係。因爲咱們不打算直接使用這個表,所以應將其定義爲表而不是模型:

deployments = db.Table("deployments",
    db.Column("deployment_id", db.Integer, db.ForeignKey("deployment.id"), primary_key=True),
    db.Column("sensor_id", db.Integer, db.ForeignKey("sensor.id"), primary_key=True)
)
複製代碼

經過同時設置兩者爲主鍵,兩個列共同造成了這個表。來自兩列的每一對值都是獨一無二的,它產生一個相似這樣的表:

deployment_id sensor_id
1 1
1 2
2 1
2 2

練習5:關係干預

咱們來練習使用關係模型。當嘗試建立錯誤時會發生什麼?在python控制檯中建立模型也是一種很好的作法。

學習目標:測試當關系限制時會發生什麼

在你開始以前:下載sensorhub應用文件: app_ex1_t5.py

將它放在虛擬環境中的文件夾中,而後打開IPython。鍵入如下兩行。其他的你必須弄清楚本身。

In [1]: from app import db, Deployment, Location, Sensor
In [2]: db.create_all()
複製代碼

你須要爲每一個位置建立兩個location實例(A 和 B)和相應的sensor(s1對應地址A,s2對應B)。最後你須要建立一個包含兩個sensor的deployment實例。你的任務是弄清如何建立這些實例。在多對多關係中,使用append來向關係中添加對象(例:deployment.sensor.append(s1)

任務步驟

該任務有兩個步驟,答案分爲四行,請使用print在控制檯查看你須要讀取的值。

注意:若是出現一場,你須要在繼續以前使用db.session.rollback()來回滾事務。

  1. 將sensor s1位置改成B。
    1. 首先改變模型中的位置,而後查看s2的location值
    2. 提交這次更改,看是否出錯,若是出錯,查看是什麼錯誤;不然查看傳感器s2的location值
  2. 重複將s1添加到deployment中
    1. 將s1 append到sensor列表中,查看deployment列表的值
    2. 提交這次更改,看是否出錯,若是出錯,查看是什麼錯誤;不然查看deployment的sensor列表

答案:
<Location 2>
IntegrityError
[<Sensor 1>, <Sensor 2>, <Sensor 1>]
[<Sensor 1>, <Sensor 2>]

web開發中的數據庫

儘管在控制檯管理數據庫有時會有用,但實際上咱們不常常這麼作。一般,當經過HTTP請求將某些內容添加到數據庫時,它將使用POST方法完成,而且行的值包含在request body中。使用Flask,訪問request body的方式因請求的MIME類型而有所不一樣。在交互式web應用中,這些值一般來自於一些表單,這能夠經過request.form這個相似字典的對象(相似request.args)訪問到。舉一個帶有最簡化錯誤碼返回的例子(並從服務器返回時間戳而非客戶端)。

@app.route("/measurements/add/", methods=["GET", "POST"])
def add_measurement():
    if request.method == "POST":
        # 當用戶提交這個表單時:
        try:
            sensor_name = request.form["sensor"]
            sensor = Sensor.query.filter_by(name=sensor_name).first()
            if sensor:
                value = float(request.form["value"])
                meas = Measurement(
                    sensor=sensor,
                    value=value,
                    time=datetime.now()
                )
                db.session.add(meas)
                db.session.commit()
            else:
                abort(404)
        except (KeyError, ValueError, IntegrityError):
            abort(400)
    else:
        # 當用戶加載表單並將其渲染爲HTML時
        pass
複製代碼

這是一個很是常見的處理表單的view函數代碼結構。它同時用這個URL對錶單進行查詢和提交,而且把兩種不一樣的請求方式區分開來(應當注意咱們必須在路由中定義容許這兩種方式)在實際應用中,咱們返回更準確的的錯誤消息。並且,咱們還須要對錶單進行驗證處理。

一般數據經過文檔符號格式(document notation format)傳輸,目前大部分是JSON格式(之前是XML)。JSON有着本身的MIME類型而且Flask經過request.json使數據得以做爲Python數據結構傳輸(其實是json.loads對request body處理後的結果)。

假設JSON文件像這樣:

{
    "sensor": "uo-donkeysensor-1",
    "value": 44.51
}
複製代碼

咱們的代碼讀取並像這樣把它保存到數據庫中。要注意此次只有一種請求方式是容許的,因此不須要過多的判斷它:

@app.route("/measurements/add/", methods=["POST"])
def add_measurement():
    # This branch happens when user submits the form
    try:
        sensor_name = request.json["sensor"]
        sensor = Sensor.query.filter_by(name=sensor_name).first()
        if sensor:
            value = float(request.json["value"])
            meas = Measurement(
                sensor=sensor,
                value=value,
                time=datetime.now()
            )
            db.session.add(meas)
            db.session.commit()
        else:
            abort(404)
    except (KeyError, ValueError, IntegrityError):
        abort(400)
複製代碼

可是,因爲此處的sensor其實是做爲數據庫中的對象存在的東西,所以一般最好將其做爲變量包含在URL中,而不是從請求主體中提取的內容。這樣,當API爲未知sensor返回404時,客戶端開發人員能夠更容易地肯定他們請求的sensor與任何現有的不匹配,而且問題出在URL中。

@app.route("/<sensor_name>/measurements/add/", methods=["POST"])
def add_measurement(sensor_name):
    # This branch happens when user submits the form
    try:
        sensor = Sensor.query.filter_by(name=sensor_name).first()
        if sensor:
            value = float(request.json["value"])
            meas = Measurement(
                sensor=sensor,
                value=value,
                time=datetime.now()
            )
            db.session.add(meas)
            db.session.commit()
        else:
            abort(404)
    except (KeyError, ValueError, IntegrityError):
        abort(400)
複製代碼

練習6:庫存管理:web api實現

總結本練習要點,暫時不考慮API設計原則建立小API應用。基於以前的任務,完善這個庫存管理系統。

學習目標:建立一個簡單的Flask應用,並容許經過接口的GET和POST請求管理數據庫 在你開始以前
在簡歷關係模型的練習中咱們已經有了這個應用的基礎。你能夠基於那個開始本次練習。

在實現使用POST功能時,你也須要了解如何進行測試

注意:因爲Flask的測試客戶端的實現方式,您的代碼必須經過訪問JSON數據request.json 第一個函數:add_product

  • 路徑: "/products/add"
  • 參數:
    • 沒有參數
  • 請求正文JSON字段:
    • "handle" (字符串)
    • "weight" (浮點)
    • "price" (浮點)
  • 返回值:
    • 201:若是成功的話
    • 400:"Weight and price must be numbers"或"Incomplete request - missing fields"
    • 405:"POST method required"或Flask的默認405消息
    • 409:"Handle already exists"- 若是已存在具備相同句柄的產品
    • 415:"Request content type must be JSON"- 若是請求正文沒法做爲json加載
      該函數將從客戶端發送的JSON文檔中獲取產品信息,並使用給定信息在數據庫中建立新產品。若是請求以多種方式出現故障,您能夠選擇要返回的錯誤代碼。據推測,客戶端開發人員將首先修復該錯誤,而後在後續嘗試中獲取第二個錯誤。

第二個函數: add_to_storage

  • 路徑:"/storage/<product>/add"
  • 參數:
    • 要添加到存儲中的產品(產品手柄)
  • 請求正文JSON字段:
    • "location" (串)
    • "qty" (INT)
  • 返回:
    • 201:若是成功的話
    • 400:"Qty must be an integer"或"Incomplete request - missing fields"
    • 404:"Product not found"- 若是手柄沒有找到產品
    • 405:"POST method required"或Flask的默認405消息
    • 415:"Request content type must be JSON"- 若是請求正文沒法做爲json加載 因爲數據庫ID並不具備任何意義,所以產品由其餘惟一字段標識:handle。所以,該函數必須檢查給定句柄是否與數據庫中的產品匹配,若是不匹配則返回404。若是請求有效,則該函數必須建立與所選產品相關的存儲條目。

第三功能: get_inventory

  • 路線: "/storage/"
  • 參數:
    • 沒有參數
  • 返回:
    • 200:包含做爲對象數組的庫存的JSON文檔
    • 405:"GET method required"- 若是方法不是GET

若是使用GET方法調用此URL,則會將完整清單提取到一個列表中,該列表未來自兩個數據庫表的數據彙總在一塊兒。該列表爲每一個現有產品都有一個項目。這個項目是一個包含如下鍵的字典:

  • "handle":產品的手柄
  • "weight":產品重量
  • "price":產品價格
  • "inventory":此產品的存儲條目列表 - 每一個指示位置和數量的元組。

這個數據結構將做爲JSON放入響應體中(您可使用json.dumps對其進行序列化),例如(爲讀者的理智添加縮進):

[
   {
       「handle」:「donkey plushie」,
       「weight」:1.20,
       「price」:20.00,
       「inventory」:[
           [「shop」,2],
           [「warehouse」,42]
       ]
   }
]
複製代碼

答案:answer_ex1_t6.py


知識共享許可協議
本做品採用 知識共享署名-相同方式共享 4.0 國際許可協議進行許可。
相關文章
相關標籤/搜索