多对多
常见业务:用户收藏文章/商品、用户与用户之间的好友关系、点赞、评论、关注、用户浏览商品的历史记录、订阅文章、专题/活动与商品/文章的关系。
# 有两种方式 # 1、非关系模型 关系表[这种表,无法提供给python进行操作的,仅仅用于在数据库中记录两个模型之间的关系] student_and_course = db.Table( "table_student_course", db.Column("id", db.Integer, primary_key=True, comment="主键ID"), db.Column("sid", db.Integer, db.ForeignKey("table_student.id"), comment="学生"), db.Column("cid", db.Integer, db.ForeignKey("table_course.id"), comment="课程"), db.Column("created_time", db.DateTime, default=datetime.now, comment="购买时间"), # 当前字段无法操作 ) class Student(db.Model): id = db.Column(db.Integer, primary_key=True,comment="主键") ... course_list = db.relationship("Course", secondary=student_and_course, backref="student_list", lazy="dynamic") class Course(db.Model): ... # 2、关系模型,[关系模型和关系表,任选其一 这种表可以被python操作] class Achievement(db.Model): ...View Code
基于第三方关系表构建多对多
1、非关系模型
代码:
from datetime import datetime from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import backref app = Flask(__name__) # 连接数据库连接url app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:123@127.0.0.1:3306/school?charset=utf8mb4" # 动态追踪修改设置,如未设置只会提示警告 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True # 查询时会显示原始SQL语句 app.config["SQLALCHEMY_ECHO"] = True # 把SQLAlchemy组件注册到项目中 db = SQLAlchemy() db.init_app(app) # 购买关系表[这种表,无法提供给flask进行数据操作的,仅仅用于在数据库中记录两个模型之间的关系 ] student_course_table = db.Table( "t_nvm_student_course", db.Column("id", db.Integer, primary_key=True, comment="主键"), db.Column("sid", db.Integer, db.ForeignKey("t_nvm_student.id"), comment="学生ID"), db.Column("cid", db.Integer, db.ForeignKey("t_nvm_course.id"), comment="课程ID"), db.Column("created_time", db.DateTime, default=datetime.now, comment="购买时间"), # 当前字段无法操作 ) class Student(db.Model): """学生信息模型""" __tablename__ = "t_nvm_student" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(15), index=True, comment="姓名") age = db.Column(db.SmallInteger, comment="年龄") sex = db.Column(db.Boolean, default=True, comment="性别") money = db.Column(db.Numeric(10, 2), default=0.0, comment="钱包") # 只有设置关联属性以后,flask中才提供模型关联的操作(只写一个关联就行 用下面的这个注释掉) # course_list = db.relationship("Course", secondary=student_course_table, backref="student_list", lazy="dynamic") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" class Course(db.Model): """课程信息模型""" __tablename__ = "t_nvm_course" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(255), unique=True, comment="课程") # 只有设置关联属性以后,flask中才提供模型关联的操作 student_list = db.relationship("Student", secondary=student_course_table, backref="course_list", lazy="dynamic") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" @app.route("/") def index(): """添加数据""" # 添加其中一个主模型数据时,同时绑定添加另外一个主模型的数据,这个过程中,关系表会自动写入2者的关系数据,绑定2个模型之间的关系 # student = Student( # name="xiaozhao", # age=13, # sex=False, # course_list=[ # Course(name="python入门"), # Course(name="python初级"), # Course(name="python进阶"), # ] # ) # db.session.add(student) # db.session.commit() # 在已有课程模型的基础上,新增学生,新增报读课程。 # student = Student( # name="xiaohong", # age=14, # sex=False, # ) # db.session.add(student) # db.session.commit() # # student = Student.query.filter(Student.name == "xiaohong").first() # # 让小红新增报读课程id为3的课程 # student.course_list.append(Course.query.get(3)) # student.course_list.append(Course(name="python高级")) # db.session.commit() # 让学生一次性报读多个已有课程 # student1 = Student.query.get(2) # course_list = Course.query.filter(Course.id.in_([1,2])).all() # student1.course_list.extend(course_list) # db.session.commit() """查询数据""" # # 查询id为1的学生购买的课程 # student = Student.query.get(1) # print(student.course_list) # # # 查询id为4的课程,有哪些学生购买了 # course = Course.query.get(4) # print(course.student_list.all()) """更新数据""" # # 给报读了4号课程的同学,返现红包200块钱 # course = Course.query.get(4) # for student in course.student_list: # student.money += 200 # db.session.commit() # db.Table的缺陷: 无法通过主模型直接操作db.Table中的外键之外的其他字段,例如:无法读取购买课程的时间 course = Course.query.get(3) print(course.student_list.all()) # 解决:在声明2个模型是多对多的关联关系时,如果需要在python中操作关系表的数据,则可以把关联关系使用第三个模型来创建声明, # 就是不要使用db.Table创建关系表了,改成第2种方式的关系模型来绑定2者的关系,把模型的多对多拆分成2个1对多 return "ok" if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=True)View Code
多对多,也可以拆解成3个模型(2个主模型,1个关系模型,关系模型保存了2个主模型的外键),其中tb_achievement作为单独模型存在。
基于第三方关系模型构建多对多
2、关系模型
在SQLAlchemy中,基于db.Table创建的关系表,如果需要新增除了外键以外其他字段,无法操作。所以将来实现多对多的时候,除了上面db.Table方案以外,还可以把关系表声明成模型的方法,如果声明成模型,则原来课程和学生之间的多对多的关系,就会变成远程的1对多了。
代码:
from datetime import datetime from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import backref app = Flask(__name__) # 连接数据库连接url app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:123@127.0.0.1:3306/school?charset=utf8mb4" # 动态追踪修改设置,如未设置只会提示警告 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True # 查询时会显示原始SQL语句 app.config["SQLALCHEMY_ECHO"] = False # 把SQLAlchemy组件注册到项目中 db = SQLAlchemy() db.init_app(app) class StudentCourse(db.Model): __tablename__ = "t_nvm_student_course_2" id = db.Column(db.Integer, primary_key=True, comment="主键") sid = db.Column(db.Integer, db.ForeignKey("t_nvm_student_2.id"), comment="学生ID") cid = db.Column(db.Integer, db.ForeignKey("t_nvm_course_2.id"), comment="课程ID") created_time = db.Column(db.DateTime, default=datetime.now, comment="购买时间") # 关联属性 student = db.relationship("Student", uselist=False, backref=backref("to_relation", uselist=True, lazy="dynamic")) course = db.relationship("Course", uselist=False, backref=backref("to_relation", uselist=True, lazy="dynamic")) class Student(db.Model): """学生信息模型""" __tablename__ = "t_nvm_student_2" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(15), index=True, comment="姓名") age = db.Column(db.SmallInteger, comment="年龄") sex = db.Column(db.Boolean, default=True, comment="性别") money = db.Column(db.Numeric(10, 2), default=0.0, comment="钱包") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" class Course(db.Model): """课程信息模型""" __tablename__ = "t_nvm_course_2" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(255), unique=True, comment="课程") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" @app.route("/") def index(): """添加数据""" # 添加其中一个主模型数据时,同时绑定添加另外一个主模型的数据 # student = Student( # name="xiaozhao", # age=13, # sex=False, # to_relation=[ # StudentCourse(course=Course(name="python入门")), # StudentCourse(course=Course(name="python初级")), # StudentCourse(course=Course(name="python进阶")), # ] # ) # db.session.add(student) # db.session.commit() """在已有课程的基础上,新增学生报读课程。""" # student = Student( # name="xiaohong", # age=14, # sex=False, # money=30000, # ) # db.session.add(student) # db.session.commit() # # student = Student.query.filter(Student.name == "xiaohong").first() # student.to_relation.extend([ # StudentCourse( # course=Course.query.get(1) # 已经存在的课程,给学生报读 # ), # StudentCourse( # course=Course(name="python高级") # 新增课程,并让当前学生报读该课程 # ) # ]) # db.session.commit() # 已有学生和课程,对学生购买课程进行记录 # student1 = Student.query.get(2) # course_list = Course.query.filter(Course.id.in_([2,3])).all() # student1.to_relation.extend([StudentCourse(course=course) for course in course_list]) # db.session.commit() """查询操作""" # 查询学生购买的课程 # student = Student.query.get(1) # print([relation.course for relation in student.to_relation]) # 查看指定课程有哪些学生购买了 # course = Course.query.get(1) # print([relation.student for relation in course.to_relation]) # 查询2号学生购买的每个课程的时间 student = Student.query.get(2) for relation in student.to_relation: print(relation.course.name, relation.created_time) """更新数据""" # # 给购买了2号课程的学生返现 # course = Course.query.get(2) # for relation in course.to_relation: # relation.student.money += 200 # db.session.commit() return "ok" if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=True) View Code
relationship还有一个设置外键级联级别的属性:cascade="all, delete, delete-orphan"
练习:
1. flask中的SQLAlchemy如何进行自关联查询? 这里自己写一个关于行政区划的1对多的自关联操作[增删查改]。
逻辑外键
也叫虚拟外键。主要就是在开发中为了减少数据库的性能消耗(连表查询),提升系统运行效率,一般项目中如果单表数据太大[千万级别]就不会使用数据库本身维护的物理外键(数据库里表之间没有关系),而是采用由ORM或者我们逻辑代码进行查询关联的逻辑外键(代码层有关系)。当然,不在使用mysql的物理外键,会给数据的一致性带来一定的风险(这种只能在代码层保持一致性 数据库中就没有约束了)。
SQLAlchemy设置外键模型的虚拟外键,有2种方案:
方案1,查询数据时临时指定逻辑外键的映射关系:
模型类.query.join(模型类,主模型.主键==外键模型.外键).join(模型类,主模型.主键==外键模型.外键).with_entities(字段1,字段2.label("字段别名"),....).all()
方案2,在模型声明时指定逻辑外键的映射关系(最常用,这种设置方案,在操作模型时与原来默认设置的物理外键的关联操作是一模一样的写法):
class Student(db.Model): id = db.Column(db.Integer, primary_key=True, comment="主键") # 虚拟外键,原有参数不变,新增2个表达关联关系的属性: # primaryjoin, 指定2个模型之间的主外键关系,相当于原生SQL语句中的join # foreign_keys,指定外键 address_list = db.relationship("StudentAddress", uselist=True, backref="student", lazy="subquery", primaryjoin="Student.id==StudentAddress.student_id", foreign_keys="StudentAddress.student_id") class StudentAddress(db.Model): # 原来的外键设置为普通索引即可。 student_id = db.Column(db.Integer, comment="学生id")
例1,虚拟外键使用的方案1,代码:
from datetime import datetime from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import backref app = Flask(__name__) # 连接数据库连接url app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:123@127.0.0.1:3306/school?charset=utf8mb4" # 动态追踪修改设置,如未设置只会提示警告 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True # 查询时会显示原始SQL语句 app.config["SQLALCHEMY_ECHO"] = True # 把SQLAlchemy组件注册到项目中 db = SQLAlchemy() db.init_app(app) class StudentCourse(db.Model): # 虚拟外键 StudentCourse Student Course 这三个表没有关系 __tablename__ = "t_virtual_foreign_key_student_course" id = db.Column(db.Integer, primary_key=True, comment="主键") student_id = db.Column(db.Integer, index=True, comment="学生ID") course_id = db.Column(db.Integer, index=True, comment="课程ID") created_time = db.Column(db.DateTime, default=datetime.now, comment="购买时间") class Student(db.Model): """学生信息模型""" __tablename__ = "t_virtual_foreign_key_student" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(15), index=True, comment="姓名") age = db.Column(db.SmallInteger, comment="年龄") sex = db.Column(db.Boolean, default=True, comment="性别") money = db.Column(db.Numeric(10, 2), default=0.0, comment="钱包") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" class Course(db.Model): """课程信息模型""" __tablename__ = "t_virtual_foreign_key_course" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(255), unique=True, comment="课程") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" @app.route("/") def index(): """分别给不同的模型添加测试数据""" # stu0 = Student(name="xiaozhao", age=15, sex=True, money=1000) # stu1 = Student(name="xiaoming", age=16, sex=True, money=1000) # stu2 = Student(name="xiaobai", age=18, sex=False, money=1000) # stu3 = Student(name="xiaohei", age=21, sex=True, money=1000) # stu4 = Student(name="xiaolan", age=18, sex=False, money=1000) # db.session.add_all([stu0, stu1, stu2, stu3, stu4]) # # course1 = Course(name="python基础") # course2 = Course(name="python入门") # course3 = Course(name="python进阶") # course4 = Course(name="python高级") # course5 = Course(name="python实战") # db.session.add_all([course1, course2, course3, course4, course5]) # # # 学生购买课程 # data = [ # StudentCourse(student_id=1,course_id=1), # StudentCourse(student_id=1,course_id=2), # StudentCourse(student_id=1,course_id=3), # StudentCourse(student_id=2,course_id=1), # StudentCourse(student_id=2,course_id=2), # StudentCourse(student_id=3,course_id=3), # StudentCourse(student_id=3,course_id=4), # StudentCourse(student_id=4,course_id=1), # StudentCourse(student_id=4,course_id=2), # StudentCourse(student_id=4,course_id=5), # StudentCourse(student_id=5,course_id=1), # StudentCourse(student_id=5,course_id=2), # StudentCourse(student_id=5,course_id=3), # StudentCourse(student_id=5,course_id=4), # ] # db.session.add_all(data) # db.session.commit() """查询3号学生购买了哪些课程?""" # # 1. 手动基于代码进行关联查询 # student_course_list = StudentCourse.query.filter(StudentCourse.student_id==3).all() # course_id_list = [relation.course_id for relation in student_course_list] # course_list = Course.query.filter(Course.id.in_(course_id_list)).all() # print(course_list) # 2. 基于临时逻辑外键来关联查询 # 主模型.query.join(从模型类名, 关系语句) # 主模型.query.join(从模型类名, 主模型.主键==从模型类名.外键) # # 两个模型的临时逻辑外键关联 # data = Student.query.join( # StudentCourse, Student.id == StudentCourse.student_id # ).with_entities(StudentCourse.course_id).filter(Student.id==3).all() # print(data) # # 两个以上模型的临时逻辑外键关联 data = Student.query.join( StudentCourse, Student.id == StudentCourse.student_id ).join( Course, StudentCourse.course_id == Course.id ).with_entities( StudentCourse.course_id, Course.name ).filter(Student.id==3).all() print(data) return "ok" if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=True) View Code
例2,虚拟外键使用的方案2,代码:
from datetime import datetime from flask import Flask from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import backref app = Flask(__name__) # 连接数据库连接url app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:123@127.0.0.1:3306/school?charset=utf8mb4" # 动态追踪修改设置,如未设置只会提示警告 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True # 查询时会显示原始SQL语句 app.config["SQLALCHEMY_ECHO"] = True # 把SQLAlchemy组件注册到项目中 db = SQLAlchemy() db.init_app(app) class StudentCourse(db.Model): __tablename__ = "t_virtual_foreign_key_student_course_2" id = db.Column(db.Integer, primary_key=True, comment="主键") student_id = db.Column(db.Integer, index=True, comment="学生ID") course_id = db.Column(db.Integer, index=True, comment="课程ID") created_time = db.Column(db.DateTime, default=datetime.now, comment="购买时间") # 设置关联属性[比原来设置物理外键,多出2个属性设置:primaryjoin与foreign_keys] student = db.relationship("Student", uselist=False, backref=backref("to_relation", uselist=True, lazy="dynamic"), primaryjoin="Student.id==StudentCourse.student_id", foreign_keys="StudentCourse.student_id" ) course = db.relationship("Course", uselist=False, backref=backref("to_relation", uselist=True, lazy="dynamic"), primaryjoin="Course.id==StudentCourse.course_id", foreign_keys="StudentCourse.course_id" ) class Student(db.Model): """学生信息模型""" __tablename__ = "t_virtual_foreign_key_student_2" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(15), index=True, comment="姓名") age = db.Column(db.SmallInteger, comment="年龄") sex = db.Column(db.Boolean, default=True, comment="性别") money = db.Column(db.Numeric(10, 2), default=0.0, comment="钱包") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" class Course(db.Model): """课程信息模型""" __tablename__ = "t_virtual_foreign_key_course_2" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(255), unique=True, comment="课程") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" @app.route("/") def index(): """分别给不同的模型添加测试数据""" # stu0 = Student(name="xiaozhao", age=15, sex=True, money=1000) # stu1 = Student(name="xiaoming", age=16, sex=True, money=1000) # stu2 = Student(name="xiaobai", age=18, sex=False, money=1000) # stu3 = Student(name="xiaohei", age=21, sex=True, money=1000) # stu4 = Student(name="xiaolan", age=18, sex=False, money=1000) # db.session.add_all([stu0, stu1, stu2, stu3, stu4]) # # course1 = Course(name="python基础") # course2 = Course(name="python入门") # course3 = Course(name="python进阶") # course4 = Course(name="python高级") # course5 = Course(name="python实战") # db.session.add_all([course1, course2, course3, course4, course5]) # # # 学生购买课程 # data = [ # StudentCourse(student_id=1,course_id=1), # StudentCourse(student_id=1,course_id=2), # StudentCourse(student_id=1,course_id=3), # StudentCourse(student_id=2,course_id=1), # StudentCourse(student_id=2,course_id=2), # StudentCourse(student_id=3,course_id=3), # StudentCourse(student_id=3,course_id=4), # StudentCourse(student_id=4,course_id=1), # StudentCourse(student_id=4,course_id=2), # StudentCourse(student_id=4,course_id=5), # StudentCourse(student_id=5,course_id=1), # StudentCourse(student_id=5,course_id=2), # StudentCourse(student_id=5,course_id=3), # StudentCourse(student_id=5,course_id=4), # ] # db.session.add_all(data) # db.session.commit() """查询3号学生购买了哪些课程?""" student = Student.query.get(3) print([{"id":relation.course.id, "name": relation.course.name} for relation in student.to_relation.all()]) """查询5个课程都有哪些学生购买了?""" course = Course.query.get(5) print([{"id":relation.student.id, "name": relation.student.name} for relation in course.to_relation.all()]) return "ok" if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=True) View Code
数据迁移
-
在开发过程中,需要修改数据库模型,而且还要在修改之后更新数据库。最直接的方式就是删除旧表,但这样会丢失数据,所以往往更常见的方式就是使用alter来改变数据结构,原有数据中的新字段值设置默认值或null=True.
-
更好的解决办法是使用数据迁移,它可以追踪数据库表结构的变化,然后把变动的历史信息记录到数据库中。
-
在Flask中可以使用Flask-Migrate的第三方扩展来实现数据迁移。并且集成到Flask终端脚本中,所有操作通过
flask db
命令就能完成。 -
为了导出数据库迁移命令,Flask-Migrate提供了一个MigrateCommand类,可以注册到flask框架中。
首先要在虚拟环境中安装Flask-Migrate。
pip install Flask-Migrate
官网地址:https://flask-migrate.readthedocs.io/en/latest/
为了方便迁移操作,我们新建一个flask_student的数据库。
create database flask_student charset=utf8mb4;
代码文件内容:
from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate # 需要再下面注册一下 from sqlalchemy.orm import backref app = Flask(__name__) # 连接数据库连接url app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:123@127.0.0.1:3306/school?charset=utf8mb4" # 动态追踪修改设置,如未设置只会提示警告 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True # 查询时会显示原始SQL语句 app.config["SQLALCHEMY_ECHO"] = True # 把SQLAlchemy组件注册到项目中 db = SQLAlchemy() db.init_app(app) # 把数据迁移绑定当前应用对象中,与SQLAlchemy的数据库ORM模块进行关联 migrate = Migrate() migrate.init_app(app, db) class Student(db.Model): """学生信息模型""" __tablename__ = "t_migrate_student" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(15), index=True, comment="姓名") age = db.Column(db.SmallInteger, comment="年龄") sex = db.Column(db.Boolean, default=True, comment="性别") email = db.Column(db.String(128), unique=True, comment="邮箱地址") money = db.Column(db.Numeric(10, 2), default=0.0, comment="钱包") address_list = db.relationship("StudentAddress", uselist=True, backref=backref("student", uselist=False), lazy="dynamic") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" class StudentAddress(db.Model): __tablename__ = "t_migrate_student_address" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(50), default="默认", comment="地址名称") province = db.Column(db.String(50), comment="省份") city = db.Column(db.String(50), comment="城市") area = db.Column(db.String(50), comment="地区") address = db.Column(db.String(500), comment="详细地址") mobile = db.Column(db.String(15), comment="收货人电话") student_id = db.Column(db.Integer, db.ForeignKey("t_migrate_student.id"), comment="student外键") def __repr__(self): return f"<{self.student.name} {self.__class__.__name__}>" @app.route("/") def index(): return "ok" if __name__ == '__main__': app.run(debug=True) View Code
创建迁移版本仓库
# 切换到项目根目录下 cd ~/Desktop/flaskdemo # 设置flask项目的启动脚本位置,例如我们现在的脚本叫manage.py export FLASK_APP=manage.py # 数据库迁移初始化,这个命令会在当前项目根目录下创建migrations文件夹,将来所有数据表相关的迁移文件都放在里面。 flask db init
创建迁移版本
-
自动创建迁移版本文件中有两个函数,用于进行数据迁移同步到数据库操作的。
-
upgrade():把迁移中的改动代码同步到数据库中。
-
downgrade():则将改动代码从数据库中进行还原。
-
-
自动创建的迁移脚本会根据模型定义和数据库当前状态的差异,生成upgrade()和downgrade()函数的内容。
-
生成的迁移文件不一定完全正确,有可能代码中存在细节遗漏导致报错,需要开发者进行检查,特别在多对多的时候
# 根据flask项目的模型生成迁移文件 -m的后面你不要使用中文!! flask db migrate -m 'initial migration' # 这里等同于django里面的 makemigrations,生成迁移版本文件 # 完成2件事情: # 1. 在migrations/versions生成一个数据库迁移文件 # 2. 如果是首次生成迁移文件的项目,则迁移工具还会在数据库创建一个记录数据库版本的alembic_version表
升级版本库的版本
把当前ORM模型中的代码改动同步到数据库。
# 从migations目录下的versions中根据迁移文件upgrade方法把数据表的结构同步到数据库中。 flask db upgrade
降级版本库的版本
# 从migations目录下的versions中根据迁移文件downgrade把数据表的结构同步到数据库中。 flask db downgrade
版本库的历史管理
可以根据history命令找到版本号,然后传给downgrade命令:
flask db history 输出格式:<base> -> 版本号 (head), initial migration
回滚到指定版本
flask db downgrade # 默认返回上一个版本 flask db downgrade 版本号 # 回滚到指定版本号对应的版本 flask db upgrade 版本号 # 升级到指定版本号对应的版本
数据迁移的步骤:
# 1. 初始化数据迁移的目录 export FLASK_APP=4-manage.py flask db init # 2. 数据库的数据迁移版本初始化,生成迁移文件 flask db migrate -m 'initial migration' # 3. 升级版本[新增一个迁移记录] flask db upgrade # 4. 降级版本[回滚一个迁移记录] flask db downgrade
注意:
使用数据迁移的过程中,不管是upgrade还是downgrade,只会影响到数据表结构,不会还原代码中的模型代码,所以,如果要恢复还原某些字段的删除操作,还需要开发者自己手动还原代码。
常用模块
Faker[阅读] (生成数据)
文档: https://faker.readthedocs.io/en/master/locales/zh_CN.html
批量生成测试数据: https://github.com/joke2k/faker
pip install faker -i https://pypi.douban.com/simple
代码:
from datetime import datetime from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) # 连接数据库连接url app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:123@127.0.0.1:3306/flask_student?charset=utf8mb4" # 动态追踪修改设置,如未设置只会提示警告 app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True # 查询时会显示原始SQL语句 app.config["SQLALCHEMY_ECHO"] = True # 把SQLAlchemy组件注册到项目中 db = SQLAlchemy() db.init_app(app) class Student(db.Model): """学生信息模型""" __tablename__ = "t_faker_student" id = db.Column(db.Integer, primary_key=True, comment="主键") name = db.Column(db.String(15), index=True, comment="姓名") age = db.Column(db.SmallInteger, comment="年龄") sex = db.Column(db.Boolean, default=True, comment="性别") email = db.Column(db.String(128), comment="邮箱地址") created_time = db.Column(db.DateTime, default=datetime.now) money = db.Column(db.Numeric(10, 2), default=0.0, comment="钱包") def __repr__(self): return f"<{self.name} {self.__class__.__name__}>" """基于Faker生成仿真数据的终端命令""" # 自定义批量生成学生 import random, click from faker import Faker faker = Faker(locale="ZH_CN") # 自定义终端命令 @app.cli.command("faker_user") @click.argument("num", default=10, type=int) # 命令的选项 def faker_user_command(num): """生成测试学生信息""" data_list = [] for i in range(num): sex = bool( random.randint(0,1) ) student = Student( name= faker.name_male() if sex else faker.name_female(), age=random.randint(15,60), sex=sex, email=faker.unique.free_email(), money=float( random.randint(100,100000) / 100 ), created_time=faker.date_time(), ) data_list.append(student) # 在循环外 db.session.add_all(data_list) db.session.commit() @app.route("/") def index(): return "ok" if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=True) View Code
终端命令:
export FLASK_APP=manage.py # 生成10条数据 flask faker_user 10
flask-session
flask框架中,默认会以cookie的方式把session数据分散保存到客户端中,并非真正意义上的session,所以这种session不能保存一些相对隐秘的数据。因此,我们可以使用第三方模块 flask-session,把session重新指定保存到服务端的数据库或缓存中。
flask-session,允许设置session到指定的存储空间中,例如:redis/mongoDB/mysql。
官方文档: https://flask-session.readthedocs.io/en/latest/
pip install Flask-Session -i https://pypi.douban.com/simple
使用session之前,必须配置一下配置项:
# session秘钥 app.config["SECRET_KEY"] = "*(%#4sxcz(^(#$#8423"
SQLAlchemy存储session的基本配置
需要手动创建session表,在项目第一次启动的时候,使用db.create_all()
来完成创建。
from datetime import datetime from flask import Flask from flask_sqlalchemy import SQLAlchemy # 引入sessio操作类,注意:引入路径不同,大小写不同的。 from flask_session import Session as SessionStore from flask import session app = Flask(__name__) # 把SQLAlchemy组件注册到项目中 db = SQLAlchemy() # 初始化session存储类 session_store = SessionStore() app.config.update({ # 使用session必须设置秘钥 "SECRET_KEY": "*(%#4sxcz(^(#$#8423", # 要把存储到SQLAlchemy,必须配置数据库连接 "SQLALCHEMY_DATABASE_URI": "mysql://root:123@127.0.0.1:3306/flask_student?charset=utf8mb4", "SQLALCHEMY_TRACK_MODIFICATIONS": True, "SQLALCHEMY_ECHO": False, # 把session通过SQLAlchmey保存到mysql中 "SESSION_TYPE": "sqlalchemy", # session类型为sqlalchemy "SESSION_SQLALCHEMY": db, # SQLAlchemy的数据库连接对象 "SESSION_SQLALCHEMY_TABLE": 'sessions', # session要保存的表名称 "SESSION_PERMANENT": True, # 如果设置为True,则关闭浏览器session就失效 "SESSION_USE_SIGNER": True, # 是否对发送到浏览器上session的cookie值进行添加签名,防止串改。 "SESSION_KEY_PREFIX": "session:" # session数据表中sessionID的前缀,默认就是 session: }) db.init_app(app) # 务必保证在数据库配置初始化以后才进行session存储类的初始化 session_store.init_app(app) @app.route("/") def index(): return "ok" @app.route("/set_session") def set_session(): session["uname"] = "xiaoming" session["age"] = 18 return "ok" @app.route("/get_session") def get_session(): print(session.get("uname")) print(session.get("age")) return "ok" @app.route("/del_session") def del_session(): # 此处的删除,不是删除用户对应的session表记录,而是删除session值而已。 print(session.pop("uname")) print(session.pop("age")) return "ok" if __name__ == '__main__': with app.app_context(): db.create_all() app.run(debug=True) View Code
redis保存session的基本配置[常用]
这个功能必须确保,服务器必须已经安装了redis而且当前项目虚拟环境中已经安装了redis扩展库
pip install flask-redis -i https://pypi.douban.com/simple
flask-redis是第三方开发者为了方便我们在flask框架中集成redis数据库操作所封装一个redis操作库,(底层还是pyredis)、
在flask中要基于flask-redis进行数据库则可以完成以下3个步骤即可:
from flask import Flask from flask_redis import FlaskRedis app = Flask(__name__) app.config.update({ "REDIS_SESSION_URL": "redis://default:default@127.0.0.1:6379/0", "REDIS_USER_URL": "redis://default:default@127.0.0.1:6379/1", "REDIS_ORDER_URL": "redis://default:default@127.0.0.1:6379/2", }) # redis中默认有16个数据库,我们可以让flask_redis初始化时,默认连接到不同的库中,当然这需要我们设置配置redis的前缀config_prefix session_redis = FlaskRedis(config_prefix="REDIS_SESSION") user_redis = FlaskRedis(config_prefix="REDIS_USER") order_redis = FlaskRedis(config_prefix="REDIS_ORDER") # 初始化 flask_redis session_redis.init_app(app) user_redis.init_app(app) order_redis.init_app(app) @app.route("/") def index(): session_redis.setnx("age", 100) user_redis.setnx("user_id", 100) order_redis.setnx("order_id", 100) return "ok" if __name__ == '__main__': app.run(debug=True) View Code
在redis中保存session,代码:
from flask import Flask from flask_redis import FlaskRedis from flask import session from flask_session import Session as SessionStore app = Flask(__name__) # redis中默认有16个数据库,我们可以让flask_redis初始化时,默认连接到不同的库中,当然这需要我们设置配置redis的前缀config_prefix session_redis = FlaskRedis(config_prefix="REDIS_SESSION") user_redis = FlaskRedis(config_prefix="REDIS_USER") order_redis = FlaskRedis(config_prefix="REDIS_ORDER") app.config.update({ "SECRET_KEY": "my-secret_key", "REDIS_SESSION_URL": "redis://default:default@127.0.0.1:6379/0", "REDIS_USER_URL": "redis://default:default@127.0.0.1:6379/1", "REDIS_ORDER_URL": "redis://default:default@127.0.0.1:6379/2", # 把session保存到redis中 "SESSION_TYPE": "redis", # session类型为sqlalchemy, redis 或 mongodb "SESSION_PERMANENT": True, # 如果设置为True,则关闭浏览器session就失效 "SESSION_USE_SIGNER": True, # 是否对发送到浏览器上session的cookie值进行添加签名,防止串改。 "SESSION_KEY_PREFIX": "session:", # session数据表中sessionID的前缀,默认就是 session: # session保存数据到redis时启用的链接对象 "SESSION_REDIS": session_redis, # 用于连接redis的配置 }) # 初始化 flask_redis session_redis.init_app(app) user_redis.init_app(app) order_redis.init_app(app) # 务必保证session存储类初始化之前,redis已经完成初始化了。 session_store = SessionStore() session_store.init_app(app) @app.route("/") def index(): session_redis.setnx("age", 100) user_redis.setnx("user_id", 100) order_redis.setnx("order_id", 100) return "ok" @app.route("/set_session") def set_session(): session["uname"] = "xiaoming" session["age"] = 18 return "ok" @app.route("/get_session") def get_session(): print(session.get("uname")) print(session.get("age")) return "ok" @app.route("/del_session") def del_session(): # 此处的删除,不是删除用户对应的session表记录,而是删除session值而已。 print(session.pop("uname")) print(session.pop("age")) return "ok" if __name__ == '__main__': app.run(debug=True) View Code
蓝图 Blueprint
(所有视图和url路由地址的绑定关系的临时容器)
模块化
随着flask程序越来越复杂,我们需要对程序进行模块化的处理,之前学习过django的子应用管理app,flask程序进行可以进行类似的模块化处理保存代码。
简单来说,Blueprint 是一个存储视图方法/模型代码的容器(目录),这些操作在这个Blueprint 被注册到flask的APP实例对象应用之后就可以被调用,Flask 可以通过Blueprint来组织URL以及处理客户端请求的视图。
Flask使用Blueprint让应用实现模块化,在Flask中Blueprint具有如下属性:
-
一个项目可以具有多个Blueprint
-
可以将一个Blueprint注册到任何一个未使用的URL下比如 “/”、“/users”或者子域名,也就是说每一个蓝图都可以像django那样有属于自己的路由前缀
-
在一个flask项目中,同一个BluePrint模块可以注册多次,也就是说一个蓝图可以对应多个不同的url地址。
-
Blueprint目录可以保存单独属于自己的模板目录保存自己的模板文件、静态文件或者其它的通用操作方法,它并不是必须要实现应用的视图和函数的
-
在一个flask项目初始化时,就应该要注册需要使用的Blueprint,否则项目不识别Blueprint蓝图
注意:flask中的Blueprint并不是一个完整的项目应用,它不能独立运行,而必须要把蓝图blueprint注册到某一个flask项目中才能使用。
在flask中,要使用蓝图Blueprint可以分为四个步骤:
1.手动创建一个蓝图的包目录,例如users,并在__init__.py
文件中创建蓝图实例对象users_blueprint
users/__init.py,代码: from flask import Blueprint # 等同于 app = Flask(__name__),只是这里并非一个独立的flask项目, # 所以需要在第一个参数中,指定蓝图名称,其他参数与之前实例化app应用对象是一样的。 users_blueprint = Blueprint("users", __name__)
2.在这个users蓝图目录下创建蓝图的子文件, 其中我们可以创建views.py文件,保存当前蓝图使用的视图函数
users/views.py
,代码:
# 光写视图,不用写路由 def login(): return "用户登录视图" def register(): return "用户注册视图"
3.在users/__init__.py
中引入views.py中所有的视图函数并绑定路由,users/__init__.py
,代码:
from flask import Blueprint from . import views # 等同于 app = Flask(__name__),只是这里并非一个独立的flask项目, # 所以需要在第一个参数中,指定蓝图名称,其他参数与之前实例化app应用对象是一样的。 users_blueprint = Blueprint("users", __name__) # 把蓝图下的视图与蓝图下的路由进行绑定url users_blueprint.add_url_rule(rule="/login", view_func=views.login) users_blueprint.add_url_rule(rule="/register", view_func=views.register)
4.在主应用下程序入口manage.py文件中把这个users_blueprint蓝图对象注册app实例对象中,运行起来。
manage.py,代码:
from flask import Flask app = Flask(__name__) # 注册蓝图 from users import users_blueprint app.register_blueprint(blueprint=users_blueprint, url_prefix="/users") if __name__ == '__main__': app.run(debug=True)
当这个应用启动后,通过/users/login或者/users/register可以访问到蓝图中定义的视图函数
蓝图运行机制
-
蓝图Blueprint实际上的作用就是,充当当前蓝图目录下的所有视图和url路由地址的绑定关系的临时容器
-
在视图函数被蓝图对象的add_url_rule方法注册时,这个操作本质就是将视图和url地址的映射关系添加到蓝图的子路由列表deferred_functions中。
-
蓝图对象根本没有路由机制的,当我们在蓝图中的视图函数上调用route装饰器(或者add_url_role函数)注册路由时,它只是在蓝图对象的内部的 deferred_functions(子路由列表)中添加了一个路由项(路由项实际上就是一个绑定了视图和url地址的lambda匿名函数)
-
当执行app.register_blueprint()注册蓝图时,app应用实例对象会将从蓝图对象的 deferred_functions列表中循环取出每一个之前注册的路由项,并把app应用实例对象自己作为参数执行路由项对应的lambda匿名函数,lambda匿名函数执行以后就会调用app.add_url_rule() 方法,这就将蓝图下子路由列表之前暂存的路由全部添加到了app应用实例对象的url_map总路由表中了,所以用户就可以在flask中访问到了蓝图中的视图。当然,能访问蓝图下的视图,自然也就可以通过视图调用其他的功能,例如:蓝图下的其他功能函数或其他的模型对象了。
蓝图的url拼接
当我们在app应用实例对象上注册一个蓝图时,可以指定一个url_prefix关键字参数(这个参数默认是/)
在app应用实例对象的最终的路由表 url_map中,在蓝图上注册的路由URL自动被加上了这个路由前缀,这个可以保证在多个蓝图中使用相同的子路由而不会最终引起冲突,只要在注册蓝图时将不同的蓝图挂接到不同的自路径即可。
注意:有了蓝图以后,在flask使用url_for在使用时,如果要生成一个蓝图里面的视图对应的路由地址,则需要声明当前蓝图名称+视图名称
# url_for('蓝图名称.视图函数名') url_for('users.login') # /users + /login /users就是蓝图中的路由前缀 /login就是子路由
users/views.py,代码:
from flask import url_for # 光写视图,不用写路由 def login(): return "用户登录视图" def register(): return f"用户注册视图,登录视图的url地址:{url_for('users.login')}"
访问:
注册蓝图下的静态文件[很少使用]
和没有学习蓝图之前的app应用对象不同,蓝图对象创建时不会默认注册静态目录的路由。需要我们在创建时手动指定 static_folder参数。
下面的代码将蓝图所在目录下的static_users目录设置为静态目录
users/__init__.py
,代码:
from flask import Blueprint from . import views # 等同于 app = Flask(__name__),只是这里并非一个独立的flask项目, # 所以需要在第一个参数中,指定蓝图名称,其他参数与之前实例化app应用对象是一样的。 users_blueprint = Blueprint("users", __name__, static_folder="static") # 把蓝图下的视图与蓝图下的路由进行绑定 users_blueprint.add_url_rule(rule="/login", view_func=views.login) users_blueprint.add_url_rule(rule="/register", view_func=views.register) print(users_blueprint.deferred_functions)View Code
现在就可以使用http://127.0.0.1:5000/users/static/6.png 访问users/static/目录下的静态文件了。
设置蓝图下的html模版[很少使用]
创建蓝图下的模板目录templates,users/__init__.py
,代码:
from flask import Blueprint from . import views # 等同于 app = Flask(__name__),只是这里并非一个独立的flask项目, # 所以需要在第一个参数中,指定蓝图名称,其他参数与之前实例化app应用对象是一样的。 users_blueprint = Blueprint("users", __name__, static_folder="static", template_folder="templates") # 把蓝图下的视图与蓝图下的路由进行绑定 users_blueprint.add_url_rule(rule="/login", view_func=views.login) users_blueprint.add_url_rule(rule="/register", view_func=views.register) print(users_blueprint.deferred_functions)View Code
视图users/views.py
,代码:
from flask import url_for, render_template # 光写视图,不用写路由 def login(): title = "用户登录视图" return render_template("login.html", **locals()) def register(): return f"用户注册视图,登录视图的url地址:{url_for('users.login')}"View Code
模板代码,users/templates/index.html
,代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>{{title}}</h1> <p>templates/login.html</p> </body> </html>View Code
标签:__,flask,app,db,session,student,id From: https://www.cnblogs.com/erhuoyuan/p/17396739.html注意:
如果公司使用了flask1.x版本,则不能出现项目根目录下和蓝图目录下2个templates目录的情况,否则项目根目录下的templates模板会覆盖蓝图目录下的同名模板,flask会优先加载项目根目录下的模板。flask2.x版本已经解决上面的问题。