tornado入門看這一篇足以

文章來源blog

建議去原文看,這排版不是太好

1.簡介

Tornado是一個python語言的web服務框架,它是基於社交聚合網站FriendFeed的實時信息服務開發而來的。

2007年由4名Google前軟件工程師一起創辦了FriendFeed,旨在使用戶能夠方便地跟蹤好友在Facebook和Twitter等多個社交網站上的活動。結果兩年後,Facebook宣佈收購FriendFeed

Tornado使FriendFeed使用的可擴展的非阻塞Web服務器及其相關工具的開源版本,這個Web框架看起來有些像web.py或 Google的webapp,不過爲了更加有效地利用非阻塞服務器環境,Tornado這個Web框架還包含了一些相關的有用工具和優化。

與其他web框架一些區別

  • 非阻塞式的服務器

速度相當快,使用了epoll非阻塞的方式,每秒可以處理數以千計的連接。

  • 單進程單線程異步IO的網絡模型

特點

  • 輕量級Web框架
  • 異步非阻塞IO處理方式(epoll)

單進程單線程異步IO的網絡模式, epoll的異步網絡IO

  • 出色的抗負載能力
  • 不依賴多進程或多線程(協程機制)
  • WSGI全棧替代產品

WSGI把應用(Application)和服務器(Server)結合起來,Tornado既可以是WSGI應用也可以是WSGI服務。
既是WebServer也是WebFramework

  • 支持長連接
  • AysncIo
  • http異步客戶端AsyncHttpClient

常用tornado參考doc

2.架構

可以先跳過架構這塊介紹,到安裝使用。(使用過了tornado,再來看整體架構就清晰容易多了)

2-1.分層

Tornado不僅僅是一個Web框架,它完整地實現了HTTP服務器和客戶端,再此基礎上提供了Web服務,它可分爲四層:

  • Web框架:最上層,包括處理器、模板、數據庫連接、認證、本地化等Web框架所需功能。
  • HTTP/HTTPS層:基於HTTP協議實現了HTTP服務器和客戶端
  • TCP層:實現TCP服務器負責數據傳輸
  • Event層:最底層、處理IO事件

在這裏插入圖片描述

注意顏色分層

  • httpserver: 服務於web模塊的一個簡單的HTTP服務器的實現

Tornado的HTTPConnection類用來處理HTTP請求,包括讀取HTTP請求頭、讀取POST傳遞的數據,調用用戶自定義的處理方法,以及把響應數據寫給客戶端的socket。

  • iostream: 對非阻塞式的socket的封裝以便於常見讀寫操作

爲了在處理請求時實現對socket的異步讀寫,Tornado實現了IOStream類用來處理socket的異步讀寫。

  • ioloop 核心的I/O循環

Tornado爲了實現高併發和高性能,使用了一個IOLoop事件循環來處理socket的讀寫事件,IOLoop事件循環是基於Linux的epoll模型,可以高效地響應網絡事件,這是Tornado高效的基礎保證。

2-2.模塊

Tornado是一個輕量級框架,它的模塊不多最重要的模塊是web,web模塊包含了Tornado大部分主要功能的Web框架,其他模塊都是工具性質的,以便讓Web模塊更加有用。

Core Web Framework 核心Web框架

  • tornado.web 包括Web框架大部分主要功能,包括RequestHandler和Application類。
  • tornado.httpserver一個無阻塞HTTP服務器的實現
  • tornado.template模板系統
  • tornado.escape HTML、JSON、URLs等編碼解碼和字符串操作
  • tornado.locale國際化支持

Asynchronous Networking 異步網絡底層模塊

  • tornado.ioloop 核心IO循環
  • tornado.iostream對非阻塞的Socket的簡單封裝以方便常用讀寫操作
  • tornado.httpclient無阻塞的HTTP服務器實現
  • tornado.netutil網絡應用的實現主要是TCPServer類

Integration With Other Services 系統集成服務

  • tornado.auth 使用OpenId和OAuth進行第三方登錄
  • tornado.databaseMySQL服務端封裝
  • tornado.platform.twisted在Tornado上運行Twisted實現的代碼
  • tornado.websocket實現和瀏覽器的雙向通信
  • tornado.wsgi其他Python網絡框架或服務器的相互操作

Utilities 應用模塊

  • tornado.autoload產生環境中自動檢查代碼更新
  • tornado.gen基於生成器的接口,使用該模塊 保證代碼異步運行。
  • tornado.httputil分析HTTP請求內容
  • tornado.options解析終端參數
  • tornado.process多進程實現的封裝
  • tornado.stack_context異步環境中對回調函數上下文保存、異常處理
  • tornado.testing單元測試

2-3.請求流程

注意請求和返回的流程(顏色區別,以IO Loop爲起點)
在這裏插入圖片描述
在這裏插入圖片描述

3.安裝

注意版本Tornado和python的版本:

  • Tornado4.3 可以運行在Python2.6、2.7、3.2+
  • Tornado6.0需要Python3.5.2+

3-1.python環境安裝

python環境可以分爲本地環境、虛擬環境、遠程環境

  • 本地環境

本機安裝個python,然後配置下環境變量。

  • 虛擬環境

借用一些軟件(比如:Anaconda、Virtualenv/Virtualenvwrapper),安裝虛擬環境,可以達到隔離效果,多個python在一臺機器上也不會衝突

  • 遠程環境

非本機,一臺遠程機器上python環境

我使用的是Anaconda,如何使用和安裝可以參照這篇blog。 python環境安裝

至於virtualenv和本機安裝環境,自行用搜索引擎解決下

3-2.開發工具

主流的python開發工具

  • pycharm
  • vscode
  • eclipse

我使用的是pycharm,如何安裝也不多介紹了。裏面有下載地址:python環境安裝

3-3.pip使用

pip如何使用,如何切換國內網,也都看這篇blog吧。python環境安裝

3-4.安裝tornado

# 使用anaconda創建一個tornado虛擬環境
conda create --name iworkh-tornado python=3.7

# 查看有哪些環境
conda info -e

# 切換到tornado虛擬環境
activate iworkh-tornado

# pip安裝tornado
pip install tornado

# 或使用condo安裝tornado
conda install tornado

3-5.創建項目

pycharm創建項目

在這裏插入圖片描述

因爲前異步使用anaconda創建一個tornado虛擬環境,所以就使用已存在的環境了。

當然如果前面沒有自己創建虛擬環境,也可以使用pycharm裏的new environment using XXX(選中虛擬化工具),來創建新的。

至此,tornado的開發環境基本ok了,如果有問題,可在留言區留言,或者QQ聯繫我。

4.tornado優勢

4-1.web框架比較

提到python web框架,那麼就想到了Django和Flask,那麼他們有啥區別呢?

可以讀下這幾篇blog

總結以幾點下:

  • 重量級

django比較重量級;flask和tornado都比較輕量級

  • 開發效率

django大而全的框架,效率高;

  • 併發能力

django和flask同步,能力查;tornado異步框架,性能相對好

  • 長連接

tornado適合用於開發長連接多的web應用。比如股票信息推送、網絡聊天等。

  • 數據庫與模板處理性能

Tornado與Flask旗鼓相當;django慢

  • 部署方面

tornado即使web框架,也是web服務;flask和tornado知識web框架,需要藉助web服務器來部署

