将ORM模型之前,先了解以下背景,为什么会有ORM模型
访问关系数据库的传统方式是:拼接 SQL 语句。例如,向数据库中插入一条数据,根据要插入的数据拼接一条 SQL INSERT 语句,下面的 Python 程序使用 SQL 语句向数据库中插入一条学生的信息:
sno = '20201916'
name = '张三'
age = 20
gender = 'male'
sql = 'INSERT INTO students(sno, name, age, gender) VALUES("%s", "%s", %d, "%s")' % (sno, name, age, gender)
rows = cursor.execute(sql)
SQL语句是通过与变量的拼接获得的,但是随着项目越来越大,通过拼接 SQL 语句访问数据库存在如下的问题:
在上面的例子中,insert语句中需要插入4个字段:
sql = 'INSERT INTO students(sno, name, age, gender) VALUES("%s", "%s", %d, "%s")' % (sno, name, age, gender)
该行代码较长,无法在一行显示。在实际的软件开发中,INSERT 语句可能需要插入 10 个以上的字段,那么拼接 INSERT 语句的代码则非常的繁琐易错。
下面的 SQL 语句来自于一个实际的项目:
sql = "INSERT INTO Flights(FlightID, AircraftModel, RegisterID, Direction, ExpectApronTime, RunwayID, ApronID, AirwayID, TaxiwayTimes, AirwayTimes, Rank) VALUES('%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', '%s', '%s', %d)" % (flightID, aircraftModel, registerID, direction, expectApronTime, runwayID, apronID, airwayID, taxiwayTimes, airwayTimes, rank)
要插入的数据包含有 11 个字段,造成 SQL 语句非常的冗长,需要在多行中才能完全显示,程序的可读性极差。
越复杂的 SQL 语句条件越多、代码越长,在实际的项目中,会出现很多很相近的 SQL 语句。
直接使用 SQL 语句存在有 Web 安全漏洞的问题:通过把 SQL 命令插入到页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令。
下面的 SQL 语句根据页面请求中的用户名和密码查询数据库:
username = 从页面请求中获取用户名
password = 从页面请求中获取密码
sql = 'select * from users where username = "%s" and password = "%s"' % (username, password)
在第 3 行的 SELECT 语句中,where 条件进行权限检查,只有 username 和 password 与数据库表 users 中的数据匹配时,才返回有效数据,因此,只有用户输入正确的用户名和密码才可以获取数据。
这条 SQL 语句存在有安全漏洞,假设用户在页面中输入的用户名为 admin"# (共 7 个字符,前 5 个字符是 admin,后面 2 个字符是 " 和 #),密码为 123456,则最终拼接的 SQL 语句如下:
select * from users where username = "admin"#" and password = "123456"
在 SQL 中,# 是行注释,因此上述 SQL 语句相当于:
select * from users where username = "admin"
只要数据库表 users 中有 admin 这条记录,执行该条 SQL 语句就会返回数据,这样对 password 的检查就彻底失效了。
随着面向对象的软件开发方法发展,出现了对象 - 关系映射 (Object Relation Mapping) 模型,简称为 ORM,ORM 通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中.
ORM:对象关系映射:
在几乎所有的程序里面,都存在对象和关系数据库。在业务逻辑层和用户界面层中,我们是面向对象的。当对象信息发生变化的时候,我们需要把对象的信息保存在关系数据库中。
当你开发一个应用程序的时候(不使用O/R Mapping),你可能会写不少数据访问层的代码,用来从数据库保存,删除,读取对象信息(各种类似的sql语句),等等。你在DAL(数据访问层
)中写了很多的方法来读取对象数据,改变状态对象等等任务。而这些代码写起来总是重复的。
ORM解决的主要问题是对象关系的映射。域模型和关系模型分别是建立在概念模型的基础上的。域模型是面向对象的,而关系模型是面向关系的。一般情况下,一个持久化类和一个表对应,类的每个实例对应表中的一条记录,类的每个属性对应表的每个字段。
具体来说,关系数据库中存在着一张表
class User(object):
def __init__(self,id,name,age):
self.id = id
self.name = name
self.age = age
这样的一个类,如果使用ORM模型映射到数据库的话,在数据库中就变成了一张表,这个表的名字叫User,它有三个字段,id,name,age(这是简单理解,实际代码也不是这样写的)
然后我们用这个类创建一个实例化对象:
user1 = User(1,'zs',20)
这个对象就相当于我们数据库表中的一条记录,一行数据,然后使用ORM模型提供的方法就可以把这个对象插入到数据库中。
同样,对于其他的操作,增删改查等等,这时候我们就不需要在拼接sql语句了,直接像操作类一样,使用一些属性和方法,就可以对数据进行操作了。
SQLAlchemy 是 Python 中一个通过 ORM 操作数据库的框架。SQLAlchemy 对象关系映射器提供了一种方法,用于将用户定义的 Python 类与数据库表相关联,并将这些类实例与其对应表中的行相关联。SQLAlchemy 可以让开发者使用类和对象的方式操作数据库,从而从繁琐的 sql 语句中解脱出来。
简单来说,你把python中的类,映射到数据库中的表,必须要借助一个工具,这个工具就是SQLALchemy
。然后呢,在flask中,为了更好的适配flask,有人做了一个flask-sqlalchemy
的工具包,本质上是对sqlalchemy的进一步封装,是基于sqlalchemy的,所以需要sqlalchemy的支持,使用起来和本来的sqlalchemy的orm基本一样。
pip3 install flask
pip3 install pymysql
pip3 install SQLAlchemy
pip3 install flask-sqlalchemy
其实在安装flask-sqlalchemy的时候,会自动安装sqlalchemy,然后操作数据库,也需要pymysql的支持。
首先,引入相关库
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
接着对访问 mysql 进行配置,如下所示:
app = Flask(__name__)
username = 'root'
password = 'root'
database = 'school'
hostname = 'localhost'
port = '3306'
uri = f'mysql+pymysql://{username}:{password}@{hostname}:{port}/{database}'
app.config['SQLALCHEMY_DATABASE_URI'] = uri
第一行就不说了,下面的代码则是因为我们需要连接数据库而必须提供的变量
mysql数据库的用户名
,密码
,需要操作的数据库名称
,主机的域名
与端口号
,
然后利用这几个变量拼接成连接数据库要使用的uri
,
这里的uri = 'mysql+pymysql://root:root@localhost:3306/school'
然后配置连接使用的uri
: app.config['SQLALCHEMY_DATABASE_URI'] = uri
接着,创建 SQLAlchemy 对象,用于映射数据库表和对象。
db = SQLAlchemy(app)
参数app是通过app = Flask(__name__)
得到的app,这是固定的写法。
用来映射到数据库表的python类通常被叫做数据库模型(model),一个数据库模型类对应数据库中的一个表。
所有的模型类都需要继承flask-sqlalchemy中提供的db.Model基类。
class Student(db.Model):
__tablename__ = 'students'
sno = db.Column(db.Integer,primary_key=True)
name = db.Column(db.String(255))
age = db.Column(db.Integer)
def dump(self):
print(self.sno,self.name,self.age)
设定 __tablename__
为 students,表示将类 Student 映射到数据库中的表 students,就是说数据库中的表名是叫students。
然后,建立属性和字段的映射关系:
这里的属性名称,sno,name,age就对应着数据库表中的字段名称,这个名称是一样的。
同时表的所有的字段(列)都是由db.Column
类的实例对象表示的。
所以创建字段时,使用db.Column
类实例化对象,然后传入参数:
常用的参数包括:
常用的字段类型有:
同时我们在类内实现了一个方法:
每一个类的实例对象调用这个方法就会输出自己的信息
def dump(self):
print(self.sno,self.name,self.age)
python类的数据库模型已经创建好了,现在需要把这个模型映射到数据库中,创建一张确实存在的数据库表students
使用db.create_all()
方法实现建表: db.create_all()
创建已经建立映射关系的表 students,表 students 已经被映射到类 Student。
注意:数据库不存在的话会报错,所以首先需要确认你的数据库时存在的。
执行之后就可以在数据库中看到students表:
数据库和表一旦创建后,之后对模型的改动不会自动作用到实际的表中。比如创建表之后,对模型类中添加或者删除字段,修改字段的名称和类型,这时候再次调用db.create_all()
也不hi更新表结构。想要使改动生效,最简单的方式是调用db.drop_all()
方法删除数据库和表,然后再调用db.create_all()
重新创建。(根据我的测试,这两个方法不会对数据库产生影响,就是说删除或者新建的时候只是对数据表的影响,对数据库不会删除或者新建)
数据表创建完成后,需要对数据表进行数据的操作,增删改查等。
添加一条新纪录到数据表students
中,使用模型类实例化一个对象,这个对象就是数据表中的一条记录,如下:
def insert_students():
tom = Student(sno=1,name='tom',age=12)
db.session.add(tom)
db.session.commit()
jerry = Student(sno = 2, name = 'jerry', age = 11)
mike = Student(sno = 3, name = 'mike', age = 11)
db.session.add_all([jerry, mike])
db.session.commit()
tom = Student(sno=1,name='tom',age=12)
使用模型类实例化对象同时传入属性值。
一个实例化对象就是表中的一条记录。
调用 db.session.add(tom)
将该实例加入到数据库连接会话中,调用 db.session.commit()
提交保存到数据库。
SQLAlchemy中使用数据库会话来管理数据库操作,数据库会话代表一个临时存储区,你对数据库的改动都会存放在这里。可以调用add()方法将新创建的对象添加到数据库会话中,或者对会话中的对象进行更新。只有当你对数据库会话对象调用commit()
方法时,改动才会被提交到数据库。
db.session.add_all()
一次添加包含所有记录对象的列表。
如何从数据库中取回数据?
使用模型类提供的query
属性附加调用各种过滤方法和查询方法可以完成。
一般来说,一个完整的查询遵循下面的模式:
<模型类>.query.<过滤方法>.<查询方法>
现在数据表中有以下记录:
我们有以下代码:
students = Student.query.filter_by(age=11)
调用<模型类>.query
之后,将返回一个类型为
查询对象,这个查询对象可以继续调用过滤方法,每个过滤方法都会返回新的查询对象。所以过滤器可以叠加。
Student.query
返回一个查询对象:
type(Student.query)
<class 'flask_sqlalchemy.BaseQuery
students = Student.query.filter_by(age=11)
查询对象通过过滤方法筛选符合条件的记录,返回的是一个新的查询对象students:
type(students)
<class 'flask_sqlalchemy.BaseQuery'>
新的查询对象students:可以直接调用查询方法all()
,查看全部查询结果:
查询结果是一个列表,有两条记录,每一条记录的类型是
列表内部有多个查询记录,也可以使用for...in...
取出每一条记录
print(students.all())
[<Student 2>, <Student 3>]
students.all()[0]则是返回列表中的第一条数据
或者调用查询方法first()
返回第一条数据:
stu = students.first() #返回第一条数据,调用dump输出字段值
stu.dump()
上面说过,students = Student.query.filter_by(age=11)
返回的是一个新的查询对象,我们可以接着使用过滤器查询。age=11
的记录有两条,现在进一步查询name=mike
的记录。
students = Student.query.filter_by(age=11)
stu = students.filter_by(name='mike')
print(stu.all()) #all()方法查询全部记录的列表,这里只有一条记录
print(stu.first()) #first()返回查询的第一条记录
stu.first().dump() #把第一条记录的信息输出
常用的查询过滤器:
常用的查询方法:
更新一条记录很简单,直接赋值给模型类字段属性就可以改变字段值,然后调用commit()
方法提交会话。
students = Student.query.filter_by(name = 'tom')
students.first().name='TIM'
db.session.commit()
使用过滤方法,返回查询对象 students
,接着使用查询方法,返回指定的那条记录 students.first()
,然后对它的name字段修改,接着commit()
或者:
直接使用查询方法,将会直接返回对应的记录,然后对字段值修改:
stu = Student.query.get(1)#返回主键为1的那条记录
stu.name = 'OTM'
db.session.commit()
注意这两种方式的区别,使用过滤方法将返回新的查询对象,必须接着使用查询方法才能获取记录,但是直接使用查询方法就可以获取记录。
删除某一条记录,有两种方式可以删除:
students = Student.query.filter_by(age = 11)
students.delete()#查询对象直接调用delete()方法
db.session.commit()
students = Student.query.filter_by(age = 11)
db.session.delete(students.first())
db.session.commit()
这里的查询对象students中包含了两条记录,但是不能直接 db.session.delete(students.all())