Flask框架

Flask框架

Flask诞生于2010年,是Armin ronacher(人名)用 Python 语言基于 Werkzeug 工具箱编写的轻量级Web开发框架。

Flask 本身相当于一个内核,其他几乎所有的功能都要用到扩展(邮件扩展Flask-Mail,用户认证Flask-Login,数据库Flask-SQLAlchemy),都需要用第三方的扩展来实现。比如可以用 Flask 扩展加入ORM、窗体验证工具,文件上传、身份验证等。Flask 没有默认使用的数据库,你可以选择 MySQL,也可以用 NoSQL。

Flask是一个基于Python开发并且依赖jinja2模板和Werkzeug WSGI服务的一个微型框架,对于Werkzeug本质是Socket服务端,其用于接收http请求并对请求进行预处理,然后触发Flask框架,开发人员基于Flask框架提供的功能对请求进行相应的处理,并返回给用户,如果要返回给用户复杂的内容时,需要借助jinja2模板来实现对模板的处理,即:将模板和数据进行渲染,将渲染后的字符串返回给用户浏览器。

“微”(micro) 并不表示你需要把整个 Web 应用塞进单个 Python 文件(虽然确实可以 ),也不意味着 Flask 在功能上有所欠缺。微框架中的“微”意味着 Flask 旨在保持核心简单而易于扩展。Flask 不会替你做出太多决策——比如使用何种数据库。而那些 Flask 所选择的——比如使用何种模板引擎——则很容易替换。除此之外的一切都由可由你掌握。如此,Flask 可以与您珠联璧合。

默认情况下,Flask 不包含数据库抽象层、表单验证,或是其它任何已有多种库可以胜任的功能。然而,Flask 支持用扩展来给应用添加这些功能,如同是 Flask 本身实现的一样。众多的扩展提供了数据库集成、表单验证、上传处理、各种各样的开放认证技术等功能。Flask 也许是“微小”的,但它已准备好在需求繁杂的生产环境中投入使用。

注:其 WSGI 工具箱采用 Werkzeug(路由模块),模板引擎则使用 Jinja2。这两个也是 Flask 框架的核心。

官网

官方文档

Falsk常用第三方扩展包
  • Flask-SQLalchemy:操作数据库,ORM;
  • Flask-script:终端脚本工具,脚手架;
  • Flask-migrate:管理迁移数据;
  • Flask-Session:Session存储方式指定;
  • Flask-WTF:表单;
  • Flask-Mail:邮件;
  • Flask-Bable:提供国际化和本地支持,翻译;
  • Flask-Login:认证用户状态;
  • Falsk-OpenID:认证,OAuth;
  • Falsk JSON-RPC:开发rpc远程服务[过程]调用
  • Flask-Bootstrap:集成前端Twitter Bootstrap框架
  • Flask-Moment:本地化日期和时间
  • Flask-Admin:简单而可扩展的管理接口的框架

可以通过 https://pypi.org/search/?c=Framework+%3A%3A+Flask 查看更多flask官方推荐的扩展

一、路由

1、路由的使用

路由和视图的名称必须全局统一,不能出现重复,否则报错

2、路由映射的方式
  • 任意路由参数

    • @app.route('/user/<username>')

      # 路由传递参数[没有限定类型]
      @app.route('/user/<username>')
      def user_info(username):
          return 'hello %s' % username
      
  • 限定路由参数

    • 限定路由参数的类型,flask系统自带转换器编写在werkzeug.routing.py文件中。底部可以看到以下字典:

      DEFAULT_CONVERTERS = {
         "default": UnicodeConverter,
         "string": UnicodeConverter,
         "any": AnyConverter,
         "path": PathConverter,
         "int": IntegerConverter,
         "float": FloatConverter,
         "uuid": UUIDConverter,
      }
      

      系统自带的转换器具体使用方式在每种转换器的注释代码中有写,请留意每种转换器初始化的参数。

      转换器名称 描述
      string 默认类型,接受不带斜杠的任何文本
      int 接受正整数
      float 接受正浮点值
      path 接收string但也接受斜线
      uuid 接受UUID(通用唯一识别码)字符串 xxxx-xxxx-xxxxx-xxxxx
    • @app.route('/post/<int:post_id>')

      # flask提供了路由转换器可以让我们对路由参数进行限定
      # 路由传递参数[限定数据类型]
      @app.route('/user/<int:user_id>')
      def user_info(user_id):
          return 'hello %d' % user_id
      
    • @app.route('/post/<float:post_id>')

      # flask提供了路由转换器可以让我们对路由参数进行限定
      # 路由传递参数[限定数据类型]
      @app.route('/user/<float:user_id>')
      def user_info(user_id):
          return 'hello %d' % user_id
      
    • @app.route('/post/<path:path>')

      # flask提供了路由转换器可以让我们对路由参数进行限定
      # 路由传递参数[限定数据类型]
      @app.route('/user/<path:user_id>')
      def user_info(user_id):
          return 'hello %d' % user_id
      
    • @app.route('/login', methods=['GET', 'POST'])

      # flask提供了路由转换器可以让我们对路由参数进行限定
      # 路由传递参数[限定数据类型]
      @app.route('/login', methods=['GET', 'POST'])
      def login():
          return 'hello World!!!'
      
3、自定义路由参数转换器

正则匹配路由参数

在web开发中,可能会出现限制用户访问规则的场景,这种情况就需要使用正则匹配,根据自己的规则去限定请求参数在进行访问

实现步骤:

  • 导入转换器基类:在Flask中,所有的路由的匹配规则都是使用转换器对象进行记录

    from werkzeug.routing import BaseConverter
    
  • 自定义转换器:自定义类继承于转换器基类

    # 自定义正则转换器
    from werkzeug.routing import BaseConverter
    class RegexConverter(BaseConverter):
        def __init__(self,map,*args):
            super().__init__(map)
            # 正则参数
            self.regex = args[0]
    
  • 添加转换器到默认的转换器字典中

    # 将自定义转换器添加到转换器字典中,并指定转换器使用时名字为: re
    app.url_map.converters['re'] = RegexConverter
    
  • 使用自定义转换器实现自定义匹配规则

    # 正则匹配路由(手机号正则)
    @app.route("/login/<re('1\d{10}'):mobile>")
    def login(mobile):
        return mobile
    
  • 测试代码:

    from flask import Flask,request
    # 初始化
    app = Flask(import_name=__name__)
    
    # 编写路由视图
    @app.route(rule='/')
    def index():
        return "<h1>hello world!</h1>"
    
    # 关于路由参数的限制,flask内置的类型不够具体,在开发中,我们经常接受参数,需要更加精确的限制
    # 这时候,可以使用正则匹配路由参数
    # 正则匹配路由参数,其实就是扩展flask内置的路由限定类型,需要完成4个步骤
    # 1. 引入flask的路由转换器
    from werkzeug.routing import BaseConverter
    # 2. 创建自定义路由转换器
    class MobileConverter(BaseConverter):
        """手机号码类型限制"""
        def __init__(self,map,*args):
            super().__init__(map)
            self.regex = "1[3-9]\d{9}"
    # 3. 把自定义转换器添加到flask默认的转换器字典中,也就是和原来的int,float等放在一块
    app.url_map.converters['mob'] = MobileConverter
    
    # 4. 类似原来的路由参数限制一样,调用自定义转换器名称即可
    @app.route(rule='/user/<mob:mobile>')
    def user(mobile):
        return mobile
    
    # 1. 引入flask的路由转换器
    from werkzeug.routing import BaseConverter
    # 2. 创建自定义路由转换器
    class RegexConverter(BaseConverter):
        """根据正则进行参数限制"""
        def __init__(self,map,*args):
            super().__init__(map)
            self.regex = args[0]
    # 3. 把自定义转换器添加到flask默认的转换器字典中,也就是和原来的int,float等放在一块
    app.url_map.converters['re'] = RegexConverter
    
    # 4. 类似原来的路由参数限制一样,调用自定义转换器名称即可
    @app.route(rule='/user/<re("\w+@\w+\.\w+"):email>')
    def user2(email):
        print(app.url_map) # 获取所有的路由列表
        return email
    
    # 声明和加载配置
    class Config():
        DEBUG = True
    app.config.from_object(Config)
    
    if __name__ == '__main__':
        # 运行flask
        app.run(host="0.0.0.0")
    

二、HTTP请求与响应

1、请求

官方文档:

  • request:flask中代表当前请求的 request 对象

  • 作用:在视图函数中取出本次请求数据

  • 导入from flask import request

  • 代码位置from flask.app import Request

  • 常用属性

    属性 说明 类型
    data 记录请求体的数据,并转换为字符串
    只要是通过其他属性无法识别转换的请求体数据
    最终都是保留到data属性中
    bytes类型
    form 记录请求中的html表单数据 MultiDict
    args 记录请求中的查询字符串,也可以是query_string MultiDict
    cookies 记录请求中的cookie信息 Dict
    headers 记录请求中的请求头 EnvironHeaders
    method 记录请求使用的HTTP方法 GET/POST
    url 记录请求的URL地址 string
    files 记录请求上传的文件列表 *
    json 记录ajax请求的json数据 json

2、获取用户请求相关的信息

  • request.method
  • request.args
  • request.form
  • request.values
  • request.files
  • request.cookies
  • request.headers
  • request.path
  • request.full_path
  • request.script_root
  • request.url
  • request.base_url
  • request.url_root
  • request.host_url
  • request.host
获取请求中各项数据
from flask import Flask,request

# 初始化
app = Flask(import_name=__name__)
# 编写路由视图
@app.route(rule='/')
def index():
    return "<h1>hello world!</h1>"

"""== 获取查询字符串 =="""
@app.route(rule="/args",methods=["post","get"])
def args():
    print(request.args) # 获取查询字符串
    """
    请求地址:
        http://127.0.0.1:5000/args?name=xiaoming&password=123&lve=swimming&lve=shopping
    打印效果:
        ImmutableMultiDict([('name', 'xiaoming'), ('password', '123')])
        ImmutableMultiDict是一个由flask封装的字典类,在字典的基础上,提供了一些其他的方法而已。
        格式:
            ImmutableMultiDict([('键', '值'), ('键', '值')])
        字典本身不支持同名键的,ImmutableMultiDict类解决了键同名问题
        操作ImmutableMultiDict,完全可以操作字典操作,同时还提供了get,getlist方法,获取指定键的1个值或多个值    
    """
    print(request.args["name"]) # xiaoming
    print(request.args.get("name")) # xiaoming
    print(request.args.getlist("lve")) # ['swimming', 'shopping']

    # 把ImmutableMultiDict转换成普通字典
    print(request.args.to_dict(flat=False)) # {'name': ['xiaoming'], 'password': ['123'], 'lve': ['swimming', 'shopping']}
    print(request.args.to_dict(flat=True)) # {'name': 'xiaoming', 'password': '123', 'lve': 'swimming'}

    return "ok"

"""== 获取请求体数据 =="""
@app.route(rule="/data",methods=["post","put","patch"])
def data():
    """接受客户端发送过来的请求体数据,是request.json,request.form,request.files等无法接受的数据,全部会保留到这里"""
    print(request.data) #

    # 接受表单提交的数据
    print(request.form) # ImmutableMultiDict([('username', 'root'), ('password', '123456')])

    # 接受ajax或其他客户端提交过来的json数据
    print(request.json) # {'username': 'root', 'password': '123456'}

    # 接受上传文件
    avatar = request.files["avatar"] # ImmutableMultiDict([('avatar', <FileStorage: '123.jpg' ('image/jpeg')>)])
    print(avatar) # <FileStorage: '123.jpg' ('image/jpeg')>


    # 获取请求头信息
    print( request.headers ) # 获取全部的而请求头信息
    print( request.headers.get("Host") )
    # 获取自定义请求头
    print( request.headers.get("company") ) # oldboy
    print( request.headers["company"] )     # oldboy
    
    # 本次请求的url地址
    print( request.url) # http://127.0.0.1:5000/data
    print( request.path ) # /data
    
    return "ok"

# 声明和加载配置
class Config():
    DEBUG = True
app.config.from_object(Config)

if __name__ == '__main__':
    # 运行flask
    app.run(host="0.0.0.0")

3、响应

flask默认支持两种相应方式:
数据响应:默认响应html文本,也可以返回JSON格式,或者其他格式
# 响应html文本
from flask import make_response

@app.route("/")
def index():
    # [默认支持]响应html文本
    return "<img src='http://flask.pocoo.org/static/logo.png'>"
	return make_response("<h1>hello user</h1>") # 等同于上面的一段

# 返回JSON数据:在Flsak中可以直接使用jsonify生成一个JSON的响应
from flask import Flask, request, jsonify
# jsonify 就是json里面的jsonify

@app.route("/")
def index():
    # 也可以响应json格式代码
    data = [
        {"id":1,"username":"liulaoshi","age":18},
        {"id":2,"username":"liulaoshi","age":17},
        {"id":3,"username":"liulaoshi","age":16},
        {"id":4,"username":"liulaoshi","age":15},
    ]
    return jsonify(data)
	# flask中返回json 数据,都是flask的jsonify方法返回就可以了.
页面响应:
  • 重定向

  • url_for 视图之间的跳转

    # 重定向到百度页面
    from flask import redirect
    # 页面跳转响应
    @app.route("/user")
    def user():
        # 页面跳转 redirect函数就是response对象的页面跳转的封装
        # Location: http://www.baidu.com
        return redirect("http://www.baidu.com")
    
    # 重定向到自己写的视图函数:可以直接填些自己的URL路径,也可以使用url_for生成指定试图函数所对应的URL----from flask import url_for
    # 内容响应
    @app.route("/")
    def index():
        # [默认支持]响应html文本
        # return "<img src='http://flask.pocoo.org/static/logo.png'>"
    
        # 也可以响应json格式代码
        data = [
            {"id":1,"username":"liulaoshi","age":18},
            {"id":2,"username":"liulaoshi","age":17},
            {"id":3,"username":"liulaoshi","age":16},
            {"id":4,"username":"liulaoshi","age":15},
        ]
        return jsonify(data)
    
    #使用url_for可以实现视图方法之间的内部跳转
    # url_for("视图方法名")
    @app.route("/login")
    def login():
        return redirect( url_for("index") )
    
    # 重定向到带有参数的视图函数:在url_for函数中传入参数
    # 路由传递参数
    @app.route('/user/<user_id>')
    def user_info(user_id):
        return 'hello %d' % user_id
    
    # 重定向
    @app.route('/demo4')
    def demo4():
        # 使用 url_for 生成指定视图函数所对应的 url
        return redirect( url_for(endpoint="user",user_id=100) )
    
    