4-2.如何高併發

  • 異步非阻塞IO(基於epoll事件驅動)
  • 通過協程來實現高併發處理

4-3.建議

  • 不在tornado中使用大量的同步IO操作 (發揮不了tornado的特性)
  • 不要將耗時的操作都往線程池中扔 (這並不能提高tornado多大的性能)
  • tornado的線程和協程是兩個概念
  • 編程時使用asnyc和await操作,而不建議coroutine協程裝飾器和yield

coroutine是一個gen方式歷史過濾方案,在python3.x後,已經使用了asnyc

5.tornado異步編程

我們都知道cpu的處理能力要遠遠大於IO的處理能力,而IO是阻塞操作,即io阻塞了,cpu即在休息,在虛度時間,在cpu資源,(浪費是可恥的,我們要利用最大化)。

當然不只本地磁盤IO,我們訪問網絡,常使用的requestsurllib的庫都是同步的。

5-1.io模型

iworkh-java-nio的linux io分類

io模型主要分爲同步/異步阻塞/非阻塞

同步和異步:是相對於調用方而言的
阻塞和非阻塞: 是相對於被調用方而言的

譬如:用戶程序(用戶空間,A)調用了文件系統內容(內核空間,B),假設B操作比較耗時要10秒鐘。

對於A而言,是立刻得到B的返回結果呢,還是一直等着。等B處理完能夠通知A,還是A要一直在等着?(同步/異步)
對於B而言,接收到A的請求了,處理完後,如何能夠通知到A,不用限制着A,要一直等着?(阻塞/非阻塞)

阻塞

安裝下requests工具表

pip install requests

代碼

import requests
blogHtml = requests.get("https://iworkh.gitee.io/blog/").content.decode("utf8")
print(blogHtml)

requests.get是阻塞,如果網不好或服務器響應很慢的話,那阻塞這回很耗時

非阻塞

import socket

# AF_INET:服務器之間網絡通信,SOCK_STREAM 流式socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 設置非阻塞
client.setblocking(False)

# 傳個tuple
address = "www.baidu.com"
try:
    client.connect(("www.baidu.com", 80))
except BlockingIOError as e:
    print("連接中...幹些其他事")
    pass

while True:
    try:
        client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format("/", address).encode("utf8"))
        print("連接成功")
        break
    except OSError as e:
        pass

# 定義bytes
data = b""

while 1:
    try:
        rec = client.recv(512)
        if rec:
            data += rec
        else:
            print(data.decode("utf8"))
            break
    except BlockingIOError as e:
        pass

setblocking設置爲非阻塞的,然後後面一直使用while循環來處理,直到正常返回結果。(比太關注死循環的寫法,而要關注於非阻塞了)

5-2.io多路複用

IO多路複用:一個進程監控多個描述符fds,當描述符狀態爲read、write等狀態時,通知程序進行相應的操作。

select、poll和epoll都是多路複用機制。它們本質上也是同步IO,它們在讀寫過程是阻塞的。

而異步IO是非阻塞,由內核將數據從內核空間拷貝到用戶空間

像瞭解其原理德胡啊,強力建議去看篇blog

5-2-1.select

select函數監控3類文件描述符: readfds,writefds,exceptfds。
window和linux都支持,但是單進監控的文件描述符有限,linux一般爲1024。

在這裏插入圖片描述

  • 沒有數據時,進程A被放到等待隊列,即cpu沒有處理進程A

在這裏插入圖片描述

  • 當有數據來,socket會處於就緒狀態,將進程A放到cpu隊列中去工作
  • 但是線程A不知道哪些socket是就緒的,所以只能全部遍歷一邊,去找到就狀態了。

5-2-3.poll

poll使用一個pollfd指針實現。pollfd監控even事件,雖們沒有最大限制,但是太多也影響性能。需要遍歷所有的描述符。

某時刻,文件描述符,處於就緒狀態很少。但是卻要遍歷所有描述符,因此當文件描述符很多時,性能會下降。

5-2-4.epoll

epoll對select和poll增強。是在2.6內核中提出的。沒有最大文件描述符的限制。

epoll使用一個文件描述符區管理多個描述符,將用戶的文件描述符event存在內核時間表中,這樣用戶空間和內核只要copy一次。

由於喚醒的進程,不知道哪些是就緒狀態,所以可以將就緒狀態的socket存下以便查找,不用都所有都遍歷一遍。

在這裏插入圖片描述

  • 使用了eventpoll來記錄,監控進程和哪些socket是處於就緒狀態

在這裏插入圖片描述

  • 當Socket接收到數據,中斷程序一方面修改 Rdlist,另一方面喚醒 eventpoll 等待隊列中的進程,進程A再次進入運行狀態
  • 也因爲 Rdlist 的存在,進程 A 可以知道哪些 Socket 發生了變化。

5-2-5.回調異步

上面異步代碼,都是while和try來處理,這我們修改下使用select方式進行改造

import socket

from selectors import DefaultSelector, EVENT_WRITE, EVENT_READ

selector = DefaultSelector()

class CallBackRequestSite:
    def get_url(self, url):
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.setblocking(False)
        self.host = url
        self.data = b""
        try:
            self.client.connect((self.host, 80))
        except BlockingIOError as e:
            print("連接中...幹些其他事")
            pass

        selector.register(self.client.fileno(), EVENT_WRITE, self.connected)

    def connected(self, key):
        selector.unregister(key.fd)
        self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format("/", self.host).encode("utf8"))
        selector.register(self.client.fileno(), EVENT_READ, self.read_data)

    def read_data(self, key):
        recv_data = self.client.recv(1024)
        if recv_data:
            self.data += recv_data
        else:
            selector.unregister(key.fd)
            print(self.data.decode("utf8"))

def loop():
    while 1:
        # 根據系統是否支持,使用epoll還是select,優先epoll。默認阻塞,有活動連接就返回活動的連接列表
        ready = selector.select()
        for key, mask in ready:
            callback = key.data
            callback(key)


if __name__ == '__main__':
    demo = CallBackRequestSite()
    demo.get_url("www.baidu.com")
    loop()

上面方式是通過回調函數的方式實現的異步操作,雖然異步了,但是回調方式的缺點也很明顯

回調方式的缺點

  • 回調太多,會導致調用鏈太深(回調地獄)
  • 不便於閱讀和維護

5-3.協程

協程(Coroutines)是一種比線程更加輕量級的存在,正如一個進程可以擁有多個線程一樣,一個線程可以擁有多個協程。

進程和線程是由系統控制,而協程是由程序自己控制的。好處是性能大幅度的提升,因爲不會像線程切換那樣消耗資源。

一個進程可以包含多個線程,一個線程也可以包含多個協程。一個線程的多個協程的運行是串行的。當一個協程運行時,其它協程必須掛起。

多核CPU,多個進程或一個進程內的多個線程是可以並行運行的,但一個線程內協程卻絕對是串行的,無論CPU有多少個核。

如對進程、線程、協程不太清除的閱讀這篇文件 進程、線程、協程

在這裏插入圖片描述

5-3-1.yield生成器

協程的一個重要基礎就是:程序運行一個協程能夠暫停,去運行另外的協程。(如何暫停?纔是關鍵)

yield就是可以滿足程序(函數/方法)執行時,暫停。

