Web开发中,一个重要的组成部分便是数据库了。Web程序中最常用的莫过于关系型数据库了,也称SQL数据库。另外,文档数据库(如 mongodb)、键值对数据库(如redis)近几年也逐渐在 web开发中流行起来,我们习惯把这两种数据库称为NoSQL数据库。
大多数的关系型数据库引擎(比如MySQL、Postgres和SQLite)都有对应的Python包。在这里,我们不直接使用这些数据库引擎提供的 Python包,而是使用对象关系映射(Object-Relational Mapper,ORM)框架,它将低层的数据库操作指令抽象成高层的面向对象操作。也就是说,如果我们直接使用数据库引擎,我们就要写SQL操作语句,但是,如果我们使用了ORM框架,我们对诸如表、文档此类的数据库实体就可以简化成对Python对象的操作。Python中最广泛使用的ORM框架是SQLAlchemy,它是一个很强大的关系型数据库框架,不仅支持高层的ORM,也支持使用低层的SQL操作,另外,它也支持多种数据库引擎,如MySQL、Postgres和SQLite等。
在Flask中,为了简化配置和操作,我们使用的ORM框架是Flask-SQLAlchemy,这个Flask扩展封装了SQLAlchemy框架。在Flask-SQLAlchemy中,数据库使用URL指定,下表列出了常见的数据库引擎和对应的 URL。
首先,我们使用pip安装Flask-SQLAlchemy,安装在你所需要的环境中,本作者使用的是虚拟环境,虚拟环境能比较好的解决依赖问题,
pip install flask-sqlalchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql+pymysql://root:[email protected]/TodoProject"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
# app.config['SECRET_KEY'] = 'westos'
app.config['SECRET_KEY'] = os.urandom(24)
Bootstrap(app)
# 实例化db对象
db = SQLAlchemy(app)
这里有几点需要注意的是:
1、app应用配置项SQLALCHEMY_DATABASE_URI指定了SQLAlchemy所要操作的数据库,这里我们使用的是mysql数据库,引号里是你需要链接的数据库://用户名和密码@本机localhost或远程链接某个数据库,/后面是你要链接数据库的名称;这里特别说一点就是数据库的编码格式问题,当你在创建数据库时可以使用下面这个命令来解决你的编码问题;
create database 数据库名称 default charset utf8;
'SQLALCHEMY_DATABASE_URI' =====》链接 数据库;
'SQLALCHEMY_TRACK_MODIFICATIONS' ======》Ture
'SECRET_KEY' =====》加密
2、db 对象是 SQLAlchemy 类的实例,表示程序使用的数据库。
3、我们定义的User模型必须继承自db.Model,这里的模型其实就对应着数据库中的表。其中,类变量 __tablename__ 定义了在数据库中使用的表名,如果该变量没有被定义,Flask-SQLAlchemy会使用一个默认名字。
在这里我们的建表有了突破性的进展,前面我们一直说mysql是一个关系型数据库,但是没有体现出“关系”二字,在这里我们就要建立有关系的数据表;
我们设想这样的一个场景:我们在公司的内部系统上每一天都会有任务发布,有任务本身和它相关的执行部门,他们两个关系在任务产生的时候就被绑定在了一起,这样也就完成了我们所说的关系,那么我们在数据库中怎样建立数据表之间的关系呢?
外键约束是在两张表中建立的。两张表存在父子关系,即:子表中的某个字段的取值有父表决定的。
结论:在一对多关系中额,外键建立在多的一方
例如:简单的例子在很多的电影网站上都有会员,超级会员,普通会员这三种会员身份,我们需要对普通用户的身份进行关联
# 创建user表
class Users(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
# unique: 指定该列信息是唯一的;
name = db.Column(db.String(50), unique=True)
passwd = db.Column(db.String(50))
# default是默认值l
add_time = db.Column(db.DateTime, default=datetime.now())
role_id = db.Column(db.Integer, db.ForeignKey('role.id')) #和哪张表的哪个表头进行关联
# 创建用户角色(超级会员, 会员, 普通会员)
class Role(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
name = db.Column(db.String(50), unique=True)
# 'Users代表的是Role表关联的数据库表'
# backref=“role”: 反向引用;让Users中有role属性, user.role返回的是role对象。
users = db.relationship('Users', backref='role') #上表和这里的进行相关联
#
# def __repr__(self):
# """如果查询数据表内容, 需要特色化设置时, 使用__repr__"""
# return "" %(self.name)
# 1. 创建表
# db.create_all()
下面的是我们更为复杂的例子:
class Todo(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
# unique: 指定该列信息是唯一的;
name = db.Column(db.String(50))
# default是默认值l
add_time = db.Column(db.DateTime, default=datetime.now())
status = db.Column(db.Boolean, default=False)
department_id = db.Column(db.Integer, db.ForeignKey('department.id'))
class Department(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
name = db.Column(db.String(50), unique=True)
todos = db.relationship('Todo', backref='department')
users = db.relationship('User', backref='department')
class User(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
# unique: 指定该列信息是唯一的;
name = db.Column(db.String(50), unique=True)
# 此处为了用户帐号的安全性, 必须对密码进行加密;
pwd = db.Column(db.String(100))
email = db.Column(db.String(20), unique=True)
phone = db.Column(db.String(20), unique=True)
info = db.Column(db.Text) # 个性简介
addtime = db.Column(db.DateTime, default=datetime.now())
department_id = db.Column(db.Integer, db.ForeignKey('department.id'))
userlogs = db.relationship('UserLog', backref="user")
def check_pwd(self, pwd):
return check_password_hash(self.pwd, 'westos1')
# 用户登录日志
class UserLog(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
ip = db.Column(db.String(100)) # 登录的IP
addtime = db.Column(db.DateTime, default=datetime.now())
area = db.Column(db.String(100)) # 登录的城市
1、建表
# 1). 创建表
# db.create_all()
2、给表内添加信息
2). 初始化数据
parts = ['开发部', '运维部', '人事部']
partObj = [Department(name=part) for part in parts]
db.session.add_all(partObj)
db.session.commit()
todos = ['打扫卫生', '开发微电影管理系统', '招聘人才']
todoObj = [Todo(name=todo, department_id=random.choice([1,2,3]) ) for todo in todos]
db.session.add_all(todoObj)
db.session.commit()
3、添加用户的测试数据
u1 = User(name="westos1", pwd=generate_password_hash('westos'), department_id=1)
u2 = User(name="westos2", pwd=generate_password_hash('westos'), department_id=2)
u3 = User(name="westos3", pwd=generate_password_hash('westos'), department_id=1)
db.session.add_all([u1, u2, u3])
db.session.commit()
注意:这里我们用到了哈希加密
什么是哈希加密:HASH主要用于信息安全领域中加密算法,他把一些不同长度的信息转化成杂乱的128位的编码里,叫做HASH值. 也可以说,hash就是找到一种数据内容和数据存放地址之间的映射关系
这里我们用到的加密模块为:
from werkzeug.security import generate_password_hash
相对应的也就有解密模块:
from werkzeug.security import check_password_hash
解密的时候我们需要两个参数:分别是哈希加密后的一串代码,再其次是我们的明文密码,返回值为Ture 或 False
# 查询所有的角色;
print(Role.query.all())
1、 如何显示会员类型与该会员类型包含的所有用户;
roles为对象。对象对应的属性我们都可以看到
roles = Role.query.all()
for role in roles:
print(role.id, role.name,role.users)
2、显示所有的用户以及用户类别
users = Users.query.all()
for user in users:
print(user.id, user.name, user.passwd, user.role)
3、根据条件查找符和条件的数据和更新数据;(filter_by(常用), filter)
# 1). 找到所有超级会员的用户;
users = Users.query.filter_by(role_id=1).all()
# 2). 更新所有超级会员的密码为0000000;
for user in users:
# print(user.passwd)
user.passwd = "000000"
print("更新用户%s的密码成功!" %(user.name))
db.session.add(user)
db.session.commit()
4、其他的查询信息显示限制条件
users = Users.query.filter_by(role_id=1).limit(10).all()
print(users, len(users))
5、排序: 根据用户创建的时间进行排序(默认情况正序, 如果要逆序desc())
users = Users.query.order_by(desc(Users.add_time)).all()
for u in users[:10]:
print(u.add_time)
6、limit可以限制查询数据的数量;
users =Users.query.order_by(desc(Users.add_time)).limit(5).all()
print(users)
7、offset: 偏移量, 可以设置查询偏移量,也就是数据的起始位置, 一般与limit结合使用;
print(Users.query.order_by(desc(Users.add_time)).limit(5).offset(2).all())
8、slice: 切片
print(Users.query.order_by(desc(Users.add_time)).slice(1,5).all())
9、分页:第一个参数为页码,第二个参数为每一页显示的内容
page_u = Users.query.paginate(1, 3)
print(page_u.items)
print(dir(page_u))
page_u = Users.query.paginate(2, 3)
print(page_u.items)
10、用户数据更新;
u = Users.query.filter_by(id=1).first()
print(u.id, u.name, u.add_time)
u.name = "西部开源1"
db.session.add(u)
db.session.commit()
u = Users.query.filter_by(id=1).first()
print(u.id, u.name, u.add_time)
11、用户信息删除;
u = Users.query.filter_by(id=1).first()
db.session.delete(u)
db.session.commit()
u = Users.query.filter_by(id=1).first()
print(u.id, u.name, u.add_time)
Bootstrap是Twitter开源的一个CSS/HTML框架,它让Web开发变得更加迅速,简单。要想在我们的Flask应用中使用Boostrap,有两种方案可供选择:
第1种,在我们的Jinja2模板中直接引入Bootstrap层叠样式表(CSS)和JavaScript文件,比如bootstrap.min.css,bootstrap.min.js;
第2种,也是更简单的方法,就是使用一个 Flask-Bootstrap 的扩展,它简化了集成Bootstrap 的过程;
pip install flask-bootstrap
from werkzeug.security import check_password_hash
from models import app, Todo, db, Department, User, UserLog
from flask import render_template, redirect, url_for, request, flash, session
from functools import wraps
from urllib.request import urlopen
import json
# 通过ip获取该IP的所在城市和国家;
def get_ip_area(ip):
# 构造url地址, 使用淘宝的API接口
url = 'http://ip.taobao.com/service/getIpInfo.php?ip=%s' %(ip)
# 获取页面返回的内容()
json_data = urlopen(url).read().decode('utf-8')
# 将json格式的数据转换为字典格式;
s_data = json.loads(json_data)
country = s_data['data']['country']
if country == 'XX':
country = ''
city = s_data['data']['city']
if city == 'XX':
city = ''
return country+city
# 编写一个判断用户是否登录的装饰器
def is_login(f):
@wraps(f)
def wrapper(*args, **kwargs):
# 判断不在session(会话)中, 跳转到登录页面;
if not "user" in session:
return redirect(url_for('login'))
return f(*args, **kwargs)
return wrapper
@app.route('/list//')
@app.route('/list/')
@is_login
def list(page=1):
todos = Todo.query.paginate(page, per_page=5)
parts = Department.query.all()
return render_template('list.html', todos=todos, parts=parts)
@app.route('/done//')
@is_login
def done(todo_id):
todo = Todo.query.filter_by(id=todo_id).first()
todo.status = True
try:
db.session.add(todo)
db.session.commit()
except Exception as e:
db.session.rollback()
return e
return redirect(url_for('list', page=1))
@app.route('/undone//')
@is_login
def undone(todo_id):
todo = Todo.query.filter_by(id=todo_id).first()
todo.status = False
try:
db.session.add(todo)
db.session.commit()
except Exception as e:
db.session.rollback()
return e
return redirect(url_for('list', page=1))
@app.route('/delete///')
@is_login
def delete(todo_id, page=1):
u = Todo.query.filter_by(id=todo_id).first()
db.session.delete(u)
db.session.commit()
return redirect(url_for('list', page=page))
@app.route('/add/', methods=['POST'])
@is_login
def add():
# 通过request获取 表单提交的内容;
data = request.form
name = data['todo_name']
part = data['part']
# 实例化对象
todo = Todo(name=name, department_id=part)
db.session.add(todo)
db.session.commit()
return redirect(url_for('list'))
@app.route('/login/', methods=['POST', 'GET'])
def login():
# 判断用户的HTTP请求方法
if request.method == 'POST':
# 1). 接收提交的数据;
data = request.form
name = data['name']
pwd = data['pwd']
# 2). 判断用户名是否存在, 密码是否正确?
u = User.query.filter_by(name=name).first()
if u and check_password_hash(u.pwd, pwd):
session['user'] = u.name
session['user_id'] = u.id
userlog = UserLog(
user_id=u.id,
ip=request.remote_addr,
area=get_ip_area(request.remote_addr)
)
db.session.add(userlog)
db.session.commit()
return redirect(url_for('list'))
else:
# flash: 消息闪现,
# 如何在前端html页面显示? Jinja2 get_flash_messages()
# 返回的是一个列表, 想依次显示内容, 需要用到for循环;
flash('用户或者密码不正确!')
return redirect(url_for('login'))
else:
return render_template('login.html')
@app.route('/logout/')
def logout():
# 将会话中的key值弹出;
session.pop('user', None)
session.pop('user_id', None)
# 注销跳转到登录页面, 或者公共首页
return redirect(url_for('login'))
@app.route('/')
@app.route('/')
def index(page=1):
parts = Department.query.all()
untodos = Todo.query.filter_by(status=False).paginate(page, per_page=5)
return render_template('index.html', parts=parts, untodos=untodos)
#
#
# #
# @app.route('/add/', methods=['POST'])
# def add():
# form = request.form
# print(form)
# name = form['content']
# part = form['part']
# todo = Todo(name = name, department_id=part)
# db.session.add(todo)
# db.session.commit()
# todos = Todo.query.all()
# return redirect(url_for('list'))
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5008)
初始化Flask-Bootstrap之后,我们的模板通过继承一个基模板,即bootstrap/base.html ,就可以使用Bootstrap了。
新建一个templates文件夹,在该文件夹中添加一个base.html的文件,代码如下:
{% extends 'bootstrap/base.html' %}
{% block title %}
首页
{% endblock %}
{% block styles %}
{{ super() }}
{% endblock %}
{% block navbar %}
{% endblock %}
{% block content %}
{% endblock %}
index.html
{% extends 'base.html' %}
{% block content %}
部门
{% for part in parts %}
{{ part.name }}
{% endfor %}
任务类型
已完成
紧急任务
注意: 登录后进行操作
编号
任务内容
创建时间
状态
所属部门
删除
{% for todo in untodos.items %}
{{ todo.id }}
{{ todo.name }}
{{ todo.add_time }}
{# 有按钮 :
如果状态为True: 则返回完成的按钮;
#}
{% if todo.status %}
已完成
{% else %}
{# {{ url_for('done', todo_id=todo.id) }} ======== '/done/1/' #}
未完成
{% endif %}
{{ todo.department.name }}
删除
{% endfor %}
{# 分页信息#}
{% from 'macro/page.html' import paginate %}
{{ paginate('list', untodos) }}
{% endblock %}
list.html
{% extends 'base.html' %}
{% block title %}用户显示{% endblock %}
{% block logout %} 登出 {% endblock %}
{% block content %}
{#
#}
{#
#}
{#
#}
{# #}
任务显示
编号
任务内容
创建时间
状态
所属部门
删除
{% for todo in todos.items %}
{{ todo.id }}
{{ todo.name }}
{{ todo.add_time }}
{# 有按钮 :
如果状态为True: 则返回完成的按钮;
#}
{% if todo.status %}
已完成
{% else %}
{# {{ url_for('done', todo_id=todo.id) }} ======== '/done/1/' #}
未完成
{% endif %}
{{ todo.department.name }}
删除
{% endfor %}
{# 分页信息#}
{% from 'macro/page.html' import paginate %}
{{ paginate('list', todos) }}
{% endblock %}
login.html
{% extends 'base.html' %}
{% block title %}用户登录{% endblock %}
{% block content %}
{% endblock %}
上面的所有的页面都继承与base.html页面,而base.html就是继承于'bootstrap/base.html',
'bootstrap/base.html'就是官方为我们提供的模板,这个模板中有帮我们“挖的坑“我们只需要将其填补上就可以比如
{% extends 'bootstrap/base.html' %} 继承于哪里
{% block title %} 首页 {% endblock %} 浏览器上的头部信息
{% block content %}...... {% endblock %} 正文部分的样式
我们也可以自己刨坑和填坑前面的文章中我们也提到过相关内容:
模板继承语法: 1. 如何继承某个模板? {% extends "模板名称" %} 2. 如何挖坑和填坑? 挖坑: {% block 名称 %} 默认值 {% endblock %} 填坑: {% block 名称 %} {% endblock %} 3. 如何调用/继承被替代的模板? 挖坑: {% block 名称 %} 默认值 {% endblock %} 填坑: {% block 名称 %} #如何继承挖坑时的默认值? {{ super() }} # 后面写新加的方法. ........ {% endblock %}