自定义http响应状态码:在Flask中,可以很方便的返回自定义状态码,可以实现不符合http协议的状态码
@app.route('/demo4')
def demo4():
    return '状态码为 666', 400
  
"""还可以使用make_response创建Response对象,然后通过response对象返回数据"""
from flask import make_response
@app.route("/rep")
def index7():
    response = make_response("ok")
    print(response)
    response.headers["Company"] = "oldboy" # 自定义响应头
    response.status_code = 201 # 自定义响应状态码
    return response

4、http的会话机制

所谓的会话,就是客户端浏览器和服务端网站之间一次完整的交互过程.

会话的开始是在用户通过浏览器第一次访问服务端网站开始.

会话的结束时在用户通过关闭浏览器以后,与服务端断开.

所谓的会话控制,就是在客户端浏览器和服务端网站之间,进行多次http请求响应之间,记录、跟踪和识别用户的信息而已。

因为 http 是一种无状态协议,浏览器请求服务器是无状态的。

无状态:指一次用户请求时,浏览器、服务器无法知道之前这个用户做过什么,每次请求都是一次新的请求。

无状态原因:浏览器与服务器是使用 socket 套接字进行通信的,服务器将请求结果返回给浏览器之后,会关闭当前的 socket 连接,而且服务器也会在处理页面完毕之后销毁页面对象。

有时需要保持下来用户浏览的状态,比如用户是否登录过,浏览过哪些商品等

实现状态保持主要有两种方式:

  • 在客户端存储信息使用Cookie,token[jwt,oauth]
  • 在服务器端存储信息使用Session

1)Cookie

Cookie是由服务器端生成,发送给客户端浏览器,浏览器会将Cookie的key/value保存,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用cookie)。Cookie的key/value可以由服务器端自己定义。

使用场景: 登录状态, 浏览历史, 网站足迹,购物车 [不登录也可以使用购物车]

Cookie是存储在浏览器中的一段纯文本信息,建议不要存储敏感信息如密码,因为电脑上的浏览器可能被其它人使用

Cookie基于域名安全,不同域名的Cookie是不能互相访问的

如访问luffy.com时向浏览器中写了Cookie信息,使用同一浏览器访问baidu.com时,无法访问到luffy.com写的Cookie信息

浏览器的同源策略针对cookie也有限制作用.

当浏览器请求某网站时,会将本网站下所有Cookie信息提交给服务器,所以在request中可以读取Cookie信息

设置cookie

设置cookie需要通过flask的Response响应对象来进行设置,由响应对象会提供了方法set_cookie给我们可以快速设置cookie信息。

from flask imoprt Flask,make_response
@app.route('/set_cookie')
def set_cookie():
    resp = make_response('this is to set cookie')
    resp.set_cookie('username', 'xiaoming', max_age=3600)
    return resp
获取cookie
from flask import Flask,request
@app.route('/get_cookie')
def resp_cookie():
    resp = request.cookies.get('username')
    return resp

2)Session

对于敏感、重要的信息,建议要存储在服务器端,不能存储在浏览器中,如用户名、余额、等级、验证码等信息

在服务器端进行状态保持的方案就是Session

Session依赖于Cookie,session的ID一般默认通过cookie来保存到客户端。

flask中的session需要加密,所以使用session之前必须配置SECRET_KEY选项,否则报错.

session的有效期默认是会话期,会话结束了,session就废弃了。

如果将来希望session的生命周期延长,可以通过修改cookie中的sessionID来完成配置。
注:session也可以通过一些方式做到不依赖cookie;
设置session
from flask import session
@app.route('/set_session')
def set_session():
    session['username'] = 'xiaoming'
    return 'ok!'
获取session
from flask import session
@app.route('/get_session')
def get_session():
    return session.get('username')

三、请求钩子函数

在客户端和服务器交互的过程中,有些准备工作需要处理,比如:

  • 在请求开始时,建立数据库连接;
  • 在请求开始时,根据需求进行权限校验;
  • 在请求结束时,指定数据的交互式;

为了让每个视图函数避免编写重复功能的代码,Flask提供了通用设置的功能,即请求钩子。

请求钩子是通过装饰器的形式实现,Flask支持如下四种请求钩子:

  • before_first_request
    • 在处理第一个请求前执行[项目初始化时的钩子]
  • before_request
    • 在每次请求前执行
    • 如果在某修饰的函数中返回了一个响应,视图函数将不再被调用
  • after_reauest
    • 如果没有抛出错误,在每次请求后执行
    • 接收一个参数:视图函数做出的响应
    • 在此函数中可以对响应值在返回之前做最后一步修改的处理
    • 需要将参数中的相应在此参数中进行返回
  • teardown_request:
    • 在每次请求后执行
    • 接收一个参数:错误信息,如果有相关错误抛出
    • 需要设置flask的配置DEBUG=False,teardown_request才会接收到异常对象
代码示例
from flask import Flask,request

# 初始化
app = Flask(import_name=__name__)

# 声明和加载配置
class Config():
    DEBUG = True
app.config.from_object(Config)

@app.before_first_request
def before_first_request():
    """
    这个钩子会在项目启动后第一次被用户访问时执行
    可以编写一些初始化项目的代码,例如,数据库初始化,加载一些可以延后引入的全局配置
    """
    print("----before_first_request----")
    print("系统初始化的时候,执行这个钩子方法")
    print("会在接收到第一个客户端请求时,执行这里的代码")

@app.before_request
def before_request():
    """
    这个钩子会在每次客户端访问视图的时候执行
    # 可以在请求之前进行用户的身份识别,以及对于本次访问的用户权限等进行判断。..
    """
    print("----before_request----")
    print("每一次接收到客户端请求时,执行这个钩子方法")
    print("一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据")

@app.after_request
def after_request(response):
    print("----after_request----")
    print("在处理请求以后,执行这个钩子方法")
    print("一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作")
    response.headers["Content-Type"] = "application/json"
    response.headers["Company"] = "python oldboy..."
    # 必须返回response参数
    return response


@app.teardown_request
def teardown_request(exc):
    print("----teardown_request----")
    print("在每一次请求以后,执行这个钩子方法")
    print("如果有异常错误,则会传递错误异常对象到当前方法的参数中")
    # 在项目关闭了DEBUG模式以后,则异常信息就会被传递到exc中,我们可以记录异常信息到日志文件中
    print(exc)

# 编写路由视图
@app.route(rule='/')
def index():
    print("-----------视图函数执行了---------------")
    return "<h1>hello world!</h1>"

if __name__ == '__main__':
    # 运行flask
    app.run(host="0.0.0.0")
  • 在第1次请求时的打印:
----before_first_request----
系统初始化的时候,执行这个钩子方法
会在接收到第一个客户端请求时,执行这里的代码
----before_request----
127.0.0.1 - - [04/Aug/2020 14:40:22] "GET / HTTP/1.1" 200 -
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
-----------视图函数执行了---------------
----after_request----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
None
  • 在第2次请求时的打印:
----before_request----
每一次接收到客户端请求时,执行这个钩子方法
一般可以用来判断权限,或者转换路由参数或者预处理客户端请求的数据
127.0.0.1 - - [04/Aug/2020 14:40:49] "GET / HTTP/1.1" 200 -
-----------视图函数执行了---------------
----after_request----
在处理请求以后,执行这个钩子方法
一般可以用于记录会员/管理员的操作历史,浏览历史,清理收尾的工作
----teardown_request----
在每一次请求以后,执行这个钩子方法
如果有异常错误,则会传递错误异常对象到当前方法的参数中
None

四、异常捕获

1、主动抛出异常

  • abort方法
    • 抛出一个给定的状态码的HTTPException或者指定响应,例如想要一个页面未找到异常来终止请求,可以调用啊abort(404)
  • 参数
    • code – HTTP的错误状态码
# abort(404)
abort(500)

抛出状态码的话,只能抛出 HTTP 协议的错误状态码

abort在工作中基本不会被使用,工作中的异常抛出往往在业务错误的时候使用raise进行抛出错误类型,而不是抛出http异常。

abort一般用于权限等页面上错误的展示提示.

2、捕获错误

  • errorhandler 装饰器
    • 注册一个错误处理程序,当程序抛出指定错误状态码的时候,就会调用该装饰器所装饰的方法
  • 参数:
    • code_or_exception – HTTP的错误状态码或指定异常
  • 例如统一处理状态码为500的错误给用户友好的提示:
@app.errorhandler(500)
def internal_server_error(e):
    return '服务器搬家了'
  • 捕获指定异常类型
@app.errorhandler(ZeroDivisionError)
def zero_division_error(e):
    return '除数不能为0'

代码:

from flask import Flask
# 创建flask应用
app = Flask(__name__)

"""加载配置"""
class Config():
    DEBUG = True
app.config.from_object(Config)

"""
flask中内置了app.errorhander提供给我们捕获异常,实现一些在业务发生错误时的自定义处理。
1. 通过http状态码捕获异常信息
2. 通过异常类进行异常捕获
"""

"""1. 捕获http异常[在装饰器中写上要捕获的异常状态码也可以是异常类]"""
@app.errorhandler(404)
def error_404(e):
    return "<h1>您访问的页面失联了!</h1>"
    # return redirect("/")

"""2. 捕获系统异常或者自定义异常"""
class APIError(Exception):
    pass

@app.route("/")
def index():
    raise APIError("api接口调用参数有误!")
    return "个人中心,视图执行了!!"

@app.errorhandler(APIError)
def error_apierror(e):
    return "错误: %s" % e

if __name__ == '__main__':
    app.run(host="localhost",port=8080)

五、context

执行上下文:即语境,语意,在程序中可以理解为在代码执行到某一行时,根据之前代码所做的操作以及下文即将要执行的逻辑,可以决定在当前时刻下可以使用到的变量,或者可以完成的事情。

Flask中上下文对象:相当于一个容器,保存了 Flask 程序运行过程中的一些信息[变量、函数、类与对象等信息]。

Flask中有两种上下文,请求上下文(request context)和应用上下文(application context)。

  1. application 指的就是当你调用app = Flask(__name__)创建的这个对象app
  2. request 指的是每次http请求发生时,WSGI server(比如gunicorn)调用Flask.__call__()之后,在Flask对象内部创建的Request对象;
  3. application 表示用于响应WSGI请求的应用本身,request 表示每次http请求;
  4. application的生命周期大于request,一个application存活期间,可能发生多次http请求,所以,也就会有多个request

请求上下文(request context)

思考:在视图函数中,如何取到当前请求的相关数据?比如:请求地址,请求方式,cookie等等

在 flask 中,可以直接在视图函数中使用 request 这个对象进行获取相关数据,而 request 就是请求上下文的对象,保存了当前本次请求的相关数据,请求上下文对象有:request、session

  • request
    • 封装了HTTP请求的内容,针对的是http请求。举例:user = request.args.get('user'),获取的是get请求的参数。
  • session
    • 用来记录请求会话中的信息,针对的是用户信息。举例:session['name'] = user.id,可以记录用户信息。还可以通过session.get('name')获取用户信息。

请求上下文提供的变量/属性/方法/函数/类与对象,只能在视图中或者被视图调用的地方使用

应用上下文(application context)

它的字面意思是 应用上下文,但它不是一直存在的,它只是request context 中操作当前falsk应用对象 app 的代理(人),所谓local proxy。它的作用主要是帮助 request 获取当前的flask应用相关的信息,它是伴 request 而生,随 request 而灭的。

应用上下文对象有:current_app,g

1、current_app

应用程序上下文,用于存储应用程序中的变量,可以通过current_app.name打印当前app的名称,也可以在current_app中存储一些变量,例如:

  • 应用的启动脚本是哪个文件,启动时指定了哪些参数
  • 加载了哪些配置文件,导入了哪些配置
  • 连接了哪个数据库
  • 有哪些可以调用的工具类、常量
  • 当前flask应用在哪个机器上,哪个IP上运行,内存多大
from flask import Flask,request,session,current_app,g

# 初始化
app = Flask(import_name=__name__)

# 声明和加载配置
class Config():
    DEBUG = True
app.config.from_object(Config)

# 编写路由视图
@app.route(rule='/')
def index():
    # 应用上下文提供给我们使用的变量,也是只能在视图或者被视图调用的地方进行使用,
    # 但是应用上下文的所有数据来源于于app,每个视图中的应用上下文基本一样
    print(current_app.config)   # 获取当前项目的所有配置信息
    print(current_app.url_map)  # 获取当前项目的所有路由信息

    return "<h1>hello world!</h1>"

if __name__ == '__main__':
    # 运行flask
    app.run(host="0.0.0.0")

2、g变量

g 作为 flask 程序全局的一个临时变量,充当者中间媒介的作用,我们可以通过它传递一些数据,g 保存的是当前请求的全局变量,不同的请求会有不同的全局变量,通过不同的thread id区别

g.name='abc' # name是举例,实际要保存什么数据到g变量中,可以根据业务而定,你可以任意的数据进去

注意:不同的请求,会有不同的全局变量g

from flask import Flask,request,session,current_app,g

# 初始化
app = Flask(import_name=__name__)

# 声明和加载配置
class Config():
    DEBUG = True
app.config.from_object(Config)

@app.before_request
def before_request():
    g.name = "root"

def get_two_func():
    name = g.name
    print("g.name=%s" % name)

def get_one_func():
    get_two_func()

# 编写路由视图
@app.route(rule='/')
def index():
    # 请求上下文提供的变量/属性/方法/函数/类与对象,只能在视图中或者被视图调用的地方使用
    # 请求上下文里面信息来源于每次客户端的请求,所以每个视图中请求上下文的信息都不一样
    # print(session)

    # 应用上下文提供给我们使用的变量,也是只能在视图或者被视图调用的地方进行使用,
    # 但是应用上下文的所有数据来源于于app,每个视图中的应用上下文基本一样
    print(current_app.config)   # 获取当前项目的所有配置信息
    print(current_app.url_map)  # 获取当前项目的所有路由信息
    get_one_func()
    return "<h1>hello world!</h1>"


