新建项目,创建demo1_bookDemo.py文件
一般通过终端连接数据库,其实也可以通过pycharm连接数据库,pycharm最右侧找到Database,然后操作如下:
然后做如下配置:(第一次需要下载Driver驱动)
连接上去之后的效果:
接下来在pycharm中打开执行sql的命令行窗口,然后创建数据库:
创建之后的结果:(如果没有出现,点击刷新按钮:)
双击“booktest”数据库就相当于“use booktest;”命令
模型表示程序使用的数据实体,在Flask-SQLAlchemy中,模型一般是Python类,继承自db.Model,db是SQLAlchemy类的实例,代表程序使用的数据库。
类中的属性对应数据库表中的列。id为主键,是由Flask-SQLAlchemy管理。db.Column类构造函数的第一个参数是数据库列和模型属性类型。
注:如果没有在创建数据库的时候指定编码的话,向数据库中插入中文后,会报错,那么需要修改数据库的编码集:
alter database 数据库名 CHARACTER SET utf8
如下示例:定义了两个模型类,作者和书名。
from flask import Flask, render_template, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
# 设置连接数据
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/test2'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
# 实例化SQLAlchemy对象
db = SQLAlchemy(app)
# 定义模型类-作者
class Author(db.Model):
"""作者模型:1的一方"""
__tablename__ = 'authors'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), unique=True)
# 定义属性,以便作者模型可以通过该属性访问其多的一方的数据(书的数据)
# backref 给 Book 也添加了一个 author 的属性,可以通过 book.author 获取 book 所对应的作者信息
books = db.relationship("Book", backref="author")
# 定义模型类-书名
class Book(db.Model):
"""书的模型:多的一方"""
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
# 记录1的一方的 id 作为外键
au_book = db.Column(db.Integer, db.ForeignKey('author.id'))
添加测试数据:
if __name__ == '__main__':
# 删除所有的表
db.drop_all()
# 创建所有的表
db.create_all()
# 生成数据
au1 = Author(name='老王')
au2 = Author(name='老尹')
au3 = Author(name='老刘')
# 把数据提交给用户会话
db.session.add_all([au1, au2, au3])
# 提交会话
db.session.commit()
bk1 = Book(name='老王回忆录', author_id=au1.id)
bk2 = Book(name='我读书少,你别骗我', author_id=au1.id)
bk3 = Book(name='如何才能让自己更骚', author_id=au2.id)
bk4 = Book(name='怎样征服美丽少女', author_id=au3.id)
bk5 = Book(name='如何征服英俊少男', author_id=au3.id)
# 把数据提交给用户会话
db.session.add_all([bk1, bk2, bk3, bk4, bk5])
# 提交会话
db.session.commit()
app.run(debug=True)
运行之后,查看结果:(双击booktest,展开所有表)
双击books表,查看数据:
查看authors表数据:
新建templates模板文件夹,变为模板文件夹,并且设置模板语言:
新建模板文件:demo1_bookDemo.html
新建视图函数:
展示作者列表:
运行:
作者下边还应该显示对应图书:
效果如下:
创建表单对应的类:
实例化并且传递到模板:
模板中使用form来处理表单:
展示效果如下:
因为点击添加,还是提交到当前url
所以增加逻辑如下:
@app.route("/", methods=["GET", "POST"])
def index():
"""返回首页"""
book_form = AddBookForm()
# 如果数据可以提交(所有数据都已填好)
if book_form.validate_on_submit():
# 1. 提取表单中的数据
# WTF 表单专用
# author_name = book_form.author.data
# book_name = book_form.book.data
# 通用(推荐使用)
# author_name = request.form.get("author")
# book_name = request.form.get("book")
# 2. 做具体业务逻辑实现代码
# 2.1 查询指定名字的作者
author = Author.query.filter(Author.name == author_name).first()
# if 指定名字的作者不存在:
if not author:
# 添加作者信息到数据库
# 初始化作者的模型对象
author = Author(name=author_name)
db.session.add(author)
db.session.commit()
# 添加书籍信息到数据库(指定其作者)
book = Book(name=book_name, author_id=author.id)
db.session.add(book)
db.session.commit()
else:
book = Book.query.filter(Book.name == book_name).first()
if not book:
# 添加书籍信息到数据库(指定其作者)
book = Book(name=book_name, author_id=author.id)
db.session.add(book)
db.session.commit()
else:
flash("已存在")
else:
if request.method == "POST":
flash("参数错误")
# 1.查询数据
authors = Author.query.all()
# 2.将数据返回到模板中进行渲染返回
return render_template("demo1_bookDemo.html", authors=authors, form=book_form)
如果出错,设置了闪现消息,所以模板中需要显示闪现消息:
因为数据库操作可能会失败,所以增加try逻辑:
删除其实有俩逻辑:
1. 删除图书
2. 删除作者,同时删除作者下所有图书
增加删除图书逻辑如下:
@app.route("/delete_book/")
def delete_book(book_id):
"""删除书籍"""
try:
book = Book.query.get(book_id)
except Exception as e:
print(e)
return "查询错误"
if not book:
return "书籍不存在"
try:
db.session.delete(book)
db.session.commit()
except Exception as e:
print(e)
db.session.rollback()
flash("删除失败")
return redirect(url_for("index"))
模板中增加删除链接:
增加删除作者逻辑如下:
@app.route("/delete_author/")
def delete_author(author_id):
"""删除作者以及作者所有的书籍"""
try:
author = Author.query.get(author_id)
except Exception as e:
print(e)
return "查询错误"
if not author:
return "没有此作者"
# 删除作者及其所有书籍
try:
# 先删除书籍
Book.query.filter(Book.author_id==author.id).delete()
# 再删除指定作者
db.session.delete(author)
db.session.commit()
except Exception as e:
print(e)
flash("删除错误")
return redirect(url_for("index"))
代码说明:
Book.query.filter().delete():先拿到一个查询结果,然后直接delete,是对查询结果做整体删除
模板增加删除超链接:
# demo1_bookDemo.py
from flask import Flask, render_template, request, flash, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import InputRequired
app = Flask(__name__)
# 设置连接数据
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/booktest'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
# 实例化SQLAlchemy对象
db = SQLAlchemy(app)
app.secret_key = "apollo_miracle"
# 定义模型类-作者
class Author(db.Model):
"""作者模型:1的一方"""
__tablename__ = 'authors'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
# 定义属性,以便作者模型可以通过该属性访问其多的一方的数据(书的数据)
# backref 给 Book 也添加了一个 author 的属性,可以通过 book.author 获取 book 所对应的作者信息
books = db.relationship("Book", backref="author")
# 定义模型类-书名
class Book(db.Model):
"""书的模型:多的一方"""
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
# 记录1的一方的 id 作为外键
author_id = db.Column(db.Integer, db.ForeignKey(Author.id))
class AddBookForm(FlaskForm):
"""自定义添加书籍的表单"""
author = StringField("作者:", validators=[InputRequired("请输入作者姓名")])
book = StringField("书名:", validators=[InputRequired("请输入书名")])
submit = SubmitField("添加")
@app.route("/delete_author/")
def delete_author(author_id):
"""删除作者以及作者所有的书籍"""
try:
author = Author.query.get(author_id)
except Exception as e:
print(e)
return "查询错误"
if not author:
return "没有此作者"
# 删除作者及其所有书籍
try:
# 先删除书籍
Book.query.filter(Book.author_id == author.id).delete()
# 再删除指定作者
db.session.delete(author)
db.session.commit()
except Exception as e:
print(e)
flash("删除错误")
return redirect(url_for("index"))
@app.route("/delete_book/")
def delete_book(book_id):
"""删除书籍"""
try:
book = Book.query.get(book_id)
except Exception as e:
print(e)
return "查询错误"
if not book:
return "书籍不存在"
try:
db.session.delete(book)
db.session.commit()
except Exception as e:
print(e)
db.session.rollback()
flash("删除失败")
return redirect(url_for("index"))
@app.route("/", methods=["GET", "POST"])
def index():
"""返回首页"""
book_form = AddBookForm()
# 如果数据可以提交(所有数据都已填好)
if book_form.validate_on_submit():
# 1. 提取表单中的数据
# WTF 表单专用
author_name = book_form.author.data
book_name = book_form.book.data
# 通用(推荐使用)
# author_name = request.form.get("author")
# book_name = request.form.get("book")
# 2. 做具体业务逻辑实现代码
# 2.1 查询指定名字的作者
author = Author.query.filter(Author.name == author_name).first()
# if 指定名字的作者不存在:
if not author:
try:
# 添加作者信息到数据库
# 初始化作者的模型对象
author = Author(name=author_name)
db.session.add(author)
db.session.commit()
# 添加书籍信息到数据库(指定其作者)
book = Book(name=book_name, author_id=author.id)
db.session.add(book)
db.session.commit()
except Exception as error:
db.session.rollback()
print(error)
flash("添加失败")
else:
book = Book.query.filter(Book.name == book_name).first()
if not book:
try:
# 添加书籍信息到数据库(指定其作者)
book = Book(name=book_name, author_id=author.id)
db.session.add(book)
db.session.commit()
except Exception as error:
db.session.rollback()
print(error)
flash("添加失败")
else:
flash("已存在")
else:
if request.method == "POST":
flash("参数错误")
# 1.查询数据
authors = Author.query.all()
# 2.将数据返回到模板中进行渲染返回
return render_template("demo1_bookDemo.html", authors=authors, form=book_form)
if __name__ == '__main__':
# 删除所有的表
db.drop_all()
# 创建所有的表
db.create_all()
# 生成数据
au1 = Author(name='老王')
au2 = Author(name='老尹')
au3 = Author(name='老刘')
# 把数据提交给用户会话
db.session.add_all([au1, au2, au3])
# 提交会话
db.session.commit()
# 生成数据
bk1 = Book(name='老王回忆录', author_id=au1.id)
bk2 = Book(name='我读书少,你别骗我', author_id=au1.id)
bk3 = Book(name='如何才能让自己更骚', author_id=au2.id)
bk4 = Book(name='怎样征服美丽少女', author_id=au3.id)
bk5 = Book(name='如何征服英俊少男', author_id=au3.id)
# 把数据提交给用户会话
db.session.add_all([bk1, bk2, bk3, bk4, bk5])
# 提交会话
db.session.commit()
app.run(debug=True)
{# demo1_bookDemo.html #}
Title
图书管理
在项目开发过程中,会遇到很多数据之间多对多关系的情况,比如:
所以在开发过程中需要使用 ORM 模型将表与表的多对多关联关系使用代码描述出来。多对多关系描述有一个唯一的点就是:需要添加一张单独的表去记录两张表之间的对应关系
学生表(Student)
主键(id) | 学生名(name) |
---|---|
1 | 张三 |
2 | 李四 |
3 | 王五 |
选修课表(Course)
主键(id) | 课程名(name) |
---|---|
1 | 物理 |
2 | 化学 |
3 | 生物 |
数据关联关系表(Student_Course)
主键(student.id) | 主键(course.id) |
---|---|
1 | 2 |
1 | 3 |
2 | 2 |
3 | 1 |
3 | 2 |
3 | 3 |
查询某个学生选修了哪些课程,例如:查询王五选修了哪些课程
查询某个课程都有哪些学生选择,例如:查询生物课程都有哪些学生选修
tb_student_course = db.Table('tb_student_course',
db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))
)
class Student(db.Model):
__tablename__ = "students"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
courses = db.relationship('Course', secondary=tb_student_course,
backref='student',
lazy='dynamic')
class Course(db.Model):
__tablename__ = "courses"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
if __name__ == '__main__':
db.drop_all()
db.create_all()
# 添加测试数据
stu1 = Student(name='张三')
stu2 = Student(name='李四')
stu3 = Student(name='王五')
cou1 = Course(name='物理')
cou2 = Course(name='化学')
cou3 = Course(name='生物')
stu1.courses = [cou2, cou3]
stu2.courses = [cou2]
stu3.courses = [cou1, cou2, cou3]
db.session.add_all([stu1, stu2, stu2])
db.session.add_all([cou1, cou2, cou3])
db.session.commit()
app.run(debug=True)
新建demo2_manytomany.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
# 设置连接数据
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/manytomany'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
# 实例化SQLAlchemy对象
db = SQLAlchemy(app)
app.secret_key = "apollo_miracle"
tb_student_course = db.Table('tb_student_course',
db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))
)
class Student(db.Model):
__tablename__ = "students"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
courses = db.relationship('Course', secondary=tb_student_course,
backref='student',
lazy='dynamic')
class Course(db.Model):
__tablename__ = "courses"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
@app.route("/")
def index():
return "index"
if __name__ == '__main__':
db.drop_all()
db.create_all()
# 添加测试数据
stu1 = Student(name='张三')
stu2 = Student(name='李四')
stu3 = Student(name='王五')
cou1 = Course(name='物理')
cou2 = Course(name='化学')
cou3 = Course(name='生物')
stu1.courses = [cou2, cou3]
stu2.courses = [cou2]
stu3.courses = [cou1, cou2, cou3]
db.session.add_all([stu1, stu2, stu2])
db.session.add_all([cou1, cou2, cou3])
db.session.commit()
app.run(debug=True)
realtionship描述了Role和User的关系:
第一个参数为对应参照的类"User"
第二个参数backref为类User申明新属性的方法
第三个参数为二次查询
注意:
tb_Student_Course就是第三张表,记录关系的表,它不是一个Model,只是一个db.Table,也就是一张数据库表。我们在程序中是不用操作这张表的,但是在数据库中必须存在,所以写法跟模型类不一样
二次查询:secondary参数
注意修改数据库名称
什么叫二次查询呢?
比如我要查询这个学生都选修了哪些课程,通过python代码即可实现:
stu = Student.query.filter(xx).first()
stu.courses
但是内部处理的sql语句可没那么简单:
首先先查询stu : select * from student where xx;
假设查询出来的id为1
然后要查询这个学生选修的课程:
- 第一次查询:select course_id from student_course where student_id = 1
假设查询结果为 1,3,5 (代表选修课程id)
- 第二次查询:select * from course where id in (1,3,5)
- 查询
查询这个学生选修的课程 需要经过两次查询, 所以叫做二次查询
我们先来看一个现象:
lazy介绍如下:
参数lazy决定了什么时候SQLALchemy从数据库中加载数据
- 如果设置为子查询方式(subquery),则会在加载完Role对象后,就立即加载与其关联的对象,这样会让总查询数量减少,但如果返回的条目数量很多,就会比较慢
设置为 subquery 的话,role.users 返回所有数据列表
- 另外,也可以设置为动态方式(dynamic),这样关联对象会在被使用的时候再进行加载,并且在返回前进行过滤,如果返回的对象数很多,或者未来会变得很多,那最好采用这种方式
设置为 dynamic 的话,role.users 返回查询对象,并没有做到真正的查询,可以利用查询对象做其他逻辑,比如:先排序再返回结果
如果不指定该值,那么当 student 查询数据之后,courses 就已经有值(已经从Course表里面把数据查询出来了)
如果指定该值,那么当 student 查询数据之后,courses 并没有具体的值,而只是查询对象
如果只是查询对象,那么就可以在用的时候再去数据库查询,避免不必要的查询操作,影响性能
我们来反过来查询一下,发现直接得到列表, 这个能不能也懒查询呢?
如下操作即可:
再次查看:
以下罗列了使用关系型数据库中常见关系定义模板代码
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')
class User(db.Model):
"""用户表"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True, index=True)
tb_student_course = db.Table('tb_student_course',
db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
db.Column('course_id', db.Integer, db.ForeignKey('courses.id'))
)
class Student(db.Model):
__tablename__ = "students"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
courses = db.relationship('Course', secondary=tb_student_course,
backref=db.backref('students', lazy='dynamic'),
lazy='dynamic')
class Course(db.Model):
__tablename__ = "courses"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
class Comment(db.Model):
"""评论"""
__tablename__ = "comments"
id = db.Column(db.Integer, primary_key=True)
# 评论内容
content = db.Column(db.Text, nullable=False)
# 父评论id
parent_id = db.Column(db.Integer, db.ForeignKey("comments.id"))
# 父评论(也是评论模型)
parent = db.relationship("Comment", remote_side=[id],
backref=db.backref('childs', lazy='dynamic'))
# 测试代码
if __name__ == '__main__':
db.drop_all()
db.create_all()
com1 = Comment(content='我是主评论1')
com2 = Comment(content='我是主评论2')
com11 = Comment(content='我是回复主评论1的子评论1')
com11.parent = com1
com12 = Comment(content='我是回复主评论1的子评论2')
com12.parent = com1
db.session.add_all([com1, com2, com11, com12])
db.session.commit()
app.run(debug=True)
网易新闻的评论:
一个主评论可能会有多个子评论 (也就是对于这个评论的回复评论)
示例场景
示例代码
tb_user_follows = db.Table(
"tb_user_follows",
db.Column('follower_id', db.Integer, db.ForeignKey('info_user.id'), primary_key=True), # 粉丝id
db.Column('followed_id', db.Integer, db.ForeignKey('info_user.id'), primary_key=True) # 被关注人的id
)
class User(db.Model):
"""用户表"""
__tablename__ = "info_user"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), unique=True, nullable=False)
# 用户所有的粉丝,添加了反向引用followed,代表用户都关注了哪些人
followers = db.relationship('User',
secondary=tb_user_follows,
primaryjoin=id == tb_user_follows.c.followed_id,
secondaryjoin=id == tb_user_follows.c.follower_id,
backref=db.backref('followed', lazy='dynamic'),
lazy='dynamic')
首先要在虚拟环境中安装Flask-Migrate。
pip install flask-migrate
#coding=utf-8
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate,MigrateCommand
from flask_script import Shell,Manager
app = Flask(__name__)
manager = Manager(app)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/Flask_test'
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy(app)
#第一个参数是Flask的实例,第二个参数是Sqlalchemy数据库实例
migrate = Migrate(app,db)
#manager是Flask-Script的实例,这条语句在flask-Script中添加一个db命令
manager.add_command('db',MigrateCommand)
#定义模型Role
class Role(db.Model):
# 定义表名
__tablename__ = 'roles'
# 定义列对象
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
user = db.relationship('User', backref='role')
#repr()方法显示一个可读字符串,
def __repr__(self):
return 'Role:'.format(self.name)
#定义用户
class User(db.Model):
__talbe__ = '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:'.format(self.username)
if __name__ == '__main__':
manager.run()
#这个命令会创建migrations文件夹,所有迁移文件都放在里面。
python database.py db init
python database.py db migrate -m 'initial migration'
python database.py db upgrade
可以根据history命令找到版本号,然后传给downgrade命令:
python app.py db history
输出格式: -> 版本号 (head), initial migration
回滚到指定版本
python app.py db downgrade 版本号
新建项目:
新建demo:
代码如下:(准备两个模型类)
创建数据库:
要想执行数据库迁移,需要增加如下代码:
接下来初始化:
初始化结果:在项目中创建了一个migrations文件夹
生成迁移文件:
结果:多了一个文件(迁移文件)
执行迁移:
结果:多出两张表
总结:
增加字段
模型类中增加字段:
生成迁移文件:
迁移文件如下:
执行迁移:
结果:
再增加字段:
生成迁移文件:
执行迁移:
结果:
删除字段
删除字段:
生成迁移,并执行迁移:
结果:
修改字段
将name变为nick_name:
生成迁移文件:
迁移文件如下:
执行迁移:
效果:
降级 downgrade
如果现在我不想修改了呢? 想回退刚刚的操作怎么办? downgrade降级:
但是这个降级的函数有问题:
修改如下:
降级:
效果:
历史版本&升级,降级指定具体版本
history查看版本:
降级可以指定具体某个版本:
效果: (fdc8fb218f90这个版本是最初的版本,所以字段也变为最初的)
升级指定版本:
效果如下:
pip install blinker
template_rendered = _signals.signal('template-rendered')
request_started = _signals.signal('request-started')
request_finished = _signals.signal('request-finished')
request_tearing_down = _signals.signal('request-tearing-down')
got_request_exception = _signals.signal('got-request-exception')
appcontext_tearing_down = _signals.signal('appcontext-tearing-down')
appcontext_pushed = _signals.signal('appcontext-pushed')
appcontext_popped = _signals.signal('appcontext-popped')
message_flashed = _signals.signal('message-flashed')
Flask-User 这个扩展中定义了名为 user_logged_in 的信号,当用户成功登入之后,这个信号会被发送。我们可以订阅该信号去追踪登录次数和登录IP:
from flask import request
from flask_user.signals import user_logged_in
@user_logged_in.connect_via(app)
def track_logins(sender, user, **extra):
user.login_count += 1
user.last_login_ip = request.remote_addr
db.session.add(user)
db.session.commit()
在 Flask-SQLAlchemy 模块中,0.10 版本开始支持信号,可以连接到信号来获取到底发生什么了的通知。存在于下面两个信号:
from flask_sqlalchemy import models_committed
# 给 models_committed 信号添加一个订阅者,即为当前 app
@models_committed.connect_via(app)
def models_committed(a, changes):
print(a, changes)
对数据库进行增删改进行测试
将追踪修改的这个配置改为true,意思就是数据库一旦发生改变就会发出信号:
我们增加一个监听信号的函数:
运行,发现打印的结果是,增加数据:
这是哪里发出的这个信号的呢? 如下: