Flask-SQLAlchemy

使用Flask-SQLAlchemy模块编写Flask应用的持久层,将数据库表结构和关系抽象成类,着重编写业务代码,避免了直接写底层SQL语句,简化开发流程

Flask框架是一个特别轻量的Python Web框架,适合中小型项目开发,框架有很多插件对底层配置和代码做了很大的抽象,所以通过简单的阅读文档就可以编写一个Web应用,并且只需要编写业务逻辑即可

Flask-SQLAlchemy通过对SQLAlchemy包装,简化了对数据库的增删改查操作

表模型

使用模型类来包装表,继承db.Model类声明一个模型类

1
2
3
4
5
6
7
8
9
10
11
12
class Address(db.Model):

id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), nullable=False)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)


class Person(db.Model):

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
addresses = db.relationship('Address', backref='person', lazy=True)

表结构

通过定义类变量来声明表结构

使用db.Colum()方法来生成列

1
2
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), nullable=False)

其中可选类型如下

类型名 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来表示unsigned类型

1
2
3
from sqlalchemy.dialects.mysql import BIGINT

db.Column(BIGINT(unsigned=True))

可选的键如下

选项名 说 明
primary_key 如果设为 True ,这列就是表的主键
unique 如果设为 True ,这列不允许出现重复的值
index 如果设为 True ,为这列创建索引,提升查询效率
nullable 如果设为 True ,这列允许使用空值;如果设为 False ,这列不允许使用空值
default 为这列定义默认值

表关系

一对多关系

如建立两个表,PersonAddress,一个Person可以有多个Address,则Person就代表一而Address就代表多,则需要在Address中设置Person_id的外键,在Flask-SQLAlchemy中还可以在Person中声明和其他模型的关系

person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)

addresses = db.relationship('Address', backref='person', lazy=True)

这样设置可以在对象中直接调用,如有一个Person对象Jack,即可以使用Jack.addresses来获得他所有的Address数组,这是通过relationship中第一个参数模型名来实现的

在地址中调用SomeAddress.person即可以获得连接的Person对象,这是通过backref来实现的

lazy

lazy参数说明将怎么加载连接的数据

方法 作用
select 就是访问到属性的时候,就会全部加载该属性的数据
joined 对关联的两个表使用联接
subquery 与joined类似,但使用子子查询
dynamic 不加载记录,但提供加载记录的查询,也就是生成query对象

注意当使用dynamic时,返回的是一个query对象,需要在其后使用.all().first()获得模型对象

多对多关系

关系型数据库多对多关系使用中间表来实现,在SQL-Alchemy中中间表不使用模型创建,直接创建表Table

1
2
3
4
article_tag = db.Table('article_tag',
db.Column('article_id', db.Integer, db.ForeignKey('article.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)

使用外键连接其他表

article表中使用relationship中的secondary来指出次要表,使用backref来说明反向引用自己的名称

1
2
3
4
5
6
7
8
9
10
11
class Article(db.Model):
__tablename__ = 'article'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String(100), nullable=False)
tags = db.relationship('Tag', secondary=article_tag, backref=db.backref('articles'))


class Tag(db.Model):
__tablename__ = 'tag'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(100), nullable=False)

就完成了连接

在访问时可以直接使用artical.tags访问文章的所有标签,使用tag.articles来访问标签所有的文章

增删改查

增删使用db.session.add()db.session.delete()传入要增加的或者要删除的实体即可

查找使用表模型的query对象

Retrieve a user by username:

1
2
3
4
5
>>> peter = User.query.filter_by(username='peter').first()
>>> peter.id
2
>>> peter.email
u'peter@example.org'

Same as above but for a non existing username gives None:

1
2
3
>>> missing = User.query.filter_by(username='missing').first()
>>> missing is None
True

Selecting a bunch of users by a more complex expression:

1
2
>>> User.query.filter(User.email.endswith('@example.com')).all()
[<User u'admin'>, <User u'guest'>]

Ordering users by something:

1
2
>>> User.query.order_by(User.username).all()
[<User u'admin'>, <User u'guest'>, <User u'peter'>]

Limiting users:

1
2
>>> User.query.limit(1).all()
[<User u'admin'>]

Getting user by primary key:

1
2
>>> User.query.get(1)
<User u'admin'>

更改使用查询出的数据库中实体,改变其属性即可

1
2
3
p = Person.query.get(1)
p.name = 'Tom'
db.session.commit()

返回JSON

使用Flask作为后端,要向前端发送JSON数据,而Flask-SQLAlchemy我还没有找到可以返回字典类型的结果的办法,所以目前找到的办法一共有两种

使用dataclasses

Python3.7以上的版本可以使用dataclasses库来直接将实体模型类返回成JSON数据的HTTP响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from dataclasses import dataclass

@dataclass
class Address(db.Model):
id: int
email: str
person_id: int

id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), nullable=False)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)