if __name__ == '__main__':
    # 运行flask
    app.run(host="0.0.0.0")

两者区别:

  • 请求上下文:保存了客户端和服务器交互的数据,一般来自于客户端。
  • 应用上下文:flask 应用程序运行过程中,保存的一些配置信息,比如路由列表,程序名、数据库连接、应用信息等

六、Flask-Script扩展

这个模块的作用可以让我们通过终端来控制flask项目的运行,类似于django的manage.py

安装命令:

pip install flask-script

集成 Flask-Script到flask应用中,创建一个主应用程序,一般我们叫manage.py/run.py/main.py都行。

from flask import Flas 

app = Flask(__name__)

"""使用flask_script启动项目"""
from flask_script import Manager
manage = Manager(app)

@app.route('/')
def index():
    return 'hello world'

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

启动终端脚本的命令:

# 端口和域名不写,默认为127.0.0.1:5000
python run.py runserver

# 通过-h设置启动域名,-p设置启动端口
python run.py runserver -h127.0.0.1 -p8888

Flask-Script 还可以为当前应用程序添加脚本命令

1. 引入Command命令基类
2. 创建命令类必须直接或间接继承Command,并在内部实现run方法,同时如果有自定义的其他参数,则必须实现__init__
3. 使用flask_script应用对象manage.add_command对命令类进行注册,并设置调用终端别名。
"""自定义flask_script终端命令"""
from flask_script import Command

class HelloCommand(Command):
    """命令的相关描述"""
    def run(self):
        with open("text.txt","w") as f:
            f.write("hello\r\nhello")
            pass
        print("这是执行了hello命令")

manage.add_command('hello', HelloCommand() )

七、Jinja2模板引擎

Falsk内置的模板语言,它的设计思想来源于Django的模板引擎,并扩展了其语法和一系列强大的功能。

渲染模版函数

  • Flask提供的 render_template 函数封装了该模板引擎
  • render_template 函数的第一个参数是模板的文件名,后面的参数都是键值对,表示模板中变量对应的真实值。

1、模板基本使用

  1. 在flask应用对象创建的时候,设置或者保留template_folder参数,创建模板目录

    默认的情况创建templates文件夹下flask可以自动找到,否则需要使用下面的方式加载

    app = Flask(__name__,template_folder='templates')
    
  2. 在项目下创建 templates 文件夹,用于存放所有的模板文件,并在目录下创建一个模板html文件 index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>{{title}}</h1>
    </body>
    </html>
    
  3. 在视图函数设置渲染模板并设置模板数据

    from flask import Flask, render_template
    # 初始化
    app = Flask(import_name=__name__,template_folder='templates')
    
    # 配置终端脚本运行项目
    from flask_script import Manager
    manager = Manager(app)
    
    # 声明和加载配置
    class Config():
        DEBUG = True
    app.config.from_object(Config)
    
    # 编写路由视图
    @app.route(rule='/')
    def index():
        data={}
        data["title"] = "我的flask项目"
        return render_template("index.html",**data)
    
    if __name__ == '__main__':
        # 运行flask
        manager.run()
    

2、输出变量

{{}} 来表示变量名,这种 {{}} 语法叫做 变量代码块

视图代码:

@app.route("/")
def index():
    data={}
    data["title"] = "我的flask项目"
    return render_template("index.html",**data)

模板代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
</head>
<body>
    <h1>{{title}}</h1>
</body>
</html>

Jinja2 模版中的变量代码块可以是任意 Python 类型或者对象,只要它能够被 Python 的 __str__ 方法或者str()转换为一个字符串就可以,比如,可以通过下面的方式显示一个字典或者列表中的某个元素:

视图代码:

from flask import Flask,render_template
from settings.dev import Config
from flask_script import Manager
"""创建flask应用"""
app = Flask(__name__,template_folder='templates')
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)

@app.route("/")
def index():
    data = {}
    data["title"] = "我的项目"
    data["data_list"] = ["a","b","c"]
    data["data_dict"] = {
        "name":"xiaoming",
        "id":100,
    }
    # return render_template("index.html",
    #                        title="我的flask项目",
    #                        data_list=data_list,
    #                        data_dict=data_dict
    #                        )
    return render_template("index.html",**data)
if __name__ == '__main__':
    manage.run()

模板代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
</head>
<body>
    <div>{{title}}</div>
    <div>{{list}}</div>
    <div>{{list[0]}}</div>
    <div>{{list.0}}</div>
    <div>{{list[-1]}}</div>
    <div>{{dict}}</div>
    <div>{{dict['name']}}</div>
    <div>{{dict.name}}</div>
</body>
</html>

使用 {# #} 进行注释,注释的内容不会在html中被渲染出来

{# {{ name }} #}

3、模板中特有的变量和函数

你可以在自己的模板中访问一些 Flask 默认内置的函数和对象

config

你可以从模板中直接访问Flask当前的config对象:

{{config.SQLALCHEMY_DATABASE_URI}}
sqlite:///database.db

request

就是flask中代表当前请求的request对象:

{{request.url}}
http://127.0.0.1

session

为Flask的session对象,显示session数据

{{session.new}}
True

g变量

在视图函数中设置g变量的 name 属性的值,然后在模板中直接可以取出

{{ g.name }}

url_for()

url_for会根据传入的路由器函数名,返回该路由对应的URL,在模板中始终使用url_for()就可以安全的修改路由绑定的URL,则不必担心模板中渲染出错的链接:

{{url_for('home')}}

如果我们定义的路由URL是带有参数的,则可以把它们作为关键字参数传入url_for(),Flask会把他们填充进最终生成的URL中:

{{ url_for('index', post_id=1)}}
/1
代码示例:

主程序 run.py:

from flask import Flask, render_template
# 初始化
app = Flask(import_name=__name__,template_folder='templates')

# 配置终端脚本运行项目
from flask_script import Manager
manager = Manager(app)

# 声明和加载配置
class Config():
    DEBUG = True
    SECRET_KEY = "abc"
app.config.from_object(Config)

# 编写路由视图
@app.route(rule='/')
def index():
    data={}
    data["title"] = "我的项目"
    data["list"] = ["a","b","c"]
    data["dict"] = {
        "name":"xiaoming",
        "id":100,
    }
    return render_template("index.html",**data)

from flask import session
@app.route("/session/set")
def set_session():
    session["name"] = "root"
    return "ok"

if __name__ == '__main__':
    # 运行flask
    manager.run()

模板 templates/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
</head>
<body>
    <div>{{title}}</div>
    <div>{{list}}</div>
    <div>{{list[0]}}</div>
    <div>{{list.0}}</div>
    <div>{{list[-1]}}</div>
    <div>{{dict}}</div>
    <div>{{dict['name']}}</div>
    {# flask模板引擎的注释 #}
    {# <div>{{dict.name}}</div> #}
    <div>{{config}}</div>
    <div>{{config.DEBUG}}</div>
    <div>name={{request.args.name}}</div>
    <div>session.name={{session.name}}</div>
    <div>{{config.PREFERRED_URL_SCHEME}}://{{request.headers.Host}}{{url_for("set_session")}}</div>
</body>
</html>

pycharm中设置当前项目的模板语言:

files/settings/languages & frameworks/python template languages。

4、流程控制

主要包含两个:

- if/elif /else / endif
- for / endfor

1)if语句

Jinja2 语法中的if语句跟 Python 中的 if 语句相似,后面的布尔值或返回布尔值的表达式将决定代码中的哪个流程会被执行.

用 {%%} 定义的控制代码块,可以实现一些语言层次的功能,比如循环或者if语句

视图代码:

from flask import Flask,render_template,request
from settings.dev import Config
from flask_script import Manager
"""创建flask应用"""
app = Flask(__name__,template_folder='templates')
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)

@app.route("/list")
def list_page():
    data = {}
    data["book_list"] = [
        {"id":1,"price":78.50,"title":"javascript入门"},
        {"id":2,"price":78.50,"title":"python入门"},
        {"id":3,"price":78.50,"title":"django项目实战"}
    ]
    data["name"] = int( request.args.get("name") )
    return render_template("list.html",**data)

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

list.html,模板代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <table border="1" align="center" width="680">
        <tr>
            <th>id</th>
            <th>标题</th>
            <th>价格</th>
        </tr>
        {# for循环 #}
        {% for book in book_list %}
        <tr>
            <td>{{ book.id }}</td>
            <td>{{ book.title }}</td>
            <td>{{ book.price }}</td>
        </tr>
        {% endfor %}
    </table>

    {# 判断一个参数是否是奇数 #}
    {% if name % 2 == 0 %}
        偶数<br>
    {% else %}
        奇数<br>
    {% endif %}
</body>
</html>

flask中也有过滤器,并且也可以被用在 if 语句或者for语句中:

视图代码:

from flask import Flask,render_template,request
from settings.dev import Config
from flask_script import Manager
"""创建flask应用"""
app = Flask(__name__,template_folder='templates')
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)

@app.route("/")
def index():
    data = {}
    data["title"] = "我的项目"
    data["data_list"] = ["a","b","c"]
    data["data_dict"] = {
        "name":"xiaoming",
        "id":100,
    }
    # return render_template("index.html",
    #                        title="我的flask项目",
    #                        data_list=data_list,
    #                        data_dict=data_dict
    #                        )
    return render_template("index.html",**data)

@app.route("/list")
def list_page():
    data = {}
    data["book_list"] = [
        {"id":1,"price":78.50,"title":"javascript入门"},
        {"id":2,"price":78.50,"title":"python入门"},
        {"id":3,"price":78.50,"title":"django项目实战"}
    ]
    data["name"] = int( request.args.get("name") )
    return render_template("list.html",**data)

@app.route("/filter")
def filter():
    data = {}
    data["text"] = "hello flask"
    data["img_url"] = '<img width="300px" src="https://github.githubassets.com/images/modules/site/heroes/octocat-paper.svg">'
    return render_template("fitler.html",**data)

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

filter.html,模板代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <p>{{ text }}</p>
    <p>{{ text|upper }}</p>
    <p>{{ text|length }}</p>
    <p>{{ img_url }}</p>
    <p>{{ img_url|safe }}</p>
    {% if request.args.get("name")| int % 2 == 0 %}
    <p>偶数</p>
    {% else %}
    <p>奇数</p>
    {% endif %}
</body>
</html>

2)循环语句

  • 我们可以在 Jinja2 中使用循环来迭代任何列表或者生成器函数
{% for post in posts %}
    <div>
        <h1>{{ post.title }}</h1>
        <p>{{ post.text | safe }}</p>
    </div>
{% endfor %}
  • 循环和if语句可以组合使用,以模拟 Python 循环中的 continue 功能,下面这个循环将只会渲染post.text不为None的那些post:
{% for post in posts if post.text %}
    <div>
        <h1>{{ post.title }}</h1>
        <p>{{ post.text | safe }}</p>
    </div>
{% endfor %}
  • 在一个 for 循环块中你可以访问这些特殊的变量:
变量 描述
loop.index 当前循环迭代的次数(从 1 开始)
loop.index0 当前循环迭代的次数(从 0 开始)
loop.revindex 到循环结束需要迭代的次数(从 1 开始)
loop.revindex0 到循环结束需要迭代的次数(从 0 开始)
loop.first 如果是第一次迭代,为 True 。
loop.last 如果是最后一次迭代,为 True 。
loop.length 序列中的项目数。
loop.cycle 在一串序列间期取值的辅助函数。见下面示例程序。
  • 在循环内部,你可以使用一个叫做loop的特殊变量来获得关于for循环的一些信息
    • 比如:要是我们想知道当前被迭代的元素序号,并模拟Python中的enumerate函数做的事情,则可以使用loop变量的index属性,例如:
{% for post in posts%}
{{loop.index}}, {{post.title}}
{% endfor %}
  • 会输出这样的结果
1, Post title
2, Second Post
  • cycle函数会在每次循环的时候,返回其参数中的下一个元素,可以拿上面的例子来说明:
{% for post in posts%}
	{{loop.cycle('odd','even')}} {{post.title}}
{% endfor %}
  • 会输出这样的结果:
odd Post Title
even Second Post

代码示例,视图代码:

from flask import Flask, render_template
# 初始化
app = Flask(import_name=__name__,template_folder='templates')

# 配置终端脚本运行项目
from flask_script import Manager
manager = Manager(app)

# 声明和加载配置
class Config():
    DEBUG = True
    SECRET_KEY = "abc"
app.config.from_object(Config)

# 编写路由视图
@app.route(rule='/')
def index():
    data={}
    data["title"] = "我的项目"
    data["book_list"] = [
        {"id":10,"price":78.50,"title":"javascript入门"},
        {"id":21,"price":78.50,"title":"python入门"},
        {"id":33,"price":78.50,"title":"django项目实战"},
        {"id":34,"price":78.50,"title":"django项目实战"},
        {"id":33,"price":78.50,"title":"django项目实战"},
    ]
    return render_template("index.html",**data)

if __name__ == '__main__':
    # 运行flask
    manager.run()

continue.html,模板代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{title}}</title>
</head>
<body>
<p>判断</p>
<!--{% if request.args.name %}-->
<!--    <p>欢迎回来,{{request.args.name}}</p>-->
<!--{% endif %}-->

<!--{% if request.args.name %}-->
<!--    <p>欢迎回来,{{request.args.name}}</p>-->
<!--{% else %}-->
<!--    <p>对不起,您尚未登录</p>-->
<!--{% endif %}-->

<!--{% if request.args.name=="root" %}-->
<!--    <p>欢迎回来,您是当前网站的超级管理员~</p>-->
<!--{% elif request.args.name %}-->
<!--    <p>尊敬的用户{{request.args.name}},欢迎回来</p>-->
<!--{% else %}-->
<!--    <p>对不起,您尚未登录</p>-->
<!--{% endif %}-->


<p>循环</p>
<table border="1" align="center" width="600">
    <tr>
        <th>序号</th>
        <th>ID</th>
        <th>price</th>
        <th>title</th>
    </tr>
    {% for book in book_list %}
        {% if loop.index %2 == 0 %}
            <tr bgcolor="#add8e6">
        {% else %}
            <tr>
        {% endif %}
            <td>{{ loop.index }}</td>
            <td>{{ book.id }}</td>
            <td>{{ book.price }}</td>
            <td>{{ book.title }}</td>
    </tr>
    {% endfor %}
</table>

</body>
</html>

5、过滤器

1)常见的内建滤器

  1. 字符串操作

    • safe:禁用转义

      <p>{{ '<em>hello</em>' | safe }}</p>
      
    • capitalize:把变量值的首字母转成是大写,其语字母转校小写

      <p>{{ 'hello' | capitalize }}</p>
      
    • lower:把值转成小写

      <p>{{ 'HELLO' | lower }}</p>
      
    • upper:把值转成小写

      <p>{{ 'HELLO' | lower }}</p>
      
    • title:把之中的每个单词的首字母都转成大写

      <p>{{ 'hello' | title }}</p>
      
    • reverse:字符串反转

      <p>{{ 'olleh' | reverse }}</p>
      
    • format:格式化输出

      <p>{{ '%s is %d' | format('name',17) }}</p>
      
    • striptags:渲染之前把值中所有的HTML标签都删掉

      如果内容中,存在大小于号的情况,则不要使用这个过滤器,容易误删内容。

      <p>{{ '<em>hello</em>' | striptags }}</p>
      <p>{{ "如果x<y,z>x,那么x和z之间是否相等?" | striptags }}</p>
      
    • truncate:字符串截断

      <p>{{ 'hello every one' | truncate(9)}}</p>
      
  2. 列表操作

    • first:取第一个元素

      <p>{{ [1,2,3,4,5,6] | first }}</p>
      
    • last:取最后一个元素

      <p>{{ [1,2,3,4,5,6] | last }}</p>
      
    • length:获取列表长度

      <p>{{ [1,2,3,4,5,6] | length }}</p>
      
    • sum:列表求和

      <p>{{ [1,2,3,4,5,6] | sum }}</p>
      
    • sort:列表排序

      <p>{{ [6,2,3,1,5,4] | sort }}</p>
      
  3. 语句块过滤

    {% filter upper %}
        #一大堆文字#
    {% endfilter %}
    