def yield_method():
    print("do start...")
    yield 1
    res2 = yield 2
    print("do c...res2:{}".format(res2))
    yield 3

這是最簡單的函數中使用yield的例子,resp = yield_method()是一個generator生成器。

  • next(resp): 可以使得程序從一個yield到下一個yield的執行。(當到最後一個yield時,再執行next會報錯)
  • send: 可以發送參數給之前yield,並執行到下一個yield
  • foreach:一般都使用foreach來處理

不懂的可以閱讀下這篇文章 python中yield的用法詳解

def yield_method():
    print("do start...")
    yield 1
    print("do a...")
    res1 = yield 2
    print("do b...res1: {}".format(res1))
    res2 = yield 3
    print("do c...res2:{}".format(res2))
    yield 4


def call_hand():
    resp = yield_method()
    print(next(resp))  # 運行到 yield 1處,返回 1
    print(resp.__next__())  # 從yield 1開始運行,到 yield 2處,返回 2
    print(resp.send("got it"))  # 從yield 2開始運行,並把send值給yield 2的變量,到 yield 3處,返回 3
    print(next(resp))  # 從yield 3開始運行,到 yield 4處,返回 4

def call_for():
    for item in yield_method():
        print(item)


if __name__ == '__main__':
    print(yield_method())
    print("*" * 30)
    call_hand()
    print("*" * 30)
    call_for()

5-3-2.異步

在tornado中,定義協程代碼有多種方式

  • @coroutine裝飾器(裝飾器又分多家的,tornado和python都有)(這不推薦使用了)
  • async方式 (推薦使用)

直接上代碼,裏面有註釋,具體自己悟

import asyncio
from asyncio import coroutine as pycoroutine
from time import sleep, time

from tornado.gen import coroutine as tcoroutine


async def async_method():
    await asyncio.sleep(2)
    print("do a...")
    await await_sleep_one_min()
    print("do b...python的async和await")
    await t_yield_sleep_one_min()
    print("do c...tornado裝飾器")
    await py_yield_from_sleep_one_min()
    print("do d...python裝飾器")
    await asyc_sleep_one_min()
    print("do e...")


# 不推薦使用, tornado裝飾器方式
@tcoroutine
def t_yield_sleep_one_min():
    print("等了又等--1")
    yield sleep(1)
    print("等了又等--2")
    yield sleep(1)
    print("等了又等--3")
    yield sleep(1)


# 不推薦使用,python裝飾器方式,python的舊語法
@pycoroutine
def py_yield_from_sleep_one_min():
    yield from asyncio.sleep(1)


# 推薦使用(async與await組合,因爲asyncio.sleep也是個異步代碼,需要用await)
async def await_sleep_one_min():
    await asyncio.sleep(1)


# 推薦使用(async單獨使用,因爲sleep不是個異步方法,不需要用await)
async def asyc_sleep_one_min():
    print('asyc_sleep_one_min----waiting')
    sleep(1)


if __name__ == '__main__':
    start = time()
    asyncio.run(async_method())
    end = time()
    print('cost time = ' + str(end - start))

簡單總結下:

  • 協程定義可以用裝飾器或者async修飾(推薦async),後面只說async形式了。
  • 協程A調用其他協程B(A和B函數都用async),協程A裏調用協程B的方法加使用await關鍵字,當然普通函數(非協程)不需要加await的

5-3-3.執行

執行協程方法,也有多種:tornado的IOLoop和python的asyncio

python方式

方式一:一直run

import asyncio

if __name__ == '__main__':
    start = time()
    # 一直run
    asyncio.ensure_future(async_method())
    asyncio.get_event_loop().run_forever()

    end = time()
    print('cost time = ' + str(end - start))

方式二:run完就結束

asyncio.run(async_method())

方式三:run完就結束

asyncio.get_event_loop().run_until_complete(async_method())

tornado方式

# 通過tornado的IOLoop來run (執行某個協程後停止事件循環)
IOLoop.current().run_sync(async_method)

5-4.AsnycHttpClient

同步方式HTTPClient

from tornado import httpclient

def get_url(url):
    http_client = httpclient.HTTPClient()
    resp = http_client.fetch(url)
    print(resp.body.decode("utf8"))

if __name__ == '__main__':
    web_site = "https://iworkh.gitee.io/blog/"
    get_url(web_site)

異步方式AsnycHttpClient

import asyncio

from tornado import httpclient


async def async_get_url(url):
    http_client = httpclient.AsyncHTTPClient()
    resp = await http_client.fetch(url)
    print(resp.body.decode("utf8"))


if __name__ == '__main__':
    web_site = "https://iworkh.gitee.io/blog/"
    asyncio.run(async_get_url(web_site))

6.tornado web基礎

tornado.web提供了一種帶有異步功能並允許它擴展到大量開放連接的簡單的web框架, 使其成爲處理長連接(long polling) 的一種理想選擇.

6-1.簡單示例

import time

from tornado import web
from tornado.ioloop import IOLoop


class HelloWorldHandlerOne(web.RequestHandler):
    async def get(self, *args, **kwargs):
        # 別在handler中寫同步的代碼,會被阻塞的
        time.sleep(5)
        self.write("hello world---one")


class HelloWorldHandlerTwo(web.RequestHandler):
    def get(self, *args, **kwargs):
        self.write("hello world---two")


url_map = [
    ("/test1", HelloWorldHandlerOne),
    ("/test2", HelloWorldHandlerTwo)
]

if __name__ == '__main__':
    app = web.Application(url_map, debug=True)
    app.listen(8888)
    IOLoop.current().start()

幾點說明

  • handler類要繼承web.RequestHandler,然後重寫rest一些接口(get,post,put,delete等)
  • 啓動類,使用了web.Application,傳了參數url和handler對應關係。debug參數
  • 在handler的方法裏不要調用同步的方法,這樣會阻塞請求。

即便請了不同的接口(test1和test2),由於test1的handler中time.sleep(5),先請求test1,後請求test2,test2也會等test1響應完,纔會響應test2(單線程的)。

debug爲true

  • debug設置爲true,修改了代碼,不用重啓服務
  • idea的Run啓動:後臺啓動。如果關閉需要控制面板後臺關閉
  • idea的Debug啓動:修改內容,會自動停了。得再次debug啓動
  • 命令行啓動:後臺啓動。ctr+c可以退出

測試url

  • http://localhost:8888/test1
  • http://localhost:8888/test2

6-2.url匹配

url匹配主要是對於python的表達式的使用,如不瞭解可以先去了解下 菜鳥教程-python-正則表達式

from tornado import web, ioloop


class IndexHandler(web.RequestHandler):
    async def get(self, *args, **kwargs):
        self.write("首頁")


class UserHandler(web.RequestHandler):
    async def get(self, name, *args, **kwargs):
        self.write("hello world, {}".format(name))


class ProductHandler(web.RequestHandler):
    def get(self, name, count, *args, **kwargs):
        self.write("購買{}個{}".format(count, name))


class DivHandler(web.RequestHandler):
    #  one,two這個要跟url裏的(?P<one>\d+)/(?P<two>\d+)裏的名稱匹配
    def get(self, one, two, *args, **kwargs):
        try:
            result = int(one) / int(two)
            self.write("{}/{}={}".format(one, two, result))
        except ZeroDivisionError:
            # 發生錯誤,會跳轉
            self.redirect(self.reverse_url('error_page', 500))
            pass


