Flask_0x04 数据库 & 电子邮件
0x1 数据库
1.1 Flask-SQLAlchemy
git checkout 5a
Python数据库框架可以直接处理Python对象,不用处理表、文档或查询语言等数据库实体
SQLAlcchemy是一个强大的关系型数据库框架,提供了高层ORM和使用原生SQL的底层功能
(venv) $ pip install flask-sqlalchemy
Flask-SQLAlchemy数据库URL
MySQL mysql://username:password@hostname/database
Postgres postgresql://username:password@hostname/database
SQLite(Unix) sqlite:////absolute/path/to/database
SQLite(Windows) sqlite:///c:/absolute/path/to/database
SQLite不需要使用服务器,因此不用指定hostname、username、password
Flask-SQLAlchemy要求每个模型都要定义主键,这一列经常命名为id
hello.py
from flask.ext.sqlalchemy import SQLAlchemy
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
db = SQLAlchemy(app)
程序使用的数据库URL必须保存到Flask配置对象的SQLALCHEMY_DATABASE_URI键中
SQLALCHEMY_COMMIT_ON_TEARDOWN设为True,每次请求结束后都会自动提交数据库中的变动
模型表示程序使用的持久化实体,ORM中,模型一般是一个Python类,类中属性对应数据库表中的列
定义模型:Flask-SQLAlchemy创建的数据库实例为模型提供了一个基类和一系列辅助类和辅助函数,用于定义模型的结构
hello.py:定义Role和User模型
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User',backref='role',lazy='dynamic')
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
__tablename__定义在数据库中使用的表名
__repr()__ 方法返回一个具有可读性的字符串表示模型,可在调试和测试时使用
添加到User模型中的role_id列被定义为外键,传给db.ForeignKey()的参数roles.id表明这列的值是roles表中行的id值
db.Column类构造函数的第一个参数是数据库列和模型属性的类型
最常用的SQLAlchemy列类型
类型名 Python类型 说 明
Integer int 普通整数,一般是 32 位
SmallInteger int 取值范围小的整数,一般是 16 位
BigInteger int 或 long 不限制精度的整数
Float float 浮点数
Numeric decimal.Decimal 定点数
String str 变长字符串
Text str 变长字符串,对较长或不限长度的字符串做了优化
Unicode unicode 变长 Unicode 字符串
UnicodeText unicode 变长 Unicode 字符串,对较长或不限长度的字符串做了优化
Boolean bool 布尔值
Date datetime.date 日期
Time datetime.time 时间
DateTime datetime.datetime 日期和时间
Interval datetime.timedelta 时间间隔
Enum str 一组字符串
PickleType 任何 Python 对象 自动使用 Pickle 序列化
LargeBinary str 二进制文件
常用的SQLAlchemy列选项
primary_key 如果设为 True ,这列就是表的主键
unique 如果设为 True ,这列不允许出现重复的值
index 如果设为 True ,为这列创建索引,提升查询效率
nullable 如果设为 True ,这列允许使用空值;如果设为 False ,这列不允许使用空值
default 为这列定义默认值
对于Role类的实例,其users属性将返回与角色相关联的用户组成的列表,db.relationship()第一个参数表明这个关系的另一端是哪个模型
常用SQLAlchemy关系选项
backref 在关系的另一个模型中添加反向引用
primaryjoin 明确指定两个模型之间使用的联结条件。只在模棱两可的关系中需要指定
lazy 指定如何加载相关记录。可选值有 select (首次访问时按需加载)、 immediate (源对象加
载后就加载)、 joined (加载记录,但使用联结)、 subquery (立即加载,但使用子查询),
noload (永不加载)和 dynamic (不加载记录,但提供加载记录的查询)
1.2 数据库操作
git checkout 5b
创建表
(venv) $ python hello.py shell
>>> from hello import db
>>> db.create_all()
删除表
>>> db.drop_all()
插入行
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)
>>> user_susan = User(username='susan', role=user_role)
>>> user_david = User(username='david', role=user_role)
添加到会话
>>> db.session.add(admin_role)
>>> db.session.add(mod_role)
>>> db.session.add(user_role)
>>> db.session.add(user_john)
>>> db.session.add(user_susan)
>>> db.session.add(user_david)
简写
>>> db.session.add_all([admin_role, mod_role, user_role,
... user_john, user_susan, user_david])
把对象写入数据库,调用commit()方法提交会话
>>> db.session.commit()
修改行:把 "Admin" 角色重命名为 "Administrator"
>>> admin_role.name = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
删除行:把 "Moderator" 角色从数据库中删除
>>> db.session.delete(mod_role)
>>> db.session.commit()
查询行
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>]
>>> User.query.all()
[<User u'john'>, <User u'susan'>, <User u'david'>]
>>> User.query.filter_by(role=user_role).all()
[<User u'susan'>, <User u'david'>]
查看 SQLAlchemy 为查询生成的原生 SQL 查询语句,把 query 对象转换成字符串:
>>> str(User.query.filter_by(role=user_role))
起一个查询,加载名为"User"的用户角色
>>> user_role = Role.query.filter_by(name='User').first()
filter_by()过滤器在query对象上调用,返回更精确query对象
常用SQLAlchemy过滤器
完整SQLAlchemy文档:http://docs.sqlalchemy.org/en/latest/
filter() 把过滤器添加到原查询上,返回一个新查询 filter_by() 把等值过滤器添加到原查询上,返回一个新查询 limit() 使用指定的值限制原查询返回的结果数量,返回一个新查询 offset() 偏移原查询返回的结果,返回一个新查询 order_by() 根据指定条件对原查询结果进行排序,返回一个新查询 group_by() 根据指定条件对原查询结果进行分组,返回一个新查询
常用的SQLAlchemy查询执行函数
all() 以列表形式返回查询的所有结果
first() 返回查询的第一个结果,如果没有结果,则返回 None
first_or_404() 返回查询的第一个结果,如果没有结果,则终止请求,返回 404 错误响应
get() 返回指定主键对应的行,如果没有对应的行,则返回 None
get_or_404() 返回指定主键对应的行,如果没找到指定的主键,则终止请求,返回 404 错误响应
count() 返回查询结果的数量
paginate() 返回一个 Paginate 对象,它包含指定范围内的结果
lazy='dynamic' 禁止自动执行查询,user_role.users回返回一个尚未执行的查询,因此可以添加过滤器
>>> user_role.users.order_by(User.username).all()
[<User u'david'>, <User u'susan'>]
>>> user_role.users.count()
2
1.3 在视图函数中操作数据库
hello.py:在视图函数中操作数据库
@app.route('/', methods=['GET','POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
session['known'] = False
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'),know=session.get('know',False))
提交表单后,程序使用filter_by()在数据库中查找提交的名字
index.html使用known参数显示用户是否在数据库中
templates/index.html
{% if not known %}
<p>Pleased to meet you</p>
{% else %}
<p>Happy to see you again!</p>
{% endif %}
集成Python shell,让Flask-Script的shell命令自动导入特定的对象
git checkout 5c
hello.py:为shell命令添加一个上下文
from flask.ext.script import Shell
def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))
1.4 Flask-Migrate数据库迁移
git checkout 5d
更新表的更好方法是使用数据库迁移框架
数据库迁移框架能跟踪数据库模式的变化,然后增量式的把变化应用到数据库中
(venv) $ pip install flask-migrate
hello.py:配置Flask-Migrate
from flask.ext.migrate import Migrate, MigrateCommand
#...
migrate = Migrate(app, db)
manager.add_command('db',MigrateCommand)
用init子命令迁移仓库:
(venv) $ python hello.py db init
这个命令会创建一个migrations文件夹,所有迁移脚本都存放其中
migrate 子命令用来自动创建迁移脚本:
(venv) $ python hello.py db migrate -m "initial migration"
更新数据库
(venv) $ python hello.py db upgrade
0x2 电子邮件
2.1 Flask-Mail
git checkout 6a
(venv) $ pip install flask-mail
Flask-Mail SMTP服务器的配置
MAIL_SERVER localhost 电子邮件服务器的主机名或 IP 地址
MAIL_PORT 25 电子邮件服务器的端口
MAIL_USE_TLS False 启用传输层安全(Transport Layer Security,TLS)协议
MAIL_USE_SSL False 启用安全套接层(Secure Sockets Layer,SSL)协议
MAIL_USERNAME None 邮件账户的用户名
MAIL_PASSWORD None 邮件账户的密码
hello.py:配置Flask-Mail使用Gmail
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
hello.py:初始化Flask-Mail
from flask.ext.mail import Mail
mail = Mail(app)
环境中定义用户名密码
Linux:
(venv) $ export MAIL_USERNAME=<Gmail username>
(venv) $ export MAIL_PASSWORD=<Gmail password>
windows:
(venv) $ set MAIL_USERNAME=<Gmail username>
(venv) $ set MAIL_PASSWORD=<Gmail password>
Python shell中发送电子邮件
(venv) $ python hello.py shell
>>> from flask.ext.mail import Message
>>> from hello import mail
>>> msg = Message('test subject', sender='xxx@qq.com',
... recipients=['xxx@qq.com'])
>>> msg.body = 'text body'
>>> msg.html = '<b>HTML</b> body'
>>> with app.app_context():
... mail.send(msg)
...
2.2 程序中集成发电子邮件功能
git checkout 6a
避免每次手动编邮件信息,还可以使用Jinja2渲染邮件正文
from flask.ext.mail import Mail,Message
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = 'Flask'
app.config['FLASKY_MAIL_SENDER'] = 'xxx@gmail.com'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
def send_mail(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' '+subject, sender = app.config['FLASKY_MAIL_SENDER'],
recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail.send(msg)
这个函数定义了邮件主题前缀和发件人地址
电子邮件收件人保存在FLASKY_ADMIN环境变量中
email函数参数分别为收件人地址、主题、渲染邮件正文的模板和关键字参数列表
指定模板时不能包含扩展名,这样才能使用两个模板分别渲染纯文本正文和富文本正文
index()要实现功能:表单提交数据库没有的用户名,程序就会向管理员发送一封电子邮件
hello.py:电子邮件示例
@app.route('/', methods=['GET','POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
session['known'] = False
if app.config['FLASKY_ADMIN']:
send_mail(app.config['FLASKY_ADMIN'], 'New User', 'mail/new_user', user=user)
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'),know=session.get('know',False))
定义FLASKY_ADMIN环境变量
Linux:(venv) $ export FLASKY_ADMIN=<your-email-address>
Windows:(venv) $ set FLASKY_ADMIN=<Gmail username>
2.3 异步发送电子邮件
git checkout 6b
mail.send()函数在发送电子邮件是停滞了几秒,为避免请求过程中不必要的延迟,可以把发送电子邮件函数移到后台线程
hello.py:异步发送电子邮件
from threading import Thread
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_mail(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' '+subject, sender = app.config['FLASKY_MAIL_SENDER'],
recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
发送大量电子邮件时可以把执行send_async_email()函数操作发给Celery(http://www.celeryproject.org/)任务队列