2)自定义过滤器

​ 过滤器的本质是函数。当模板内置的过滤器不能满足需求,可以自定义过滤器。自定义过滤器有两种实现方式:

  • 一种是通过Flask应用对象的 add_template_filter 方法
  • 通过装饰器来实现自定义过滤器

重要:自定义的过滤器名称如果和内置的过滤器重名,会覆盖内置的过滤器。

需求:添加列表的过滤器

方式一:

​ 通过调用应用程序实例的add_template_filter方法实现系定义的过滤器。该方法第一个参数是函数名,第二个参数是自定义的过滤器名称:

# 自定义过滤器
# 自定义过滤器
def do_list_reverse(old_list):
    # 因为字典/列表是属于复合类型的数据,所以改动数据的结构,也会应该能影响到原来的变量
    # 通过list新建一个列表进行操作,就不会影响到原来的数据
    new_list = list(old_list)
    new_list.reverse()
    return new_list

# 注册过滤器
app.add_template_filter(do_list_reverse, "lrev")

方式二

用装饰器来实现自定义过滤器。装饰器传入的参数是自定义的过滤器名称。

  • 主程序中创建和注册过滤器

    @app.template_filter('lrev')
    def do_list_reverse(old_list):
        # 因为字典/列表是属于复合类型的数据,所以改动数据的结构,也会应该能影响到原来的变量
        # 通过list新建一个列表进行操作,就不会影响到原来的数据
        new_list = list(old_list)
        new_list.reverse()
        return new_list
    
    @app.route(rule='/')
    def index():
        data={}
        data["user_list"] = ["xiaoming","小黑白","小红"]
        return render_template("index.html",**data)
    
  • html调用过滤器

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>title</title>
    </head>
    <body>
        <p>{{ user_list }}</p>
        <p>{{ user_list | lrev }}</p>
        <p>{{ user_list }}</p>
    </body>
    </html>
    

手机进行部分屏蔽示例:

# 后端代码
from flask import Flask,render_template,request
from settings.dev import Config
from flask_script import Manager
"""创建flask应用"""
app = Flask(__name__,template_folder='templates')
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)

@app.template_filter("mobile")
def do_mobile(data,string):
    return data[:3]+string+data[7:]

@app.route("/")
def index():
    data = {}
    data["user_list"] = [
        {"id":1,"name":"张三","mobile":"13112345678"},
        {"id":2,"name":"张三","mobile":"13112345678"},
        {"id":3,"name":"张三","mobile":"13112345678"},
        {"id":4,"name":"张三","mobile":"13112345678"},
    ]
    return render_template("index2.html",**data)

if __name__ == '__main__':
    manage.run()
<!-- 前端模板代码 --> 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<table border="1" align="center" width="680">
    <tr>
        <th>ID</th>
        <th>姓名</th>
        <th>手机</th>
    </tr>
    {% for user in user_list %}
    <tr>
        <td>{{ user.id }}</td>
        <td>{{ user.name }}</td>
        <td>{{ user.mobile | mobile(string="****") }}</td>
    </tr>
    {% endfor %}

</table>
</body>
</html>

6、模板继承

在模板中,可能会遇到以下情况:

  • 多个模板具有完全相同的顶部和底部内容
  • 多个模板中具有相同的模板代码内容,但是内容中部分值不一样
  • 多个模板中具有相同的Html代码块内容

遇到这种情况,可以使用jinjia2模板中的继承来实现

模板继承是为了重用模板中的工共内容。一般Web开法中,继承主要使用在网站的顶部菜单、底部。这些内容可以定义在父模板中,子模版直接继承,而不需要重复书写

  • 标签定义的内容

    {% block top %} {% endblock %}
    
  • 相当于在父模板中挖个坑,当子模版继承父模板时,可以进行填充

  • 子模版使用extends指令声明这个模板继承自那个模板

  • 父膜板中定义的块在子模版中被重新定义,在子模版中调用父模板的内容时可以使用super()

父模板代码:

base.html

{% block top %}
  顶部菜单
{% endblock top %}

{% block content %}
{% endblock content %}

{% block bottom %}
  底部
{% endblock bottom %}

子模板代码:

  • extends指令声明这个模板继承自哪
{% extends 'base.html' %}
{% block content %}
 需要填充的内容
{% endblock content %}

模板继承使用时需要注意:

  1. 不支持多继承,支持多级继承
  2. 为了便于阅读,在子模版中使用extends时,尽量写在模板的第一行。
  3. 不能在一个模版文件中定义多个相同名字的block标签。
  4. 当在页面中使用多个block标签时,建议给结束标签起个名字,当多个block嵌套时,阅读性更好。

7、CSRF跨站脚本攻击

pip install flask_wtf

在Flask中,Flask-wtf扩展有一套完善的csrf防护体系,对于我们开发者来说,使用起来非常简单

  1. 设置应用程序的secret_key,用于加密生成的csrf_token的值

    # 1. session加密的时候已经配置过了.如果没有在配置项中设置,则如下:
    app.secret_key = "#此处可以写随机字符串#"
    
    # 2. 也可以写在配置类中。
    class Config(object):
        DEBUG = True
        SECRET_KEY = "dsad32DASSLD*13%^32"
        
    """加载配置"""
    app.config.from_object(Config)
    
  2. 导入flask_wtf.csrf中的CSRFProtect类,进行初始化,并在初始化的时候关联app

    from flask.ext.wtf import CSRFProtect
    CSRFProtect(app)
    
  3. 在表单中使用CSRF令牌

    <form method="post" action="/">
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
    </form>
    

视图代码;

from flask import Flask,render_template
from settings.dev import Config
from flask_script import Manager
from flask_wtf.csrf import CSRFProtect
# from flask.ext.wtf import CSRFProtect  # 低版本的flask直接可以引入
"""创建flask应用"""
app = Flask(__name__,template_folder='templates')
"""使用脚手架[终端脚本]启动项目"""
manage = Manager(app)
"""加载配置"""
app.config.from_object(Config)

"""初始化csrf防范机制"""
CSRFProtect(app)

@app.route("/login",methods=["get"])
def loginform():
    return render_template("login.html")


@app.route("/dologin",methods=["post"])
def login():
    return "ok"

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

模板代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="{{ url_for('login') }}" method="post">
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
        <input type="submit" value="登录">
    </form>
</body>
</html>

八、ORM

1、数据库操作

1)ORM

ORM 全拼Object-Relation Mapping,中文意为 对象-关系映射。主要实现模型对象到关系数据库数据的映射

优点:

  • 只需要面向对象编程,不需要面向数据库编写代码
    • 对数据库的操作都转化成对类属性和方法的操作
    • 不用编写各种数据库的sql语句
  • 实现了数据模型与数据库的解耦,屏蔽了不同数据库操作上的差异
    • 不再需要关注当前项目是那种数据库
    • 通过简单的配置就可以轻松更换数据库,而不需要修改代码

缺点

  • 相比较直接使用SOL语句操作数据库,有性能上的损失
  • 根据对象的操作转换成SQL语句,根据查询的结果转化成对象时,在映射过程中都有修女功能的损失

2)Flask-SQLAlchemy

Flask默认提供模型操作,但是并没有提供ORM,所以一般开发的时候我们会采取Flask-SQLAlchemy是一个简化了SQLAlchemy操作的flask扩展

SQLAlchemy: 地址链接

  • 安装flask-sqlalchemy【清华源】

    pip install flask-sqlalchemy -i https://pypi.tuna.tsinghua.edu.cn/simple
    
  • 连接MySQL数据库需要下载mysqldb驱动

    pip install flask-mysqldb -i https://pypi.tuna.tsinghua.edu.cn/simple
    
  • 安装flask-mysqldb时,注意

    安装 flask-mysqldb的时候,python底层依赖于一个底层的模块 mysql-client模块
    如果没有这个模块,则会报错如下:
    
    Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-install-21hysnd4/mysqlclient/
    
  • 解决方案

    sudo apt-get install libmysqlclient-dev python3-dev
    
    运行上面的安装命令如果再次报错如下:
       dpkg 被中断,您必须手工运行 ‘sudo dpkg --configure -a’ 解决此问题。
    
    则根据提示执行命令以下命令,再次安装mysqlclient
    	sudo dpkg --configure -a
    	apt-get install libmysqlclient-dev python3-dev
    
    解决了mysqlclient问题以后,重新安装 flask-mysqldb即可。
    pip install flask-mysqldb -i https://pypi.tuna.tsinghua.edu.cn/simple
    
(1)数据库连接设置
  • 在Flask-SQLAlchemy中,数据库使用URL指定,而且程序使用的数据库必须保存到Flask配置对象的SQLALCHEMY_DATABASE_URI 键中
    config.py配置文件代码:

    class Config(object):
        DEBUG = True
        SECRET_KEY = "*(%#4sxcz(^(#$#8423"
        # 数据库链接配置 = 数据库名称://登录账号:登录密码@数据库主机IP:数据库访问端口/数据库名称?charset=编码类型
        SQLALCHEMY_DATABASE_URI = "mysql://root:123@127.0.0.1:3306/students?charset=utf8mb4"
    
  • 其他设置

    # 动态追踪修改设置,如未设置只会提示警告
    SQLALCHEMY_TRACK_MODIFICATIONS = True
    #查询时会显示原始SQL语句
    SQLALCHEMY_ECHO = True
    
  • 配置完成需要去MySQL中创建项目所使用的数据库

    $ mysql -uroot -p123
    mysql > create database students charset=utf8mb4;
    
(2)常用的SQLAlchemy字段类型
模型字段类型名 python中数据类型 说明
Integer int 普通整数,一般是32位
SmallInteger int 取值范围小的整数,一般是16位
BigInteger int或long 不限制精度的整数
Float float 浮点数
Numeric decimal.Decimal 普通数值,一般是32位
String str 变长字符串
Text str 变长字符串,对较长或不限长度的字符串做了优化
Unicode unicode 变长Unicode字符串
UnicodeText unicode 变长Unicode字符串,对较长或不限长度的字符串做了优化
Boolean bool 布尔值
Date datetime.date 日期
Time datetime.time 时间
DateTime datetime.Datetime 日期和时间
LargeBinary str 二进制文件内容
(3)常用的SQLAlchemy列约束
选项名 说明
primary_key 如果为True,代表表的主键
unique 如果为True,代表这列不允许出现重复的值
index 如果为True,为这列创建索引,提高查询效率
nullable 如果为True,允许有空值,如果为False,不允许有空值
default 为这列定义默认值
(4)数据库的基本操作
  • 在Flask-SQLAlchemy中,添加、修改、删除操作,均由数据库会话管理。

    • 会话用 db.session 表示。在准备把数据写入数据库前,要先将数据添加到会话中然后调用 db.commit() 方法提交会话。
  • 在 Flask-SQLAlchemy 中,查询操作是通过 query 对象操作数据。

    • 最基本的查询是返回表中所有数据,可以通过过滤器进行更精确的数据库查询。
  • 定义模型类

    我们后面会把模型创建到单独的文件中,但是现在先把模型类写在main.py文件中。

    from flask import Flask
    # 初始化
    app = Flask(import_name=__name__)
    
    # 声明和加载配置
    class Config():
        DEBUG = True
        # 数据库链接配置 = 数据库名称://登录账号:登录密码@数据库主机IP:数据库访问端口/数据库名称?charset=编码类型
        SQLALCHEMY_DATABASE_URI = "mysql://root:123@127.0.0.1:3306/students?charset=utf8"
        # 动态追踪修改设置,如未设置只会提示警告
        SQLALCHEMY_TRACK_MODIFICATIONS = False
        # 显示原始SQL语句
        SQLALCHEMY_ECHO = True
    
    app.config.from_object(Config)
    
    # 初始化SQLAlchemy
    from flask_sqlalchemy import SQLAlchemy
    db = SQLAlchemy() # 初始化数据库操作对象
    db.init_app(app)  # 初始化数据库链接
    
    class Student(db.Model):
        # 表结构声明
        __tablename__ = "tb_student"
    
        # 字段声明
        id   = db.Column(db.Integer, primary_key=True, comment="主键")
        name = db.Column(db.String(64), index=True, comment="姓名")
        sex  = db.Column(db.Boolean, default=True, comment="性别")
        age  = db.Column(db.SmallInteger, nullable=True, comment="年龄")
        email = db.Column(db.String(128), unique=True, comment="邮箱地址")
        money = db.Column(db.Numeric(8,2), default=0, comment="钱包")
    
        # 自定义方法
        def __repr__(self):
            return 'Student:%s' % self.name
    
    class Teacher(db.Model):
        # 表结构声明
        __tablename__ = 'tb_teacher'
    
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(64), unique=True)
        option = db.Column(db.Enum("讲师","助教","班主任"), default="讲师")
        def __repr__(self):
            return 'Teacher:%s' % self.name
    
    class Course(db.Model):
        # 定义表名
        __tablename__ = 'tb_course'
        # 定义字段对象
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(64), unique=True)
        price = db.Column(db.Numeric(6,2))
        # repr()方法类似于django的__str__,用于打印模型对象时显示的字符串信息
        def __repr__(self):
            return 'Course:%s'% self.name
    
    @app.route(rule='/')
    def index():
        return "ok"
    
    if __name__ == '__main__':
        # 运行flask
        app.run(debug=True)
    