@dataclass
class Person(db.Model):
id: int
name: str
addresses: Address

id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
addresses = db.relationship('Address', backref='p', lazy=True, passive_deletes=True)

在使用时可以直接使用Flask中的jsonify

1
2
3
4
5
6
7
8
9
@app.route('/persons/')
def persons():
ps = Person.query.all()
return jsonify(ps)

@app.route('/addrs/')
def addrs():
addrs = Address.query.all()
return jsonify(addrs)

结果为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[
{
"addresses": [
{
"email": "gjss@a.com",
"id": 2,
"person_id": 1
}
],
"id": 1,
"name": "Tom"
},
{
"addresses": [
{
"email": "gjs@a.com",
"id": 1,
"person_id": 2
}
],
"id": 2,
"name": "jack"
}
]
1
2
3
4
5
6
7
8
9
10
11
12
[
{
"email": "gjs@a.com",
"id": 1,
"person_id": 2
},
{
"email": "gjss@a.com",
"id": 2,
"person_id": 1
}
]

可以发现,当使用dataclass时,可以将基础类型的数据转换成JSON,还可以将可以将已经声明过dataclass类型的变量转换成JSON

但是可能发现,如果想实现在Address的结果中加入Person好像有些困难,即显示Address是哪个Person的Person信息,经过我分析尝试后,可以声明一个空变量来实现

1
2
3
4
5
6
7
8
9
10
11
@dataclass
class Address(db.Model):
id: int
email: str
person_id: int
p: Person

id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), nullable=False)
person_id = db.Column(db.Integer, db.ForeignKey('person.id'), nullable=False)
p = None

其中Person变量的名称要和addresses = db.relationship('Address', backref='p', lazy=True, passive_deletes=True)relationshipbackref参数相同,即可以返回出Person的信息,但要注意删除之前的addresses: Address,因为这样会互相引用导致无限循环超出JSON嵌套的范围而报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
{
"email": "gjs@a.com",
"id": 1,
"p":
{
"id": 2,
"name": "jack"
},
"person_id": 2
},
{
"email": "gjss@a.com",
"id": 2,
"p":
{
"id": 1,
"name": "Tom"
},
"person_id": 1
}
]

没有翻出源码,但是根据观察和relationship的注释

​ :param backref:

​ Indicates the string name of a property to be placed on the related mapper’s class that will handle this relationship in the other direction. The other property will be created automatically when the mappers are configured. Can also be passed as a :func:.backref object to control the configuration of the new relationship.

可以知道配置了backref映射后,会自动生成变量

在查询出的Address对象中可以使用Address.p来访问其Person对象,实现的方式应该是通过relationship对Address注入了backref的变量,但因为变量是在运行时所注入,而在程序启动时没有p变量,因此不可以直接在类下声明p变量的类型,所以先定义一个空对象,其目的在于告诉classdata这个类存在这个对象,其次在定义p的类型,而在查询获得这个Address实体时,已经被注入,所以进而会生成p变量的JSON字符串

使用查询结果拼接成字典

这个方法好理解并且简单,就是通过访问模型实体中的数据,拼接成JSON字符串如

1
2
3
4
5
6
7
@app.route('/index/')
def router():
ps = Person.query.all()
return {
'name': ps[0].name,
'id': ps[0].id
}

或者在模型类中加入方法as_dict()

1
2
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}

调用ps[0].as_dict()方法即可返回字典类型数据,本质还是拼接

优点和缺点

  • 第一种方法简单方便,在返回成字符串后不需要使用更多的代码,可以说是直接生成HTTP响应一步到位,看起来也更简约,满足了强迫症,但是我试了好多方法,其中的数据改变是很困难的,包括更改删除和增加,唯一可以实现的途径就是更改查询后的实体模型中的变量,但事后要注意要回滚,否则很容易更改源数据库中的数据,并且所有可以生成的变量全部在代码中写死,jsonify返回的更是Response类型的对象,更改其中数据变得十分困难和麻烦
  • 第二种方法要自己一个个拼接字典生成JSON,操作麻烦但是可变型高,可以根据需求酌情选择数据,并且通过模型实体中的关系可以很好的找到其连接的表的数据

在实际中应根据情况选择两者使用的方式和场景,目前只找到这两种方式,其实如果可以直接返回字典类型的数据,根据情况删减是最好的,不知道SQLAlchemy是否支持,也没有找到类似的配置