class ErrorHandler(web.RequestHandler):
    def get(self, code, *args, **kwargs):
        self.write("發生錯誤:error_code: {}".format(code))


url_map = [
    ("/user/(\w+)/?", UserHandler),
    ("/product/(\w+)/(\d+)/?", ProductHandler),
    ("/calc/div/(?P<one>\d+)/(?P<two>\d+)/?", DivHandler),
    web.URLSpec("/index/?", IndexHandler, name="index"),
    web.URLSpec("/error/(?P<code>\d+)/?", ErrorHandler, name="error_page")
]

if __name__ == '__main__':
    app = web.Application(url_map, debug=True)
    app.listen(8888)
    print('started...')
    ioloop.IOLoop.current().start()

幾點說明

  • /?表示0或1個/,即url最後可以有/也可以沒有/
  • url定義,可以使用tuple簡單實現,也可以使用web.URLSpec來創建對象,可以指定一些參數(url,handler,kwargs,name)
  • (?P<xxx>\d+),表示取一個組名,這個組名必須是唯一的,不重複的,沒有特殊符號,然後跟參數里名稱要一樣
  • redirect重定向,reverse_url根據名稱類獲取url

測試url

  • http://localhost:8888/index/
    結果:

首頁

  • http://localhost:8888/user/iworkh
    結果:

hello world, iworkh

  • http://localhost:8888/product/apple/3
    結果:

購買3個apple

  • http://localhost:8888/calc/div/4/2
    結果:

4/2=2.0

  • http://localhost:8888/calc/div/4/0
    結果:

發生錯誤:error_code: 500

  • http://localhost:8888/errpr/404
    結果:

發生錯誤:error_code: 404

6-3.動態傳參

動態傳參:將一些配置參數通過命令行、配置文件方式動態傳給程序,而不是代碼寫死。增加靈活性。

官網關於options的介紹 options-doc

動態傳參主要使用的tornado.options某塊,使用步驟:

  • 先定義哪些參數options.define()
  • 從命令行或文件獲取參數值options.parse_command_line(),options.parse_config_file("/server.conf")
  • 使用參數options.xxx

6-3-1.命令方式

直接上代碼

from tornado import options, web, ioloop

options.define("port", default="8888", type=int, help="端口號")
options.define("static_path", default="static", type=str, help="靜態資源路徑")


class IndexHandler(web.RequestHandler):
    async def get(self):
        await self.finish('welcome to iworkh !!!')

url_map = [
    ("/?", IndexHandler)
]

setting = {
    'debug': True
}

if __name__ == '__main__':
    # 命令行方式
    options.parse_command_line()
    app = web.Application(url_map, **setting)
    app.listen(options.options.port)
    ioloop.IOLoop.current().start()

這注意: url_map是個list,而setting是個dic,去web.Application的初始化方法中就可以看到需要的參數類型

啓動命令

python option_demo.py --port=8080

通過--port指定啓動端口

測試url

  • http://localhost:8080/

6-3-2.文件方式

將上面啓動main代碼修改爲以下:

if __name__ == '__main__':
    # 配置文件方式
    options.parse_config_file('config.ini')
    app = web.Application(url_map, **setting)
    app.listen(options.options.port)
    ioloop.IOLoop.current().start()

配置文件內容

port = 8889

就加了端口號

測試url

  • http://localhost:8889/

6-4.handler

因爲後面內容,請求的方式不僅僅是get,還有post等,所以使用Postman工具來請求

Postman很簡單,自行下載使用下,這就不多介紹了

handler裏內容和細節相當得多,具體可以參照官網 RequestHandler-doc

我們主要從以下幾個方面入手

  • input (前臺傳來的參數,如何解析.)
  • process (RequestHandler常用的方法get,post,put,delete)
  • output (後臺處理後,如何返回給前臺)
  • 常用的handler(RequestHandler,ErrorHandler,FallbackHandler,RedirectHandler,StaticFileHandler)
  • 還有cookie和application,這部分看下官網

6-4-1.input

常用的函數

`get_argument`,`get_arguments`: 從post的form中解析參數,如果form中沒有,那會從url中解析
`get_query_argument`,`get_query_arguments`: 從url中解析參數
`get_body_argument`,`get_body_arguments`: json格式數據解析
`request`
`path_args`
`path_kwargs`