2、表操作

  • 创建和删除表

    # 创建表
    db.create_all()  # 注意,create_all()方法执行的时候,需要放在模型的后面
    # 上面这段语句,后面我们需要转移代码到flask-script的自定义命令中。
    # 执行了一次以后,需要注释掉。
    
    # 删除表
    db.drop_all()
    

    代码示例

    from flask import Flask
    # 初始化
    app = Flask(import_name=__name__)
    
    # 声明和加载配置
    class Config():
        DEBUG = True
        # 数据库链接配置 = 数据库名称://登录账号:登录密码@数据库主机IP:数据库访问端口/数据库名称?charset=编码类型
        SQLALCHEMY_DATABASE_URI = "mysql://root:123@127.0.0.1:3306/students?charset=utf8"
        # 动态追踪修改设置,如未设置只会提示警告
        SQLALCHEMY_TRACK_MODIFICATIONS = False
        # 显示原始SQL语句
        SQLALCHEMY_ECHO = True
    
    app.config.from_object(Config)
    
    # 初始化SQLAlchemy
    from flask_sqlalchemy import SQLAlchemy
    db = SQLAlchemy() # 初始化数据库操作对象
    db.init_app(app)  # 初始化数据库链接
    
    class Student(db.Model):
        # 表结构声明
        __tablename__ = "tb_student"
    
        # 字段声明
        id   = db.Column(db.Integer, primary_key=True, comment="主键")
        name = db.Column(db.String(64), index=True, comment="姓名")
        sex  = db.Column(db.Boolean, default=True, comment="性别")
        age  = db.Column(db.SmallInteger, nullable=True, comment="年龄")
        email = db.Column(db.String(128), unique=True, comment="邮箱地址")
        money = db.Column(db.Numeric(8,2), default=0, comment="钱包")
    
        # 自定义方法
        def __repr__(self):
            return 'Student:%s' % self.name
    
    class Teacher(db.Model):
        # 表结构声明
        __tablename__ = 'tb_teacher'
    
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(64), unique=True)
        option = db.Column(db.Enum("讲师","助教","班主任"), default="讲师")
        def __repr__(self):
            return 'Teacher:%s' % self.name
    
    class Course(db.Model):
        # 定义表名
        __tablename__ = 'tb_course'
        # 定义字段对象
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(64), unique=True)
        price = db.Column(db.Numeric(6,2))
        # repr()方法类似于django的__str__,用于打印模型对象时显示的字符串信息
        def __repr__(self):
            return 'Course:%s'% self.name
    
    @app.route(rule='/')
    def index():
        return "ok"
    
    if __name__ == '__main__':
        with app.app_context():
            # db.drop_all()   # 删除所有的数据表
            db.create_all() # 创建所有的数据表
        # 运行flask
        app.run(debug=True)
    

3、数据操作

基本数据操作

  • 添加一条数据

    student1 = Student(name="小明", sex=True, age=17, email="123456@qq.com", money=100)
    db.session.add(student1)
    db.session.commit()
    
    #再次插入 一条数据
    student2 = Student(name='小红', sex=False, age=13, email="16565666@qq.com", money=600)
    db.session.add(student2)
    db.session.commit()
    
  • 一次插入多条数据

    st1 = Student(name='wang',email='wang@163.com',age=22)
    st2 = Student(name='zhang',email='zhang@189.com',age=22)
    st3 = Student(name='chen',email='chen@126.com',age=22)
    st4 = Student(name='zhou',email='zhou@163.com',age=22)
    st5 = Student(name='tang',email='tang@163.com',age=22)
    st6 = Student(name='wu',email='wu@gmail.com',age=22)
    st7 = Student(name='qian',email='qian@gmail.com',age=22)
    st8 = Student(name='liu',email='liu@163.com',age=22)
    st9 = Student(name='li',email='li@163.com',age=22)
    st10 = Student(name='sun',email='sun@163.com',age=22)
    db.session.add_all([st1,st2,st3,st4,st5,st6,st7,st8,st9,st10])
    db.session.commit()
    
  • 删除数据

    # 方法1
    student = Student.query.first()
    db.session.delete(student)
    db.session.commit()
    
    # 方法2【事务中使用,就是乐观锁】
    ret = Student.query.filter(Student.name=='sun').delete()
    db.session.commit()
    
  • 更新数据

    # 方法1
    student = Student.query.first()
    student.name = 'dong'
    db.session.commit()
    
    # 方法2【事务中使用,就是乐观锁】
    ret = Student.query.filter(Student.name == 'liu').update({'money': 1000})
    db.session.commit()
    
    # 方法3【批量操作, 实现类似django里面F函数的效果】
    ret = Student.query.filter(Student.age == 22).update({Student.money: Student.money+'200'})
    db.session.commit()
    

(1)数据的基本查询

  1. 常用的SQLAlchemy查询过滤器
过滤器 说明
filter() 把过滤器添加到原查询上,返回一个新查询
filter_by() 把等值过滤器添加到原查询上,返回一个新查询
limit() 使用指定的值限定原查询返回的结果
offset() 偏移原查询返回的结果,返回一个新查询
order_by() 根据指定条件对原查询结果进行排序,返回一个新查询
group_by() 根据指定条件对原查询结果进行分组,返回一个新查询
  1. 常用的SQLAlchemy查询结果的方法
方法 说明
all() 以列表形式返回查询的所有结果
first() 返回查询的第一个结果,如果未查到,返回None
first_or_404() 返回查询的第一个结果,如果未查到,返回404
get() 返回指定主键对应的行,如不存在,返回None
get_or_404() 返回指定主键对应的行,如不存在,返回404
count() 返回查询结果的数量
paginate() 返回一个Paginate分页器对象,它包含指定范围内的结果
having 返回结果中符合条件的数据,必须跟在group by后面,其他地方无法使用。

get():参数为数字,表示根据主键查询数据,如果主键不存在返回None

Student.query.get()

all()返回查询到的所有对象

Student.query.all()

first()返回查询到的第一个对象【first获取一条数据,all获取更多数据】

Student.query.first()

filter模糊查询,支持各种运算符和查询方法

返回名字结尾字符位g的所有数据

   # name姓名中以"g"结尾的学生
    ret = Student.query.filter(Student.name.endswith("g")).all()
    # name姓名中包含"u"的学生
    ret = Student.query.filter(Student.name.contains("u")).all()
    # name姓名中以"w"开头的学生
    ret = Student.query.filter(Student.name.startswith("w")).all()
    
    
    # 也可以使用filter进行精确查找,
    # 则需要指定条件格式为: 模型.字段 比较运算符 值。
    # 运算符可以是: ==表示相等,!=不相等,> 表示大于  < 表示小于,>=大于等于,<=小于等于
    # ret = Student.query.filter(Student.age==22).all()

    # 另一种写法的查询方式
    # db.session.query(Student) 相当于 Student.query
    # ret = db.session.query(Student).filter(Student.age==22).all()

filter_by精确查询,只支持字段的值是否相等这种条件

例如:返回名字等于wang的学生

# name=wang的学生
ret = Student.query.filter_by(name="wang").first()
# age = 22的所有学生
ret = Student.query.filter_by(age=22).all()

练习:

查询所有男生的数量
	# ret = Student.query.filter(Student.sex==True).all()
查询所有年龄大于20的女生
	# ret = Student.query.filter(Student.sex==False).all()
查询id为4的学生[2种方式]
    # ret = Student.query.filter(Student.id==4).first()
    # ret = Student.query.get(4)
    # ret = Student.query.filter_by(id=4).first()    
查询年龄等于22的所有学生数据
	# ret = Student.query.filter_by(age=22).all()
查询name为liu的学生数据
    # ret = Student.query.filter(Student.name == "liu").all()
    # ret = Student.query.filter_by(name="liu").all()
  1. 逻辑查询条件
  • 逻辑非:返回名字不等于wang的所有数据

    """
    查询出名字不等于liu的所有男学生
    """
    ret = Student.query.filter(Student.name!="liu",Student.sex==True).all()
    
  • not_:相当于取反

    from sqlalchemy import not_
    """
    查询出名字不等于liu的所有男学生
    """
    ret = Student.query.filter(not_(Student.name=="liu"),Student.sex==True).all()
    
  • 逻辑与:需要导入and,返回and()条件满足的所有数据

    from sqlalchemy import and_
    """
    and_ 且,与
    查询出年龄在18-21之间的女生
    """
    ret1 = Student.query.filter(and_(Student.age>=18,Student.age<=21,Student.sex==False)).all()
    ret2 = Student.query.filter(Student.age>=18,Student.age<=21,Student.sex==False).all()
    print(ret1)
    print(ret2)
    
  • 逻辑或,需要导入or_

    from sqlalchemy import or_
    """
    查询出大于21,小于20的男生
    """
    ret = Student.query.filter(or_(Student.age>21,Student.age<20),Student.sex==True).all()
    
    """
    查询大于21的男生和小于20的女生
    """
    ret1 = Student.query.filter(or_(and_(Student.age>21,Student.sex==True),and_(Student.age<20,Student.sex==False))).all()
    print(ret1) # [wang, zhang, chen, zhou, wu, qian, liu, li]
    
  • in_:范围查询

    """查询id为2, 3, 5, 7, 8这几个学生信息"""
    student_list = Student.query.filter(Student.id.in_([2, 3, 5, 7, 8])).all()
    print(student_list)
    
  • order_by:排序

    # 查询所有学生,并按年龄进行倒序排列
    ret = Student.query.order_by(Student.age.desc()).all()
    
    # 查询所有学生,并按年龄进行倒序排列,年龄相同,则按id进行降序排序.
    ret = Student.query.order_by(Student.age.desc(),Student.id.desc()).all()
    
  • count:统计

    # 查询age>=19的男生的数量
        from sqlalchemy import and_
        # ret = Student.query.filter( and_(Student.age>=19,Student.sex==True) ).count()
        ret = Student.query.filter( Student.age>=19, Student.sex==True ).count()
    
  • 对结果进行偏移量和数量的限制

        # 查询年龄最大的3个学生
        ret1 = Student.query.order_by(Student.age.desc()).limit(3).all()
    
        # 查询年龄排第4到第7名的学生
        ret2 = Student.query.order_by(Student.age.desc(),Student.id.desc()).offset(4).limit(4).all()
        print(ret1,ret2)
    
    # 查询名字和邮箱都以 li 开头的所有数据[2种方式]
    	ret = Student.query.filter(Student.name.startswith("li"),Student.email.startswith("li")).all()
    # 查询age是 18 或者 `email` 以 `163.com` 结尾的所有学生
    	Student.query.filter(or_(Student.age==18,Student.email.endswith("163.com"))).all()
    # 查询id为 [1, 3, 5, 7, 9] 的学生列表
    	student_list = Student.query.filter(Student.id.in_([1, 3, 5, 7, 9])).all()
    print(student_list)
    # 查询男生和女生的数量
    	ret = Student.query.filter_by(sex=True).count()
        ret = Student.query.filter_by(sex=False).count()
    
  • 分页器的使用

    # run.py 代码示例:
    from flask import Flask,request,jsonify,render_template
    from config import Config
    from models import db,Student,Course,Teacher
    
    # 初始化
    app = Flask(import_name=__name__,template_folder='templates')
    app.config.from_object(Config)
    db.init_app(app)  # 初始化数据库链接
    
    """分页器使用"""
    @app.route(rule="/list")
    def list():
        pagination = Student.query.paginate(per_page=3)
    
        # 获取当前页面所有数据
        # print( pagination.items )
        # data = {
        #     "items": [],
        #     "pages": pagination.pages,
        #     "page": pagination.page,
        #     "has_prev": pagination.has_prev,
        #     "has_next": pagination.has_next,
        # }
    
        # for item in pagination.items:
        #     data["items"].append({
        #         "id": item.id,
        #         "sex": "男" if item.sex else "女",
        #         "age": item.age,
        #         "name": item.name,
        #     })
        #
        # if pagination.has_prev:
        #     print( pagination.prev() ) # 上一页数据的分页器对象
        #     print( pagination.prev().items ) # 上一页数据
        #
        # if pagination.has_next:
        #     print( pagination.next() ) # 下一页数据的分页器对象
        #     print( pagination.next().items ) # 下一页数据
    
        return render_template("list.html",pagination=pagination)
    
    if __name__ == '__main__':
        # 运行flask
        app.run(debug=True)
    
    # list.html 代码示例:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <style>
        .page a,.page span{
            padding: 2px 6px;
            color: #fff;
            background: #6666ff;
            text-decoration: none;
        }
        .page span{
            color: #fff;
            background: orange;
        }
    
        </style>
    </head>
    <body>
        <table border="1" align="center" width="600">
            <tr>
               <th>ID</th>
               <th>age</th>
               <th>name</th>
               <th>sex</th>
               <th>money</th>
            </tr>
            {% for student in pagination.items %}
            <tr>
               <td>{{ student.id }}</td>
               <td>{{ student.age }}</td>
               <td>{{ student.name }}</td>
               <td>{{ "男" if student.sex else "女" }}</td>
               <td>{{ student.money }}</td>
            </tr>
            {% endfor %}
            <tr align="center">
                <td colspan="5" class="page">
                    {% if pagination.has_prev %}
                    <a href="?page=1">首  页</a>
                    <a href="?page={{ pagination.page-1 }}">上一页</a>
                    <a href="?page={{ pagination.page-1 }}">{{ pagination.page-1 }}</a>
                    {% endif %}
                    <span>{{ pagination.page }}</span>
                    {% if pagination.has_next %}
                    <a href="?page={{ pagination.page+1 }}">{{ pagination.page+1 }}</a>
                    <a href="?page={{ pagination.page+1 }}">下一页</a>
                    <a href="?page={{ pagination.pages }}">尾  页</a>
                    {% endif %}
                </td>
            </tr>
        </table>
    </body>
    </html>
    

(2)分组查询和分组查询结果过滤

一般分都会结合聚合函数来一起使用。SQLAlchemy中所有的聚合函数都在func模块中声明。

from sqlalchemy import func

函数名 说明
func.count 统计总数
func.avg 平均值
func.min 最小值
func.max 最大值
func.sum

代码示例:

# 查询当前所有男生女生的数量
from sqlalchemy import func
    # ret = db.session.query(Student.sex,func.count(Student.id)).group_by(Student.sex).all()
    # 查询当前不同年龄的学生数量
    ret = db.session.query(Student.age,func.count(Student.id)).group_by(Student.age).having(Student.age>19).all()
    
    # 查询男生和女生中,年龄最小的是几岁?
    ret = db.session.query(Student.sex,func.min(Student.age)).group_by(Student.sex).all()

执行原生SQL语句

# 读取多条数据
ret = db.session.execute("select * from tb_student").fetchall()
# 读取一条数据
ret = db.session.execute("select * from tb_student").fetchone()
# 添加/修改/删除
db.session.execute("UPDATE tb_student SET money=(tb_student.money + %s) WHERE tb_student.age = %s" % (200, 22))
    db.session.commit()

(3)关联查询(多表)

  1. 常用的SQLAlchemy关系选项
选项名 说明
backref 在关系的另一模型中添加反向引用,用于设置外键名称,在1查多的
类似django里面的related选项
primary join 明确指定两个模型之间使用的连表条件
lazy 指定如何加载关联模型数据的方式。参数值:
select(立即加载,查询所有相关数据显示,相当于lazy=True)
subquery(立即加载,但使用子查询)
dynamic(不加载记录,但提供加载记录的查询对象)
uselist 如果为False,不使用列表,而使用标量值。
一对一关系中,需要设置relationship中的uselist=Flase,其他数据库操作一样。
secondary 指定多对多关系中关系表的名字。
多对多关系中,需建立关系表,设置 secondary=关系表
secondary join 在SQLAlchemy中无法自行决定时,指定多对多关系中的二级连表条件
  1. 模型之间的关联

    • 一对一

      class Student(db.Model):
          """个人信息主表"""
      	....
          # 关联属性,这个不会被视作表字段,只是模型的属性。
          # 因为StudentInfo和Student是一对一的关系,所以uselist=False表示关联一个数据
          info = db.relationship("StudentInfo",uselist=False,backref="own")
      
      
      class StudentInfo(db.Model):
          """个人信息附加表"""
      
          # 外键,
          # 如果是一对一,则外键放在附加表对应的模型中
          # 如果是一对多,则外键放在多的表对象的模型中
          uid = db.Column(db.Integer, db.ForeignKey(Student.id),comment="外键")
      
    • 代码示例

      # run.py 代码块
      from flask import Flask,request,jsonify,render_template
      from config import Config
      from models2 import db,Student,Course,Teacher,StudentInfo
      
      # 初始化
      app = Flask(import_name=__name__,template_folder='templates')
      app.config.from_object(Config)
      db.init_app(app)  # 初始化数据库链接
      
      @app.route(rule='/')
      def index():
          """1对1模型操作"""
          # 获取数据[从主表读取数据,获取附加表数据]
          # student = Student.query.get(3)
          # print( student.info.address )
          # print( student.info.edu )
      
          # 获取数据[从附加表读取数据,获取主表数据]
          # student_info = StudentInfo.query.filter(StudentInfo.address=="北京市昌平区沙河地铁站对面").first()
          # print(student_info.own.name)
      
          # 添加数据[添加数据,把关联模型的数据也一并添加]
          # student = Student(name="liu", sex=True, age=22, email="33523@qq.com", money=100)
          # student.info = StudentInfo(address="深圳市宝安区创业2路103号", edu="本科")
          # db.session.add(student)
          # db.session.commit()
      
          # 修改数据[通过主表可以修改附加表的数据,也可以通过附加表模型直接修改主表的数据]
          # student = Student.query.get(4)
          # student.info.address = "广州市天河区天河东路103号"
          # db.session.commit()
      
          return "ok"
      
      if __name__ == '__main__':
          # 运行flask
          app.run(debug=True)
      
      # conf.py 代码块
      # 声明和加载配置
      class Config():
          DEBUG = True
          # 数据库链接配置 = 数据库名称://登录账号:登录密码@数据库主机IP:数据库访问端口/数据库名称?charset=编码类型
          SQLALCHEMY_DATABASE_URI = "mysql://root:123@127.0.0.1:3306/students?charset=utf8"
          # 动态追踪修改设置,如未设置只会提示警告
          SQLALCHEMY_TRACK_MODIFICATIONS = False
          # 显示原始SQL语句
          SQLALCHEMY_ECHO = True
          # 调整json数据转换中文的配置
          JSON_AS_ASCII=False
      
    • 测试数据

      INSERT INTO students.tb_student (id, name, sex, age, email, money) VALUES (3, 'li', 1, 17, '333@qq.com', 300.00);
      INSERT INTO students.tb_student (id, name, sex, age, email, money) VALUES (4, 'wang', 0, 15, '123@qq.com', 300.00);
      INSERT INTO students.tb_student (id, name, sex, age, email, money) VALUES (5, 'zhao', 0, 17, '333@baidu.com', 300.00);
      INSERT INTO students.tb_student (id, name, sex, age, email, money) VALUES (6, 'long', 0, 18, '333@163.com', 300.00);
      INSERT INTO students.tb_student (id, name, sex, age, email, money) VALUES (7, 'zhang', 1, 21, '333@sina.com.cn', 300.00);
      INSERT INTO students.tb_student (id, name, sex, age, email, money) VALUES (8, 'liu', 1, 22, '33523@qq.com', 100.00);
      
      INSERT INTO students.tb_student_info (id, address, edu, uid) VALUES (1, '天津市静海区静海路2号', '本科', 3);
      INSERT INTO students.tb_student_info (id, address, edu, uid) VALUES (2, '广州市天河区天河东路103号', '高中以下', 4);
      INSERT INTO students.tb_student_info (id, address, edu, uid) VALUES (3, '天津市静海区静海路2号', '本科', 5);
      INSERT INTO students.tb_student_info (id, address, edu, uid) VALUES (4, '北京市昌平区沙河地铁站对面', '本科', 6);
      INSERT INTO students.tb_student_info (id, address, edu, uid) VALUES (5, '天津市静海区静海路2号', '本科', 7);
      INSERT INTO students.tb_student_info (id, address, edu, uid) VALUES (6, '深圳市宝安区创业2路103号', '本科', 8);
      
    • 一对多

      class Teacher(db.Model):
      	...
          # 关联属性,一的一方添加模型关联属性
          course = db.relationship("Course", uselist=True, backref="teacher",lazy='dynamic')
         
      class Course(db.Model):
      	...
          # 外键,多的一方模型中添加外键
          teacher_id = db.Column(db.Integer, db.ForeignKey(Teacher.id))
      
      • 其中realtionship描述了Course和Teacher的关系。第一个参数为对应参照的类“Course”
      • 第二个参数为backref为类Teacher声明新属性的方法
      • 第三个参数lazy决定了什么时候SQLAlchemy从数据库zh哦那个加载数据
        • lazy=“subquery”,查询当前数据模型时,采用子查询(subquery),把外键模型的属性也瞬间查询出来
        • lazy=True或lazy=“select”,查询当前数据模型时,不会把外键模型的数据查询出来,只有操作到外键关联属性时,才进行表查询数据【执行SQL】
        • lazy="dynamic",查询当前数据模型时,不会把外键模型的数据查询出来,只有操作到外键关联属并操作外键模型具体属性时,才进行连表查询数据【执行SQL】
      • 代码示例
      # models2.py 代码块
      # 初始化SQLAlchemy
      from flask_sqlalchemy import SQLAlchemy
      db = SQLAlchemy() # 初始化数据库操作对象
      
      class Student(db.Model):
          # 表名
          __tablename__ = "tb_student"
          # 字段
          id   = db.Column(db.Integer, primary_key=True, comment="主键")
          name = db.Column(db.String(64), index=True, comment="姓名")
          sex  = db.Column(db.Boolean, default=True, comment="性别")
          age  = db.Column(db.SmallInteger, nullable=True, comment="年龄")
          email = db.Column(db.String(128), unique=True, comment="邮箱地址")
          money = db.Column(db.Numeric(8,2), default=0, comment="钱包")
          # 关联属性,这个不会被视作表字段,只是模型的属性。
          # 因为StudentInfo和Student是一对一的关系,所以uselist=False表示关联一个数据
          info = db.relationship("StudentInfo",uselist=False,backref="own")
          # 自定义方法
          def __repr__(self):
              return 'Student:%s' % self.name
      
      class StudentInfo(db.Model):
          # 表明
          __tablename__ = "tb_student_info"
          # 字段
          id = db.Column(db.Integer, primary_key=True, comment="主键")
          address = db.Column(db.String(299), comment="住址")
          edu = db.Column(db.Enum("高中以下","大专高技","本科","硕士","博士以上"))
          # uid = db.Column(db.Integer, db.ForeignKey("tb_student.id"),comment="外键")
          # 外键,
          # 如果是一对一,则外键放在附加表对应的模型中
          # 如果是一对多,则外键放在多的表对象的模型中
          uid = db.Column(db.Integer, db.ForeignKey(Student.id),comment="外键")
      
      class Teacher(db.Model):
          # 表结构声明
          __tablename__ = 'tb_teacher'
      
          id = db.Column(db.Integer, primary_key=True)
          name = db.Column(db.String(64), unique=True)
          option = db.Column(db.Enum("讲师","助教","班主任"), default="讲师")
          # 关联属性,一的一方添加模型关联属性
          course = db.relationship("Course", uselist=True, backref="teacher",lazy='dynamic')
          def __repr__(self):
              return 'Teacher:%s' % self.name
      
      class Course(db.Model):
          # 定义表名
          __tablename__ = 'tb_course'
          # 定义字段对象
          id = db.Column(db.Integer, primary_key=True)
          name = db.Column(db.String(64), unique=True)
          price = db.Column(db.Numeric(6,2))
          # 外键,多的一方模型中添加外间
          teacher_id = db.Column(db.Integer, db.ForeignKey(Teacher.id))
          # repr()方法类似于django的__str__,用于打印模型对象时显示的字符串信息
          def __repr__(self):
              return 'Course:%s'% self.name
      
      # run.py 代码块
      from flask import Flask,request,jsonify,render_template
      from config import Config
      from models2 import db,Student,Course,Teacher,StudentInfo
      
      # 初始化
      app = Flask(import_name=__name__,template_folder='templates')
      app.config.from_object(Config)
      db.init_app(app)  # 初始化数据库链接
      
      @app.route(rule='/more')
      def more():
          """一对多/多对一模型操作"""
          # 从1的一方的模型中获取多的一方模型的数据
          # teacher = Teacher.query.get(1)
          # print(teacher)
          # # ret = teacher.course
          # for course in teacher.course:
          #     print(course.name,course.price)
      
          # 从多的一方获取1的一方数据
          # course = Course.query.get(1)
          # print(course.teacher)
          # print(course.teacher.name)
      
          # 添加数据
          # 从1的一方添加数据,同时给多的一方也添加
          # teacher = Teacher(name="蓝老师",option="讲师")
          # teacher.course = [Course(name="插画入门",price=199.00),Course(name="素描入门",price=129.00),]
          # db.session.add(teacher)
          # db.session.commit()
      
          return "ok"
      if __name__ == '__main__':
          # 运行flask
          app.run(debug=True)
      

      测试数据

      INSERT INTO students.tb_teacher (id, name, `option`) VALUES (1, '王老师', '讲师');
      INSERT INTO students.tb_teacher (id, name, `option`) VALUES (2, '许老师', '讲师');
      INSERT INTO students.tb_teacher (id, name, `option`) VALUES (3, '徐老师', '讲师');
      INSERT INTO students.tb_teacher (id, name, `option`) VALUES (4, '张老师', '讲师');
      INSERT INTO students.tb_teacher (id, name, `option`) VALUES (5, '留老师', '讲师');
      
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (1, 'JAVA入门', 299.00, 1);
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (2, 'Python入门', 399.00, 1);
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (3, 'linux入门', 199.00, 2);
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (4, 'django入门', 399.00, 3);
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (5, 'flask入门', 299.00, 3);
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (6, '数据分析入门', 199.00, 3);
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (7, '爬虫入门', 299.00, 4);
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (8, '前端入门', 199.00, 5);
      INSERT INTO students.tb_course (id, name, price, teacher_id) VALUES (9, 'python项目', 199.00, 5);
      
    • 多对多

      achievement = db.Table(
          'tb_achievement',  
          db.Column('student_id', db.Integer, db.ForeignKey('tb_student.id')),  
          db.Column('course_id', db.Integer, db.ForeignKey('tb_course.id')),
      )
      
      class Course(db.Model):
          ...
      	students = db.relationship('Student',secondary=achievement,  
                                          backref='courses',  
                                          lazy='dynamic')
      class Student(db.Model):
          ...
      

      多对多,也可以拆解成3个模型,其中tb_achievement作为单独模型存在。

      • 查询学的所有课程
      # 查xiaoming的所有课程列表
      student = Student.query.filter(Student.name=="xiaoming").first()
      print(student.course_list) # [3天python入门, 6天python入门]
      
      # 查指定课程的所有学生
      course = Course.query.filter(Course.name=="6天python入门").first()
      print(course.student_list) # [xiaoming]
      

    其它操作

    # 代码示例
     """1. 添加主键模型, 同时把外键模型也添加"""
        # student = Student(name="xiaoming", age=17,sex=True,money=10000,email="1231@qq.com")
        # student.course_list = [
        #     Course(name="3天python入门",price=39.99),
        #     Course(name="6天python入门",price=89.99),
        # ]
        # db.session.add(student)
        # db.session.commit()
    
        """2. 查询其中一方模型数据,把多的另一方也查询出来"""
        # student = Student.query.filter(Student.name=="xiaoming").first()
        # print(student.course_list) # [3天python入门, 6天python入门]
    
        # course = Course.query.filter(Course.name=="6天python入门").first()
        # print(course.student_list) # [xiaoming]
    
        """3. 更新操作"""
        # student = Student.query.filter(Student.name == "xiaoming").first()
        # student.course_list[0].price=666.99
        # db.session.commit()
    
        """4. 删除操作"""
        # student = Student.query.filter(Student.name == "xiaoming").first()
        # db.session.delete(student) # 删除主模型信息,会自动同步删除关联数据表中的信息,但是外键模型不会被删除
        # db.session.commit()
    

    把上面的db.Table关系表对象转换成关系模型,把Achievement单独声明成一个独立模型.

    # 代码示例
    from flask import Flask,render_template
    
    app = Flask(__name__)
    class Config(object):
        DEBUG = True
        SECRET_KEY = "dsad32DASSLD*13%^32"
        # 数据库链接配置
        SQLALCHEMY_DATABASE_URI="mysql://root:123@127.0.0.1:3306/students?charset=utf8mb4"
        # 动态追踪修改设置,如未设置只会提示警告
        SQLALCHEMY_TRACK_MODIFICATIONS = False
        # 查询时会显示原始SQL语句
        SQLALCHEMY_ECHO = True
    
    app.config.from_object(Config)
    
    from flask_sqlalchemy import SQLAlchemy
    db = SQLAlchemy()
    db.init_app(app)
    
    class Student(db.Model):
        """学生信息"""
        # 1. 表相关属性
        __tablename__ = "tb_student" # 设置表明
        # 2. 字段类型
        # db.Column 表示当前属性对应数据库字段
        id = db.Column(db.Integer, primary_key=True, comment="ID")
        name = db.Column(db.String(64), index=True, comment="姓名")
        sex  = db.Column(db.Boolean, default=True, comment="性别")
        age  = db.Column(db.SmallInteger, nullable=True, comment="年龄")
        email = db.Column(db.String(128), unique=True, comment="邮箱地址")
        money = db.Column(db.Numeric(8,2), default=0, comment="钱包")
        # 1. 在主键中设置关联模型属性,这个属性不是数据库的表字段,所以不会在数据库的表中显示出来.
        # 主要是提供给模型操作的.
        # 在一对一或者,多对一的时候, lazy的值不能是 dynamic.否则报错
        info = db.relationship("StudentInfo", uselist=False, backref="student",lazy="subquery")
        course_list = db.relationship("Achievement", backref="student", lazy=True)
        # 3. 模型方法
        def __repr__(self):
            return "%s" % self.name
    
    class StudentInfo(db.Model):
        """学生信息附加表"""
        __tablename__ = "tb_student_info"  # 设置表名
        id = db.Column(db.Integer, primary_key=True, comment="ID")
        # 数据库中的外键,数据库的表中是存在的
        sid = db.Column(db.Integer, db.ForeignKey("tb_student.id"),comment="学生ID")
        address = db.Column(db.String(250), nullable=True, comment="家庭地址")
        mobile = db.Column(db.String(15), nullable=True, comment="紧急联系人")
        def __repr__(self):
            return "%s" % self.student.name
    
    class Teacher(db.Model):
        # 表结构声明
        __tablename__ = 'tb_teacher'
    
        id = db.Column(db.Integer, primary_key=True, comment="ID")
        name = db.Column(db.String(64), comment="姓名")
        option = db.Column(db.Enum("讲师","助教","班主任"), default="讲师", comment="身份")
        course_list = db.relationship("Course", backref="teacher",lazy=True)
    
        def __repr__(self):
            return '%s[%s]' % (self.option,self.name)
    
    class Course(db.Model):
        # 定义表名
        __tablename__ = 'tb_course'
        # 定义字段对象
        id = db.Column(db.Integer, primary_key=True)
        teahcer_id = db.Column(db.Integer, db.ForeignKey(Teacher.id), comment="老师ID")
        name = db.Column(db.String(64), unique=True)
        price = db.Column(db.Numeric(6,2))
        student_list = db.relationship("Achievement", backref="course",lazy=True)
        def __repr__(self):
            return '%s' % self.name
    
    class Achievement(db.Model):
        """学生和课程之间的成绩关系模型"""
        __tablename__ = 'tb_achievement'
        id = db.Column(db.Integer, primary_key=True)
        student_id = db.Column(db.Integer,db.ForeignKey(Student.id), comment="学生")
        course_id = db.Column(db.Integer,db.ForeignKey(Course.id), comment="课程")
        def __repr__(self):
            return '%s-%s' % (self.student,self.course)
    
    @app.route("/")
    def index():
        """
        多对多
        """
        """1. 添加主键模型,给外键模型添加数据"""
        # course1 = Course(name="3天Python入门",price=99.99)
        # course2 = Course(name="7天Python入门",price=399.99)
        # db.session.add_all([course1,course2])
        #
        # student = Student(name="xiaoming",age=17,sex=True, email="64123@qq.com",money=10000)
        # student.course_list = [
        #     Achievement(course=course1),
        #     Achievement(course=course2),
        # ]
        # db.session.add(student)
        # db.session.commit()
        #
        """2. 查询操作"""
        student = Student.query.filter(Student.name=="xiaoming").first()
        print(student.course_list)
        for achievement in student.course_list:
            print(achievement.course.name)
        
        return "ok"
    
    if __name__ == '__main__':
        # with app.app_context():
        #     db.drop_all()
        #     db.create_all()
        app.run(debug=True)
    