注意函數名有s和沒有s的區別:

  • 有s表示接受的是一個數組,即使一個數據,也會轉爲數組形式.
  • 沒有s的,但是傳了多個值(比如:http://localshot:8888/user?name=lilei&&name=wangwu),最後一個會生效(name=wangwu)

結論:

方法 傳參
get_query_argument(s) 參數在url中
get_argument(s) 參數在form中,沒有頭信息,form中沒有還可以從url中獲取
get_body_argument(s) 參數在form中,帶有頭信息
request.body 參數是json

演示代碼

import json

from tornado import web, ioloop


class UrlParamHandler(web.RequestHandler):
    async def get(self):
        name = self.get_query_argument("name")
        age = self.get_query_argument("age")
        self.write("name: {}, age: {}".format(name, age))

        self.write('<br/>')
        names = self.get_query_arguments("name")
        ages = self.get_query_arguments("age")
        self.write("names: {}, ages: {}".format(names, ages))


class FormParamHandler(web.RequestHandler):
    async def post(self):
        name = self.get_argument("name")
        age = self.get_argument("age")
        self.write("name: {}, age: {}".format(name, age))

        self.write('<br/>')
        names = self.get_arguments("name")
        ages = self.get_arguments("age")
        self.write("names: {}, ages: {}".format(names, ages))


class JsonWithFormHeadersParamHandler(web.RequestHandler):
    async def post(self):
        name = self.get_body_argument("name")
        age = self.get_body_argument("age")
        self.write("name: {}, age: {}".format(name, age))


class JsonParamHandler(web.RequestHandler):
    async def post(self):
        parm = json.loads(self.request.body.decode("utf8"))
        name = parm["name"]
        age = parm["age"]
        self.write("name: {}, age: {}".format(name, age))


url_handers = [
    ("/url", UrlParamHandler),
    ("/form", FormParamHandler),
    ("/jsonwithformheaders", JsonWithFormHeadersParamHandler),
    ("/json", JsonParamHandler)
]

if __name__ == '__main__':
    app = web.Application(url_handers, debug=True)
    app.listen(8888)
    print("started...")
    ioloop.IOLoop.current().start()

測試url
測試工具Postman,當然也可以使用python的requests來請求

1. 參數在url中,get請求

代碼請求接口方式測試

import requests
url="http://localhost:8888/url?name=zhangsan&&age=20"
resp = requests.request('GET', url).content.decode("utf8")
print(resp)

postman或瀏覽器方式測試

  • http://localhost:8888/url?name=zhangsan&&age=20
    結果:

name: zhangsan, age: 20
names: [‘zhangsan’], ages: [‘20’]

  • http://localhost:8888/url?name=zhangsan&&age=20&&name=lisi&&age=28
    結果:

name: lisi, age: 28
names: [‘zhangsan’, ‘lisi’], ages: [‘20’, ‘28’]

由上面結果,可以看出函數名有s和沒s的區別了,下面就其他方法就演示了,因爲有s的,就不演示了跟這類似.

個人覺得,不用s,傳多個值,爲啥不直接傳數組了.特別後面是json格式交互的時候,多個值的場景完全可以數組作爲值來傳遞

2. 參數在form中,沒有請求頭,post請求

代碼請求接口方式測試

import requests
url = "http://localhost:8888/form"
data = {
    "name": "zhangsan",
    "age": 20
}
resp = requests.request('POST', url, data=data).content.decode("utf8")

postman方式測試

  • http://localhost:8888/form
    結果:

name: lisi, age: 28
names: [‘zhangsan’, ‘lisi’], ages: [‘20’, ‘28’]

在這裏插入圖片描述

3. 參數在from中,並含有請求頭Content-Type:application/x-www-form-urlencoded,post請求
代碼請求接口方式測試

import requests
url = "http://localhost:8888/jsonwithformheaders"
headers = {"Content-Type": "application/x-www-form-urlencoded;"}
data = {
    "name": "zhangsan",
    "age": 20
}
resp = requests.request('POST', url, headers=headers, data=data).content.decode("utf8")
print(resp)

postman方式測試

  • http://localhost:8888/jsonwithform
    在這裏插入圖片描述
    4. 參數是json,沒有頭,post請求

代碼請求接口方式測試

import requests
url = "http://localhost:8888/json"
data = {
    "name": "zhangsan",
    "age": 20
}
resp = requests.request('POST', url, json=data).content.decode("utf8")
print(resp)

注意這這參數傳的是json=data,而不是data=data

postman方式測試

  • http://localhost:8888/json

沒有頭,只能通過request.body來拿到參數,而且是bytes類型,需要decode後,json解析後得到dict

在這裏插入圖片描述

提示: 注意json值傳遞時候,有沒header,取值方式不同.結論如下:

方法 傳參
get_query_argument(s) 參數在url中
get_argument(s) 參數在form中,沒有頭信息
get_body_argument(s) 參數在form中,帶有頭信息
request.body 參數是json

6-4-2.process

注意: process裏函數的執行順序

initialize: 初始化,通過`URLSpec`可將參數傳遞到方法
prepare: 在get/post等操作之前,會調用
get/head/post/delete/patch/put/options: 一般業務邏輯處理
on_finish: 在out操作之後,會調用

直接上代碼

from tornado import web, ioloop


class ProcessDemoHandler(web.RequestHandler):

    # initialize方法同步
    def initialize(self, dbinfo):
        print("initialize...")
        self.dbinfo = dbinfo
        print("數據庫host:{}".format(self.dbinfo['db_host']))
        pass

    async def prepare(self):
        print("prepare...")
        pass

    async def get(self):
        print("get...")
        self.write("success!!!")
        pass

    def on_finish(self):
        print("finish...")
        pass


init_param = {
    'dbinfo': {
        'db_host': 'localhost',
        'db_port': 3306,
        'db_user': 'root',
        'db_password': '123',
    }
}

url_handers = [
    (r"/?", ProcessDemoHandler, init_param),
]

if __name__ == '__main__':
    app = web.Application(url_handers, debug=True)
    app.listen(8888)
    print("started...")
    ioloop.IOLoop.current().start()

需要關注以下幾點:

1.順序: initialize 👉 prepare 👉 get/post/put/delete/… 👉 on_finish
2.initialize和on_finish方法同步
3.如何將參數通過url_handers傳給initialize方法

結果:

initialize...
數據庫host:localhost
prepare...
get...

6-4-3.output

set_status: 設置狀態碼
set_header
add_header
clear_header
write: 寫數據,可以write多次,放緩存中,而不會中斷當flush或者finish或者沒消息斷開時發送
flush: 刷新數據到客戶端
finish: 寫數據,寫完斷開了
render/render_string: 模板方式渲染輸出
redirect: 重定向
send_error/write_error: 發送錯誤
render_embed_js/render_embed_css: 嵌入的js和css
render_linked_js/render_linked_css: 鏈接的js和css
import asyncio
from tornado import web, ioloop

class OutputDemoHandler(web.RequestHandler):
    async def get(self):
        self.set_status(200)
        self.write("error!!!")
        self.write("warning!!!")
        self.write("<br/>")
        await self.flush()
        await asyncio.sleep(5)
        self.write("success!!!")
        await self.finish("done")


url_handers = [
    (r"/?", OutputDemoHandler),
]

if __name__ == '__main__':
    app = web.Application(url_handers, debug=True)
    app.listen(8888)
    print("started...")
    ioloop.IOLoop.current().start()

這裏主要使用了write,flush以及finish操作,其他前面用過就不多介紹了.
沒用過的render_xxx形式的函數,在模板中使用的。

結果:

error!!!warning!!!
success!!!done

第一行先出來,應爲flush了,過了5秒後,第二行數據出來

6-5.setting

setting裏每項值類型都是dic類型

配置主要分以下幾種類型

  • General settings
  • Authentication and security settings:
  • Template settings
  • Static file settings

這不會全部介紹,只介紹下常用的,在實際開發中,去官網查閱.

官網setting配置項 setting配置

打開後,直接搜索關鍵字settings,就能定位到

常用配置

debug

是否開啓debug模式

default_handler_class

當url沒有配置到handler時,默認處理器.

static_path

靜態資源路徑

static_url_prefix

靜態資源前綴

static_handler_class

靜態資源handler(默認是tornado.web.StaticFileHandler)

靜態資源代碼演示

from tornado import web, ioloop

class SettingDemoHandler(web.RequestHandler):
    async def get(self):
        await self.finish("success")


url_handers = [
    (r"/?", SettingDemoHandler),
]

settings={
    'debug': True,
    'static_path': 'public/static',
    'static_url_prefix': '/static/'
}

if __name__ == '__main__':
    app = web.Application(url_handers, **settings)
    app.listen(8888)
    print("started...")
    ioloop.IOLoop.current().start()

在相對路徑下,創建public/static/images文件夾,並放一張圖片進去

測試url

  • http://localhost:8888/static/images/moon.jpg

端口號後面的前綴url緊跟的路徑,要跟static_url_prefix配置一致

6-6.模板

模板就不多介紹了,工作中我們一般開發都前後端分離,因此這就不過多糾纏了。(單爲了文章的完整性,我還是寫了個簡單例子)

前端:react、vue等前端框架
後端:java、go、python等語言編寫的web框架跟前端進行數據交互

有興趣的可以看下官網:模板-docs

主要包含以下幾點

1.普通字符串模板,加載使用
2.文件模板,加載使用
3.常用的模板語法 {{ data }},{% set ...%},{% raw %}
4.導入python,調用函數,循環輸出
5.模板繼承 {% extends "xxx.html" %}
6.UIModule使用 {% module Template("foo.html", arg=42) %}

6-6-1.頁面結構

基本結構

在這裏插入圖片描述

6-6-1.結果

結果

在這裏插入圖片描述

6-6-2.代碼

代碼主要分模板、UIMoudle、orderHandler、啓動類

啓動類和handler

from typing import Optional
from tornado import web, ioloop

class OrderHandler(web.RequestHandler):

    def get(self):
        data = {
            'title': '書本列表',
            'orderList': [
                {'id': 1, 'name': 'java', 'price': 80, 'count': 1,
                 'link_to': '<a href="http://www.baidu.com">查看</a>'},
                {'id': 2, 'name': 'springboot', 'price': 100, 'count': 2,
                 'link_to': '<a href="http://www.baidu.com">查看</a>'},
                {'id': 3, 'name': 'springcloud', 'price': 120, 'count': 1,
                 'link_to': '<a href="http://www.baidu.com">查看</a>'},
            ]
        }
        self.render("tp_order.html", **data)


class FooterUIMoudle(web.UIModule):

    def render(self):
        return self.render_string("uimodules/tp_footer.html")

    def embedded_css(self) -> Optional[str]:
        css = '''
        .content { background: gray}
        a {text-decoration-line: none}
        '''
        return css


url_handlers = [
    (r"/order", OrderHandler)
]

settings = {
    "debug": True,
    "template_path": 'public/templates',
    "static_path": 'public/static',
    "static_url_prefix": "/static/",
    "ui_modules": {
        'FooterUIMoudle': FooterUIMoudle
    }
}

if __name__ == '__main__':
    app = web.Application(url_handlers, **settings)
    app.listen(8888)
    print("started successfully")
    ioloop.IOLoop.current().start()

settings裏定義ui_modulestemplate_path,static_path
注意給模板傳參數的類型,在目錄中也需要跟其對於方式取出值

tp_base.html模板
相對啓動類路徑:public/templates/tp_base.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>商城</title>
    <link rel="stylesheet" type="text/css" href="{{ static_url('css/base.css') }}">
    {% block custom_css %}
    {% end %}
</head>
<body>

    <div class="header">
        {% module Template("uimodules/tp_header.html") %}
    </div>
    <div class="content">
        {% block order_html_block%}
            <h2>訂單</h2>
        {% end %}
    </div>


    {% module FooterUIMoudle() %}


    {% block custom_js %}
    {% end %}
</body>
</html>

裏面使用UIMoudle、繼承等手段

tp_base.html模板
相對啓動類路徑:public/templates/tp_base.html

{% extends 'tp_base.html' %}

{% block order_html_block %}
<div class="center">
    <table style="margin: 0 auto">
        <caption>{{ title }}</caption>
        <thead>
        <tr>
            <td>id</td>
            <td>名稱</td>
            <td>價格</td>
            <td>數量</td>
            <td>詳情</td>
        </tr>
        </thead>
        <tbody>
        {% set total_price = 0 %}
        {% for item in orderList %}
        <tr>
            <td>{{ item["id"] }}</td>
            <td>{{ item["name"] }}</td>
            <td>{{ item["price"] }}元</td>
            <td>{{ item["count"] }}</td>
            <td>{% raw item["link_to"] %}</td>
            <div style="display: none">{{ total_price = total_price + item["price"] * item["count"] }}</div>
        </tr>
        {% end %}
        </tbody>
    </table>

    總價格: {{ total_price }}
</div>
{% end %}

裏面使用繼承、循環、raw、賦值等手段

uimoudles下的tp_footer.html模板
相對啓動類路徑:public/templates/uimodules/tp_header.html

<h2>welcome to iworkh !!!</h2>

uimoudles下的tp_footer.html模板
相對啓動類路徑:public/templates/uimodules/tp_footer.html

<div class="footer">
    <div style="padding: 0px 4px 10px; text-align: center; font-size: 12px;"><span>©2019&nbsp;iworkh&nbsp;</span><a href="http://beian.miit.gov.cn/" target="_blank"><span style="margin-left: 10px; color: rgb(147, 147, 147);">蘇ICP備19061993號</span></a><a target="_blank" href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=32010602010683"><span style="margin-left: 10px; color: rgb(147, 147, 147);">蘇公網安備 32010602010683號</span></a></div>
</div>

7.mysql

python裏PyMySQL是同步,所以在tornado我們得用異步的,這介紹下aiomysql,它是基於PyMySQL的。用法差不錯,只不過實現了異步操作。

安裝依賴

pip install aiomysql

7-1.簡單示例

import asyncio
import aiomysql

async def test_example(loop):
    pool = await aiomysql.create_pool(host='127.0.0.1', port=3306,
                                      user='root', password='',
                                      db='mysql', loop=loop)
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            await cur.execute("SELECT 42;")
            print(cur.description)
            (r,) = await cur.fetchone()
            assert r == 42
    pool.close()
    await pool.wait_closed()


loop = asyncio.get_event_loop()
loop.run_until_complete(test_example(loop))

這是官網Github上給的示例,主要注意以下幾點

  • async異步編寫
  • 調用異步操作時,要使用await
  • async with的用法,異步上下文管理器,接受會將對應的資源關閉的(異步上下文管理器指的是在enter和exit方法處能夠暫停執行的上下文管理器,__aenter____aexit__方法。)

有興趣可以去了解下async withasync for,百度下隨便挑一篇閱讀下就明白了(基本都雷同的😊)

創建表

CREATE TABLE `tb_order` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  `count` int(5) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入一條數據

INSERT INTO `iworkh_tornado`.`tb_order`(`id`, `name`, `price`, `count`)
VALUES (1, 'java', 80.00, 1);

7-1.查詢數據

使用get請求來請求rest接口,參數方法可以放在url中,也可以方法在form中 (通過get_argument獲取參數)

查詢邏輯

import aiomysql
from tornado import web, ioloop

class OrderHandler(web.RequestHandler):
    def initialize(self, db_info) -> None:
        self.db_info = db_info

    async def get(self):
        id = self.get_argument("id", None)
        if not id:
            raise Exception("id can not None")
        db_id = db_name = db_price = db_count = None
        async with aiomysql.create_pool(host=self.db_info["db_host"], port=self.db_info["db_port"],
                                        user=self.db_info["db_user"], password=self.db_info["db_password"],
                                        db=self.db_info["db_name"], charset=self.db_info["db_charset"]) as pool:
            async with pool.acquire() as conn:
                async with conn.cursor() as cur:
                    sql = "SELECT `id`, `name`, `price`, `count` FROM TB_ORDER WHERE id = {};".format(id)
                    print(sql)
                    await cur.execute(sql)
                    result = await cur.fetchone()
                    try:
                        if (result):
                            db_id, db_name, db_price, db_count = result
                    except Exception:
                        pass

            self.write("success, {}, {}, {}, {}".format(db_id, db_name, db_price, db_count))

initParam = {
    "db_info": {
        'db_host': 'localhost',
        'db_name': 'iworkh_tornado',
        'db_port': 3306,
        'db_user': 'iworkh',
        'db_password': 'iworkh123',
        'db_charset': 'utf8'
    }
}

url_handlers = [
    (r"/order/?", OrderHandler, initParam)
]

if __name__ == '__main__':
    app = web.Application(url_handlers, debug=True)
    app.listen(8888)
    print("started successfully")
    ioloop.IOLoop.current().start()

測試代碼

import requests

url = "http://localhost:8888/order"
data={
    'id':'1'
}
resp = requests.get(url, data=data).content.decode("utf8")
print(resp)

7-2.插入數據

查詢邏輯

import aiomysql
from tornado import web, ioloop


class OrderHandler(web.RequestHandler):
    def initialize(self, db_info) -> None:
        self.db_info = db_info

    async def post(self):
        db_name= self.get_argument("name")
        db_price= self.get_argument("price")
        db_count= self.get_argument("count")

        async with aiomysql.create_pool(host=self.db_info["db_host"], port=self.db_info["db_port"],
                                        user=self.db_info["db_user"], password=self.db_info["db_password"],
                                        db=self.db_info["db_name"], charset=self.db_info["db_charset"]) as pool:

            async with pool.acquire() as conn:
                async with conn.cursor() as cur:
                    sql = "INSERT INTO TB_ORDER (`name`, `price`, `count`) VALUES ('{}','{}','{}');"\
                        .format(db_name, db_price, db_count)
                    print(sql)
                    await cur.execute(sql)
                    await conn.commit()
            self.write("save successfully")

initParam = {
    "db_info": {
        'db_host': 'localhost',
        'db_name': 'iworkh_tornado',
        'db_port': 3306,
        'db_user': 'iworkh',
        'db_password': 'iworkh123',
        'db_charset': 'utf8'
    }
}

url_handlers = [
    (r"/order/?", OrderHandler, initParam)
]

if __name__ == '__main__':
    app = web.Application(url_handlers, debug=True)
    app.listen(8888)
    print("started successfully")
    ioloop.IOLoop.current().start()

測試代碼

import requests

url = "http://localhost:8888/order"
data = {
    'name':'springboot',
    'price': 100.0,
    'count':2
}
resp = requests.post(url, data=data).content.decode("utf8")
print(resp)

7-3.更新數據

查詢邏輯

import aiomysql
from tornado import web, ioloop

class OrderHandler(web.RequestHandler):
    def initialize(self, db_info) -> None:
        self.db_info = db_info

    async def put(self):
        db_id= self.get_argument("id")
        db_name= self.get_argument("name")
        db_price= self.get_argument("price")
        db_count= self.get_argument("count")

        async with aiomysql.create_pool(host=self.db_info["db_host"], port=self.db_info["db_port"],
                                        user=self.db_info["db_user"], password=self.db_info["db_password"],
                                        db=self.db_info["db_name"], charset=self.db_info["db_charset"]) as pool:


            async with pool.acquire() as conn:
                async with conn.cursor() as cur:
                    sql = "UPDATE TB_ORDER SET `name` = '{}', `price` = '{}', `count` = '{}' WHERE `id` = {};"\
                        .format(db_name, db_price, db_count, db_id)                   
                    print(sql)
                    await cur.execute(sql)
                    await conn.commit()
            self.write("save successfully")

initParam = {
    "db_info": {
        'db_host': 'localhost',
        'db_name': 'iworkh_tornado',
        'db_port': 3306,
        'db_user': 'iworkh',
        'db_password': 'iworkh123',
        'db_charset': 'utf8'
    }
}

url_handlers = [
    (r"/order/?", OrderHandler, initParam)
]

if __name__ == '__main__':
    app = web.Application(url_handlers, debug=True)
    app.listen(8888)
    print("started successfully")
    ioloop.IOLoop.current().start()

測試代碼

import requests
url = "http://localhost:8888/order"
data = {
    'id': 1,
    'name':'java',
    'price': 60.0,
    'count':1
}
resp = requests.put(url, data=data).content.decode("utf8")
print(resp)

通過上面的aiomysl操作,雖然實現了異步操作,但是我們可以發現一些問題

  • 線程池操作,每次一個請求來都創建了個線程池,然後關閉
  • 原生sql操作,拼sql很累
  • 查詢結果,解析很麻煩
  • model對象和數據庫表關聯操作,進行查詢、更新、插入操作(有hibernate、jpa、Django orm等開發經驗着,應該明白ORM框架帶來的好處)

8.orm框架peewee

8-1.orm好處

我們通知之前對aiomysql對數據庫的操作,我們發現了一些問題,操作很麻煩。

那使用orm有啥好處呢?

  • 操作更加方便,不用跟原生sql交互太多,便於維護。(特別寫sql少了,,'等一些低價錯誤,很難發現,浪費開發時間)
  • orm可以屏蔽調底層數據(不管mysql、postgresql、sqlite等,都通過orm來操作,來適配)
  • 還能防止sql注入

python領域常用的一些ORM框架

  • Django’s ORM
  • peewee
  • SQLAlchemy

Django’s ORM

優點:
    易用,學習曲線短
    和Django緊密集合,用Django時使用約定俗成的方法去操作數據庫

缺點:
    QuerySet速度不給力,會逼我用Mysqldb來操作原生sql語句。
    不好處理複雜的查詢,強制開發者回到原生SQL 

peewee

優點:
    Django式的API,使其易用
    輕量實現,很容易和任意web框架集成
    aysnc-peewee異步操作

缺點:
    不支持自動化schema遷移
    不能像Django那樣,使線上的mysql表結構生成結構化的模型。
    多對多查詢寫起來不直觀 

SQLAlchemy

優點:
    巨牛逼的企業級API,使得代碼有健壯性和適應性
    靈活的設計,使得能輕鬆寫複雜查詢

缺點:
    工作單元概念不常見
    重量級 API,導致長學習曲線

8-2.peewee同步操作

數據操作,主要就增刪改查功能,下面分別演示如何使用。peewee官網doc

工作中使用前,最好把官網瀏覽一遍,掌握一些常用的api和細節

先pip安裝

pip install peewee

8-2-1.定義model

base_model.py

import datetime

from peewee import MySQLDatabase, Model, DateTimeField

db_info = {'host': '127.0.0.1', 'user': 'iworkh', 'password': 'iworkh123', 'charset': 'utf8', 'port': 3306}

db = MySQLDatabase('iworkh_tornado', **db_info)

class BaseModel(Model):
    created_date = DateTimeField(default=datetime.datetime.now)

    class Meta:
        database = db

user_model.py

from peewee import CharField, DateField, IntegerField, BooleanField

from lesson05.models.base_model import *

class User(BaseModel):
    username = CharField(column_name='username', unique=True, null=False, max_length=50, verbose_name="姓名")
    birthday = DateField(column_name='birthday', null=True, default=None, verbose_name='生日')
    phone = CharField(column_name='phone', max_length=11)
    mail = CharField(column_name='mail', max_length=50, verbose_name='郵件')
    gender = IntegerField(column_name='gender', null=False, default=1, verbose_name='姓別')
    is_admin = BooleanField(column_name='is_admin', default=False, verbose_name='是否是管理員')

    class Meta:
        table_name = "T_USER"
from peewee import ForeignKeyField, CharField, IntegerField

from lesson05.models.base_model import BaseModel
from lesson05.models.user_model import User

class Pet(BaseModel):
    user = ForeignKeyField(User, backref='pets')
    name = CharField(index=True, column_name='name', max_length=50, verbose_name='名字')
    age = IntegerField(column_name='age', verbose_name='年齡')
    type = CharField(column_name='type', max_length=50, verbose_name='類型')
    color = CharField(column_name='color', max_length=50, verbose_name='顏色')
    description = CharField(column_name='description', max_length=500, verbose_name='描述')

    class Meta:
        table_name = "T_PET"

8-2-2.初始化表

import 省略

if __name__ == '__main__':
    db.connect()
    table_list = [User, Pet]
    db.create_tables(table_list)
    print("end...")

8-2-3.插入

插入數據,可以使用savecreate方法來操作

  • save: 對象操作
  • create: 是類操作,看create函數上有@classmethod修飾符
import datetime
import 省略

userList = [
    {'username': 'zhangsan', 'birthday': '1988-08-08', 'gender': 1, 'mail': '[email protected]', 'phone': '111',
     'is_admin': False},
    {'username': 'lisi', 'birthday': datetime.date(1999, 8, 8), 'gender': 1, 'mail': '[email protected]', 'phone': '222',
     'is_admin': False},
    {'username': 'wangwu', 'birthday': datetime.date(2025, 8, 8), 'gender': 0, 'mail': '[email protected]', 'phone': '333',
     'is_admin': False},
    {'username': 'admin', 'birthday': datetime.date(2000, 8, 8), 'gender': 1, 'mail': '[email protected]', 'phone': '444',
     'is_admin': True}
]


def save():
    user = User()
    user.username = 'iworkh_save'
    user.birthday = datetime.date(1988, 8, 8)
    user.gender = 0
    user.mail = "[email protected]"
    user.phone = "888888888"
    user.is_admin = True

    # 使用對象調用方法
    row = user.save()
    print("save的返回值是值:{}".format(row))


def save_list():
    for item in userList:
        user = User(**item)
        user.save()


def create():
    user_dic = {
        'username': 'iworkh_create2',
        'birthday': datetime.date(1989, 8, 8),
        'gender': 1,
        'mail': '[email protected]',
        'phone': '888888888',
        'is_admin': False
    }

    # 使用類調用方法
    row = User.create(**user_dic)
    print("create的返回值值:{}".format(row))


if __name__ == '__main__':
    save()
    create()
    save_list()
    print("end...")

8-2-4.查詢

查詢比較重要,但是peewee的api使用,跟原生sql語法優點類似

sql: select * from user where username like %iworkh%
peewee寫法:User.select().where(User.username.contains(‘iworkh’))

import 省略

def select_all():
    # modelSelect = User.select() # sql: select * from user
    fields = [User.username, User.phone]
    modelSelect = User.select(*fields)  # sql: select username, phone from user
    # 這還沒有立即執行,返回的是modelSelect對象,當for或者execute時纔會執行
    for user in modelSelect:
        print("{} --- {} --- {}".format(user.username, user.phone, user.is_admin))


def select_conditon():
    # 根據id取記錄,如果id不存在,那麼轉化時候會報錯
    user_res_get_by_id = User.get_by_id(1)
    print("get_by_id 結果:{}".format(user_res_get_by_id.username))

    user_res_get = User.get(User.id == 1)
    print("user_res_get 結果:{}".format(user_res_get.username))

    user_res_dic = User[1]
    print("user_res_dic 結果:{}".format(user_res_dic.username))

    print("*" * 50)

    # select * from user where username like %iworkh%
    modelSelect = User.select().where(User.username.contains('iworkh'))
    for user in modelSelect:
        print("{} --- {}".format(user.username, user.is_admin))

    print("*" * 50)

    # select * from user ordery by birthday desc
    modelSelect_order = User.select().order_by(User.birthday.desc())
    for user in modelSelect_order:
        print("{} --- {}".format(user.username, user.birthday))

    print("*" * 50)
    # 按每頁3的大小分頁,取第2頁數據(從1開始計數)
    modelSelect_pagable = User.select().paginate(page=2, paginate_by=3)
    for user in modelSelect_pagable:
        print("{} --- {}".format(user.username, user.id))


if __name__ == '__main__':
    select_all()
    select_conditon()

8-2-5.更新

更新很簡單,使用的是save函數,當save裏傳id值時,則認爲是更新;id沒有值None時,則是插入操作

import 省略

def upate():
    # 使用save方法即可更新,只要傳的值中還有id
    user_res_get_by_id = User.get_by_id(1)
    user_res_get_by_id.username = user_res_get_by_id.username + '_update'
    # 從數據中取處記錄,並修改值後更新數據庫
    user_res_get_by_id.save()

if __name__ == '__main__':
   upate()

8-2-6.刪除

刪除記錄有兩種操作:

  • 類操作,根據id刪除
  • 對象操作,這個對象要id值,底層也是根據id來刪除記錄的
import 省略

def delete_by_id():
    # 類操作
    User.delete_by_id(3)


def delete_instance():
    user = User()
    user.id = 4
    # 對象操作,recursive參數控制關聯數據是否刪除
    user.delete_instance()


if __name__ == '__main__':
    delete_by_id()
    delete_instance()
    print("end...")

8-2-7.技巧

技巧

peewee裏還有很其他知識點,除了閱讀官網提供的doc文檔外,還有一些地方值得我們開發者注意的,就是源碼下面的test和example模塊。

留個問題:

peewee裏transaction如何使用?(如何去官網doc和源碼下快速找到你需要的答案呢?)

8-3.peewee-async操作

tornado調用接口,那得用異步的,所以peewee的同步操作,在tornado中肯定玩不轉的。peewee-async官網doc

peewee-async的接口主要分兩部分

  • 高級API (推薦使用)

主要通過manager來操作

  • 低級API (不推薦使用)

主要通過peewee_async來操作

安裝

pip install --pre peewee-async

8-3-1.定義models

定義model還是跟peewee一樣,不過db的方式使用了peewee_async.MySQLDatabase

import datetime

import peewee_async
from peewee import Model, DateTimeField, TextField, CharField

db_info = {'host': '127.0.0.1', 'user': 'iworkh', 'password': 'iworkh123', 'charset': 'utf8', 'port': 3306}

db = peewee_async.MySQLDatabase('iworkh_tornado', **db_info)


class BaseModel(Model):
    created_date = DateTimeField(default=datetime.datetime.now)

    class Meta:
        database = db


class BlogModel(BaseModel):
    title = CharField(column_name='title', max_length=50)
    content = TextField(column_name='content')

    class Meta:
        table_name = 't_blog'

8-3-2.基本操作

通過高級API的方式,進行增刪改查操作

import asyncio

import peewee_async

import 省略了models的引入

objects = peewee_async.Manager(db)

async def save():
    blog = {'title': 'tornado入門', 'content': '詳細內容請看文章'}
    await objects.create(BlogModel, **blog)
    print("保存成功")


async def get_all():
    all_blogs = await objects.execute(BlogModel.select())
    for blog in all_blogs:
        print("id: {}, 文章標題:{}".format(blog.id, blog.title))


async def delete():
    blog = BlogModel()
    blog.id = 1
    await objects.delete(blog)
    print("刪除成功")

def create_table():
    BlogModel.create_table(True)

def drop_table():
    BlogModel.drop_table(True)


async def test():
    create_table()
    await save()
    print("*"*50)
    await get_all()
    print("*"*50)
    await delete()
    print("*"*50)
    await get_all()
    drop_table()


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(test())
    print("end")

9.參考

10.推薦

整理不打字易,覺得有幫助就點個贊吧。

  • iworkh博客
    個人博客剛開不久(內容還不多),主要用來輔助手冊,寫些零碎的知識點

  • iworkh網站
    註冊下個人用戶,就可以管理自己的鏈接、享用各類學習手冊,主要用來寫手冊,分享常用工具。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章