4、数据迁移

  • 在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库。最直接的方式就是删除旧表,但这样会丢失数据。
  • 更好的解决办法是使用数据库迁移框架,它可以追踪数据库模式的变化,然后把变动应用到数据库中。
  • 在Flask中可以使用Flask-Migrate扩展,未来实现数据迁移。并且集成到Flsask-Script中,所有操作通过命令就能完成。
  • 为了导出数据库迁移命令,Flask-Migrate提供了一个MigrateCommand类,可以附加到flask-script的manager对象上。

首先在虚拟环境中安装Flask-Migrate。

pip install flask-migrate -i https://pypi.doubanio.com/simple

模型采用之前的即可,把数据库所有的表删除

# ruhn.py
from flask import Flask,request,jsonify,render_template
from config import Config
from models2 import db,Student,Course,Teacher,StudentInfo

# 初始化
app = Flask(import_name=__name__,template_folder='templates')
app.config.from_object(Config)
db.init_app(app)  # 初始化数据库链接

from flask_script import Manager
manager = Manager(app)
from flask_migrate import Migrate,MigrateCommand
#第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
migrate = Migrate(app,db)
#manager是Flask-Script的实例,这条语句在flask-Script中添加一个db命令
manager.add_command('db',MigrateCommand)

@app.route(rule='/')
def index():
   
    return "ok"
if __name__ == '__main__':
    # 运行flask
    manager.run()

创建迁移版本仓库

#这个命令会创建migrations文件夹,所有迁移文件都放在里面。
python main.py db init

创建迁移版本

  • 自动创建迁移版本有两个函数
    • upgrade():函数把迁移中的改动应用到数据库中。
    • downgrade():函数则将改动的删除。一般表示把upgrade的操作进行恢复/删除/还原。
  • 自动创建的迁移脚本会根据模型定义和数据库当前状态的差异,生成upgrade()和downgrade()函数的内容。
  • 对比不一定完全正确,有可能会遗漏一些细节,需要进行检查
python main.py db migrate -m "initial migration"

# 这里等同于django里面的 makemigrations,生成迁移版本文件

升级版本库的版本

python main.py db upgrade

降级版本库的版本

python main.py db downgrade

版本库的历史管理

可以根据history命令找到版本号,然后传给downgrade命令:

python manage.py db history

输出格式:<base> ->  版本号 (head), initial migration

回滚到指定版本

python manage.py db downgrade # 默认返回上一个版本
python manage.py db downgrade 版本号   # 返回到指定版本号对应的版本

数据迁移的步骤

1. 初始化数据迁移的目录【项目最开始的时候需要执行,后面不需要了。】
python manage.py db init

2. 数据库的数据迁移版本的创建和添加版本的描述
python manage.py db migrate -m "initial migration"

3. 升级版本,把migrate命令记录到模型相关修改操作同步到数据库中。
python manage.py db upgrade 

4. 降级版本,把upgrade执行的操作进行还原/恢复/删除
python manage.py db downgrade

5、数据迁移中回滚的bug

flask-migrate扩展包在对于复杂的数据表关系时,生成的迁移文件有时候会出现紊乱.在迁移文件中,原来要创建或者删除的数据表信息会丢失,变成None,这种迁移文件有可能无法执行upgrade或者downgrade,所以每次如果遇到数据表的操作存在1对多或者多对多的情况下,最好检查下migrations/versions版本目录下的新生成迁移文件中的代码.

有bug的代码:

"""change achievement table's field sid and cid

Revision ID: 0eb86bbc17dd
Revises: 7aa27c78e0fd
Create Date: 2020-11-03 15:27:55.563944

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = '0eb86bbc17dd'
down_revision = '7aa27c78e0fd'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('tb_achievement', sa.Column('course_id', sa.Integer(), nullable=True, comment='课程ID'))
    op.add_column('tb_achievement', sa.Column('student_id', sa.Integer(), nullable=True, comment='学生ID'))
    op.drop_constraint('tb_achievement_ibfk_1', 'tb_achievement', type_='foreignkey')
    op.drop_constraint('tb_achievement_ibfk_2', 'tb_achievement', type_='foreignkey')
    op.create_foreign_key(None, 'tb_achievement', 'tb_course', ['course_id'], ['id'])
    op.create_foreign_key(None, 'tb_achievement', 'tb_student', ['student_id'], ['id'])
    op.drop_column('tb_achievement', 'sid')
    op.drop_column('tb_achievement', 'cid')
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('tb_achievement', sa.Column('cid', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True, comment='课程ID'))
    op.add_column('tb_achievement', sa.Column('sid', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True, comment='学生ID'))
    op.drop_constraint(None, 'tb_achievement', type_='foreignkey')
    op.drop_constraint(None, 'tb_achievement', type_='foreignkey')
    op.create_foreign_key('tb_achievement_ibfk_2', 'tb_achievement', 'tb_student', ['sid'], ['id'])
    op.create_foreign_key('tb_achievement_ibfk_1', 'tb_achievement', 'tb_course', ['cid'], ['id'])
    op.drop_column('tb_achievement', 'student_id')
    op.drop_column('tb_achievement', 'course_id')
    # ### end Alembic commands ###

调整如下:

"""change achievement table's field sid and cid

Revision ID: 0eb86bbc17dd
Revises: 7aa27c78e0fd
Create Date: 2020-11-03 15:27:55.563944

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision = '0eb86bbc17dd'
down_revision = '7aa27c78e0fd'
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('tb_achievement', sa.Column('course_id', sa.Integer(), nullable=True, comment='课程ID'))
    op.add_column('tb_achievement', sa.Column('student_id', sa.Integer(), nullable=True, comment='学生ID'))
    op.drop_constraint('tb_achievement_ibfk_1', 'tb_achievement', type_='foreignkey')
    op.drop_constraint('tb_achievement_ibfk_2', 'tb_achievement', type_='foreignkey')
    op.create_foreign_key('tb_achievement_ibfk_3', 'tb_achievement', 'tb_course', ['course_id'], ['id'])
    op.create_foreign_key('tb_achievement_ibfk_4', 'tb_achievement', 'tb_student', ['student_id'], ['id'])
    op.drop_column('tb_achievement', 'sid')
    op.drop_column('tb_achievement', 'cid')
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.add_column('tb_achievement', sa.Column('cid', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True, comment='课程ID'))
    op.add_column('tb_achievement', sa.Column('sid', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True, comment='学生ID'))
    op.drop_constraint('tb_achievement_ibfk_3', 'tb_achievement', type_='foreignkey')
    op.drop_constraint('tb_achievement_ibfk_4', 'tb_achievement', type_='foreignkey')
    op.create_foreign_key('tb_achievement_ibfk_2', 'tb_achievement', 'tb_student', ['sid'], ['id'])
    op.create_foreign_key('tb_achievement_ibfk_1', 'tb_achievement', 'tb_course', ['cid'], ['id'])
    op.drop_column('tb_achievement', 'student_id')
    op.drop_column('tb_achievement', 'course_id')
    # ### end Alembic commands ###

6、模块推荐:Faker生成仿真测试数据

文档

批量生成测试数据

pip install faker -i https://pypi.doubanio.com/simple

comands.py代码:

from flask_script import Command
from models2 import Student,db
from faker import Faker
import random
class FakerCommand(Command):
    """数据种子生成器"""
    def run(self):
        data = []
        faker = Faker(locale="zh_CN")
        for _ in range(300):
            sex = bool(random.randint(0,1))
            data.append(Student(
                name=faker.last_name()+(faker.first_name_male() if sex else faker.first_name_female()),
                sex=sex,
                age=random.randint(12,30),
                email=faker.company_email(),
                money=random.randint(1,20) * 100 + 100
            ))

        db.session.add_all(data)
        db.session.commit()
# run.py 代码块
from flask import Flask,request,jsonify,render_template
from config import Config
from models2 import db,Student,Course,Teacher,StudentInfo

# 初始化
app = Flask(import_name=__name__,template_folder='templates')
app.config.from_object(Config)
db.init_app(app)  # 初始化数据库链接

from flask_script import Manager
manager = Manager(app)
from flask_migrate import Migrate,MigrateCommand
#第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
migrate = Migrate(app,db)
#manager是Flask-Script的实例,这条语句在flask-Script中添加一个db命令
manager.add_command('db',MigrateCommand)

from comands import FakerCommand
manager.add_command("faker",FakerCommand)
@app.route(rule='/')
def index():
    
    return "ok"
if __name__ == '__main__':
    # 运行flask
    manager.run()

九、flask-session

允许设置session到指定存储的空间中,安装命令

pip install flask-Session -i https://pypi.douban.com/simple

使用session之前,必须配置一下配置项

SECRET_KEY = "*(%#4sxcz(^(#$#8423" # session秘钥

1、resdis保存session的基本配置

先安装配置flask-redis模块到项目中

pip install flask-redis -i https://pypi.douban.com/simple

run.py注册redis模块

# config.py 代码块:
# 声明和加载配置
class Config():
    DEBUG = True
    # 数据库链接配置 = 数据库名称://登录账号:登录密码@数据库主机IP:数据库访问端口/数据库名称?charset=编码类型
    SQLALCHEMY_DATABASE_URI = "mysql://root:123@127.0.0.1:3306/students?charset=utf8"
    # 动态追踪修改设置,如未设置只会提示警告
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    # 显示原始SQL语句
    SQLALCHEMY_ECHO = False
    # 调整json数据转换中文的配置
    JSON_AS_ASCII=False
    # session秘钥
    SECRET_KEY = "L>PG#5394&()%#34"
    # redis的链接配置
    # REDIS_URL = "redis://:密码@IP地址:端口/数据库下标"
    REDIS_URL = "redis://@127.0.0.1:6379/0"
# run.py
from flask import Flask,request,jsonify,render_template
from config import Config
from models2 import db,Student,Course,Teacher,StudentInfo

# 初始化
app = Flask(import_name=__name__,template_folder='templates')
app.config.from_object(Config)

# ...

# redis初始化
redis.init_app(app)

@app.route(rule='/')
def index():

    return "ok"
if __name__ == '__main__':
    # 运行flask
    manager.run()

2、把session配置到redis中进行保存

config.py 配置文件信息:

from flask_redis import FlaskRedis
redis = FlaskRedis()

# 声明和加载配置
class Config():
    DEBUG = True
    # 数据库链接配置 = 数据库名称://登录账号:登录密码@数据库主机IP:数据库访问端口/数据库名称?charset=编码类型
    SQLALCHEMY_DATABASE_URI = "mysql://root:123@127.0.0.1:3306/students?charset=utf8"
    # 动态追踪修改设置,如未设置只会提示警告
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    # 显示原始SQL语句
    SQLALCHEMY_ECHO = False
    # 调整json数据转换中文的配置
    JSON_AS_ASCII=False
    # session秘钥
    SECRET_KEY = "L>PG#5394&()%#34"
    # redis的链接配置
    # REDIS_URL = "redis://:密码@IP地址:端口/数据库下标"
    REDIS_URL = "redis://@127.0.0.1:6379/0"

    # session存储方式为redis
    SESSION_TYPE = "redis"
    # 如果设置session的生命周期是否是会话期, 为True,则关闭浏览器session就失效
    SESSION_PERMANENT = False
    # 是否对发送到浏览器上session的cookie值进行加密
    SESSION_USE_SIGNER = False
    # 保存到redis的session数的名称前缀
    SESSION_KEY_PREFIX = "session:"
    # session保存数据到redis时启用的链接对象
    SESSION_REDIS = redis  # 用于连接redis的配置
# run.py 代码块
from flask import Flask,session
from config import Config,redis
from models2 import db,Student,Course,Teacher,StudentInfo

# 初始化
app = Flask(import_name=__name__,template_folder='templates')
app.config.from_object(Config)
db.init_app(app)  # 初始化数据库链接

from flask_script import Manager
manager = Manager(app)
from flask_migrate import Migrate,MigrateCommand
#第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
migrate = Migrate(app,db)
#manager是Flask-Script的实例,这条语句在flask-Script中添加一个db命令
manager.add_command('db',MigrateCommand)

from comands import FakerCommand
manager.add_command("faker",FakerCommand)

from flask_session import Session
Session(app)

redis.init_app(app)


@app.route(rule='/')
def index():
    session["uname"] = "hehehe"
    return "ok"

@app.route(rule="/get_session")
def get_session():
    print(session.get("uname"))
    return "ok"

if __name__ == '__main__':
    # 运行flask
    manager.run()

3、SQLAlchemy存储session的基本配置

需要手动创建session表,在项目第一次启动的时候,使用db.create_all()来完成创建。

config.py,代码块:

from flask_redis import FlaskRedis
redis = FlaskRedis()
from models2 import db
# 声明和加载配置
class Config():
    DEBUG = True
    # 数据库链接配置 = 数据库名称://登录账号:登录密码@数据库主机IP:数据库访问端口/数据库名称?charset=编码类型
    SQLALCHEMY_DATABASE_URI = "mysql://root:123@127.0.0.1:3306/students?charset=utf8"
    # 动态追踪修改设置,如未设置只会提示警告
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    # 显示原始SQL语句
    SQLALCHEMY_ECHO = False
    # 调整json数据转换中文的配置
    JSON_AS_ASCII=False
    # session秘钥
    SECRET_KEY = "L>PG#5394&()%#34"
    # redis的链接配置
    # REDIS_URL = "redis://:密码@IP地址:端口/数据库下标"
    REDIS_URL = "redis://@127.0.0.1:6379/0"

    """session存储方式为redis"""
    # SESSION_TYPE = "redis"
    # # 如果设置session的生命周期是否是会话期, 为True,则关闭浏览器session就失效
    # SESSION_PERMANENT = False
    # # 是否对发送到浏览器上session的cookie值进行加密
    # SESSION_USE_SIGNER = False
    # # 保存到redis的session数的名称前缀
    # SESSION_KEY_PREFIX = "session:"
    # # session保存数据到redis时启用的链接对象
    # SESSION_REDIS = redis  # 用于连接redis的配置

    """session存储方式为SQLAlchemy"""
    # session类型为sqlalchemy
    SESSION_TYPE = "sqlalchemy"
    # SQLAlchemy数据库链接对象
    SESSION_SQLALCHEMY = db
    # session要保存的表名称
    SESSION_SQLALCHEMY_TABLE = "tb_session"
    # 如果设置为True,则关闭浏览器session就失效
    SESSION_PERMANENT = "True"
    # 对发送到浏览器上的session_id进行加密
    SESSION_USE_SIGNER=True
    # 保存到session中的值的前缀
    SESSION_KEY_PREFIX = "session:"

run.py 代码块:

from flask import Flask,session
from config import Config,redis
from models2 import db,Student,Course,Teacher,StudentInfo

# 初始化
app = Flask(import_name=__name__,template_folder='templates')
app.config.from_object(Config)
db.init_app(app)  # 初始化数据库链接

from flask_script import Manager
manager = Manager(app)
from flask_migrate import Migrate,MigrateCommand
#第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
migrate = Migrate(app,db)
#manager是Flask-Script的实例,这条语句在flask-Script中添加一个db命令
manager.add_command('db',MigrateCommand)

from comands import FakerCommand
manager.add_command("faker",FakerCommand)

from flask_session import Session
Session(app)

redis.init_app(app)

@app.route(rule='/')
def index():
    session["uname"] = "hehehe"
    return "ok"

@app.route(rule="/get_session")
def get_session():
    print(session.get("uname"))
    return "ok"

if __name__ == '__main__':
    # 运行flask
    manager.run()

十、蓝图 Blueprint

1、模块化

随着flask程序越来越复杂,我们需要对程序进行模块化的处理,针对一个简单的Flask程序进行模块化处理

简单来说,Blueprint是一个存储视图方法的容器,这些操作在这个Blueprint被注册到一个应用之后就可以被调用,Flask可以通过Blueprint来组织URL以及处理请求。

Flask使用Blueprint让应用实现模块化,在Flask中,Blueorint具有如下属性:

  • 一个项目可以具有多个Blueprint
  • 可以将一个Blueprin注册到任何一个未使用的URL下比如“/”、“/sample”或者子域名
  • 在一个应用中,一个模块可以注册多次
  • Blueorint可以单独具有自己的模板、静态文件或者其它的通用操作方法,它并不是必须要实现应的视图和函数的
  • 在一个应用初始化时,就应该要注册需要使用的Blueprint

但是一个Blueprint并不是一个完整的应用,它不能独立于应用运行,而必须要注册到某一个应用中。

Blueprint对象用起来和一个应用/Flask对象差不多,最大的区别在于一个蓝图对象没有办法独立运行,必须将它注册到一个应用对象上才能生效

使用蓝图可以分为四个步骤:

  1. 创建一个蓝图的包,例如home,并在__init__.py文件中创建蓝图对象

    from flask import Blueprint
    home = Blueprint('home',__name__)
    
  2. 在这个蓝图目录下,创建views.py文件,保存当前蓝图使用的视图函数

    def index():
        return "ok"
    
  3. home/__init__.py中引入views.py中所有的视图函数和给视图匹配路由

    from flask import Blueprint
    home_blu = Blueprint('home',__name__)
    
    from . import views
    home_blu.add_url_rule("/index",endpoint="index", view_func=views.index) # 子路由
    
  4. 在主程序run.py文件中的app对象上注册这个home蓝图对象

    # 注册蓝图
    from home import home_blu
    app.register_blueprint(home_blu,url_prefix='/home') # url_prefix='' 总路由
    

当这个应用启动过后,通过http://127.0.0.1:5000/home/index可以访问到蓝图中定义的视图函数

2、运行机制

  • 蓝图是保存了一组将来可以在应用app对象上执行的操作,注册路由就是一种操作
  • 当在app对象上调用route装饰器注册路由时,这个操作将修改对象的url_map路由表
  • 然而,蓝图对象根本没有路由表,当我们在蓝图对象上调用route装饰器注册路由时,它只是在内部的一个延迟操作记录列表defered_functions中添加了一个项
  • 当执行了app对象的register_blueprint()方法时,app应用对象将从蓝图对象的defered_functions列表中取出每一项,并以自身作为参数执行该匿名函数,即调用app应用对象的add_url_rule()方法,将蓝图的路由信息全部注册到应用对象app的url_map路由表

3、蓝图的url前缀

  • 当我们在应用对象上注册一个蓝图时,可以指定一个url_prefix关键字参数(这个参数默认是/)
  • 在应用最终的路由表url_map中,在蓝图上注册的路由URL自动被加上了这个前缀,这个可以保证在多个蓝图中使用相同的URL规则而不会最终引起冲突,只要在注册蓝图时将不同的蓝图挂接到不同的子路径即可
  • 有了蓝图以后,url_for在使用时,如果要生成一个蓝图里面的视图对应的路由地址,则需要声明当前蓝图名称.视图名称
print(url_for("home.index"))

4、注册蓝图中的静态文件的相关路由

和app应用对象不同,蓝图对象创建时不会默认注册静态目录的路由。需要我们在 创建时指定 static_folder 参数。

下面的示例将蓝图所在目录下的static_home目录设置为静态目录

# home/__init__.py,代码:
from flask import Blueprint
home_blu = Blueprint('home',__name__,static_folder="static_home")

from . import views
home_blu.add_url_rule("/index",endpoint="index", view_func=views.index)

# 主程序文件 run.py,代码:
# 注册蓝图
from home import home_blu
app.register_blueprint(home_blu,url_prefix='/home')

现在就可以使用/home/static_home/ 访问static_home目录下的静态文件了 定制静态目录URL规则 :可以在创建蓝图对象时使用 static_url_path 来改变静态目录的路由。

下面的示例将为 static_home 文件夹的路由设置为 /lib

from flask import Blueprint
# static_url_path="/lib" 
# 设置静态文件访问的子路由,如果不设置,则默认蓝图的静态文件子路由是static_folder的值
# 静态文件的访问路径规则: http://127.0.0.1:5000/总路由/子路由/文件名
home_blu = Blueprint('home',__name__,static_folder="static_home",static_url_path="/lib")

from . import views
home_blu.add_url_rule("/index",endpoint="index", view_func=views.index)

有了蓝图的静态目录以后,并不会影响原来项目中提供的static总静态文件目录的使用。

5、设置蓝图中模板的目录

蓝图对象默认的模板目录为系统的模版目录,可以在创建蓝图对象时使用 template_folder 关键字参数设置模板目录

创建蓝图中的模板目录template_home:

from flask import Blueprint
# static_url_path="/lib" 设置静态文件访问的子路由,如果不设置,则默认蓝图的静态文件子路由是static_folder的值
# 静态文件的访问路径规则: http://127.0.0.1:5000/总路由/子路由/文件名
home_blu = Blueprint('home',__name__,
                     static_folder="static_home",
                     static_url_path="/lib",
                     template_folder="template_home")

from . import views
home_blu.add_url_rule("/index",endpoint="index", view_func=views.index)

注:如果在 app应用对象中的templates 中存在和 蓝图中存在的templates_home 有同名模板文件时, 则系统会优先使用 templates 中的文件。所以要么不要使用app应用的templates,要么,就app应用对象的templates只保存特殊模板,名字与蓝图中的进行明显区分。

Message

message是一个基于Session实现的用于保存数据的集合,其特点是:使用一次就删除

from flask import Flask, flash, render_template, request,get_flashed_messages
app = Flask(__name__)


@app.route('/')
def index1():
    # 12. 获取消息
    v = get_flashed_messages()
    print(v)
    return render_template('s4.html')


@app.route('/set')
def index2():
    v = request.args.get('p')
    # 13. 设置消息
    flash('kkkk')
    return 'ok'

if __name__ == "__main__":
    app.run()
posted @ 2020-11-04 08:18  HashFlag  阅读(221)  评论(0编辑  收藏  举报